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

/* Honeybuns family-specific configuration */
#include "console.h"
#include "cros_board_info.h"
#include "driver/mp4245.h"
#include "gpio.h"
#include "hooks.h"
#include "i2c.h"
#include "usb_pd.h"
#include "system.h"
#include "task.h"
#include "timer.h"
#include "usbc_ppc.h"
#include "driver/tcpm/tcpm.h"
#include "util.h"

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

#define POWER_BUTTON_SHORT_USEC (300 * MSEC)
#define POWER_BUTTON_LONG_USEC (5000 * MSEC)
#define POWER_BUTTON_DEBOUNCE_USEC (30)

#define BUTTON_EVT_CHANGE   BIT(0)
#define BUTTON_EVT_INFO     BIT(1)

enum power {
	POWER_OFF,
	POWER_ON
};

enum button {
	BUTTON_RELEASE,
	BUTTON_PRESS,
	BUTTON_PRESS_POWER_ON,
	BUTTON_PRESS_SHORT,
	BUTTON_PRESS_LONG,
};

#define LED_ON_OFF_BIT BIT(0)
#define LED_COLOR_BIT BIT(2)
#define LED_FLASH_SEQ_LENGTH 8

enum led_color {
	GREEN,
	YELLOW,
	OFF,
};

static enum power dock_state;
#ifdef SECTION_IS_RW
static int button_level;
static int button_level_pending;
static int dock_mf;
static int led_count;
#endif

/******************************************************************************/

__maybe_unused static void board_power_sequence(int enable)
{
	int i;

	if (enable) {
		for(i = 0; i < board_power_seq_count; i++) {
			gpio_set_level(board_power_seq[i].signal,
				       board_power_seq[i].level);
			CPRINTS("power seq: rail = %d", i);
			if (board_power_seq[i].delay_ms)
				msleep(board_power_seq[i].delay_ms);
		}
	} else {
		for(i = board_power_seq_count - 1; i >= 0; i--) {
			gpio_set_level(board_power_seq[i].signal,
				       !board_power_seq[i].level);
			CPRINTS("sequence[%d]: level = %d", i,
				!board_power_seq[i].level);
		}
	}

	dock_state = enable;
	CPRINTS("board: Power rails %s", dock_state ? "on" : "off");
}

/******************************************************************************/
/* I2C port map configuration */
const struct i2c_port_t i2c_ports[] = {
	{"i2c1",  I2C_PORT_I2C1,  400, GPIO_EC_I2C1_SCL, GPIO_EC_I2C1_SDA},
	{"i2c3",  I2C_PORT_I2C3,  400, GPIO_EC_I2C3_SCL, GPIO_EC_I2C3_SDA},
};
const unsigned int i2c_ports_used = ARRAY_SIZE(i2c_ports);

#ifdef SECTION_IS_RW
static void baseboard_set_led(enum led_color color)
{
	/*
	 * TODO(b/164157329): The power button feature should be connected to a
	 * 2 color LED which is part of the button. Currently, the power button
	 * LED is a single color LED which is controlled by on the of the power
	 * rails. Using the status LED now to demonstrate the LED behavior
	 * associated with a power button press.
	 */
	CPRINTS("led: color = %d", color);

	/* Not all boards may have LEDs under EC control */
#if defined(GPIO_PWR_BUTTON_RED) && defined(GPIO_PWR_BUTTON_GREEN)
	if (color == OFF) {
		gpio_set_level(GPIO_PWR_BUTTON_RED, 1);
		gpio_set_level(GPIO_PWR_BUTTON_GREEN, 1);
	} else if (color == GREEN) {
		gpio_set_level(GPIO_PWR_BUTTON_RED, 1);
		gpio_set_level(GPIO_PWR_BUTTON_GREEN, 0);
	} else if (color == YELLOW) {
		gpio_set_level(GPIO_PWR_BUTTON_RED, 0);
		gpio_set_level(GPIO_PWR_BUTTON_GREEN, 0);
	}
#endif
}

static void baseboard_led_callback(void);
DECLARE_DEFERRED(baseboard_led_callback);

static void baseboard_led_callback(void)
{
	/*
	 * Flash LED on transition using a simple 3 bit counter. Bit 0 controls
	 * LED on/off and bit 2 controls which color to set during the on phase.
	 */
	int color = led_count & LED_COLOR_BIT ? dock_mf : dock_mf ^ 1;

	/*
	 * TODO(b/164157329): This function implements a simple flashing
	 * transition when the MF preference bit is changed via a long power
	 * button press sequence. This might need to move to the board function
	 * if not required/desired on all variants.
	 */

	if (led_count & LED_ON_OFF_BIT)
		baseboard_set_led(color);
	else
		baseboard_set_led(OFF);

	/* Flash sequence is 8 steps */
	if (++led_count < LED_FLASH_SEQ_LENGTH)
		hook_call_deferred(&baseboard_led_callback_data, 150 * MSEC);
}

static void baseboard_change_mf_led(void)
{
	led_count = 0;
	baseboard_led_callback();
}

void baseboard_set_mst_lane_control(int mf)
{
	/*
	 * The parameter mf reflects the desired lane control value. If the
	 * current value does not match the desired, then the MST hub must first
	 * be put into reset, so the MST hub will latch in the correct value
	 * when it's taken out of reset.
	 */
	if (mf != gpio_get_level(GPIO_MST_HUB_LANE_SWITCH)) {
		/* put MST into reset */
		gpio_set_level(GPIO_MST_RST_L, 0);
		msleep(1);
		gpio_set_level(GPIO_MST_HUB_LANE_SWITCH, mf);
		CPRINTS("MST: lane control = %s", mf ? "high" : "low");
		msleep(1);
		/* lane control is set, take MST out of reset */
		gpio_set_level(GPIO_MST_RST_L, 1);
	}
}

static void baseboard_enable_mp4245(void)
{
	int mv;
	int ma;

	mp4245_set_voltage_out(5000);
	mp4245_votlage_out_enable(1);
	msleep(MP4245_VOUT_5V_DELAY_MS);
	mp3245_get_vbus(&mv, &ma);
	CPRINTS("mp4245: vout @ %d mV enabled", mv);
}

#endif /* SECTION_IS_RW */

static void baseboard_init(void)
{
#ifdef SECTION_IS_RW
	uint32_t fw_config;
#endif

	/* Turn on power rails */
	board_power_sequence(1);
	CPRINTS("board: Power rails enabled");

#ifdef SECTION_IS_RW
	/* Force TC state machine to start in TC_ERROR_RECOVERY */
	system_clear_reset_flags(EC_RESET_FLAG_POWER_ON);
	/* Make certain SN5S330 PPC does full initialization */
	system_set_reset_flags(EC_RESET_FLAG_EFS);

	/*
	 * Dock multi function (mf) preference is stored in bit 0 of fw_config
	 * field of the CBI. If this value is programmed, then make sure the
	 * MST_LANE_CONTROL gpio matches the mf bit.
	 */
	if (cbi_get_fw_config(&fw_config)) {
		dock_mf = CBI_FW_MF_PREFERENCE(fw_config);
		baseboard_set_mst_lane_control(dock_mf);
	}

#ifdef GPIO_USBC_UF_ATTACHED_SRC
	/* Configure UF usbc ppc and check usbc state */
	baseboard_config_usbc_usb3_ppc();
#endif /* GPIO_USBC_UF_ATTACHED_SRC */

	/* Enable power button interrupt */
	gpio_enable_interrupt(GPIO_PWR_BTN);
	/* Set dock mf preference LED */
	baseboard_set_led(dock_mf);
	/* Setup VBUS to default value */
	baseboard_enable_mp4245();

#else
	/* Set up host port usbc to present Rd on CC lines */
	if(baseboard_usbc_init(USB_PD_PORT_HOST))
		CPRINTS("usbc: Failed to set up sink path");
	else
		CPRINTS("usbc: sink path configure success!");
#endif /* SECTION_IS_RW */
}
/*
 * Power sequencing must run before any other chip init is attempted, so run
 * power sequencing as soon as I2C bus is initialized.
 */
DECLARE_HOOK(HOOK_INIT, baseboard_init, HOOK_PRIO_INIT_I2C + 1);

#ifdef SECTION_IS_RW
static void baseboard_power_on(void)
{
	int port_max = board_get_usb_pd_port_count();
	int port;

	CPRINTS("pwrbtn: power on: mf = %d", dock_mf);
	/* Adjust system flags to full PPC init occurs */
	system_clear_reset_flags(EC_RESET_FLAG_POWER_ON);
	system_set_reset_flags(EC_RESET_FLAG_EFS);
	/* Enable power rails and release reset signals */
	board_power_sequence(1);
	/* Set VBUS to 5V and enable output from mp4245 */
	baseboard_enable_mp4245();
	/* Set dock mf preference LED */
	baseboard_set_led(dock_mf);
	/*
	 * Lane control (realtek MST) must be set prior to releasing MST
	 * reset.
	 */
	baseboard_set_mst_lane_control(dock_mf);
	/*
	 * When the power to the PPC is turned off, then back on, the PPC will
	 * default into dead battery mode. Dead battery resistors are disabled
	 * as part of the full PPC intializaiton sequence. This is required to
	 * force a detach event with port parter which can be attached as usbc
	 * source when honeybuns power rails are off.
	 */
	for (port = 0; port < port_max; port++) {
		ppc_init(port);
		msleep(1000);
		/* Inform TC state machine that it can resume */
		pd_set_suspend(port, 0);
	}
	/* Enable usbc interrupts */
	board_enable_usbc_interrupts();

#ifdef GPIO_USBC_UF_ATTACHED_SRC
	baseboard_config_usbc_usb3_ppc();
#endif
}

static void baseboard_power_off(void)
{
	int port_max = board_get_usb_pd_port_count();
	int port;

	CPRINTS("pwrbtn: power off");
	/* Put ports in TC suspend state */
	for (port = 0; port < port_max; port++)
		pd_set_suspend(port, 1);

	/* Disable ucpd peripheral (prevents interrupts) */
	tcpm_release(USB_PD_PORT_HOST);
	/* Disable PPC/TCPC interrupts */
	board_disable_usbc_interrupts();

#ifdef GPIO_USBC_UF_ATTACHED_SRC
	/* Disable PPC interrupts for PS8803 managed port */
	baseboard_usbc_usb3_enable_interrupts(0);
#endif
	/* Set dock power button/MF preference LED */
	baseboard_set_led(OFF);
	/* Go into power off state */
	board_power_sequence(0);
}

static void baseboard_toggle_mf(void)
{
	uint32_t fw_config;

	if (!cbi_get_fw_config(&fw_config)) {
		/* Update the user MF preference stored in CBI */
		fw_config ^= CBI_FW_MF_MASK;
		cbi_set_fw_config(fw_config);
		/* Update variable used to track user MF preference */
		dock_mf = CBI_FW_MF_PREFERENCE(fw_config);
		/* Flash led for visual indication of user MF change */
		baseboard_change_mf_led();

		/*
		 * Suspend, then release host port to force new MF setting to
		 * take effect.
		 */
		pd_set_suspend(USB_PD_PORT_HOST, 1);
		msleep(250);
		pd_set_suspend(USB_PD_PORT_HOST, 0);
	}
}

/*
 * Main task entry point for UCPD task
 */
void power_button_task(void *u)
{
	int timer_us = POWER_BUTTON_DEBOUNCE_USEC * 4;
	enum button state = BUTTON_RELEASE;
	uint32_t evt;

	/*
	 * Capture current button level in case it's being pressed when the dock
	 * is powered on. Note timer_us is initialized for debounce time to
	 * double check.
	 */
	button_level = gpio_get_level(GPIO_PWR_BTN);

	while (1) {
		evt = task_wait_event(timer_us);
		timer_us = -1;

		if (evt == BUTTON_EVT_INFO) {
			/* Only used for console command for debug */
			CPRINTS("pwrbtn: pwr = %d, state = %d, level = %d",
				dock_state, state, button_level);
			continue;
		}

		switch (state) {
		case BUTTON_RELEASE:
			/*
			 * Default wait state: Only need to check if the button
			 * is pressed and start the short press timer.
			 */
			if (evt & BUTTON_EVT_CHANGE && button_level ==
			    BUTTON_PRESSED_LEVEL) {
				state = BUTTON_PRESS;
				timer_us = (POWER_BUTTON_SHORT_USEC -
						 POWER_BUTTON_DEBOUNCE_USEC);
			}
			break;
		case BUTTON_PRESS:
			/*
			 * Validate short press by ensuring that button is still
			 * pressed after short press timer expires.
			 */
			if (evt & BUTTON_EVT_CHANGE &&
			    button_level == BUTTON_RELEASED_LEVEL) {
				state = BUTTON_RELEASE;
			} else {
				/* Start long press timer */
				timer_us = POWER_BUTTON_LONG_USEC -
					POWER_BUTTON_SHORT_USEC;
				/*
				 * If dock is currently off, then change to the
				 * power on state. If dock is already on, then
				 * advance to short press state.
				 */
				if (dock_state == POWER_OFF) {
					baseboard_power_on();
				        state = BUTTON_PRESS_POWER_ON;
				} else {
					state = BUTTON_PRESS_SHORT;
				}
			}
			break;
		case BUTTON_PRESS_POWER_ON:
			/*
			 * Short press recognized and dock was just powered
			 * on. If button is no longer pressed, then just return
			 * to the default state. Else, button is still pressed
			 * after long press timer has expired.
			 */
			if (evt & BUTTON_EVT_CHANGE &&
			    button_level == BUTTON_RELEASED_LEVEL) {
				state = BUTTON_RELEASE;
			} else {
				state = BUTTON_PRESS_LONG;
				baseboard_toggle_mf();
			}
			break;
		case BUTTON_PRESS_SHORT:
			/*
			 * Short press was recognized and dock power state was
			 * already on. If button is now released, then turn dock
			 * off.
			 */
			if (evt & BUTTON_EVT_CHANGE &&
			    button_level == BUTTON_RELEASED_LEVEL) {
				state = BUTTON_RELEASE;
				baseboard_power_off();
			} else {
				state = BUTTON_PRESS_LONG;
				baseboard_toggle_mf();
			}
			break;
		case BUTTON_PRESS_LONG:
			if (evt & BUTTON_EVT_CHANGE &&
			    button_level == BUTTON_RELEASED_LEVEL) {
				state = BUTTON_RELEASE;
			}
			break;
		}
	}
}

static void baseboard_power_button_debounce(void)
{
	int level = gpio_get_level(GPIO_PWR_BTN);

	/* Sanity check, level should be same after debounce interval */
	if (level != button_level_pending)
		return;

	button_level = level;
	task_set_event(TASK_ID_POWER_BUTTON, BUTTON_EVT_CHANGE);
}
DECLARE_DEFERRED(baseboard_power_button_debounce);

void baseboard_power_button_evt(int level)
{
	button_level_pending = level;

	hook_call_deferred(&baseboard_power_button_debounce_data,
			   POWER_BUTTON_DEBOUNCE_USEC);
}

static int command_pwr_btn(int argc, char **argv)
{

	if (argc == 1) {
		task_set_event(TASK_ID_POWER_BUTTON, BUTTON_EVT_INFO);
		return EC_SUCCESS;
	}

	if (!strcasecmp(argv[1], "on")) {
		baseboard_power_on();
	} else if (!strcasecmp(argv[1], "off")) {
		baseboard_power_off();
	} else if (!strcasecmp(argv[1], "mf")) {
		baseboard_toggle_mf();
	} else {
		return EC_ERROR_PARAM1;
	}

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(pwr_btn, command_pwr_btn,
			"<on|off|mf>",
			"Simulate Power Button Press");

#endif
