/******************************************************************************
 * The MIT License
 *
 * Copyright (c) 2010 LeafLabs LLC.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *****************************************************************************/

/**
 * @file libmaple/usb/usb.c
 * @brief USB support.
 *
 * This is a mess. What we need almost amounts to a ground-up rewrite.
 */

#include <libmaple/usb.h>

#include <libmaple/libmaple.h>
#include <libmaple/rcc.h>

/* Private headers */
#include "usb_reg_map.h"
#include "usb_lib_globals.h"

/* usb_lib headers */
#include "usb_type.h"
#include "usb_core.h"

static void dispatch_ctr_lp(void);

/*
 * usb_lib/ globals
 */

uint16 SaveTState;              /* caches TX status for later use */
uint16 SaveRState;              /* caches RX status for later use */

/*
 * Other state
 */

typedef enum {
    RESUME_EXTERNAL,
    RESUME_INTERNAL,
    RESUME_LATER,
    RESUME_WAIT,
    RESUME_START,
    RESUME_ON,
    RESUME_OFF,
    RESUME_ESOF
} RESUME_STATE;

struct {
  volatile RESUME_STATE eState;
  volatile uint8 bESOFcnt;
} ResumeS;

static usblib_dev usblib = {
    .irq_mask = USB_ISR_MSK,
    .state = USB_UNCONNECTED,
    .clk_id = RCC_USB,
};
usblib_dev *USBLIB = &usblib;

/*
 * Routines
 */

void usb_init_usblib(usblib_dev *dev,
                     void (**ep_int_in)(void),
                     void (**ep_int_out)(void)) {
    rcc_clk_enable(dev->clk_id);

    dev->ep_int_in = ep_int_in;
    dev->ep_int_out = ep_int_out;

    /* usb_lib/ declares both and then assumes that pFoo points to Foo
     * (even though the names don't always match), which is stupid for
     * all of the obvious reasons, but whatever.  Here we are. */
    pInformation = &Device_Info;
    pProperty = &Device_Property;
    pUser_Standard_Requests = &User_Standard_Requests;

    pInformation->ControlState = 2; /* FIXME [0.0.12] use
                                       CONTROL_STATE enumerator */
    pProperty->Init();
}

static void usb_suspend(void) {
  uint16 cntr;

  /* TODO decide if read/modify/write is really what we want
   * (e.g. usb_resume_init() reconfigures CNTR). */
  cntr = USB_BASE->CNTR;
  cntr |= USB_CNTR_FSUSP;
  USB_BASE->CNTR = cntr;
  cntr |= USB_CNTR_LP_MODE;
  USB_BASE->CNTR = cntr;

  USBLIB->state = USB_SUSPENDED;
}

static void usb_resume_init(void) {
  uint16 cntr;

  cntr = USB_BASE->CNTR;
  cntr &= ~USB_CNTR_LP_MODE;
  USB_BASE->CNTR = cntr;

  /* Enable interrupt lines */
  USB_BASE->CNTR = USB_ISR_MSK;
}

static void usb_resume(RESUME_STATE eResumeSetVal) {
  uint16 cntr;

  if (eResumeSetVal != RESUME_ESOF)
    ResumeS.eState = eResumeSetVal;

  switch (ResumeS.eState)
    {
    case RESUME_EXTERNAL:
      usb_resume_init();
      ResumeS.eState = RESUME_OFF;
      break;
    case RESUME_INTERNAL:
      usb_resume_init();
      ResumeS.eState = RESUME_START;
      break;
    case RESUME_LATER:
      ResumeS.bESOFcnt = 2;
      ResumeS.eState = RESUME_WAIT;
      break;
    case RESUME_WAIT:
      ResumeS.bESOFcnt--;
      if (ResumeS.bESOFcnt == 0)
        ResumeS.eState = RESUME_START;
      break;
    case RESUME_START:
      cntr = USB_BASE->CNTR;
      cntr |= USB_CNTR_RESUME;
      USB_BASE->CNTR = cntr;
      ResumeS.eState = RESUME_ON;
      ResumeS.bESOFcnt = 10;
      break;
    case RESUME_ON:
      ResumeS.bESOFcnt--;
      if (ResumeS.bESOFcnt == 0) {
          cntr = USB_BASE->CNTR;
          cntr &= ~USB_CNTR_RESUME;
          USB_BASE->CNTR = cntr;
          ResumeS.eState = RESUME_OFF;
      }
      break;
    case RESUME_OFF:
    case RESUME_ESOF:
    default:
      ResumeS.eState = RESUME_OFF;
      break;
    }
}

#define SUSPEND_ENABLED 1
void __irq_usb_lp_can_rx0(void) {
  uint16 istr = USB_BASE->ISTR;

  /* Use USB_ISR_MSK to only include code for bits we care about. */

#if (USB_ISR_MSK & USB_ISTR_RESET)
  if (istr & USB_ISTR_RESET & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_RESET;
    pProperty->Reset();
  }
#endif

#if (USB_ISR_MSK & USB_ISTR_PMAOVR)
  if (istr & ISTR_PMAOVR & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_PMAOVR;
  }
#endif

#if (USB_ISR_MSK & USB_ISTR_ERR)
  if (istr & USB_ISTR_ERR & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_ERR;
  }
#endif

#if (USB_ISR_MSK & USB_ISTR_WKUP)
  if (istr & USB_ISTR_WKUP & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_WKUP;
    usb_resume(RESUME_EXTERNAL);
  }
#endif

#if (USB_ISR_MSK & USB_ISTR_SUSP)
  if (istr & USB_ISTR_SUSP & USBLIB->irq_mask) {
    /* check if SUSPEND is possible */
    if (SUSPEND_ENABLED) {
        usb_suspend();
    } else {
        /* if not possible then resume after xx ms */
        usb_resume(RESUME_LATER);
    }
    /* clear of the ISTR bit must be done after setting of CNTR_FSUSP */
    USB_BASE->ISTR = ~USB_ISTR_SUSP;
}
#endif

#if (USB_ISR_MSK & USB_ISTR_SOF)
  if (istr & USB_ISTR_SOF & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_SOF;
  }
#endif

#if (USB_ISR_MSK & USB_ISTR_ESOF)
  if (istr & USB_ISTR_ESOF & USBLIB->irq_mask) {
    USB_BASE->ISTR = ~USB_ISTR_ESOF;
    /* resume handling timing is made with ESOFs */
    usb_resume(RESUME_ESOF); /* request without change of the machine state */
  }
#endif

  /*
   * Service the correct transfer interrupt.
   */

#if (USB_ISR_MSK & USB_ISTR_CTR)
  if (istr & USB_ISTR_CTR & USBLIB->irq_mask) {
    dispatch_ctr_lp();
  }
#endif
}

/*
 * Auxiliary routines
 */

static inline uint8 dispatch_endpt_zero(uint16 istr_dir);
static inline void dispatch_endpt(uint8 ep);
static inline void set_rx_tx_status0(uint16 rx, uint16 tx);

static void handle_setup0(void);
static void handle_in0(void);
static void handle_out0(void);

static void dispatch_ctr_lp() {
    uint16 istr;
    while (((istr = USB_BASE->ISTR) & USB_ISTR_CTR) != 0) {
        /* TODO WTF, figure this out: RM0008 says CTR is read-only,
         * but ST's firmware claims it's clear-only, and emphasizes
         * the importance of clearing it in more than one place. */
        USB_BASE->ISTR = ~USB_ISTR_CTR;
        uint8 ep_id = istr & USB_ISTR_EP_ID;
        if (ep_id == 0) {
            /* TODO figure out why it's OK to break out of the loop
             * once we're done serving endpoint zero, but not okay if
             * there are multiple nonzero endpoint transfers to
             * handle. */
            if (dispatch_endpt_zero(istr & USB_ISTR_DIR))
                return;
        } else {
            dispatch_endpt(ep_id);
        }
    }
}

/* FIXME Dataflow on endpoint 0 RX/TX status is based off of ST's
 * code, and is ugly/confusing in its use of SaveRState/SaveTState.
 * Fixing this requires filling in handle_in0(), handle_setup0(),
 * handle_out0(). */
static inline uint8 dispatch_endpt_zero(uint16 istr_dir) {
    uint32 epr = (uint16)USB_BASE->EP[0];

    if (!(epr & (USB_EP_CTR_TX | USB_EP_SETUP | USB_EP_CTR_RX))) {
        return 0;
    }

    /* Cache RX/TX statuses in SaveRState/SaveTState, respectively.
     * The various handle_foo0() may clobber these values
     * before we reset them at the end of this routine. */
    SaveRState = epr & USB_EP_STAT_RX;
    SaveTState = epr & USB_EP_STAT_TX;

    /* Set actual RX/TX statuses to NAK while we're thinking */
    set_rx_tx_status0(USB_EP_STAT_RX_NAK, USB_EP_STAT_TX_NAK);

    if (istr_dir == 0) {
        /* ST RM0008: "If DIR bit=0, CTR_TX bit is set in the USB_EPnR
         * register related to the interrupting endpoint.  The
         * interrupting transaction is of IN type (data transmitted by
         * the USB peripheral to the host PC)." */
        ASSERT_FAULT(epr & USB_EP_CTR_TX);
        usb_clear_ctr_tx(USB_EP0);
        handle_in0();
    } else {
        /* RM0008: "If DIR bit=1, CTR_RX bit or both CTR_TX/CTR_RX
         * are set in the USB_EPnR register related to the
         * interrupting endpoint. The interrupting transaction is of
         * OUT type (data received by the USB peripheral from the host
         * PC) or two pending transactions are waiting to be
         * processed."
         *
         * [mbolivar] Note how the following control flow (which
         * replicates ST's) doesn't seem to actually handle both
         * interrupts that are ostensibly pending when both CTR_RX and
         * CTR_TX are set.
         *
         * TODO sort this mess out.
         */
        if (epr & USB_EP_CTR_TX) {
            usb_clear_ctr_tx(USB_EP0);
            handle_in0();
        } else {                /* SETUP or CTR_RX */
            /* SETUP is held constant while CTR_RX is set, so clear it
             * either way */
            usb_clear_ctr_rx(USB_EP0);
            if (epr & USB_EP_SETUP) {
                handle_setup0();
            } else {            /* CTR_RX */
                handle_out0();
            }
        }
    }

    set_rx_tx_status0(SaveRState, SaveTState);
    return 1;
}

static inline void dispatch_endpt(uint8 ep) {
    uint32 epr = USB_BASE->EP[ep];
    /* If ISTR_CTR is set and the ISTR gave us this EP_ID to handle,
     * then presumably at least one of CTR_RX and CTR_TX is set, but
     * again, ST's control flow allows for the possibility of neither.
     *
     * TODO try to find out if neither being set is possible. */
    if (epr & USB_EP_CTR_RX) {
        usb_clear_ctr_rx(ep);
        (USBLIB->ep_int_out[ep - 1])();
    }
    if (epr & USB_EP_CTR_TX) {
        usb_clear_ctr_tx(ep);
        (USBLIB->ep_int_in[ep - 1])();
    }
}

static inline void set_rx_tx_status0(uint16 rx, uint16 tx) {
    usb_set_ep_rx_stat(USB_EP0, rx);
    usb_set_ep_tx_stat(USB_EP0, tx);
}

/* TODO Rip out usb_lib/ dependency from the following functions: */

static void handle_setup0(void) {
    Setup0_Process();
}

static void handle_in0(void) {
    In0_Process();
}

static void handle_out0(void) {
    Out0_Process();
}