/*
 * Copyright (c) 2017 Petr Manek
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - 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.
 * - 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 ``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 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.
 */

/** @addtogroup drvusbxhci
 * @{
 */
/** @file
 * @brief The host controller endpoint management.
 */

#include <usb/host/utils/malloc32.h>
#include <usb/host/endpoint.h>
#include <usb/descriptor.h>

#include <errno.h>

#include "bus.h"
#include "commands.h"
#include "endpoint.h"

int xhci_endpoint_init(xhci_endpoint_t *xhci_ep, xhci_bus_t *xhci_bus)
{
	assert(xhci_ep);
	assert(xhci_bus);

	bus_t *bus = &xhci_bus->base;
	endpoint_t *ep = &xhci_ep->base;

	endpoint_init(ep, bus);
	xhci_ep->device = NULL;

	return EOK;
}

void xhci_endpoint_fini(xhci_endpoint_t *xhci_ep)
{
	assert(xhci_ep);

	/* FIXME: Tear down TR's? */
}

int xhci_device_init(xhci_device_t *dev, xhci_bus_t *bus, usb_address_t address)
{
	memset(&dev->endpoints, 0, sizeof(dev->endpoints));
	dev->active_endpoint_count = 0;
	dev->address = address;
	dev->slot_id = 0;

	return EOK;
}

void xhci_device_fini(xhci_device_t *dev)
{
	// TODO: Check that all endpoints are dead.
	assert(dev);
}

/** Return an index to the endpoint array. The indices are assigned as follows:
 * 0	EP0 BOTH
 * 1	EP1 OUT
 * 2	EP1 IN
 *
 * For control endpoints >0, the IN endpoint index is used.
 *
 * The index returned must be usually offset by a number of contexts preceding
 * the endpoint contexts themselves.
 */
uint8_t xhci_endpoint_index(xhci_endpoint_t *ep)
{
	return  (2 * ep->base.target.endpoint)
	    - (ep->base.direction == USB_DIRECTION_OUT);
}

static int xhci_endpoint_type(xhci_endpoint_t *ep)
{
	const bool in = ep->base.direction == USB_DIRECTION_IN;

	switch (ep->base.transfer_type) {
	case USB_TRANSFER_CONTROL:
		return EP_TYPE_CONTROL;

	case USB_TRANSFER_ISOCHRONOUS:
		return in ? EP_TYPE_ISOCH_IN
			  : EP_TYPE_ISOCH_OUT;

	case USB_TRANSFER_BULK:
		return in ? EP_TYPE_BULK_IN
			  : EP_TYPE_BULK_OUT;

	case USB_TRANSFER_INTERRUPT:
		return in ? EP_TYPE_INTERRUPT_IN
			  : EP_TYPE_INTERRUPT_OUT;
	}

	return EP_TYPE_INVALID;
}

static void setup_control_ep_ctx(xhci_endpoint_t *ep, xhci_ep_ctx_t *ctx,
	xhci_trb_ring_t *ring)
{
	// EP0 is configured elsewhere.
	assert(ep->base.target.endpoint > 0);

	XHCI_EP_TYPE_SET(*ctx, xhci_endpoint_type(ep));
	XHCI_EP_MAX_PACKET_SIZE_SET(*ctx, ep->base.max_packet_size);
	XHCI_EP_ERROR_COUNT_SET(*ctx, 3);
	XHCI_EP_TR_DPTR_SET(*ctx, ring->dequeue);
	XHCI_EP_DCS_SET(*ctx, 1);
}

static void setup_bulk_ep_ctx(xhci_endpoint_t *ep, xhci_ep_ctx_t *ctx,
	xhci_trb_ring_t *ring, usb_superspeed_endpoint_companion_descriptor_t *ss_desc)
{

	XHCI_EP_TYPE_SET(*ctx, xhci_endpoint_type(ep));
	XHCI_EP_MAX_PACKET_SIZE_SET(*ctx, ep->base.max_packet_size);
	XHCI_EP_MAX_BURST_SIZE_SET(*ctx, ep->device->usb3 ? ss_desc->max_burst : 0);
	XHCI_EP_ERROR_COUNT_SET(*ctx, 3);

	// FIXME: Get maxStreams and other things from ss_desc
	const uint8_t maxStreams = 0;
	if (maxStreams > 0) {
		// TODO: allocate and clear primary stream array
		// TODO: XHCI_EP_MAX_P_STREAMS_SET(ctx, psa_size);
		// TODO: XHCI_EP_TR_DPTR_SET(ctx, psa_start_phys_addr);
		// TODO: set HID
		// TODO: set LSA
	} else {
		XHCI_EP_MAX_P_STREAMS_SET(*ctx, 0);
		/* FIXME physical pointer? */
		XHCI_EP_TR_DPTR_SET(*ctx, ring->dequeue);
		XHCI_EP_DCS_SET(*ctx, 1);
	}
}

static void setup_isoch_ep_ctx(xhci_endpoint_t *ep, xhci_ep_ctx_t *ctx,
	xhci_trb_ring_t *ring, usb_superspeed_endpoint_companion_descriptor_t *ss_desc)
{
	XHCI_EP_TYPE_SET(*ctx, xhci_endpoint_type(ep));
	XHCI_EP_MAX_PACKET_SIZE_SET(*ctx, ep->base.max_packet_size & 0x07FF);
	XHCI_EP_MAX_BURST_SIZE_SET(*ctx, ss_desc->max_burst);
	// FIXME: get Mult field from SS companion descriptor somehow
	XHCI_EP_MULT_SET(*ctx, 0);
	XHCI_EP_ERROR_COUNT_SET(*ctx, 0);
	/* FIXME physical pointer? */
	XHCI_EP_TR_DPTR_SET(*ctx, ring->dequeue);
	XHCI_EP_DCS_SET(*ctx, 1);
	// TODO: max ESIT payload
}

static void setup_interrupt_ep_ctx(xhci_endpoint_t *ep, xhci_ep_ctx_t *ctx,
	xhci_trb_ring_t *ring, usb_superspeed_endpoint_companion_descriptor_t *ss_desc)
{
	XHCI_EP_TYPE_SET(*ctx, xhci_endpoint_type(ep));
	XHCI_EP_MAX_PACKET_SIZE_SET(*ctx, ep->base.max_packet_size & 0x07FF);
	XHCI_EP_MAX_BURST_SIZE_SET(*ctx, ss_desc->max_burst);
	XHCI_EP_MULT_SET(*ctx, 0);
	XHCI_EP_ERROR_COUNT_SET(*ctx, 3);
	/* FIXME physical pointer? */
	XHCI_EP_TR_DPTR_SET(*ctx, ring->dequeue);
	XHCI_EP_DCS_SET(*ctx, 1);
	// TODO: max ESIT payload
}

int xhci_device_add_endpoint(xhci_device_t *dev, xhci_endpoint_t *ep)
{
	assert(dev->address == ep->base.target.address);
	assert(!dev->endpoints[ep->base.target.endpoint]);
	assert(!ep->device);

	int err;
	xhci_input_ctx_t *ictx = NULL;
	xhci_trb_ring_t *ep_ring = NULL;
	if (ep->base.target.endpoint > 0) {
		// FIXME: Retrieve this from somewhere, if applicable.
		usb_superspeed_endpoint_companion_descriptor_t ss_desc;
		memset(&ss_desc, 0, sizeof(ss_desc));

		// Prepare input context.
		ictx = malloc32(sizeof(xhci_input_ctx_t));
		if (!ictx) {
			return ENOMEM;
		}

		memset(ictx, 0, sizeof(xhci_input_ctx_t));

		// Quoting sec. 4.6.6: A1, D0, D1 are down, A0 is up.
		XHCI_INPUT_CTRL_CTX_ADD_CLEAR(ictx->ctrl_ctx, 1);
		XHCI_INPUT_CTRL_CTX_DROP_CLEAR(ictx->ctrl_ctx, 0);
		XHCI_INPUT_CTRL_CTX_DROP_CLEAR(ictx->ctrl_ctx, 1);
		XHCI_INPUT_CTRL_CTX_ADD_SET(ictx->ctrl_ctx, 0);

		const uint8_t ep_idx = xhci_endpoint_index(ep);
		XHCI_INPUT_CTRL_CTX_ADD_SET(ictx->ctrl_ctx, ep_idx + 1); /* Preceded by slot ctx */

		ep_ring = malloc(sizeof(xhci_trb_ring_t));
		if (!ep_ring) {
			err = ENOMEM;
			goto err_ictx;
		}

		// FIXME: This ring need not be allocated all the time.
		err = xhci_trb_ring_init(ep_ring);
		if (err)
			goto err_ring;

		switch (ep->base.transfer_type) {
		case USB_TRANSFER_CONTROL:
			setup_control_ep_ctx(ep, &ictx->endpoint_ctx[ep_idx], ep_ring);
			break;

		case USB_TRANSFER_BULK:
			setup_bulk_ep_ctx(ep, &ictx->endpoint_ctx[ep_idx], ep_ring, &ss_desc);
			break;

		case USB_TRANSFER_ISOCHRONOUS:
			setup_isoch_ep_ctx(ep, &ictx->endpoint_ctx[ep_idx], ep_ring, &ss_desc);
			break;

		case USB_TRANSFER_INTERRUPT:
			setup_interrupt_ep_ctx(ep, &ictx->endpoint_ctx[ep_idx], ep_ring, &ss_desc);
			break;

		}

		dev->hc->dcbaa_virt[dev->slot_id].trs[ep->base.target.endpoint] = ep_ring;

		// Issue configure endpoint command (sec 4.3.5).
		xhci_cmd_t cmd;
		xhci_cmd_init(&cmd);

		cmd.slot_id = dev->slot_id;
		xhci_send_configure_endpoint_command(dev->hc, &cmd, ictx);
		if ((err = xhci_cmd_wait(&cmd, XHCI_DEFAULT_TIMEOUT)) != EOK)
			goto err_cmd;

		xhci_cmd_fini(&cmd);
	}

	ep->device = dev;
	dev->endpoints[ep->base.target.endpoint] = ep;
	++dev->active_endpoint_count;
	return EOK;

err_cmd:
err_ring:
	if (ep_ring) {
		xhci_trb_ring_fini(ep_ring);
		free(ep_ring);
	}
err_ictx:
	free(ictx);
	return err;
}

int xhci_device_remove_endpoint(xhci_device_t *dev, xhci_endpoint_t *ep)
{
	assert(dev->address == ep->base.target.address);
	assert(dev->endpoints[ep->base.target.endpoint]);
	assert(dev == ep->device);

	// TODO: Issue configure endpoint command to drop this endpoint.

	ep->device = NULL;
	dev->endpoints[ep->base.target.endpoint] = NULL;
	--dev->active_endpoint_count;
	return EOK;
}

xhci_endpoint_t * xhci_device_get_endpoint(xhci_device_t *dev, usb_endpoint_t ep)
{
	return dev->endpoints[ep];
}

int xhci_device_configure(xhci_device_t *dev, xhci_hc_t *hc)
{
	int err;

	// Prepare input context.
	xhci_input_ctx_t *ictx = malloc(sizeof(xhci_input_ctx_t));
	if (!ictx) {
		return ENOMEM;
	}

	memset(ictx, 0, sizeof(xhci_input_ctx_t));

	// Quoting sec. 4.6.6: A1, D0, D1 are down, A0 is up.
	XHCI_INPUT_CTRL_CTX_ADD_CLEAR(ictx->ctrl_ctx, 1);
	XHCI_INPUT_CTRL_CTX_DROP_CLEAR(ictx->ctrl_ctx, 0);
	XHCI_INPUT_CTRL_CTX_DROP_CLEAR(ictx->ctrl_ctx, 1);
	XHCI_INPUT_CTRL_CTX_ADD_SET(ictx->ctrl_ctx, 0);

	// TODO: Set slot context and other flags. (probably forgot a lot of 'em)

	// Issue configure endpoint command (sec 4.3.5).
	xhci_cmd_t cmd;
	xhci_cmd_init(&cmd);

	cmd.slot_id = dev->slot_id;
	xhci_send_configure_endpoint_command(hc, &cmd, ictx);
	if ((err = xhci_cmd_wait(&cmd, XHCI_DEFAULT_TIMEOUT)) != EOK)
		goto err_cmd;

	xhci_cmd_fini(&cmd);
	return EOK;

err_cmd:
	free(ictx);
	return err;
}

/**
 * @}
 */
