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

/* Poppy/Soraka base detection code */

#include "acpi.h"
#include "adc.h"
#include "adc_chip.h"
#include "board.h"
#include "chipset.h"
#include "common.h"
#include "console.h"
#include "gpio.h"
#include "hooks.h"
#include "host_command.h"
#include "system.h"
#include "tablet_mode.h"
#include "timer.h"
#include "util.h"

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

/* Base detection and debouncing */
#define BASE_DETECT_DEBOUNCE_US (20 * MSEC)

/*
 * If the base status is unclear (i.e. not within expected ranges, read
 * the ADC value again every 500ms.
 */
#define BASE_DETECT_RETRY_US (500 * MSEC)

/*
 * rev0: Lid has 100K pull-up, base has 5.1K pull-down, so the ADC
 * value should be around 5.1/(100+5.1)*3300 = 160.
 * >=rev1: Lid has 604K pull-up, base has 30.1K pull-down, so the
 * ADC value should be around 30.1/(604+30.1)*3300 = 156
 *
 * We add a significant marging on the maximum value, due to noise on the line,
 * especially when PWM is active. See b/64193554 for details.
 */
#define BASE_DETECT_MIN_MV 120
#define BASE_DETECT_MAX_MV 300

/*
 * When the base is connected in reverse, it presents a 100K pull-down,
 * so the ADC value should be around 100/(604+100)*3300 = 469
 *
 * TODO(b:64370797): Do something with these values.
 */
#define BASE_DETECT_REVERSE_MIN_MV 450
#define BASE_DETECT_REVERSE_MAX_MV 500

/* Minimum ADC value to indicate base is disconnected for sure */
#define BASE_DETECT_DISCONNECT_MIN_MV 1500

/*
 * Base EC pulses detection pin for 500 us to signal out of band USB wake (that
 * can be used to wake system from deep S3).
 */
#define BASE_DETECT_PULSE_MIN_US 400
#define BASE_DETECT_PULSE_MAX_US 650

static uint64_t base_detect_debounce_time;

static void base_detect_deferred(void);
DECLARE_DEFERRED(base_detect_deferred);

enum base_status {
	BASE_UNKNOWN = 0,
	BASE_DISCONNECTED = 1,
	BASE_CONNECTED = 2,
	BASE_CONNECTED_REVERSE = 3,
};

static enum base_status current_base_status;

/*
 * This function is called whenever there is a change in the base detect
 * status. Actions taken include:
 * 1. Change in power to base
 * 2. Indicate mode change to host.
 * 3. Indicate tablet mode to host. Current assumption is that if base is
 * disconnected then the system is in tablet mode, else if the base is
 * connected, then the system is not in tablet mode.
 */
static void base_detect_change(enum base_status status)
{
	int connected = (status == BASE_CONNECTED);

	if (current_base_status == status)
		return;

	CPRINTS("Base %sconnected", connected ? "" : "not ");
	gpio_set_level(GPIO_PP3300_DX_BASE, connected);
	tablet_set_mode(!connected);
	current_base_status = status;

	if (connected)
		acpi_dptf_set_profile_num(DPTF_PROFILE_BASE_ATTACHED);
	else
		acpi_dptf_set_profile_num(DPTF_PROFILE_BASE_DETACHED);

}

/* Measure detection pin pulse duration (used to wake AP from deep S3). */
static uint64_t pulse_start;
static uint32_t pulse_width;

static void print_base_detect_value(int v, int tmp_pulse_width)
{
	CPRINTS("%s = %d (pulse %d)", adc_channels[ADC_BASE_DET].name,
			v, tmp_pulse_width);
}

static void base_detect_deferred(void)
{
	uint64_t time_now = get_time().val;
	int v;
	uint32_t tmp_pulse_width = pulse_width;
	static int reverse_debounce = 1;

	if (base_detect_debounce_time > time_now) {
		hook_call_deferred(&base_detect_deferred_data,
				   base_detect_debounce_time - time_now);
		return;
	}

	v = adc_read_channel(ADC_BASE_DET);
	if (v == ADC_READ_ERROR)
		return;

	print_base_detect_value(v, tmp_pulse_width);

	if (v >= BASE_DETECT_REVERSE_MIN_MV &&
	    v <= BASE_DETECT_REVERSE_MAX_MV) {
		/*
		 * If we are unlucky when we sample the ADC, we may think that
		 * the base is connected in reverse, while this may just be a
		 * transient. Force debouncing a little longer in that case.
		 */
		if (current_base_status == BASE_CONNECTED_REVERSE)
			return;

		if (reverse_debounce == 0) {
			base_detect_change(BASE_CONNECTED_REVERSE);
			return;
		}

		reverse_debounce = 0;
		hook_call_deferred(&base_detect_deferred_data,
				   BASE_DETECT_DEBOUNCE_US);
		return;
	}
	/* Reset reverse debounce */
	reverse_debounce = 1;

	if (v >= BASE_DETECT_MIN_MV && v <= BASE_DETECT_MAX_MV) {
		if (current_base_status != BASE_CONNECTED) {
			base_detect_change(BASE_CONNECTED);
		} else if (tmp_pulse_width >= BASE_DETECT_PULSE_MIN_US &&
			   tmp_pulse_width <= BASE_DETECT_PULSE_MAX_US) {
			CPRINTS("Sending event to AP");
			host_set_single_event(EC_HOST_EVENT_KEY_PRESSED);
		}
		return;
	}

	if (v >= BASE_DETECT_DISCONNECT_MIN_MV) {
		base_detect_change(BASE_DISCONNECTED);
		return;
	}

	/* Unclear base status, schedule again in a while. */
	hook_call_deferred(&base_detect_deferred_data, BASE_DETECT_RETRY_US);
}

static inline int detect_pin_connected(enum gpio_signal det_pin)
{
	return gpio_get_level(det_pin) == 0;
}

void base_detect_interrupt(enum gpio_signal signal)
{
	uint64_t time_now = get_time().val;

	if (base_detect_debounce_time <= time_now) {
		/*
		 * Detect and measure detection pin pulse, when base is
		 * connected. Only a single pulse is measured over a debounce
		 * period. If no pulse, or multiple pulses are detected,
		 * pulse_width is set to 0.
		 */
		if (current_base_status == BASE_CONNECTED &&
		    !detect_pin_connected(signal)) {
			pulse_start = time_now;
		} else {
			pulse_start = 0;
		}
		pulse_width = 0;

		hook_call_deferred(&base_detect_deferred_data,
				   BASE_DETECT_DEBOUNCE_US);
	} else {
		if (current_base_status == BASE_CONNECTED &&
		    detect_pin_connected(signal) && !pulse_width &&
		    pulse_start) {
			/* First pulse within period. */
			pulse_width = time_now - pulse_start;
		} else {
			pulse_start = 0;
			pulse_width = 0;
		}
	}

	base_detect_debounce_time = time_now + BASE_DETECT_DEBOUNCE_US;
}

static void base_enable(void)
{
	/* Enable base detection interrupt. */
	base_detect_debounce_time = get_time().val;
	hook_call_deferred(&base_detect_deferred_data, 0);
	gpio_enable_interrupt(GPIO_BASE_DET_A);
}
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, base_enable, HOOK_PRIO_DEFAULT);

static void base_disable(void)
{
	/* Disable base detection interrupt and disable power to base. */
	gpio_disable_interrupt(GPIO_BASE_DET_A);
	base_detect_change(BASE_DISCONNECTED);
}
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, base_disable, HOOK_PRIO_DEFAULT);

static void base_init(void)
{
	/*
	 * If we jumped to this image and chipset is already in S0, enable
	 * base.
	 */
	if (system_jumped_late() && chipset_in_state(CHIPSET_STATE_ON))
		base_enable();
}
DECLARE_HOOK(HOOK_INIT, base_init, HOOK_PRIO_DEFAULT+1);

void base_force_state(int state)
{
	if (state == 1) {
		gpio_disable_interrupt(GPIO_BASE_DET_A);
		base_detect_change(BASE_CONNECTED);
		CPRINTS("BD forced connected");
	} else if (state == 0) {
		gpio_disable_interrupt(GPIO_BASE_DET_A);
		base_detect_change(BASE_DISCONNECTED);
		CPRINTS("BD forced disconnected");
	} else {
		base_enable();
		CPRINTS("BD forced reset");
	}
}
