/*
 * cdc-dlm.c
 *
 * CDC Direct Line Modem control for MC97 modem
 *
 * Copyright (C) 2021 Sylvain Munaut
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include <string.h>

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

#include "cdc-dlm.h"
#include "mc97.h"


#define INTF_CDC_DLM		4
#define EP_CDC_DLM_NOTIF	0x83


static void
dlm_send_notif_ring_detect(void)
{
	const struct usb_ctrl_req notif = {
		.bmRequestType	= USB_REQ_READ | USB_REQ_TYPE_CLASS | USB_REQ_RCPT_INTF,
		.bRequest	= USB_NOTIF_CDC_RING_DETECT,
		.wValue		= 0,
		.wIndex		= INTF_CDC_DLM,
		.wLength	= 0,
	};

	usb_data_write(usb_ep_regs[3].in.bd[0].ptr, &notif, sizeof(struct usb_ctrl_req));
	usb_ep_regs[3].in.bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(sizeof(struct usb_ctrl_req));
}


static int16_t g_cc = 0;

static bool
dlm_set_comm_feature_country_cb(struct usb_xfer *xfer)
{
	uint16_t new_cc;

	if (xfer->len != sizeof(uint16_t))
		return USB_FND_ERROR;

	memcpy(&new_cc, xfer->data, sizeof(uint16_t));

	if (mc97_select_country(new_cc)) {
		g_cc = new_cc;
		return USB_FND_SUCCESS;
	}

	return USB_FND_ERROR;
}

static enum usb_fnd_resp
dlm_set_comm_feature(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
	/* Only country selection supported */
	if (req->wValue != 0x02 /* COUNTRY_SETTING */)
		return USB_FND_ERROR;

	/* Setup call back */
	xfer->len = sizeof(uint16_t);
	xfer->cb_done = dlm_set_comm_feature_country_cb;

	return USB_FND_SUCCESS;
}

static enum usb_fnd_resp
dlm_get_comm_feature(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
	/* Only country selection supported */
	if (req->wValue != 0x02 /* COUNTRY_SETTING */)
		return USB_FND_ERROR;

	/* Send the currently selected country code */
	xfer->len = sizeof(uint16_t);
	memcpy(xfer->data, &g_cc, sizeof(uint16_t));

	return USB_FND_SUCCESS;
}

static enum usb_fnd_resp
dlm_clear_comm_feature(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
	/* Only country selection supported */
	if (req->wValue != 0x02 /* COUNTRY_SETTING */)
		return USB_FND_ERROR;

	/* Restore default */
	g_cc = 0;
	mc97_select_country(0);

	return USB_FND_SUCCESS;
}


static enum usb_fnd_resp
dlm_ctrl_req(struct usb_ctrl_req *req, struct usb_xfer *xfer)
{
	/* Check it's a class request to an interface */
	if (USB_REQ_TYPE_RCPT(req) != (USB_REQ_TYPE_CLASS | USB_REQ_RCPT_INTF))
		return USB_FND_CONTINUE;

	/* Check it's for the DLM interface */
	if ((req->wIndex & 0xff) != INTF_CDC_DLM)
		return USB_FND_CONTINUE;

	switch (req->wRequestAndType)
	{
	case USB_RT_CDC_SET_HOOK_STATE:
		switch (req->wValue) {
		case 0:  mc97_set_hook(ON_HOOK);   break;
		case 1:  mc97_set_hook(OFF_HOOK);  break;
		case 2:  mc97_set_hook(CALLER_ID); break;
		default: return USB_FND_ERROR;
		}
		return USB_FND_SUCCESS;

	case USB_RT_CDC_SET_AUX_LINE_STATE:
		/* Control the relay with that */
		mc97_set_aux_relay(!req->wValue);
		return USB_FND_SUCCESS;

	case USB_RT_CDC_RING_AUX_JACK:
		/* Can't do that ... */
		return USB_FND_SUCCESS;

	/* Pulse is not supported yet (also disabled in bmCapabilities) */
	case USB_RT_CDC_PULSE_SETUP:
	case USB_RT_CDC_SEND_PULSE:
	case USB_RT_CDC_SET_PULSE_TIME:
		return USB_FND_ERROR;

	/* Implement SET_COMM_FEATURE for country selection ? */
	/* In theory not part of DLM but it's the closest to a standard
	 * thing to support tweaking the codec params to match local specs
	 * for a phone line */
	case USB_RT_CDC_SET_COMM_FEATURE:
		return dlm_set_comm_feature(req, xfer);

	case USB_RT_CDC_GET_COMM_FEATURE:
		return dlm_get_comm_feature(req, xfer);

	case USB_RT_CDC_CLEAR_COMM_FEATURE:
		return dlm_clear_comm_feature(req, xfer);
	}

	return USB_FND_ERROR;
}

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

	intf = usb_desc_find_intf(conf, INTF_CDC_DLM, 0, NULL);
        usb_ep_boot(intf, EP_CDC_DLM_NOTIF, false);

	return USB_FND_SUCCESS;
}


static struct usb_fn_drv _dlm_drv = {
	.ctrl_req = dlm_ctrl_req,
	.set_conf = dlm_set_conf,
};


void
cdc_dlm_init(void)
{
	/* Register function driver */
	usb_register_function_driver(&_dlm_drv);
}

void
cdc_dlm_poll(void)
{
	/* Ring detection */
	static bool ring;

	if (mc97_get_ring_detect()) {
		if (!ring) {
			dlm_send_notif_ring_detect();
			ring = true;
		}
	} else {
		ring = false;
	}

	/* TODO:
	 *  - Pulse timing when pulse is implemented
	 */
}