/*-
 * Copyright 2022 John-Mark Gurney.
 *
 * 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. If you are STMicroelectronics N.V., one of it's subsidiaries, a
 *    subsidiary of an owner of STMicroelectronics N.V., or an employee,
 *    contractor, or agent of any of the preceeding entities, you are not
 *    allowed to use this code, in either source or binary forms.
 *
 * 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 "usb_hid_def.h"

#include <misc.h> /* debug_printf */

#include <stdbool.h>

#include <usb_hid_base.h>

static uint8_t Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx);
static uint8_t Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req);
static uint8_t DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum);
#if 0
static uint8_t DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum);
#endif
static uint8_t ep0rxready(USBD_HandleTypeDef  *pdev);

#define USB_HID_EPIN_ADDR 0x81

#define REPORT_CNT	10

enum hid_state {
	IDLE,
	BUSY,
};

struct usb_hid_softc {
	enum hid_state	state;
	uint8_t		led_status[2];
	uint8_t		idle_time;
	uint8_t		report_protocol;
};

static USBD_HandleTypeDef *hid_handle;
static uint32_t next_idle_report;

static uint8_t reports[REPORT_CNT][REPORT_SIZE] = {
	/*      mod   pad   keys... */
#if 0
	[0] = { 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[1] = { 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[2] = { 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[3] = { 0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[4] = { 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[5] = { 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[6] = { 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[7] = { 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
	[9] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
#endif
};
/*
 * Ring buffer for reports.
 *
 * reporthead: next available report
 * reporttail: next empty slot
 *
 * reporthead == reporttail: ring buffer is empty
 * reporttail == reporthead - 1: ring buffer is full
 */
static uint8_t reporthead = 0;
static uint8_t reporttail = 0;
static uint32_t reportoverflow;

/* as -1 % posint == -1, adding REPORT_CNT keeps it positive if x is val - 1 when val is 0 */
#define MAKE_POS(x)	(((x) + REPORT_CNT) % REPORT_CNT)

static bool
report_is_avail(void)
{
	return reporthead != reporttail;
}

static uint8_t *
report_next(void)
{
	int oldrep;

	/* no new report, don't advance, return previous */
	if (!report_is_avail())
		return reports[MAKE_POS(reporthead - 1)];

	oldrep = reporthead;
	reporthead = MAKE_POS(reporthead + 1);

	return reports[oldrep];
}

void
report_process(void)
{
	struct usb_hid_softc *hid;
	uint32_t tick;

	if (hid_handle == NULL)
		return;

	hid = (struct usb_hid_softc *)hid_handle->pClassData;

	if (hid_handle->dev_state != USBD_STATE_CONFIGURED ||
	    hid->state != IDLE) {
		return;
	}

	tick = HAL_GetTick();

	if (!report_is_avail() && tick < next_idle_report)
		return;

	next_idle_report = tick + hid->idle_time;

	hid->state = BUSY;
	USBD_LL_Transmit(hid_handle, USB_HID_EPIN_ADDR, report_next(), REPORT_SIZE);
}

/*
 * This could be smarter by collapsing reports.  The algorithm to do
 * so is a bit tricky, and likely not needed, BUT it does mean for a
 * simple algorithm, you could be limited to 50 chars per second assuming
 * 10ms report timing (one down, one up).
 *
 * Return true if successful, false if overflowed.
 */ 
int
report_insert(uint8_t rep[REPORT_SIZE])
{
	uint8_t newtail;

	newtail = MAKE_POS(reporttail + 1);
	if (newtail == reporthead) {
		reportoverflow++;
		return 0;
	}

	memcpy(reports[reporttail], rep, sizeof reports[reporttail]);

	reporttail = newtail;

	return 1;
}

enum {
	HID_REQ_GET_REPORT = 0x01,
	HID_REQ_GET_IDLE = 0x02,
	HID_REQ_GET_PROTOCOL = 0x03,
	HID_REQ_SET_REPORT = 0x09,
	HID_REQ_SET_IDLE = 0x0a,
	HID_REQ_SET_PROTOCOL = 0x0b,
};

USBD_ClassTypeDef usb_hid_def = {
	.Init = Init,
	.DeInit = DeInit,
	.Setup = Setup,
	.EP0_RxReady = ep0rxready,
	.DataIn = DataIn,
#if 0
	.DataOut = DataOut,
#endif
	/* Get*Desc not used */
};

static uint8_t
Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{
	struct usb_hid_softc *hid;
	int ret;

	ret = 0;

	usb_hid_epopen(pdev);

	/* allocate state data */
	hid = (void *)USBD_malloc(sizeof(struct usb_hid_softc));
	if (hid == NULL) {
		ret = 1;
	} else {
		*hid = (struct usb_hid_softc){
			.idle_time = 10,
		};

		pdev->pClassData = hid;

		hid_handle = pdev;
	}

	return ret;
}

static uint8_t
DeInit(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{

	usb_hid_epclose(pdev);

	if (pdev->pClassData != NULL) {
		USBD_free(pdev->pClassData);
		pdev->pClassData = NULL;
		hid_handle = NULL;
	}

	return 0;
}

#define	MAKE_CASE_REQ(type, req)	((((type) << 8) & USB_REQ_TYPE_MASK) | (req))
#define	USB_CASE_REQ(req)	MAKE_CASE_REQ((req)->bmRequest, (req)->bRequest)

static void
send_ctlresp(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req, const uint8_t *buf, size_t len)
{

	len = MIN(req->wLength, len);

	USBD_CtlSendData(pdev, (uint8_t *)(uintptr_t)buf, len);
}

/*
 * Handle non-standard control SETUP requests.
 *
 * This includes requests sent to the interface and other special
 * requests.
 */
static uint8_t
Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
	struct usb_hid_softc *hid;
	uint8_t ret;

	ret = USBD_OK;
	hid = (struct usb_hid_softc *)pdev->pClassData;

	switch (USB_CASE_REQ(req)) {
	case MAKE_CASE_REQ(USB_REQ_TYPE_STANDARD, USB_REQ_GET_DESCRIPTOR):
		USBD_GetDescriptor(pdev, req);
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_GET_REPORT):
		send_ctlresp(pdev, req, report_next(), REPORT_SIZE);
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_SET_REPORT):
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_GET_IDLE):
		send_ctlresp(pdev, req, &hid->idle_time, sizeof hid->idle_time);
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_SET_IDLE):
		hid->idle_time = (uint8_t)(req->wValue >> 8);
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_GET_PROTOCOL):
		send_ctlresp(pdev, req, &hid->report_protocol, sizeof hid->idle_time);
		break;

	case MAKE_CASE_REQ(USB_REQ_TYPE_CLASS, HID_REQ_SET_PROTOCOL):
		hid->report_protocol = !!req->wValue;
		break;

	default:
		USBD_CtlError(pdev, req);
		ret = USBD_FAIL;
		break;
	}

	return ret;
}

static uint8_t
DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
	struct usb_hid_softc *hid;

	hid = (struct usb_hid_softc *)pdev->pClassData;

	/* Previously queued data was sent */
	hid->state = IDLE;

	return USBD_OK;
}

#if 0
static uint8_t
DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{

	/* received led status */

	return USBD_OK;
}
#endif

static uint8_t
ep0rxready(USBD_HandleTypeDef  *pdev)
{

	return USBD_OK;
}