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

/* Board specific gesture recognition */

#include "accelgyro.h"
#include "common.h"
#include "console.h"
#include "hooks.h"
#include "gesture.h"
#include "lid_switch.h"
#include "lightbar.h"
#include "motion_sense.h"
#include "task.h"
#include "timer.h"
#include "util.h"

/* Console output macros */
#define CPUTS(outstr) cputs(CC_GESTURE, outstr)
#define CPRINTS(format, args...) cprints(CC_GESTURE, format, ## args)
#define CPRINTF(format, args...) cprintf(CC_GESTURE, format, ## args)


/*
 * Double tap detection parameters
 * Double tap works by looking for two isolated Z-axis accelerometer impulses
 * preceded and followed by relatively calm periods of accelerometer motion.
 *
 * Define an outer and inner window. The inner window specifies how
 * long the tap impulse is expected to last. The outer window specifies the
 * period before the initial tap impluse and after the final tap impulse for
 * which to check for relatively calm periods. In between the two impulses
 * there is a minimum and maximum interstice time allowed.
 */
#define OUTER_WINDOW \
	(CONFIG_GESTURE_TAP_OUTER_WINDOW_T / \
	 CONFIG_GESTURE_SAMPLING_INTERVAL_MS)
#define INNER_WINDOW \
	(CONFIG_GESTURE_TAP_INNER_WINDOW_T / \
	 CONFIG_GESTURE_SAMPLING_INTERVAL_MS)
#define MIN_INTERSTICE \
	(CONFIG_GESTURE_TAP_MIN_INTERSTICE_T / \
	 CONFIG_GESTURE_SAMPLING_INTERVAL_MS)
#define MAX_INTERSTICE \
	(CONFIG_GESTURE_TAP_MAX_INTERSTICE_T / \
	 CONFIG_GESTURE_SAMPLING_INTERVAL_MS)
#define MAX_WINDOW OUTER_WINDOW

/* State machine states for detecting double tap */
enum tap_states {
	/* Look for calm before the storm */
	TAP_IDLE,
	/* Record first Z impulse */
	TAP_IMPULSE_1,

	/* Eye of the storm, expect Z motion to drop and then suddenly spike */
	TAP_INTERSTICE_DROP,
	TAP_INTERSTICE_RISE,

	/* Record second Z impulse */
	TAP_IMPULSE_2,
	/* Should be quiet after the storm */
	TAP_AFTER_EVENT
};

/* Tap sensor to use */
static struct motion_sensor_t *sensor =
&motion_sensors[CONFIG_GESTURE_TAP_SENSOR];

/* Tap state information */
static int history_z[MAX_WINDOW];  /* Changes in Z */
static int history_xy[MAX_WINDOW]; /* Changes in X and Y */
static int state, history_idx;
static int history_initialized, history_init_index;
static int tap_debug;

/* Tap detection flag */
static int tap_detection;

/*
 * TODO(crosbug.com/p/33102): Cleanup this function: break into multiple
 * functions and generalize so it can be used for other boards.
 */
static int gesture_tap_for_battery(void)
{
	/* Current and previous accel x,y,z */
	int x, y, z;
	static int x_p, y_p, z_p;

	/* Number of iterations in this state */
	static int state_cnt;

	/*
	 * Running sums of data diffs for inner and outer windows.
	 * Z data kept separate from X and Y data
	 */
	static int sum_z_inner, sum_z_outer, sum_xy_inner, sum_xy_outer;

	/* Total variation in each signal, normalized for window size */
	int delta_z_outer, delta_z_inner, delta_xy_outer, delta_xy_inner;

	/* Max variation seen during tap event and state cnts since max */
	static int delta_z_inner_max;
	static int cnts_since_max;

	/* Interstice Z motion thresholds */
	static int z_drop_thresh, z_rise_thresh;

	int history_idx_inner, state_p;
	int ret = 0;

	/* Get data */
	x = sensor->xyz[0];
	y = sensor->xyz[1];
	z = sensor->xyz[2];

	/*
	 * Calculate history of change in Z sensor and keeping
	 * running sums for the past.
	 */
	history_idx_inner = history_idx - INNER_WINDOW;
	if (history_idx_inner < 0)
		history_idx_inner += MAX_WINDOW;
	sum_z_inner -= history_z[history_idx_inner];
	sum_z_outer -= history_z[history_idx];
	history_z[history_idx] = ABS(z - z_p);
	sum_z_inner += history_z[history_idx];
	sum_z_outer += history_z[history_idx];

	/*
	 * Calculate history of change in X and Y sensors combined
	 * and keep a running sum of the change over the past.
	 */
	sum_xy_inner -= history_xy[history_idx_inner];
	sum_xy_outer -= history_xy[history_idx];
	history_xy[history_idx] = ABS(x - x_p) + ABS(y - y_p);
	sum_xy_inner += history_xy[history_idx];
	sum_xy_outer += history_xy[history_idx];

	/* Increment history index */
	history_idx = (history_idx == MAX_WINDOW - 1) ? 0 : (history_idx + 1);

	/* Store previous X, Y, Z data */
	x_p = x;
	y_p = y;
	z_p = z;

	/*
	 * Ignore data until we fill history buffer and wrap around. If
	 * detection is paused, history_init_index will store the index
	 * when paused, so that when re-started, we will wait until we
	 * wrap around again.
	 */
	if (history_idx == history_init_index)
		history_initialized = 1;
	if (!history_initialized)
		return 0;

	/*
	 * Normalize data based on window size and isolate outer and inner
	 * window data.
	 */
	delta_z_outer = (sum_z_outer - sum_z_inner) * 1000 /
			(OUTER_WINDOW - INNER_WINDOW);
	delta_z_inner = sum_z_inner * 1000 / INNER_WINDOW;
	delta_xy_outer = (sum_xy_outer - sum_xy_inner) * 1000 /
			(OUTER_WINDOW - INNER_WINDOW);
	delta_xy_inner = sum_xy_inner * 1000 / INNER_WINDOW;

	state_cnt++;
	state_p = state;

	switch (state) {
	case TAP_IDLE:
		/* Look for a sudden increase in Z movement */
		if (delta_z_inner > 30000 &&
		    delta_z_inner > 13 * delta_z_outer &&
		    delta_z_inner > 1 * delta_xy_inner) {
			delta_z_inner_max = delta_z_inner;
			state_cnt = 0;
			state = TAP_IMPULSE_1;
		}
		break;

	case TAP_IMPULSE_1:
		/* Find the peak inner window of Z movement */
		if (delta_z_inner > delta_z_inner_max) {
			delta_z_inner_max = delta_z_inner;
			cnts_since_max = state_cnt;
		}

		/* After inner window has passed, move to next state */
		if (state_cnt >= INNER_WINDOW) {
			state = TAP_INTERSTICE_DROP;
			z_drop_thresh = delta_z_inner_max / 12;
			z_rise_thresh = delta_z_inner_max / 3;
			state_cnt += INNER_WINDOW - cnts_since_max;
		}
		break;

	case TAP_INTERSTICE_DROP:
		/* Check for z motion to go back down first */
		if (delta_z_inner < z_drop_thresh)
			state = TAP_INTERSTICE_RISE;

		if (state_cnt > MAX_INTERSTICE)
			state = TAP_IDLE;

		break;

	case TAP_INTERSTICE_RISE:
		/* Then, check for z motion to go back up */
		if (delta_z_inner > z_rise_thresh) {
			if (state_cnt < MIN_INTERSTICE) {
				state = TAP_IDLE;
			} else {
				delta_z_inner_max = delta_z_inner;
				state_cnt = 0;
				state = TAP_IMPULSE_2;
			}
		}

		if (state_cnt > MAX_INTERSTICE)
			state = TAP_IDLE;
		break;

	case TAP_IMPULSE_2:
		/* Find the peak inner window of Z movement */
		if (delta_z_inner > delta_z_inner_max) {
			delta_z_inner_max = delta_z_inner;
			cnts_since_max = state_cnt;
		}

		/* After inner window has passed, move to next state */
		if (state_cnt >= INNER_WINDOW) {
			state = TAP_AFTER_EVENT;
			state_cnt += INNER_WINDOW - cnts_since_max;
		}

	case TAP_AFTER_EVENT:
		/* Check for small Z movement after the event */
		if (state_cnt < OUTER_WINDOW)
			break;

		if (2 * delta_z_inner_max > 3 * delta_z_outer &&
		    delta_z_outer > 1 * delta_xy_outer)
			ret = 1;

		state = TAP_IDLE;
		break;
	}

	/* On state transitions, print debug info */
	if (tap_debug &&
	    (state != state_p ||
	     (state_cnt % 10000 == 9999))) {
		/* make sure we don't divide by 0 */
		if (delta_z_outer == 0 || delta_xy_inner == 0)
			CPRINTS("tap st %d->%d, error div by 0",
				state_p, state);
		else
			CPRINTS("tap st %d->%d, st_cnt %-3d "
				"Z_in:Z_out %-3d, Z_in:XY_in %-3d "
				"dZ_in %-8.3d, dZ_in_max %-8.3d, "
				"dZ_out %-8.3d",
				state_p, state, state_cnt,
				delta_z_inner / delta_z_outer,
				delta_z_inner / delta_xy_inner,
				delta_z_inner,
				delta_z_inner_max,
				delta_z_outer);
	}

	return ret;
}

static void gesture_chipset_resume(void)
{
	/* disable tap detection */
	tap_detection = 0;
}
DECLARE_HOOK(HOOK_CHIPSET_RESUME, gesture_chipset_resume,
	     GESTURE_HOOK_PRIO);

static void gesture_chipset_suspend(void)
{
	/*
	 * Clear tap init and history initialized so that we have to
	 * record a whole new set of data, and enable tap detection
	 */
	history_initialized = 0;
	history_init_index = history_idx;
	state = TAP_IDLE;
	tap_detection = 1;
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, gesture_chipset_suspend,
	     GESTURE_HOOK_PRIO);

void gesture_calc(uint32_t *event)
{
	/* Only check for gesture if lid is closed and tap detection is on */
	if (!tap_detection || lid_is_open())
		return;

	if (gesture_tap_for_battery())
		*event |= TASK_EVENT_MOTION_ACTIVITY_INTERRUPT(
				MOTIONSENSE_ACTIVITY_DOUBLE_TAP);
}

/*****************************************************************************/
/* Console commands */
static int command_tap_info(int argc, char **argv)
{
	int val;

	ccprintf("tap:   %s\n", (tap_detection && !lid_is_open()) ?
					"on" : "off");

	if (argc > 1) {
		if (!parse_bool(argv[1], &val))
			return EC_ERROR_PARAM1;
		tap_debug = val;
	}

	ccprintf("debug: %s\n", tap_debug ? "on" : "off");
	ccprintf("odr:   %d\n", sensor->drv->get_data_rate(sensor));

	return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(tapinfo, command_tap_info,
			"debug on/off",
			"Print tap information");

