/*
 * usb_gps.c
 *
 * Copyright (C) 2019-2022  Sylvain Munaut <tnt@246tNt.com>
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include <no2usb/usb.h>
#include <no2usb/usb_hw.h>
#include <no2usb/usb_priv.h>

#include <no2usb/usb_proto.h>
#include <no2usb/usb_cdc_proto.h>

#include "console.h"
#include "usb_desc_ids.h"


#define BUF_SIZE_LOG	9
#define BUF_SIZE	(1 << BUF_SIZE_LOG)
#define BUF_MASK	(BUF_SIZE - 1)

static struct {
	/* State */
	bool active;

	/* Buffer */
	struct {
		unsigned int wr;
		unsigned int rd;
		char data[BUF_SIZE] __attribute__((aligned(4)));
	} buf;
} g_usb_gps;


static void
_usb_gps_set_active(bool active)
{
	/* Save new state */
	g_usb_gps.active = active;

	/* Reset FIFO if disabled */
	if (!active)
		g_usb_gps.buf.wr = g_usb_gps.buf.rd = 0;
}

static int
_usb_gps_fill_packet(unsigned int dst_ofs)
{
	unsigned int len;

	/* Len available, limited to 64 */
	len = (g_usb_gps.buf.wr - g_usb_gps.buf.rd) & BUF_MASK;

	if (len > 64)
		len = 64;

	/* Copy block */
	usb_data_write(dst_ofs, &g_usb_gps.buf.data[g_usb_gps.buf.rd], (len + 3) & ~3);

	/* Increment read pointer */
	g_usb_gps.buf.rd = (g_usb_gps.buf.rd + len) & BUF_MASK;

	/* If length was not multiple of 4, we emptied the FIFO,
	 * so we reset it to 0 so we're aligned again */
	if (len & 3)
		g_usb_gps.buf.wr = g_usb_gps.buf.rd = 0;

	return len;
}


void
usb_gps_puts(const char *str)
{
	unsigned int nxt;
	char c;

	if (!g_usb_gps.active)
		return;

	while ((c = *str++) != '\0')
	{
		/* Next write pointer pos and full check */
		nxt = (g_usb_gps.buf.wr + 1) & BUF_MASK;
		if (nxt == g_usb_gps.buf.rd)
			/* If overflow, we keep the latest content ... */
			g_usb_gps.buf.rd = (g_usb_gps.buf.rd + 1) & BUF_MASK;

		/* Write data */
		g_usb_gps.buf.data[g_usb_gps.buf.wr] = c;

		/* Update write pointer */
		g_usb_gps.buf.wr = nxt;
	}
}

void
usb_gps_poll(void)
{
	volatile struct usb_ep *ep_regs;

	/* OUT EP: We accept data and throw it away */
	ep_regs = &usb_ep_regs[USB_EP_GPS_CDC_OUT & 0x1f].out;
	ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(64);

	/* IN EP: Send whatever is queued */
	ep_regs = &usb_ep_regs[USB_EP_GPS_CDC_OUT & 0x1f].in;

	if ((ep_regs->bd[0].csr & USB_BD_STATE_MSK) != USB_BD_STATE_RDY_DATA)
	{
		int len = _usb_gps_fill_packet(ep_regs->bd[0].ptr);

		if (len)
			ep_regs->bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(len);
		else
			ep_regs->bd[0].csr = 0;
	}
}


static enum usb_fnd_resp
_cdc_set_conf(const struct usb_conf_desc *conf)
{
	const struct usb_intf_desc *intf;

	if (!conf)
		return USB_FND_SUCCESS;

	/* Configure control interface */
	intf = usb_desc_find_intf(conf, USB_INTF_GPS_CDC_CTL, 0, NULL);
	if (!intf)
		return USB_FND_ERROR;

	usb_ep_boot(intf, USB_EP_GPS_CDC_CTL, false);

	/* Configure data interface */
	intf = usb_desc_find_intf(conf, USB_INTF_GPS_CDC_DATA, 0, NULL);
	if (!intf)
		return USB_FND_ERROR;

	usb_ep_boot(intf, USB_EP_GPS_CDC_OUT, false);
	usb_ep_boot(intf, USB_EP_GPS_CDC_IN,  false);

	return USB_FND_SUCCESS;
}

static enum usb_fnd_resp
_cdc_ctrl_req(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
	static const struct usb_cdc_line_coding linecoding = {
		.dwDTERate   = 115200,
		.bCharFormat = 2,
		.bParityType = 0,
		.bDataBits   = 8,
	};

	/* Check this is handled here */
	if (USB_REQ_TYPE_RCPT(req) != (USB_REQ_TYPE_CLASS | USB_REQ_RCPT_INTF))
		return USB_FND_CONTINUE;

	if (req->wIndex != USB_INTF_GPS_CDC_CTL)
		return USB_FND_CONTINUE;

	/* Process request */
	switch (req->bRequest) {
	case USB_REQ_CDC_SEND_ENCAPSULATED_COMMAND:
		/* We don't support any, so just accept and don't care */
		return USB_FND_SUCCESS;

	case USB_REQ_CDC_GET_ENCAPSULATED_RESPONSE:
		/* Never anything to return */
		xfer->len = 0;
		return USB_FND_SUCCESS;

	case USB_REQ_CDC_SET_LINE_CODING:
		/* We only support 1 config, doesn't matter what the hosts sends */
		return USB_FND_SUCCESS;

	case USB_REQ_CDC_GET_LINE_CODING:
		/* We only support 1 config, send that back */
		xfer->data = (void*)&linecoding;
		xfer->len  = sizeof(linecoding);
		return USB_FND_ERROR;

	case USB_REQ_CDC_SET_CONTROL_LINE_STATE:
		/* Enable if DTR is set */
		_usb_gps_set_active((req->wValue & 1) != 0);
		return USB_FND_SUCCESS;
	}

	/* Anything else is not handled */
	return USB_FND_ERROR;
}


static struct usb_fn_drv _cdc_drv = {
	.set_conf	= _cdc_set_conf,
	.ctrl_req	= _cdc_ctrl_req,
};

void
usb_gps_init(void)
{
	memset(&g_usb_gps, 0x00, sizeof(g_usb_gps));
	usb_register_function_driver(&_cdc_drv);
}
