/* Copyright 2018 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.
 */

/* NX20P348x USB-C Power Path Controller */

#include "common.h"
#include "console.h"
#include "nx20p348x.h"
#include "gpio.h"
#include "hooks.h"
#include "i2c.h"
#include "system.h"
#include "tcpm/tcpm.h"
#include "usb_charge.h"
#include "usb_pd_tcpm.h"
#include "usb_pd.h"
#include "usbc_ppc.h"
#include "util.h"

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

static uint32_t irq_pending; /* Bitmask of ports signaling an interrupt. */

#define NX20P348X_DB_EXIT_FAIL_THRESHOLD 10
static int db_exit_fail_count[CONFIG_USB_PD_PORT_MAX_COUNT];

#define NX20P348X_FLAGS_SOURCE_ENABLED BIT(0)
static uint8_t flags[CONFIG_USB_PD_PORT_MAX_COUNT];

#if !defined(CONFIG_USBC_PPC_NX20P3481) && !defined(CONFIG_USBC_PPC_NX20P3483)
#error "Either the NX20P3481 or NX20P3483 must be selected"
#endif

static int read_reg(uint8_t port, int reg, int *regval)
{
	return i2c_read8(ppc_chips[port].i2c_port,
			 ppc_chips[port].i2c_addr_flags,
			 reg,
			 regval);
}

static int write_reg(uint8_t port, int reg, int regval)
{
	return i2c_write8(ppc_chips[port].i2c_port,
			  ppc_chips[port].i2c_addr_flags,
			  reg,
			  regval);
}

static int nx20p348x_set_ovp_limit(int port)
{
	int rv;
	int reg;

	/* Set VBUS over voltage threshold (OVLO) */
	rv = read_reg(port, NX20P348X_OVLO_THRESHOLD_REG, &reg);
	if (rv)
		return rv;
	/* OVLO threshold is 3 bit field */
	reg &= ~NX20P348X_OVLO_THRESHOLD_MASK;
	/* Set SNK OVP to 23.0 V */
	reg |= NX20P348X_OVLO_23_0;
	rv = write_reg(port, NX20P348X_OVLO_THRESHOLD_REG, reg);
	if (rv)
		return rv;

	return EC_SUCCESS;
}

static int nx20p348x_is_sourcing_vbus(int port)
{
	return flags[port] & NX20P348X_FLAGS_SOURCE_ENABLED;
}

static int nx20p348x_set_vbus_source_current_limit(int port,
						 enum tcpc_rp_value rp)
{
	int regval;
	int status;

	status = read_reg(port, NX20P348X_5V_SRC_OCP_THRESHOLD_REG, &regval);
	if (status)
		return status;

	regval &= ~NX20P348X_ILIM_MASK;

	/* We need buffer room for all current values. */
	switch (rp) {
	case TYPEC_RP_3A0:
		regval |= NX20P348X_ILIM_3_200;
		break;

	case TYPEC_RP_1A5:
		regval |= NX20P348X_ILIM_1_600;
		break;

	case TYPEC_RP_USB:
	default:
		regval |= NX20P348X_ILIM_0_600;
		break;
	};


	return write_reg(port, NX20P348X_5V_SRC_OCP_THRESHOLD_REG, regval);
}

static int nx20p348x_discharge_vbus(int port, int enable)
{
	int regval;
	int status;

	status = read_reg(port, NX20P348X_DEVICE_CONTROL_REG, &regval);
	if (status)
		return status;

	if (enable)
		regval |= NX20P348X_CTRL_VBUSDIS_EN;
	else
		regval &= ~NX20P348X_CTRL_VBUSDIS_EN;

	status = write_reg(port, NX20P348X_DEVICE_CONTROL_REG, regval);
	if (status) {
		CPRINTS("Failed to %s vbus discharge",
			enable ? "enable" : "disable");
		return status;
	}

	return EC_SUCCESS;
}

__maybe_unused static int nx20p3481_vbus_sink_enable(int port, int enable)
{
	int status;
	int rv;
	int control = enable ? NX20P3481_SWITCH_CONTROL_HVSNK : 0;

	rv = write_reg(port, NX20P348X_SWITCH_CONTROL_REG, control);
	if (rv)
		return rv;

	/*
	 * Read switch status register. The bit definitions for switch control
	 * and switch status resister are identical, so the control value can be
	 * compared against the status value. The control switch has a debounce
	 * (15 msec) before the status will reflect the control command.
	 */
	msleep(NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC);
	rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &status);
	if (rv)
		return rv;

	return (status & NX20P348X_SWITCH_STATUS_HVSNK) == control ?
		EC_SUCCESS : EC_ERROR_UNKNOWN;
}

__maybe_unused static int nx20p3481_vbus_source_enable(int port, int enable)
{
	int status;
	int rv;
	uint8_t previous_flags = flags[port];
	int control = enable ? NX20P3481_SWITCH_CONTROL_5VSRC : 0;

	rv = write_reg(port, NX20P348X_SWITCH_CONTROL_REG, control);
	if (rv)
		return rv;

	/* Cache the anticipated Vbus state */
	if (enable)
		flags[port] |= NX20P348X_FLAGS_SOURCE_ENABLED;
	else
		flags[port] &= ~NX20P348X_FLAGS_SOURCE_ENABLED;

	/*
	 * Read switch status register. The bit definitions for switch control
	 * and switch status resister are identical, so the control value can be
	 * compared against the status value. The control switch has a debounce
	 * (15 msec) before the status will reflect the control command.
	 */
	msleep(NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC);

	if (IS_ENABLED(CONFIG_USBC_PPC_NX20P3481)) {
		rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &status);
		if (rv) {
			flags[port] = previous_flags;
			return rv;
		}
		if ((status & NX20P348X_SWITCH_STATUS_MASK) != control) {
			flags[port] = previous_flags;
			return EC_ERROR_UNKNOWN;
		}
	}

	return EC_SUCCESS;
}

__maybe_unused static int nx20p3483_vbus_sink_enable(int port, int enable)
{
	int rv;

	enable = !!enable;

	/*
	 * We cannot use an EC GPIO for EN_SNK since an EC reset
	 * will float the GPIO thus browning out the board (without
	 * a battery).
	 */
	rv = tcpm_set_snk_ctrl(port, enable);
	if (rv)
		return rv;

	for (int i = 0; i < NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC; ++i) {
		int sw;

		rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &sw);
		if (rv != EC_SUCCESS)
			return rv;
		if (!!(sw & NX20P348X_SWITCH_STATUS_HVSNK) == enable)
			return EC_SUCCESS;
		msleep(1);
	}

	return EC_ERROR_TIMEOUT;
}

__maybe_unused static int nx20p3483_vbus_source_enable(int port, int enable)
{
	int rv;

	enable = !!enable;

	/*
	 * For parity's sake, we should not use an EC GPIO for
	 * EN_SRC since we cannot use it for EN_SNK (for brown
	 * out reason listed above).
	 */
	rv = tcpm_set_src_ctrl(port, enable);
	if (rv)
		return rv;

	/*
	 * Wait up to NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC for the status
	 * to reflect the control command.
	 */

	for (int i = 0; i < NX20P348X_SWITCH_STATUS_DEBOUNCE_MSEC; ++i) {
		int s;

		rv = read_reg(port, NX20P348X_SWITCH_STATUS_REG, &s);
		if (rv != EC_SUCCESS)
			return rv;

		if (!!(s & (NX20P348X_SWITCH_STATUS_5VSRC |
			    NX20P348X_SWITCH_STATUS_HVSRC)) == enable) {
			if (enable)
				flags[port] |= NX20P348X_FLAGS_SOURCE_ENABLED;
			else
				flags[port] &= ~NX20P348X_FLAGS_SOURCE_ENABLED;
			return EC_SUCCESS;
		}
		msleep(1);
	}

	return EC_ERROR_TIMEOUT;
}

static int nx20p348x_init(int port)
{
	int reg;
	int mask;
	int mode;
	int rv;
	enum tcpc_rp_value initial_current_limit;

	/* Mask interrupts for interrupt 2 register */
	mask = ~NX20P348X_INT2_EN_ERR;
	rv = write_reg(port, NX20P348X_INTERRUPT2_MASK_REG, mask);
	if (rv)
		return rv;

	/* Mask interrupts for interrupt 1 register */
	mask = ~(NX20P348X_INT1_OC_5VSRC | NX20P348X_INT1_SC_5VSRC |
		 NX20P348X_INT1_RCP_5VSRC | NX20P348X_INT1_DBEXIT_ERR);
	if (IS_ENABLED(CONFIG_USBC_PPC_NX20P3481)) {
		/* Unmask Fast Role Swap detect interrupt */
		mask &= ~NX20P3481_INT1_FRS_DET;
	}
	rv = write_reg(port, NX20P348X_INTERRUPT1_MASK_REG, mask);
	if (rv)
		return rv;

	/* Clear any pending interrupts by reading interrupt registers */
	read_reg(port, NX20P348X_INTERRUPT1_REG, &reg);
	read_reg(port, NX20P348X_INTERRUPT2_REG, &reg);

	/* Get device  mode */
	rv = read_reg(port, NX20P348X_DEVICE_STATUS_REG, &mode);
	if (rv)
		return rv;

	if (IS_ENABLED(CONFIG_USBC_PPC_NX20P3481))
		mode &= NX20P3481_DEVICE_MODE_MASK;
	else if (IS_ENABLED(CONFIG_USBC_PPC_NX20P3483))
		mode &= NX20P3483_DEVICE_MODE_MASK;

	/* Check if dead battery mode is active. */
	if (mode == NX20P348X_MODE_DEAD_BATTERY) {
		/*
		 * If in dead battery mode, must enable HV SNK mode prior to
		 * exiting dead battery mode or VBUS path will get cut off and
		 * system will lose power. Before DB mode is exited, the device
		 * mode will not reflect the correct value and therefore the
		 * return value isn't useful here.
		 */
		nx20p348x_drv.vbus_sink_enable(port, 1);

		/* Exit dead battery mode. */
		rv = read_reg(port, NX20P348X_DEVICE_CONTROL_REG, &reg);
		if (rv)
			return rv;
		reg |= NX20P348X_CTRL_DB_EXIT;
		rv = write_reg(port, NX20P348X_DEVICE_CONTROL_REG, reg);
		if (rv)
			return rv;
	}

	/*
	 * Set VBUS over voltage threshold (OVLO). Note that while the PPC is in
	 * dead battery mode, OVLO is forced to 6.8V, so this setting must be
	 * done after dead battery mode is exited.
	 */
	nx20p348x_set_ovp_limit(port);

	/* Set the Vbus current limit after dead battery mode exit */
#ifdef CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT
	initial_current_limit = CONFIG_USB_PD_MAX_SINGLE_SOURCE_CURRENT;
#else
	initial_current_limit = TYPEC_RP_1A5;
#endif
	nx20p348x_set_vbus_source_current_limit(port, initial_current_limit);

	return EC_SUCCESS;
}

static void nx20p348x_handle_interrupt(int port)
{
	int reg;
	int control_reg;

	/*
	 * Read interrupt 1 status register. Note, interrupt register is
	 * automatically cleared by reading.
	 */
	read_reg(port, NX20P348X_INTERRUPT1_REG, &reg);

	/* Check for DBEXIT error */
	if (reg & NX20P348X_INT1_DBEXIT_ERR) {
		int mask_reg;

		/*
		 * This failure is not expected. If for some reason, this
		 * keeps happening, then log an error and mask the interrupt to
		 * prevent interrupt floods.
		 */
		if (++db_exit_fail_count[port] >=
		    NX20P348X_DB_EXIT_FAIL_THRESHOLD) {
			ppc_prints("failed to exit DB mode", port);
			if (read_reg(port, NX20P348X_INTERRUPT1_MASK_REG,
				    &mask_reg)) {
				mask_reg |= NX20P348X_INT1_DBEXIT_ERR;
				write_reg(port, NX20P348X_INTERRUPT1_MASK_REG,
					  mask_reg);
			}
		}
		read_reg(port, NX20P348X_DEVICE_CONTROL_REG, &control_reg);
		reg |= NX20P348X_CTRL_DB_EXIT;
		write_reg(port, NX20P348X_DEVICE_CONTROL_REG, control_reg);
		/*
		 * If DB exit mode failed, then the OVP limit setting done in
		 * the init routine will not be successful. Set the OVP limit
		 * again here.
		 */
		nx20p348x_set_ovp_limit(port);
	}

	/* Check for 5V OC interrupt */
	if (reg & NX20P348X_INT1_OC_5VSRC) {
		ppc_prints("detected Vbus overcurrent!", port);
		pd_handle_overcurrent(port);
	}

	/* Check for Vbus reverse current protection */
	if (reg & NX20P348X_INT1_RCP_5VSRC)
		ppc_prints("detected Vbus reverse current!", port);

	/* Check for Vbus short protection */
	if (reg & NX20P348X_INT1_SC_5VSRC)
		ppc_prints("Vbus short detected!", port);

	/* Check for FRS detection */
	if (IS_ENABLED(CONFIG_USBC_PPC_NX20P3481) &&
	    (reg & NX20P3481_INT1_FRS_DET)) {
		/*
		 * TODO(b/113069469): Need to check for CC status and verifiy
		 * that a sink is attached to continue with FRS. If a sink is
		 * not attached, then this FRS detect is a false detect which is
		 * triggered when removing an external charger. If FRS was
		 * detected by the PPC, then it has automatically  enabled the
		 * 5V SRC mode and this must be undone for a proper detach.
		 */
		/* Check CC status */

		/*
		 * False detect, disable SRC mode which was enabled by
		 * NX20P3481.
		 */
		ppc_prints("FRS false detect, disabling SRC mode!", port);
		nx20p3481_vbus_source_enable(port, 0);
	}

	/*
	 * Read interrupt 2 status register. Note, interrupt register is
	 * automatically cleared by reading.
	 */
	/*
	 * TODO (b/75272421): Not sure if any of these interrupts
	 * will be used. Might want to use EN_ERR which tracks when both
	 * SNK_EN and SRC_EN are set. However, since for the Analogix TCPC
	 * these values aren't controlled by the EC directly, not sure what
	 * action if any can be taken.
	 */
	read_reg(port, NX20P348X_INTERRUPT2_REG, &reg);
}

static void nx20p348x_irq_deferred(void)
{
	int i;
	uint32_t pending = atomic_clear(&irq_pending);

	for (i = 0; i < board_get_usb_pd_port_count(); i++)
		if (BIT(i) & pending)
			nx20p348x_handle_interrupt(i);
}
DECLARE_DEFERRED(nx20p348x_irq_deferred);

void nx20p348x_interrupt(int port)
{
	atomic_or(&irq_pending, BIT(port));
	hook_call_deferred(&nx20p348x_irq_deferred_data, 0);
}

#ifdef CONFIG_CMD_PPC_DUMP
static int nx20p348x_dump(int port)
{
	int reg_addr;
	int reg;
	int rv;

	ccprintf("Port %d NX20P348X registers\n", port);
	for (reg_addr = NX20P348X_DEVICE_ID_REG; reg_addr <=
		     NX20P348X_DEVICE_CONTROL_REG; reg_addr++) {
		rv = read_reg(port, reg_addr, &reg);
		if (rv) {
			ccprintf("nx20p: Failed to read register 0x%x\n",
				 reg_addr);
			return rv;
		}
		ccprintf("[0x%02x]: 0x%02x\n", reg_addr, reg);

		/* Flush every call otherwise buffer may get full */
		cflush();
	}

	return EC_SUCCESS;
}
#endif /* defined(CONFIG_CMD_PPC_DUMP) */

/*
 * TODO (b/112697473): The NX20P348x PPCs do not support vbus detection or vconn
 * generation. However, if a different PPC does support these features and needs
 * these config options, then these functions do need to exist. The
 * configuration for what each PPC supports should be converted to bits within
 * a flags variable that is part of the ppc_config_t struct.
 */
#ifdef CONFIG_USB_PD_VBUS_DETECT_PPC
static int nx20p348x_is_vbus_present(int port)
{
	return EC_ERROR_UNIMPLEMENTED;
}
#endif /* defined(CONFIG_USB_PD_VBUS_DETECT_PPC) */

#ifdef CONFIG_USBC_PPC_POLARITY
static int nx20p348x_set_polarity(int port, int polarity)
{
	return EC_ERROR_UNIMPLEMENTED;
}
#endif

#ifdef CONFIG_USBC_PPC_VCONN
static int nx20p348x_set_vconn(int port, int enable)
{
	return EC_ERROR_UNIMPLEMENTED;
}
#endif

const struct ppc_drv nx20p348x_drv = {
	.init = &nx20p348x_init,
	.is_sourcing_vbus = &nx20p348x_is_sourcing_vbus,
#ifdef CONFIG_USBC_PPC_NX20P3481
	.vbus_sink_enable = &nx20p3481_vbus_sink_enable,
	.vbus_source_enable = &nx20p3481_vbus_source_enable,
#endif /* CONFIG_USBC_PPC_NX20P3481 */
#ifdef CONFIG_USBC_PPC_NX20P3483
	.vbus_sink_enable = &nx20p3483_vbus_sink_enable,
	.vbus_source_enable = &nx20p3483_vbus_source_enable,
#endif /* CONFIG_USBC_PPC_NX20P3483 */
#ifdef CONFIG_CMD_PPC_DUMP
	.reg_dump = &nx20p348x_dump,
#endif /* defined(CONFIG_CMD_PPC_DUMP) */
	.set_vbus_source_current_limit =
		&nx20p348x_set_vbus_source_current_limit,
	.discharge_vbus = &nx20p348x_discharge_vbus,
#ifdef CONFIG_USB_PD_VBUS_DETECT_PPC
	.is_vbus_present = &nx20p348x_is_vbus_present,
#endif /* defined(CONFIG_USB_PD_VBUS_DETECT_PPC) */
#ifdef CONFIG_USBC_PPC_POLARITY
	.set_polarity = &nx20p348x_set_polarity,
#endif /* defined(CONFIG_USBC_PPC_POLARITY) */
#ifdef CONFIG_USBC_PPC_VCONN
	.set_vconn = &nx20p348x_set_vconn,
#endif /* defined(CONFIG_USBC_PPC_VCONN) */
};
