/* Copyright 2017 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.
 *
 * Power and battery LED control for Nami and its variants
 *
 * This is an event-driven LED control library. It does not use tasks or
 * periodical hooks (HOOK_TICK, HOOK_SECOND), thus, it's more resource
 * efficient.
 *
 * The library defines LED states and assigns an LED behavior to each state.
 * The state space consists of tuple of (charge state, power state).
 * In each LED state, a color and a pulse interval can be defined.
 *
 * Charging states are queried each time there is a state transition, thus, not
 * stored. We hook power state transitions (e.g. s0->s3) and save the
 * destination states (e.g. s3) in power_state.
 *
 * When system is suspending and AC is unplugged, there will be race condition
 * between a power state hook and a charge state hook but whichever is called
 * first or last the result will be the same.
 *
 * Currently, it supports two LEDs, called 'battery LED' and 'power LED'.
 * It assumes the battery LED is connected to a PWM pin and the power LED is
 * connected to a regular GPIO pin.
 */

#include "cros_board_info.h"
#include "charge_state.h"
#include "chipset.h"
#include "console.h"
#include "ec_commands.h"
#include "gpio.h"
#include "hooks.h"
#include "led_common.h"
#include "power.h"
#include "pwm.h"
#include "timer.h"
#include "util.h"

const enum ec_led_id supported_led_ids[] = {
		EC_LED_ID_BATTERY_LED, EC_LED_ID_POWER_LED};
const int supported_led_ids_count = ARRAY_SIZE(supported_led_ids);

enum led_color {
	LED_OFF = 0,
	LED_RED,
	LED_GREEN,
	LED_AMBER,
	LED_WHITE,
	LED_WARM_WHITE,
	LED_FACTORY,
	/* Number of colors, not a color itself */
	LED_COLOR_COUNT
};

/* Charging states of LED's interests */
enum led_charge_state {
	LED_STATE_DISCHARGE = 0,
	LED_STATE_CHARGE,
	LED_STATE_FULL,
	LED_CHARGE_STATE_COUNT,
};

/* Power states of LED's interests */
enum led_power_state {
	LED_STATE_S0 = 0,
	LED_STATE_S3,
	LED_STATE_S5,
	LED_POWER_STATE_COUNT,
};

/* Defines a LED pattern for a single state */
struct led_pattern {
	uint8_t color;
	/* Bit 0-5: Interval in 100 msec. 0=solid. Max is 3.2 sec.
	 * Bit 6: 1=alternate (on-off-off-off), 0=regular (on-off-on-off)
	 * Bit 7: 1=pulse, 0=blink */
	uint8_t pulse;
};

#define PULSE_NO		0
#define PULSE(interval)		(BIT(7) | (interval))
#define BLINK(interval) 	(interval)
#define ALTERNATE(interval)	(BIT(6) | (interval))
#define IS_PULSING(pulse)	((pulse) & 0x80)
#define IS_ALTERNATE(pulse)	((pulse) & 0x40)
#define PULSE_INTERVAL(pulse)	(((pulse) & 0x3f) * 100 * MSEC)

/* 40 msec for nice and smooth transition. */
#define LED_PULSE_TICK_US	(40 * MSEC)

typedef struct led_pattern led_patterns[LED_CHARGE_STATE_COUNT]
					[LED_POWER_STATE_COUNT];

/*
 * Nami/Vayne - One dual color LED:
 * Charging               Amber on (S0/S3/S5)
 * Charging (full)        White on (S0/S3/S5)
 * Discharge in S0        White on
 * Discharge in S3/S0ix   Pulsing (rising for 2 sec , falling for 2 sec)
 * Discharge in S5        Off
 * Battery Error          Amber on 1sec off 1sec
 * Factory mode	          White on 2sec, Amber on 2sec
 */
const static led_patterns battery_pattern_0 = {
	/* discharging: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE(10)}, {LED_OFF,  PULSE_NO}},
	/* charging: s0, s3, s5 */
	{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};

/*
 * Sona - Battery LED (dual color)
 */
const static led_patterns battery_pattern_1 = {
	/* discharging: s0, s3, s5 */
	{{LED_OFF,   PULSE_NO}, {LED_OFF,   PULSE_NO}, {LED_OFF,   PULSE_NO}},
	/* charging: s0, s3, s5 */
	{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};

/*
 * Pantheon - AC In/Battery LED(dual color):
 * Connected to AC power / Charged (100%)        White (solid on)
 * Connected to AC power / Charging(1% -99%)     Amber (solid on)
 * Not connected to AC power                     Off
 */
const static led_patterns battery_pattern_2 = {
	/* discharging: s0, s3, s5 */
	{{LED_OFF,   PULSE_NO}, {LED_OFF,   PULSE_NO}, {LED_OFF,   PULSE_NO}},
	/* charging: s0, s3, s5 */
	{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};

/*
 * Sona - Power LED (single color)
 */
const static led_patterns power_pattern_1 = {
	/* discharging: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
	/* charging: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
};

/*
 * Pantheon - Power LED
 * S0:        White on
 * S3/S0ix:   White 1 second on, 3 second off
 * S5:        Off
 */
const static led_patterns power_pattern_2 = {
	/* discharging: s0, s3, s5 */
	{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
	/* charging: s0, s3, s5 */
	{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
};

/*
 * Akali - battery LED
 * Charge:           Amber on (s0/s3/s5)
 * Full:             Blue on (s0/s3/s5)
 * Discharge in S0:  Blue on
 * Discharge in S3:  Amber on 1 sec off 3 sec
 * Discharge in S5:  Off
 * Battery Error:    Amber on 1sec off 1sec
 * Factory mode :    Blue on 2sec, Amber on 2sec
 */
const static led_patterns battery_pattern_3 = {
	/* discharging: s0, s3, s5 */
	{{LED_WHITE, 0}, {LED_AMBER, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
	/* charging: s0, s3, s5 */
	{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER,  PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE,  PULSE_NO}},
};

const static led_patterns battery_pattern_4 = {
	/* discharging: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
	/* charging: s0, s3, s5 */
	{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO},  {LED_AMBER, PULSE_NO}},
	/* full: s0, s3, s5 */
	{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO},  {LED_WHITE, PULSE_NO}},
};

/* Patterns for battery LED and power LED. Initialized at run-time. */
static led_patterns const *patterns[2];
/* Pattern for battery error. Only blinking battery LED is supported. */
static struct led_pattern battery_error = {LED_AMBER, BLINK(10)};
/* Pattern for low state of charge. Only battery LED is supported. */
static struct led_pattern low_battery = {LED_WHITE, BLINK(10)};
/* Pattern for factory mode. Blinking 2-color battery LED. */
static struct led_pattern battery_factory = {LED_FACTORY, BLINK(20)};
static int low_battery_soc;
static void led_charge_hook(void);
static enum led_power_state power_state;

static void led_init(void)
{
	switch (oem) {
	case PROJECT_NAMI:
	case PROJECT_VAYNE:
		patterns[0] = &battery_pattern_0;
		break;
	case PROJECT_SONA:
		if (model == MODEL_SYNDRA) {
			/* Syndra doesn't have power LED */
			patterns[0] = &battery_pattern_4;
		} else {
			patterns[0] = &battery_pattern_1;
			patterns[1] = &power_pattern_1;
		}
		battery_error.pulse = BLINK(5);
		low_battery_soc = 100;  /* 10.0% */
		break;
	case PROJECT_PANTHEON:
		patterns[0] = &battery_pattern_2;
		patterns[1] = &power_pattern_2;
		battery_error.color = LED_OFF;
		battery_error.pulse = 0;
		break;
	case PROJECT_AKALI:
		patterns[0] = &battery_pattern_3;
		break;
	default:
		break;
	}

	pwm_enable(PWM_CH_LED1, 1);
	pwm_enable(PWM_CH_LED2, 1);

	/* After sysjump, power_state is cleared. Thus, we need to actively
	 * retrieve it. */
	if (chipset_in_state(CHIPSET_STATE_ANY_OFF))
		power_state = LED_STATE_S5;
	else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND))
		power_state = LED_STATE_S3;
	else
		power_state = LED_STATE_S0;
}
DECLARE_HOOK(HOOK_INIT, led_init, HOOK_PRIO_DEFAULT);

static int set_color_battery(enum led_color color, int duty)
{
	int led1 = 0;
	int led2 = 0;

	if (duty < 0 || 100 < duty)
		return EC_ERROR_UNKNOWN;

	switch (color) {
	case LED_OFF:
		break;
	case LED_AMBER:
		led2 = 1;
		break;
	case LED_WHITE:
		led1 = 1;
		break;
	case LED_WARM_WHITE:
		led1 = 1;
		led2 = 1;
		break;
	case LED_FACTORY:
		break;
	default:
		return EC_ERROR_UNKNOWN;
	}

	if (color != LED_FACTORY) {
		pwm_set_duty(PWM_CH_LED1, led1 ? duty : 0);
		pwm_set_duty(PWM_CH_LED2, led2 ? duty : 0);
	} else {
		pwm_set_duty(PWM_CH_LED1, duty ? 100 : 0);
		pwm_set_duty(PWM_CH_LED2, duty ? 0 : 100);
	}

	return EC_SUCCESS;
}

static int set_color_power(enum led_color color, int duty)
{
	if (color == LED_OFF)
		duty = 0;
	gpio_set_level(GPIO_LED1, !duty /* Reversed logic */);
	return EC_SUCCESS;
}

static int set_color(enum ec_led_id id, enum led_color color, int duty)
{
	switch (id) {
	case EC_LED_ID_BATTERY_LED:
		return set_color_battery(color, duty);
	case EC_LED_ID_POWER_LED:
		return set_color_power(color, duty);
	default:
		return EC_ERROR_UNKNOWN;
	}
}

static struct {
	uint32_t interval;
	int duty_inc;
	enum led_color color;
	int duty;
	int alternate;
	uint8_t pulse;
} tick[2];

static void tick_battery(void);
DECLARE_DEFERRED(tick_battery);
static void tick_power(void);
DECLARE_DEFERRED(tick_power);
static void cancel_tick(enum ec_led_id id)
{
	if (id == EC_LED_ID_BATTERY_LED)
		hook_call_deferred(&tick_battery_data, -1);
	else
		hook_call_deferred(&tick_power_data, -1);
}

static int config_tick(enum ec_led_id id, const struct led_pattern *pattern)
{
	static const struct led_pattern *patterns[2];
	uint32_t stride;

	if (pattern == patterns[id])
		/* This pattern was already set */
		return -1;

	patterns[id] = pattern;

	if (!pattern->pulse) {
		/* This is a steady pattern. cancel the tick */
		cancel_tick(id);
		set_color(id, pattern->color, 100);
		return 1;
	}

	stride = PULSE_INTERVAL(pattern->pulse);
	if (IS_PULSING(pattern->pulse)) {
		tick[id].interval = LED_PULSE_TICK_US;
		tick[id].duty_inc = 100 / (stride / LED_PULSE_TICK_US);
	} else {
		tick[id].interval = stride;
		tick[id].duty_inc = 100;
	}
	tick[id].color = pattern->color;
	tick[id].duty = 0;
	tick[id].alternate = 0;
	tick[id].pulse = pattern->pulse;

	return 0;
}

/*
 * When pulsing, brightness is incremented by <duty_inc> every <interval> usec
 * from 0 to 100%. Then it's decremented from 100% to 0.
 */
static void pulse_led(enum ec_led_id id)
{
	if (tick[id].duty + tick[id].duty_inc > 100) {
		tick[id].duty_inc = tick[id].duty_inc * -1;
	} else if (tick[id].duty + tick[id].duty_inc < 0) {
		if (IS_ALTERNATE(tick[id].pulse)) {
			/* Falling phase landing. Flip the alternate flag. */
			tick[id].alternate = !tick[id].alternate;
			if (tick[id].alternate)
				return;
		}
		tick[id].duty_inc = tick[id].duty_inc * -1;
	}
	tick[id].duty += tick[id].duty_inc;
	set_color(id, tick[id].color, tick[id].duty);
}

static uint32_t tick_led(enum ec_led_id id)
{
	uint32_t elapsed;
	uint32_t start = get_time().le.lo;
	uint32_t next;

	if (led_auto_control_is_enabled(id))
		pulse_led(id);
	if (tick[id].alternate)
		/* Skip 2 phases (rising & falling) */
		next = PULSE_INTERVAL(tick[id].pulse) * 2;
	else
		next = tick[id].interval;
	elapsed = get_time().le.lo - start;
	return next > elapsed ? next - elapsed : 0;
}

static void tick_battery(void)
{
	hook_call_deferred(&tick_battery_data, tick_led(EC_LED_ID_BATTERY_LED));
}

static void tick_power(void)
{
	hook_call_deferred(&tick_power_data, tick_led(EC_LED_ID_POWER_LED));
}

static void start_tick(enum ec_led_id id, const struct led_pattern *pattern)
{
	if (config_tick(id, pattern))
		/*
		 * If this pattern is already active, ticking must have started
		 * already. So, we don't re-start ticking to prevent LED from
		 * blinking at every SOC change.
		 *
		 * If this pattern is static, we skip ticking as well.
		 */
		return;

	if (id == EC_LED_ID_BATTERY_LED)
		tick_battery();
	else
		tick_power();
}

static void led_alert(int enable)
{
	if (enable)
		start_tick(EC_LED_ID_BATTERY_LED, &battery_error);
	else
		led_charge_hook();
}

static void led_factory(int enable)
{
	if (enable)
		start_tick(EC_LED_ID_BATTERY_LED, &battery_factory);
	else
		led_charge_hook();
}

void config_led(enum ec_led_id id, enum led_charge_state charge)
{
	const led_patterns *pattern;

	pattern = patterns[id];
	if (!pattern)
		return;	/* This LED isn't present */

	start_tick(id, &(*pattern)[charge][power_state]);
}

void config_leds(enum led_charge_state charge)
{
	config_led(EC_LED_ID_BATTERY_LED, charge);
	config_led(EC_LED_ID_POWER_LED, charge);
}

static void call_handler(void)
{
	int soc;
	enum charge_state cs;

	if (!led_auto_control_is_enabled(EC_LED_ID_BATTERY_LED))
		return;

	cs = charge_get_state();
	soc = charge_get_display_charge();
	if (soc < 0)
		cs = PWR_STATE_ERROR;

	switch (cs) {
	case PWR_STATE_DISCHARGE:
	case PWR_STATE_DISCHARGE_FULL:
		if (soc < low_battery_soc)
			start_tick(EC_LED_ID_BATTERY_LED, &low_battery);
		else
			config_led(EC_LED_ID_BATTERY_LED, LED_STATE_DISCHARGE);
		config_led(EC_LED_ID_POWER_LED, LED_STATE_DISCHARGE);
		break;
	case PWR_STATE_CHARGE_NEAR_FULL:
	case PWR_STATE_CHARGE:
		if (soc >= 1000)
			config_leds(LED_STATE_FULL);
		else
			config_leds(LED_STATE_CHARGE);
		break;
	case PWR_STATE_ERROR:
		/* It doesn't matter what 'charge' state we pass because power
		 * LED (if it exists) is orthogonal to battery state. */
		config_led(EC_LED_ID_POWER_LED, 0);
		led_alert(1);
		break;
	case PWR_STATE_IDLE:
		/* External power connected in IDLE. This is also used to show
		 * factory mode when 'ectool chargecontrol idle' is run during
		 * factory process. */
		if (charge_get_flags() & CHARGE_FLAG_FORCE_IDLE)
			led_factory(1);
		break;
	default:
		;
	}
}

/* LED state transition handlers */
static void s0(void)
{
	power_state = LED_STATE_S0;
	call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_RESUME, s0, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, s0, HOOK_PRIO_DEFAULT);

static void s3(void)
{
	power_state = LED_STATE_S3;
	call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, s3, HOOK_PRIO_DEFAULT);

static void s5(void)
{
	power_state = LED_STATE_S5;
	call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, s5, HOOK_PRIO_DEFAULT);

static void led_charge_hook(void)
{
	call_handler();
}
DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE, led_charge_hook, HOOK_PRIO_DEFAULT);

static void print_config(enum ec_led_id id)
{
	ccprintf("ID:%d\n", id);
	ccprintf("  Color:%d\n", tick[id].color);
	ccprintf("  Duty:%d\n", tick[id].duty);
	ccprintf("  Duty Increment:%d\n", tick[id].duty_inc);
	ccprintf("  Interval:%d\n", tick[id].interval);
}

static int command_led(int argc, char **argv)
{
	enum ec_led_id id = EC_LED_ID_BATTERY_LED;
	static int alert = 0;
	static int factory;

	if (argc < 2)
		return EC_ERROR_PARAM_COUNT;

	if (!strcasecmp(argv[1], "debug")) {
		led_auto_control(id, !led_auto_control_is_enabled(id));
		ccprintf("o%s\n", led_auto_control_is_enabled(id) ? "ff" : "n");
	} else if (!strcasecmp(argv[1], "off")) {
		set_color(id, LED_OFF, 0);
	} else if (!strcasecmp(argv[1], "red")) {
		set_color(id, LED_RED, 100);
	} else if (!strcasecmp(argv[1], "white")) {
		set_color(id, LED_WHITE, 100);
	} else if (!strcasecmp(argv[1], "amber")) {
		set_color(id, LED_AMBER, 100);
	} else if (!strcasecmp(argv[1], "alert")) {
		alert = !alert;
		led_alert(alert);
	} else if (!strcasecmp(argv[1], "s0")) {
		s0();
	} else if (!strcasecmp(argv[1], "s3")) {
		s3();
	} else if (!strcasecmp(argv[1], "s5")) {
		s5();
	} else if (!strcasecmp(argv[1], "conf")) {
		print_config(id);
	} else if (!strcasecmp(argv[1], "factory")) {
		factory = !factory;
		led_factory(factory);
	} else {
		return EC_ERROR_PARAM1;
	}
	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(led, command_led,
			"[debug|red|green|amber|off|alert|s0|s3|s5|conf|factory]",
			"Turn on/off LED.");

void led_get_brightness_range(enum ec_led_id led_id, uint8_t *brightness_range)
{
	/*
	 * We return amber=100, white=100 regardless of OEM ID or led_id. This
	 * function is for ectool led command, which is used to test LED
	 * functionality.
	 */
	brightness_range[EC_LED_COLOR_AMBER] = 100;
	brightness_range[EC_LED_COLOR_WHITE] = 100;
}

int led_set_brightness(enum ec_led_id id, const uint8_t *brightness)
{
	if (brightness[EC_LED_COLOR_AMBER])
		return set_color(id, LED_AMBER, brightness[EC_LED_COLOR_AMBER]);
	else if (brightness[EC_LED_COLOR_WHITE])
		return set_color(id, LED_WHITE, brightness[EC_LED_COLOR_WHITE]);
	else
		return set_color(id, LED_OFF, 0);
}
