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

/* PWM LED control to conform to Chrome OS LED behaviour specification. */

/*
 * This assumes that a single logical LED is shared between both power and
 * charging/battery status.  If multiple logical LEDs are present, they all
 * follow the same patterns.
 */

#include "battery.h"
#include "charge_manager.h"
#include "charge_state.h"
#include "chipset.h"
#include "common.h"
#include "console.h"
#include "ec_commands.h"
#include "hooks.h"
#include "led_common.h"
#include "led_pwm.h"
#include "pwm.h"
#include "timer.h"
#include "util.h"

/* Battery percentage thresholds to blink at different rates. */
#define CRITICAL_LOW_BATTERY_PERCENTAGE 3
#define LOW_BATTERY_PERCENTAGE 10

#define PULSE_TICK (250 * MSEC)

static uint8_t led_is_pulsing;

static int get_led_id_color(enum pwm_led_id id, int color)
{
#ifdef CONFIG_LED_PWM_ACTIVE_CHARGE_PORT_ONLY
	int active_chg_port = charge_manager_get_active_charge_port();

	/* We should always be able to turn off a LED. */
	if (color == -1)
		return -1;

	if (led_is_pulsing)
		return color;

	/* The inactive charge port LEDs should be off. */
	if ((int)id != active_chg_port)
		return -1;
#endif /* CONFIG_LED_PWM_ACTIVE_CHARGE_PORT_ONLY */
	return color;
}

void set_pwm_led_color(enum pwm_led_id id, int color)
{
	struct pwm_led duty = { 0 };

	if ((id >= CONFIG_LED_PWM_COUNT) || (id < 0) ||
	    (color >= EC_LED_COLOR_COUNT) || (color < -1))
		return;

	if (color != -1) {
		duty.ch0 = led_color_map[color].ch0;
		duty.ch1 = led_color_map[color].ch1;
		duty.ch2 = led_color_map[color].ch2;
	}

	if (pwm_leds[id].ch0 != PWM_LED_NO_CHANNEL)
		pwm_set_duty(pwm_leds[id].ch0, duty.ch0);
	if (pwm_leds[id].ch1 != PWM_LED_NO_CHANNEL)
		pwm_set_duty(pwm_leds[id].ch1, duty.ch1);
	if (pwm_leds[id].ch2 != PWM_LED_NO_CHANNEL)
		pwm_set_duty(pwm_leds[id].ch2, duty.ch2);
}

static void set_led_color(int color)
{
	/*
	 *  We must check if auto control is enabled since the LEDs may be
	 *  controlled from the AP at anytime.
	 */
	if ((led_auto_control_is_enabled(EC_LED_ID_POWER_LED)) ||
	    (led_auto_control_is_enabled(EC_LED_ID_LEFT_LED)))
		set_pwm_led_color(PWM_LED0, get_led_id_color(PWM_LED0, color));

#if CONFIG_LED_PWM_COUNT >= 2
	if (led_auto_control_is_enabled(EC_LED_ID_RIGHT_LED))
		set_pwm_led_color(PWM_LED1, get_led_id_color(PWM_LED1, color));
#endif /* CONFIG_LED_PWM_COUNT >= 2 */
}

static void set_pwm_led_enable(enum pwm_led_id id, int enable)
{
	if ((id >= CONFIG_LED_PWM_COUNT) || (id < 0))
		return;

	if (pwm_leds[id].ch0 != PWM_LED_NO_CHANNEL)
		pwm_enable(pwm_leds[id].ch0, enable);
	if (pwm_leds[id].ch1 != PWM_LED_NO_CHANNEL)
		pwm_enable(pwm_leds[id].ch1, enable);
	if (pwm_leds[id].ch2 != PWM_LED_NO_CHANNEL)
		pwm_enable(pwm_leds[id].ch2, enable);
}

static void init_leds_off(void)
{
	/* Turn off LEDs such that they are in a known state with zero duty. */
	set_led_color(-1);

	/* Enable pwm modules for each channels of LEDs */
	set_pwm_led_enable(PWM_LED0, 1);

#if CONFIG_LED_PWM_COUNT >= 2
	set_pwm_led_enable(PWM_LED1, 1);
#endif /* CONFIG_LED_PWM_COUNT >= 2 */
}
DECLARE_HOOK(HOOK_INIT, init_leds_off, HOOK_PRIO_INIT_PWM + 1);

static uint8_t pulse_period;
static uint8_t pulse_ontime;
static enum ec_led_colors pulse_color;
static void update_leds(void);
static void pulse_leds_deferred(void);
DECLARE_DEFERRED(pulse_leds_deferred);
static void pulse_leds_deferred(void)
{
	static uint8_t tick_count;

	if (!led_is_pulsing) {
		tick_count = 0;
		/*
		 * Since we're not pulsing anymore, turn the colors off in case
		 * we were in the "on" time.
		 */
		set_led_color(-1);
		/* Then show the desired state. */
		update_leds();
		return;
	}

	if (tick_count < pulse_ontime)
		set_led_color(pulse_color);
	else
		set_led_color(-1);

	tick_count = (tick_count + 1) % pulse_period;
	hook_call_deferred(&pulse_leds_deferred_data, PULSE_TICK);
}

static void pulse_leds(enum ec_led_colors color, int ontime, int period)
{
	pulse_color = color;
	pulse_ontime = ontime;
	pulse_period = period;
	led_is_pulsing = 1;
	pulse_leds_deferred();
}

static int show_charge_state(void)
{
	enum charge_state chg_st = charge_get_state();

	/*
	 * The colors listed below are the default, but can be overridden.
	 *
	 * Solid Amber == Charging
	 * Solid Green == Charging (near full)
	 * Fast Flash Red == Charging error or battery not present
	 */
	if (chg_st == PWR_STATE_CHARGE) {
		led_is_pulsing = 0;
		set_led_color(CONFIG_LED_PWM_CHARGE_COLOR);
	} else if (chg_st == PWR_STATE_CHARGE_NEAR_FULL ||
		   chg_st == PWR_STATE_DISCHARGE_FULL) {
		led_is_pulsing = 0;
		set_led_color(CONFIG_LED_PWM_NEAR_FULL_COLOR);
	} else if ((battery_is_present() != BP_YES) ||
		   (chg_st == PWR_STATE_ERROR)) {
		/* 500 ms period, 50% duty cycle. */
		pulse_leds(CONFIG_LED_PWM_CHARGE_ERROR_COLOR, 1, 2);
	} else {
		/* Discharging or not charging. */
#ifdef CONFIG_LED_PWM_CHARGE_STATE_ONLY
		/*
		 * If we only show the charge state, the only reason we
		 * would pulse the LEDs is if we had an error.  If it no longer
		 * exists, stop pulsing the LEDs.
		 */
		led_is_pulsing = 0;
#endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */
		return 0;
	}
	return 1;
}

#ifndef CONFIG_LED_PWM_CHARGE_STATE_ONLY
static int show_battery_state(void)
{
	int batt_percentage = charge_get_percent();

	/*
	 * The colors listed below are the default, but can be overridden.
	 *
	 * Fast Flash Amber == Critical Battery
	 * Slow Flash Amber == Low Battery
	 */
	if (batt_percentage < CRITICAL_LOW_BATTERY_PERCENTAGE) {
		/* Flash amber faster (1 second period, 50% duty cycle) */
		pulse_leds(CONFIG_LED_PWM_LOW_BATT_COLOR, 2, 4);
	} else if (batt_percentage < LOW_BATTERY_PERCENTAGE) {
		/* Flash amber (4 second period, 50% duty cycle) */
		pulse_leds(CONFIG_LED_PWM_LOW_BATT_COLOR, 8, 16);
	} else {
		/* Sufficient charge, no need to show anything for this. */
		return 0;
	}
	return 1;
}

static int show_chipset_state(void)
{
	/* Reflect the SoC state. */
	led_is_pulsing = 0;
	if (chipset_in_state(CHIPSET_STATE_ON)) {
		/* The LED must be on in the Active state. */
		set_led_color(CONFIG_LED_PWM_SOC_ON_COLOR);
	} else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND)) {
		/* The power LED must pulse in the suspend state. */
		pulse_leds(CONFIG_LED_PWM_SOC_SUSPEND_COLOR, 4, 16);
	} else {
		/* Chipset is off, no need to show anything for this. */
		return 0;
	}
	return 1;
}
#endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */

static void update_leds(void)
{
	/* Reflecting the charge state is the highest priority. */
	if (show_charge_state())
		return;

#ifndef CONFIG_LED_PWM_CHARGE_STATE_ONLY
	if (show_battery_state())
		return;

	if (show_chipset_state())
		return;
#endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */

	set_led_color(-1);
}
DECLARE_HOOK(HOOK_TICK, update_leds, HOOK_PRIO_DEFAULT);

#ifdef CONFIG_CMD_LEDTEST
int command_ledtest(int argc, char **argv)
{
	int enable;
	int pwm_led_id;
	int led_id;

	if (argc < 2)
		return EC_ERROR_PARAM_COUNT;

	pwm_led_id = atoi(argv[1]);
	if ((pwm_led_id < 0) || (pwm_led_id >= CONFIG_LED_PWM_COUNT))
		return EC_ERROR_PARAM1;
	led_id = supported_led_ids[pwm_led_id];

	if (argc == 2) {
		ccprintf("PWM LED %d: led_id=%d, auto_control=%d\n",
			 pwm_led_id, led_id,
			 led_auto_control_is_enabled(led_id) != 0);
		return EC_SUCCESS;
	}
	if (!parse_bool(argv[2], &enable))
		return EC_ERROR_PARAM2;

	/* Inverted because this drives auto control. */
	led_auto_control(led_id, !enable);

	if (argc == 4) {
		/* Set the color. */
		if (!strncmp(argv[3], "red", 3))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_RED);
		else if (!strncmp(argv[3], "green", 5))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_GREEN);
		else if (!strncmp(argv[3], "amber", 5))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_AMBER);
		else if (!strncmp(argv[3], "blue", 4))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_BLUE);
		else if (!strncmp(argv[3], "white", 5))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_WHITE);
		else if (!strncmp(argv[3], "yellow", 6))
			set_pwm_led_color(pwm_led_id, EC_LED_COLOR_YELLOW);
		else if (!strncmp(argv[3], "off", 3))
			set_pwm_led_color(pwm_led_id, -1);
		else
			return EC_ERROR_PARAM3;
	}

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(ledtest, command_ledtest,
			"<pwm led idx> <enable|disable> [color|off]", "");
#endif /* defined(CONFIG_CMD_LEDTEST) */
