/*
 *
 * Copyright (C) 2017 Patrick Rudolph <siro@das-labor.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <libpayload-config.h>
#include <libpayload.h>

static int x_axis;
static int y_axis;
static int z_axis;
static u32 buttons;
static u8 is_intellimouse;
static u8 is_explorer_intellimouse;
static u8 initialized;
static unsigned char mouse_buf[4];
static unsigned char mouse_buf_idx;

static u8 mouse_cmd(unsigned char cmd)
{
	i8042_cmd(0xd4);

	i8042_write_data(cmd);

	return i8042_wait_read_aux() == 0xfa;
}

static u8 mouse_cmd_data(u8 cmd, u8 val)
{
	if (!mouse_cmd(cmd))
		return 0;

	return mouse_cmd(val);
}

/** Try to detect Microsoft Intelli mouse */
static u8 mouse_is_intellimouse(void)
{
	/* Silence mouse. */
	if (!mouse_cmd(0xf5))
		return 0;

	/* Set standard. */
	if (!mouse_cmd(0xf6))
		return 0;

	/* Magic sequence. */
	if (!mouse_cmd_data(0xf3, 0xc8))
		return 0;
	if (!mouse_cmd_data(0xf3, 0x64))
		return 0;
	if (!mouse_cmd_data(0xf3, 0x50))
		return 0;

	/* Get mouse id */
	if (!mouse_cmd(0xf2))
		return 0;

	if (i8042_wait_read_aux() != 0x03)
		return 0;

	return 1;
}

/** Try to detect Microsoft Explorer mouse */
static u8 mouse_is_intellimouse_explorer(void)
{
	/* Silence mouse. */
	if (!mouse_cmd(0xf5))
		return 0;

	/* Set standard. */
	if (!mouse_cmd(0xf6))
		return 0;

	/* Magic sequence. */
	if (!mouse_cmd_data(0xf3, 0xc8))
		return 0;
	if (!mouse_cmd_data(0xf3, 0xc8))
		return 0;
	if (!mouse_cmd_data(0xf3, 0x50))
		return 0;

	/* Get mouse id */
	if (!mouse_cmd(0xf2))
		return 0;

	if (i8042_wait_read_aux() != 4)
		return 0;

	return 1;
}

/** Decode temporary buffer
 * Sanity check to prevent out of order decode.
 * Decode PS/2 data.
 * Supported devices:
 *  Generic 3 button mouse
 *  Microsoft Intelli mouse
 *  Microsoft Explorer mouse
 */
static void mouse_decode(void)
{
	/* Buffer full check and sanity check */
	if (is_intellimouse) {
		if (mouse_buf_idx < 4)
			return;
		if ((mouse_buf[3] & 0x10) != (mouse_buf[3] & 0x08)) {
			mouse_buf_idx = 0;
			return;
		}
	} else if (is_explorer_intellimouse) {
		if (mouse_buf_idx < 4)
			return;
		if (mouse_buf[3] & 0xc0) {
			mouse_buf_idx = 0;
			return;
		}
	} else {
		if (mouse_buf_idx < 3)
			return;
	}

	/* Common protocol */
	x_axis += mouse_buf[1] ? mouse_buf[1] - ((mouse_buf[0] << 4) & 0x100) : 0;
	y_axis += mouse_buf[2] ? ((mouse_buf[0] << 3) & 0x100) - mouse_buf[2] : 0;
	buttons = mouse_buf[0] & 0x7;

	/* Extended protocol */
	if (is_intellimouse) {
		z_axis += (mouse_buf[3] & 0x7) - (mouse_buf[3] & 0x08) ? 8 : 0;
	} else if (is_explorer_intellimouse) {
		z_axis += (mouse_buf[3] & 0x7) - (mouse_buf[3] & 0x08) ? 8 : 0;
		buttons = (mouse_buf[0] & 0x7) | (mouse_buf[3] & 0x30) >> 1;
	}

	mouse_buf_idx = 0;
}

/** Insert data into internal temporary buffer. */
static void insert_buf(unsigned char c)
{
	/* Validate input:
	 * First byte shall have bit 3 set ! */
	if (!mouse_buf_idx && !(c & 8))
		return;

	mouse_buf[mouse_buf_idx++] = c;
}

/** Probe i8042 for new aux data and try to decode it. */
static void mouse_sample(void)
{
	if (!initialized)
		return;

	while (i8042_data_ready_aux()) {
		insert_buf(i8042_read_data_aux());
		mouse_decode();
	}
}

/** Mouse cursor interface method
 * Return and reset internal state.
 */
static void mouse_state(int *x, int *y, int *z, u32 *b)
{
	if (!initialized)
		return;

	mouse_sample();

	if (x) {
		*x = x_axis;
		x_axis = 0;
	}
	if (y) {
		*y = y_axis;
		y_axis = 0;
	}
	if (z) {
		*z = z_axis;
		z_axis = 0;
	}
	if (b)
		*b = buttons;
}

static struct mouse_cursor_input_driver curs = {
	.get_state = mouse_state,
	.input_type = CURSOR_INPUT_TYPE_PS2,
};

/** Probe for PS/2 mouse */
void i8042_mouse_init(void)
{
	int ret;

	/**
	 * Initialize keyboard controller.
	 * Might fail in case no AUX port or firmware disabled the AUX port.
	 */
	if (!i8042_probe() || !i8042_has_aux())
		return;

	/* Empty mouse buffer. */
	while (i8042_data_ready_aux())
		i8042_read_data_aux();

	/* Enable mouse.
	 * Documentation is unclear at this point.
	 * Some recommend to wait for response, some claim there's none.
	 * No response on Lenovo H8 EC.
	 * Ignore it ... */
	ret = i8042_cmd(0xa8);
	if (ret == -1)
		return;

	/* Silence mouse. */
	if (!mouse_cmd(0xf5))
		return;

	/* Read mouse id. */
	if (!mouse_cmd(0xf2))
		return;

	ret = i8042_wait_read_aux();
	if (ret)
		return;

	/* Get and enable features (scroll wheel and 5 buttons) */
	is_intellimouse = mouse_is_intellimouse();
	is_explorer_intellimouse = mouse_is_intellimouse_explorer();

	/* Set defaults. */
	if (!mouse_cmd(0xf6))
		return;

	/* Enable data transmission. */
	if (!mouse_cmd(0xf4))
		return;

	initialized = 1;

	/* Register mouse cursor driver */
	mouse_cursor_add_input_driver(&curs);
}

/* Disable PS/2 mouse. */
void i8042_mouse_disconnect(void)
{
	/* If 0x64 returns 0xff, then we have no keyboard
	 * controller */
	if (inb(0x64) == 0xFF || !initialized)
		return;

	/* Empty keyboard buffer */
	while (i8042_data_ready_aux())
		i8042_read_data_aux();

	/* Disable mouse. */
	i8042_cmd(0xa7);

	initialized = 0;

	/* Release keyboard controller driver */
	i8042_close();
}