/* 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 "common.h"
#include "console.h"
#include "chip/stm32/ucpd-stm32gx.h"
#include "cros_board_info.h"
#include "driver/mp4245.h"
#include "driver/tcpm/tcpci.h"
#include "driver/mp4245.h"
#include "hooks.h"
#include "task.h"
#include "timer.h"
#include "usb_common.h"
#include "usb_mux.h"
#include "usb_pd.h"
#include "usb_pd_dp_ufp.h"
#include "usb_tc_sm.h"
#include "usbc_ppc.h"

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

#define MP4245_VOLTAGE_WINDOW BIT(2)
#define MP4245_VOLTAGE_WINDOW_MASK (MP4245_VOLTAGE_WINDOW - 1)

#define PDO_FIXED_FLAGS (PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP |\
			 PDO_FIXED_COMM_CAP | PDO_FIXED_UNCONSTRAINED)

/* Voltage indexes for the PDOs */
enum volt_idx {
	PDO_IDX_5V   = 0,
	PDO_IDX_9V   = 1,
	PDO_IDX_15V  = 2,
	PDO_IDX_20V  = 3,
	PDO_IDX_COUNT
};

/* PDOs */
const uint32_t pd_src_host_pdo[] = {
	[PDO_IDX_5V]  = PDO_FIXED(5000,   3000, PDO_FIXED_FLAGS),
	[PDO_IDX_9V]  = PDO_FIXED(9000,   3000, 0),
	[PDO_IDX_15V]  = PDO_FIXED(15000, 3000, 0),
	[PDO_IDX_20V]  = PDO_FIXED(20000, 3000, 0),
};
BUILD_ASSERT(ARRAY_SIZE(pd_src_host_pdo) == PDO_IDX_COUNT);

#ifdef BOARD_C1_1A5_LIMIT
const uint32_t pd_src_display_pdo[] = {
	[PDO_IDX_5V]  = PDO_FIXED(5000,   1500, PDO_FIXED_FLAGS),
};
#else
const uint32_t pd_src_display_pdo[] = {
	[PDO_IDX_5V]  = PDO_FIXED(5000,   3000, PDO_FIXED_FLAGS),
};
#endif

const uint32_t pd_snk_pdo[] = {
	[PDO_IDX_5V]  = PDO_FIXED(5000,   0, PDO_FIXED_FLAGS),
};
const int pd_snk_pdo_cnt = ARRAY_SIZE(pd_snk_pdo);

static int src_host_pdo_cnt_override;

#define PD_DR_SWAP_ATTEMPT_MAX 3
static int pd_dr_swap_attempt_count[CONFIG_USB_PD_PORT_MAX_COUNT];

static int command_hostpdo(int argc, char **argv)
{
	char *e;
	int limit;

	if (argc >= 2) {

		limit = strtoi(argv[1], &e, 10);
		if ((limit < 0) || (limit > PDO_IDX_COUNT))
			return EC_ERROR_PARAM1;

		src_host_pdo_cnt_override = limit;
	}
	ccprintf("src host pdo override = %d\n", src_host_pdo_cnt_override);

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(hostpdo, command_hostpdo,
			"<0|1|2|3|4>",
			"Limit number of PDOs for C0");

int dpm_get_source_pdo(const uint32_t **src_pdo, const int port)
{
	int pdo_cnt = 0;

	if (port == USB_PD_PORT_HOST) {
		*src_pdo =  pd_src_host_pdo;
		pdo_cnt = ARRAY_SIZE(pd_src_host_pdo);
		/*
		 * This override is only active via a console command. Only used
		 * for debug to limit the level of VBUS offered to port partner
		 * if desired. The console command only allows 0 ->
		 * PDO_IDX_COUNT for this value.
		 */
		if (src_host_pdo_cnt_override)
			pdo_cnt = src_host_pdo_cnt_override;
	} else {
		*src_pdo =  pd_src_display_pdo;
		pdo_cnt = ARRAY_SIZE(pd_src_display_pdo);
	}

	return pdo_cnt;
}

/*
 * Default Port Discovery DR Swap Policy.
 *
 * 1) If port == 0 and port data role is DFP, transition to pe_drs_send_swap
 * 2) If port == 1 and port data role is UFP, transition to pe_drs_send_swap
 */
__override bool port_discovery_dr_swap_policy(int port,
			enum pd_data_role dr, bool dr_swap_flag)
{
	/*
	 * Port0: test if role is DFP
	 * Port1: test if role is UFP
	 */
	enum pd_data_role role_test =
		(port == USB_PD_PORT_HOST) ? PD_ROLE_DFP : PD_ROLE_UFP;

	/*
	 * Request data role swap if not in the port's desired data role and if
	 * the attempt count is less than the max allowed. This function is
	 * called for each PE run once in a PD contract. If the port partner
	 * rejects data role swap requests (eg compliance tester), want to limit
	 * how many DR swap requests are attempted.
	 */
	if (dr == role_test && (pd_dr_swap_attempt_count[port]++ <
				PD_DR_SWAP_ATTEMPT_MAX))
		return true;

	/* Do not perform a DR swap */
	return false;
}

/*
 * Default Port Discovery VCONN Swap Policy.
 *
 * 1) No need to Vconn swap.  This board does not require any cable information.
 */
__override bool port_discovery_vconn_swap_policy(int port,
			bool vconn_swap_flag)
{
	return false;
}

int pd_check_vconn_swap(int port)
{
	/*TODO: Dock is the Vconn source */
	return 1;
}

void pd_power_supply_reset(int port)
{
	int prev_en;

	if (port < 0 || port >= CONFIG_USB_PD_PORT_MAX_COUNT)
		return;

	if (IS_ENABLED(BOARD_C1_NO_PPC) && port) {
		prev_en = c1_ps8805_is_sourcing_vbus(port);
		/* Disable VBUS via PPC. */
		c1_ps8805_vbus_source_enable(port, 0);
	} else {
		prev_en = ppc_is_sourcing_vbus(port);
		/* Disable VBUS via PPC. */
		ppc_vbus_source_enable(port, 0);
	}

	/* Enable discharge if we were previously sourcing 5V */
	if (prev_en)
		pd_set_vbus_discharge(port, 1);

	if (port == USB_PD_PORT_HOST) {
		int mv;
		int ma;
		int unused_mv;

		/*
		 * Because VBUS on C0 is turned on/off via the PPC, the
		 * voltage from the mp4245 does not need to be turned off, or
		 * set to 0V. Instead, reset VBUS voltage to default value
		 * (fixed 5V SRC_CAP) so VBUS is ready to be applied at the next
		 * attached.src condition.
		 */
		pd_extract_pdo_power(pd_src_host_pdo[0], &ma, &mv,
				     &unused_mv);
		mp4245_set_voltage_out(mv);
		/* Ensure voltage is back to 5V */
		pd_transition_voltage(1);
	}
}

int pd_set_power_supply_ready(int port)
{
	int rv;

	/*
	 * Note: For host port, the mp4245 output voltage is set for 5V by
	 * default and each time VBUS is turned off. VOUT from the mp4245 is
	 * left enabled as there is a switch (either PPC or discrete) to turn
	 * VBUS on/off on the wire.
	 */
	if (IS_ENABLED(BOARD_C1_NO_PPC) && port)
		rv = c1_ps8805_vbus_source_enable(port, 1);
	else
		rv = ppc_vbus_source_enable(port, 1);

	if (rv)
		return rv;

	return EC_SUCCESS;
}

void pd_transition_voltage(int idx)
{
	int port = TASK_ID_TO_PD_PORT(task_get_current());
	int mv;
	int target_mv;
	int mv_average = 0;
	int ma;
	int vbus_hi;
	int vbus_lo;
	int i;
	int mv_buffer[MP4245_VOLTAGE_WINDOW];

	/* Only C0 can provide more than 5V */
	if (port != USB_PD_PORT_HOST)
		return;

	/*
	 * Set the VBUS output voltage and current limit to the values specified
	 * by the PDO requested by sink. Note that USB PD uses idx = 1 for 1st
	 * PDO of SRC_CAP which must always be 5V fixed supply.
	 */
	pd_extract_pdo_power(pd_src_host_pdo[idx - 1], &ma, &target_mv,
			     &mv);

	/* Initialize sample delay buffer */
	for (i = 0; i < MP4245_VOLTAGE_WINDOW; i++)
		mv_buffer[i] = 0;

	/* Set VBUS level to value specified in the requested PDO */
	mp4245_set_voltage_out(target_mv);
	/* Wait for vbus to be within ~5% of its target value */
	vbus_hi = target_mv + (target_mv >> 4);
	vbus_lo = target_mv - (target_mv >> 4);

	for (i = 0; i < 20; i++) {
		/* Read current sample */
		mv = 0;
		mp3245_get_vbus(&mv, &ma);
		/* Add new sample to cicrcular delay buffer */
		mv_buffer[i & MP4245_VOLTAGE_WINDOW_MASK] = mv;
		/*
		 * Don't compute average until sample delay buffer is
		 * full.
		 */
		if (i >= (MP4245_VOLTAGE_WINDOW_MASK)) {
			int sum = 0;
			int j;

			/* Sum the voltage samples */
			for (j = 0; j < MP4245_VOLTAGE_WINDOW; j++)
				sum += mv_buffer[j];
			/* Add rounding */
			sum += MP4245_VOLTAGE_WINDOW / 2;
			mv_average = sum / MP4245_VOLTAGE_WINDOW;
			/*
			 * Check if average is within the target
			 * voltage range.
			 */
			if ((mv_average >= vbus_lo) &&
			    (mv_average <= vbus_hi)) {
				CPRINTS("usbc[%d]: VBUS to %d mV in %d steps",
					port, target_mv, i);
				return;
			}
		}

		/*
		 * The voltage ramp from 5V to 20V requires ~30
		 * msec. The max loop count and this sleep time gives plenty
		 * of time for this change.
		 */
		msleep(2);
	}

	CPRINTS("usbc[%d]: Vbus transition timeout: target = %d, measure = %d",
		port, target_mv, mv_average);
}

int pd_snk_is_vbus_provided(int port)
{
	if (IS_ENABLED(BOARD_C1_NO_PPC) && port)
		return c1_ps8805_is_vbus_present(port);
	else
		return ppc_is_vbus_present(port);
}

__override bool pd_check_vbus_level(int port, enum vbus_level level)
{
	if (level == VBUS_PRESENT)
		return pd_snk_is_vbus_provided(port);
	else
		return !pd_snk_is_vbus_provided(port);
}

int board_vbus_source_enabled(int port)
{
	if (IS_ENABLED(BOARD_C1_NO_PPC) && port)
		return c1_ps8805_is_sourcing_vbus(port);
	else
		return ppc_is_sourcing_vbus(port);
}

void pd_set_input_current_limit(int port, uint32_t max_ma,
				uint32_t supply_voltage)
{

}

int pd_check_data_swap(int port,
	enum pd_data_role data_role)
{
	int swap = 0;

	if (port == 0)
		swap = (data_role == PD_ROLE_DFP);
	else if (port == 1)
		swap = (data_role == PD_ROLE_UFP);

	return swap;
}

int pd_check_power_swap(int port)
{

	if (pd_get_power_role(port) == PD_ROLE_SINK)
		return 1;

	return 0;
}

#ifdef BOARD_C1_1A5_LIMIT
__override int typec_get_default_current_limit_rp(int port)
{
	int rp = TYPEC_RP_USB;

	if (port == USB_PD_PORT_HOST)
		rp = TYPEC_RP_3A0;
	else if (port == USB_PD_PORT_DP)
		rp = TYPEC_RP_1A5;

	return rp;
}
#endif

static void usb_tc_connect(void)
{
	int port = TASK_ID_TO_PD_PORT(task_get_current());

	/* Clear data role swap attempt counter at each usbc attach */
	pd_dr_swap_attempt_count[port] = 0;

	/*
	 * The EC needs to indicate to the USB hub when the host port is
	 * attached so that the USB-EP can be properly enumerated. GPIO_BPWR_DET
	 * is used for this purpose.
	 */
	if (port == USB_PD_PORT_HOST)
		gpio_set_level(GPIO_BPWR_DET, 1);
}
DECLARE_HOOK(HOOK_USB_PD_CONNECT, usb_tc_connect, HOOK_PRIO_DEFAULT);

static void usb_tc_disconnect(void)
{
	int port = TASK_ID_TO_PD_PORT(task_get_current());

	/* Only the host port disconnect is relevant */
	if (port == USB_PD_PORT_HOST)
		gpio_set_level(GPIO_BPWR_DET, 0);
}
DECLARE_HOOK(HOOK_USB_PD_DISCONNECT, usb_tc_disconnect, HOOK_PRIO_DEFAULT);

__override bool pd_can_charge_from_device(int port, const int pdo_cnt,
				      const uint32_t *pdos)
{
	/*
	 * This function is called to determine if this port can be charged by
	 * the port partner. We always want to be a power role source, so always
	 * return false.
	 */

	return false;
}

static int vdm_is_dp_enabled(int port)
{
	mux_state_t mux_state = usb_mux_get(port);

	return !!(mux_state & USB_PD_MUX_DP_ENABLED);
}

/* ----------------- Vendor Defined Messages ------------------ */

const uint32_t vdo_idh = VDO_IDH(0, /* data caps as USB host */
				 1, /* data caps as USB device */
				 IDH_PTYPE_HUB, /* UFP product type usbpd hub */
				 1, /* supports alt modes */
				 USB_VID_GOOGLE);

static const uint32_t vdo_idh_rev30 = VDO_IDH_REV30(
				 0, /* Data caps as USB host     */
				 1, /* Data caps as USB device   */
				 IDH_PTYPE_HUB,
				 1, /* Supports alt modes */
				 IDH_PTYPE_DFP_UNDEFINED,
				 USB_TYPEC_RECEPTACLE,
				 USB_VID_GOOGLE);

const uint32_t vdo_product = VDO_PRODUCT(CONFIG_USB_PID, CONFIG_USB_BCD_DEV);

static const uint32_t vdo_ufp1 = VDO_UFP1(
				   (VDO_UFP1_CAPABILITY_USB20
				   | VDO_UFP1_CAPABILITY_USB32),
				   USB_TYPEC_RECEPTACLE,
				   VDO_UFP1_ALT_MODE_RECONFIGURE,
				   USB_R30_SS_U32_U40_GEN2);

static int svdm_response_identity(int port, uint32_t *payload)
{
	int vdo_count;

	/* Verify that SVID is PD SID */
	if (PD_VDO_VID(payload[0]) != USB_SID_PD) {
		return 0;
	}

	/* Cstat and Product VDOs don't depend on spec revision */
	payload[VDO_INDEX_CSTAT] = VDO_CSTAT(0);
	payload[VDO_INDEX_PRODUCT] = vdo_product;

	if (pd_get_rev(port, TCPCI_MSG_SOP) == PD_REV30) {
		/* PD Revision 3.0 */
		payload[VDO_INDEX_IDH] = vdo_idh_rev30;
		payload[VDO_INDEX_PTYPE_UFP1_VDO] = vdo_ufp1;
		vdo_count = VDO_INDEX_PTYPE_UFP1_VDO;
	} else {
		payload[VDO_INDEX_IDH] = vdo_idh;
		vdo_count = VDO_INDEX_PRODUCT;
	}

	/* Adjust VDO count for VDM header */
	return vdo_count + 1;
}

static int svdm_response_svids(int port, uint32_t *payload)
{
	/* Verify that SVID is PD SID */
	if (PD_VDO_VID(payload[0]) != USB_SID_PD) {
		return 0;
	}

	payload[1] = USB_SID_DISPLAYPORT << 16;
	/* number of data objects VDO header + 1 SVID for DP */
	return 2;
}

#define OPOS_DP 1

const uint32_t vdo_dp_modes[1] =  {
	VDO_MODE_DP(/* Must support C and E. D is required for 2 lanes */
		    MODE_DP_PIN_C | MODE_DP_PIN_D | MODE_DP_PIN_E,
		    0, /* DFP pin cfg supported */
		    0,		   /* usb2.0 signalling in AMode may be req */
		    CABLE_RECEPTACLE,	   /* its a receptacle */
		    MODE_DP_V13,   /* DPv1.3 Support, no Gen2 */
		    MODE_DP_SNK)   /* Its a sink only */
};

static int svdm_response_modes(int port, uint32_t *payload)
{
	if (PD_VDO_VID(payload[0]) == USB_SID_DISPLAYPORT) {
		memcpy(payload + 1, vdo_dp_modes, sizeof(vdo_dp_modes));
		return ARRAY_SIZE(vdo_dp_modes) + 1;
	} else {
		return 0; /* nak */
	}
}

static int amode_dp_status(int port, uint32_t *payload)
{
	int opos = PD_VDO_OPOS(payload[0]);
	int hpd = gpio_get_level(GPIO_DP_HPD);
	int mf = dock_get_mf_preference();

	if (opos != OPOS_DP)
		return 0; /* nak */

	payload[1] = VDO_DP_STATUS(0,		/* IRQ_HPD */
				   (hpd == 1),	/* HPD_HI|LOW */
				   0,		/* request exit DP */
				   0,		/* request exit USB */
				   mf,		/* MF pref */
				   vdm_is_dp_enabled(port),
				   0,		/* power low */
				   0x2);
	return 2;
}

static void svdm_configure_demux(int port, int enable, int mf)
{
	mux_state_t demux = usb_mux_get(port);

	if (enable) {
		demux |= USB_PD_MUX_DP_ENABLED;
		/* 4 lane mode if MF is not preferred */
		if (!mf)
			demux &= ~USB_PD_MUX_USB_ENABLED;
		/*
		 * Make sure the MST_LANE_CONTROL gpio is set to match the DP
		 * pin configuration selected by the host. Note that the mf
		 * passed into this function reflects the pin configuration
		 * selected by the host and not the user mf preference which is
		 * stored in bit 0 of CBI fw_config.
		 */
		baseboard_set_mst_lane_control(mf);
		CPRINTS("DP[%d]: DFP-D selected pin config %s",
			port, mf ? "D" : "C");
	} else {
		demux &= ~USB_PD_MUX_DP_ENABLED;
		demux |= USB_PD_MUX_USB_ENABLED;
	}

	/* Configure demux for 2/4 lane DP and USB3 configuration */
	usb_mux_set(port, demux, USB_SWITCH_CONNECT, pd_get_polarity(port));
}

static int amode_dp_config(int port, uint32_t *payload)
{
	uint32_t dp_config = payload[1];
	int mf;

	/*
	 * Check pin assignment selected by DFP_D to determine if 2 lane or 4
	 * lane DP ALT-MODe is required. (note PIN_C is for 4 lane and PIN_D is
	 * for 2 lane mode).
	 */
	mf = ((dp_config >> 8) & 0xff) == MODE_DP_PIN_D ? 1 : 0;
	/* Configure demux for DP mode */
	svdm_configure_demux(port, 1, mf);
	/* Notify hpd->pd conv that a DP_CONFIG message has been received */
	pd_ufp_enable_hpd_send(port);

	return 1;
}

static int svdm_enter_mode(int port, uint32_t *payload)
{
	int rv = 0; /* will generate a NAK */

	/* SID & mode request is valid */
	if ((PD_VDO_VID(payload[0]) == USB_SID_DISPLAYPORT) &&
	    (PD_VDO_OPOS(payload[0]) == OPOS_DP)) {

		/* Store valid object position to indicate mode is active */
		pd_ufp_set_dp_opos(port, OPOS_DP);

		/* Entering ALT-DP mode, enable DP connection in demux */
		usb_pd_hpd_converter_enable(1);

		/* ACK response has 1 VDO */
		rv = 1;
	}

	CPRINTS("svdm_enter[%d]: svid = %x, ret = %d", port,
		PD_VDO_VID(payload[0]), rv);

	return rv;
}

static int svdm_exit_mode(int port, uint32_t *payload)
{
	int opos = pd_ufp_get_dp_opos(port);

	if ((PD_VDO_VID(payload[0]) == USB_SID_DISPLAYPORT) &&
	    (opos == OPOS_DP)) {
		/* Clear mode active object position */
		pd_ufp_set_dp_opos(port, 0);
		/* Configure demux to disable DP mode */
		svdm_configure_demux(port, 0, 0);
		usb_pd_hpd_converter_enable(0);

		return 1;
	} else {
		CPRINTF("Unknown exit mode req:0x%08x\n", payload[0]);
		return 0;
	}
}

static struct amode_fx dp_fx = {
	.status = &amode_dp_status,
	.config = &amode_dp_config,
};

const struct svdm_response svdm_rsp = {
	.identity = &svdm_response_identity,
	.svids = &svdm_response_svids,
	.modes = &svdm_response_modes,
	.enter_mode = &svdm_enter_mode,
	.amode = &dp_fx,
	.exit_mode = &svdm_exit_mode,
};

int pd_custom_vdm(int port, int cnt, uint32_t *payload,
		  uint32_t **rpayload)
{
	/* We don't support, so ignore this message */
	return 0;
}
