/* Copyright 2020 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "console.h"
#include "hooks.h"
#include "i2c.h"
#include "system.h"
#include "util.h"
#include "pwr_defs.h"

#include "gl3590.h"

#define CPRINTS(format, args...) cprints(CC_SYSTEM, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_I2C, format, ## args)

/* GL3590 is unique in terms of i2c_read, since it doesn't support repeated
 * start sequence. One need to issue two separate transactions - first is write
 * with a register offset, then after a delay second transaction is actual read.
 */
int gl3590_read(int hub, uint8_t reg, uint8_t *data, int count)
{
	int rv;
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	i2c_lock(uhub_p->i2c_host_port, 1);
	rv = i2c_xfer_unlocked(uhub_p->i2c_host_port,
			       uhub_p->i2c_addr,
			       &reg, 1,
			       NULL, 0,
			       I2C_XFER_SINGLE);
	i2c_lock(uhub_p->i2c_host_port, 0);

	if (rv)
		return rv;

	/* GL3590 requires at least 1ms between consecutive i2c transactions */
	udelay(MSEC);

	i2c_lock(uhub_p->i2c_host_port, 1);
	rv = i2c_xfer_unlocked(uhub_p->i2c_host_port,
			       uhub_p->i2c_addr,
			       NULL, 0,
			       data, count,
			       I2C_XFER_SINGLE);
	i2c_lock(uhub_p->i2c_host_port, 0);

	/*
	 * GL3590 requires at least 1ms between consecutive i2c transactions.
	 * Make sure that we are safe across API calls.
	 */
	udelay(MSEC);

	return rv;
};

int gl3590_write(int hub, uint8_t reg, uint8_t *data, int count)
{
	int rv;
	uint8_t buf[5];
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	/* GL3590 registers accept 4 bytes at max */
	if (count > (sizeof(buf) - 1)) {
		ccprintf("Too many bytes to write");
		return EC_ERROR_INVAL;
	}

	buf[0] = reg;
	memcpy(&buf[1], data, count);

	i2c_lock(uhub_p->i2c_host_port, 1);
	rv = i2c_xfer_unlocked(uhub_p->i2c_host_port,
			       uhub_p->i2c_addr,
			       buf, count + 1,
			       NULL, 0,
			       I2C_XFER_SINGLE);
	i2c_lock(uhub_p->i2c_host_port, 0);

	/*
	 * GL3590 requires at least 1ms between consecutive i2c transactions.
	 * Make sure that we are safe across API calls.
	 */
	udelay(MSEC);

	return rv;
}

/*
 * Basic initialization of GL3590 I2C interface.
 *
 * Please note, that I2C interface is online not earlier than ~50ms after
 * RESETJ# is deasserted. Platform should check that PGREEN_A_SMD pin is
 * asserted. This init function shouldn't be invoked until that time.
 */
void gl3590_init(int hub)
{
	uint8_t tmp;
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	if (uhub_p->initialized)
		return;

	if (gl3590_read(hub, GL3590_HUB_MODE_REG, &tmp, 1)) {
		CPRINTF("GL3590: Cannot read HUB_MODE register");
		return;
	}
	if ((tmp & GL3590_HUB_MODE_I2C_READY) == 0)
		CPRINTF("GL3590 interface isn't ready, consider deferring "
			"this init\n");

	/* Deassert INTR# signal */
	tmp = GL3590_INT_CLEAR;
	if (gl3590_write(hub, GL3590_INT_REG, &tmp, 1)) {
		CPRINTF("GL3590: Cannot write to INT register");
		return;
	};

	uhub_p->initialized = 1;
}

/*
 * GL3590 chip may drive I2C_SDA and I2C_SCL lines for 200ms (max) after it is
 * released from reset (through gpio de-assertion in main()). In order to avoid
 * broken I2C transactions, we need to add an extra delay before any activity on
 * the I2C bus in the system.
 */
static void gl3590_delay_on_init(void)
{
	CPRINTS("Applying 200ms delay for GL3590 to release I2C lines");
	udelay(200 * MSEC);
}
DECLARE_HOOK(HOOK_INIT, gl3590_delay_on_init, HOOK_PRIO_INIT_I2C - 1);

void gl3590_irq_handler(int hub)
{
	uint8_t buf = 0;
	uint8_t res_reg[2];
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	if (!uhub_p->initialized)
		return;

	/* Verify that irq is pending */
	if (gl3590_read(hub, GL3590_INT_REG, &buf, sizeof(buf))) {
		ccprintf("Cannot read from the host hub i2c\n");
		goto exit;
	}

	if ((buf & GL3590_INT_PENDING) == 0) {
		ccprintf("Invalid hub event\n");
		goto exit;
	}

	/* Get the hub event reason */
	if (gl3590_read(hub, GL3590_RESPONSE_REG, res_reg, sizeof(res_reg))) {
		ccprintf("Cannot read from the host hub i2c\n");
		goto exit;
	}

	if ((res_reg[0] & GL3590_RESPONSE_REG_SYNC_MASK) == 0)
		ccprintf("Host hub response: ");
	else
		ccprintf("Host hub event! ");

	switch(res_reg[0]) {
	case 0x0:
		ccprintf("No response");
		break;
	case 0x1:
		ccprintf("Successful");
		break;
	case 0x2:
		ccprintf("Invalid command");
		break;
	case 0x3:
		ccprintf("Invalid arguments");
		break;
	case 0x4:
		ccprintf("Invalid port: %d", res_reg[1]);
		break;
	case 0x5:
		ccprintf("Command not completed");
		break;
	case 0x80:
		ccprintf("Reset complete");
		break;
	case 0x81:
		ccprintf("Power operation mode change");
		break;
	case 0x82:
		ccprintf("Connect change");
		break;
	case 0x83:
		ccprintf("Error on the specific port");
		break;
	case 0x84:
		ccprintf("Hub state change");
		break;
	case 0x85:
		ccprintf("SetFeature PORT_POWER failure");
		break;
	default:
		ccprintf("Unknown value: 0x%0x", res_reg[0]);
	}
	ccprintf("\n");

	if (res_reg[1])
		ccprintf("Affected port %d\n", res_reg[1]);

exit:
	/* Try to clear interrupt */
	buf = GL3590_INT_CLEAR;
	gl3590_write(hub, GL3590_INT_REG, &buf, sizeof(buf));
}

enum ec_error_list gl3590_ufp_pwr(int hub, struct pwr_con_t *pwr)
{
	uint8_t hub_sts, hub_mode;
	int rv = 0;
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	if (!uhub_p->initialized)
		return EC_ERROR_HW_INTERNAL;

	if (gl3590_read(hub, GL3590_HUB_STS_REG, &hub_sts, sizeof(hub_sts))) {
		CPRINTF("Error reading HUB_STS %d\n", rv);
		return EC_ERROR_BUSY;
	}

	pwr->volts = 5;

	switch ((hub_sts & GL3590_HUB_STS_HOST_PWR_MASK) >>
		GL3590_HUB_STS_HOST_PWR_SHIFT) {
	case GL3590_DEFAULT_HOST_PWR_SRC:
		if (gl3590_read(hub, GL3590_HUB_MODE_REG, &hub_mode,
				sizeof(hub_mode))) {
			CPRINTF("Error reading HUB_MODE %d\n", rv);
			return EC_ERROR_BUSY;
		}
		if (hub_mode & GL3590_HUB_MODE_USB3_EN) {
			pwr->milli_amps = 900;
			return EC_SUCCESS;
		} else if (hub_mode & GL3590_HUB_MODE_USB2_EN) {
			pwr->milli_amps = 500;
			return EC_SUCCESS;
		} else {
			CPRINTF("GL3590: Neither USB3 nor USB2 hubs "
				 "configured\n");
			return EC_ERROR_HW_INTERNAL;
		}
	case GL3590_1_5_A_HOST_PWR_SRC:
		pwr->milli_amps = 1500;
		return EC_SUCCESS;
	case GL3590_3_0_A_HOST_PWR_SRC:
		pwr->milli_amps = 3000;
		return EC_SUCCESS;
	default:
		CPRINTF("GL3590: Unkown host power source %d\n", hub_sts);
		return EC_ERROR_UNKNOWN;
	}
}

#define GL3590_EN_PORT_MAX_RETRY_COUNT	10

int gl3590_enable_ports(int hub, uint8_t port_mask, bool enable)
{
	uint8_t buf[4] = {0};
	uint8_t en_mask = 0;
	uint8_t tmp;
	int rv, i;
	struct uhub_i2c_iface_t *uhub_p = &uhub_config[hub];

	if (!uhub_p->initialized)
		return EC_ERROR_HW_INTERNAL;

	if (!enable)
		en_mask = port_mask;

	buf[0] = en_mask;
	buf[2] = port_mask;

	for (i = 1; i <= GL3590_EN_PORT_MAX_RETRY_COUNT; i++) {
		rv = gl3590_write(hub, GL3590_PORT_DISABLED_REG, buf,
				  sizeof(buf));
		if (rv)
			return rv;

		usleep(200 * MSEC);

		/* Verify whether port is enabled/disabled */
		rv = gl3590_read(hub, GL3590_PORT_EN_STS_REG, &tmp, 1);
		if (rv)
			return rv;

		if (enable && ((tmp & port_mask) == port_mask))
			break;
		if (!enable && ((tmp & port_mask) == 0))
			break;

		if (i > GL3590_EN_PORT_MAX_RETRY_COUNT) {
			CPRINTF("GL3590: Failed to %s port 0x%x\n",
				enable ? "enable" : "disable", port_mask);
			return EC_ERROR_HW_INTERNAL;
		}

		CPRINTF("GL3590: Port %s retrying.. %d/%d\n"
			"Port status is 0x%x\n", enable ? "enable" : "disable",
			i, GL3590_EN_PORT_MAX_RETRY_COUNT, tmp);
	}

	return EC_SUCCESS;
}

#ifdef CONFIG_CMD_GL3590
static int command_gl3590(int argc, char **argv)
{
	char *e;
	int port;

	if (argc < 2)
		return EC_ERROR_PARAM_COUNT;

	port = strtoi(argv[2], &e, 0);
	if (*e)
		return EC_ERROR_PARAM2;

	if (strcasecmp(argv[1], "enable") == 0) {
		if (!gl3590_enable_ports(0, port, 1))
			return EC_SUCCESS;
		else
			return EC_ERROR_HW_INTERNAL;
	} else if (strcasecmp(argv[1], "disable") == 0) {
		if (!gl3590_enable_ports(0, port, 0))
			return EC_SUCCESS;
		else
			return EC_ERROR_HW_INTERNAL;
	}

	return EC_ERROR_PARAM1;
}
DECLARE_CONSOLE_COMMAND(gl3590, command_gl3590,
			"<enable | disable> <port_bitmask>",
			"Manage GL3590 USB3.1 hub and its ports");
#endif /* CONFIG_CMD_GL3590 */
