/*
    ChibiOS/RT - Copyright (C) 2006,2007,2008,2009,2010,
                 2011 Giovanni Di Sirio.

    This file is part of ChibiOS/RT.

    ChibiOS/RT is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    ChibiOS/RT is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*-*
 * @file    usb_msc.c
 * @brief   USB Mass Storage Class code.
 *
 * @addtogroup USB_MSC
 * @{
 */

#include "ch.h"
#include "hal.h"

#include "usb_msc.h"

/*===========================================================================*/
/* Driver exported variables.                                                */
/*===========================================================================*/

/*===========================================================================*/
/* Driver local variables.                                                   */
/*===========================================================================*/

/**
 * @brief   Zero-filled constant buffer.
 */
static const uint8_t zerobuf[4] = {0, 0, 0, 0};

/**
 * @brief   Answer to the INQUIRY command.
 */
static const uint8_t scsi_inquiry_data[] = {
  0x00,             /* Direct Access Device.      */
  0x80,             /* RMB = 1: Removable Medium. */
  0x02,             /* ISO, ECMA, ANSI = 2.       */
  0x00,             /* UFI response format.       */

  36 - 4,           /* Additional Length.         */
  0x00,
  0x00,
  0x00,
  /* Vendor Identification */
  'C', 'h', 'i', 'b', 'i', 'O', 'S', ' ',
  /* Product Identification */
  'S', 'D', ' ', 'F', 'l', 'a', 's', 'h',
  ' ', 'D', 'i', 's', 'k', ' ', ' ', ' ',
  /* Product Revision Level */
  '1', '.', '0', ' '
};

/**
 * @brief   Generic buffer.
 */
uint8_t buf[16];

/*===========================================================================*/
/* MMC interface code.                                                       */
/*===========================================================================*/

/*===========================================================================*/
/* SCSI emulation code.                                                      */
/*===========================================================================*/

static uint8_t scsi_read_format_capacities(uint32_t *nblocks,
                                           uint32_t *secsize) {

  *nblocks = 1024;
  *secsize = 512;
  return 3; /* No Media.*/
}

/*===========================================================================*/
/* Mass Storage Class related code.                                          */
/*===========================================================================*/

/**
 * @brief   MSC state machine current state.
 */
static mscstate_t msc_state;

/**
 * @brief   Received CBW.
 */
static msccbw_t CBW;

/**
 * @brief   CSW to be transmitted.
 */
static msccsw_t CSW;

/**
 * @brief   MSC state machine initialization.
 *
 * @param[in] usbp      pointer to the @p USBDriver object
 */
static void msc_reset(USBDriver *usbp) {

  msc_state = MSC_IDLE;
  chSysLockFromIsr();
  usbStartReceiveI(usbp, MSC_DATA_OUT_EP, (uint8_t *)&CBW, sizeof CBW);
  chSysUnlockFromIsr();
}

static void msc_transmit(USBDriver *usbp, const uint8_t *p, size_t n) {

  if (n > CBW.dCBWDataTransferLength)
    n = CBW.dCBWDataTransferLength;
  CSW.dCSWDataResidue = CBW.dCBWDataTransferLength - (uint32_t)n;
  chSysLockFromIsr();
  usbStartTransmitI(usbp, MSC_DATA_IN_EP, p, n);
  chSysUnlockFromIsr();
}

static void msc_sendstatus(USBDriver *usbp) {

  msc_state = MSC_SENDING_CSW;
  chSysLockFromIsr();
  usbStartTransmitI(usbp, MSC_DATA_IN_EP, (uint8_t *)&CSW, sizeof CSW);
  chSysUnlockFromIsr();
}

static bool_t msc_decode(USBDriver *usbp) {
  uint32_t nblocks, secsize;

  switch (CBW.CBWCB[0]) {
  case SCSI_REQUEST_SENSE:
    break;
  case SCSI_INQUIRY:
    msc_transmit(usbp, (uint8_t *)&scsi_inquiry_data,
                 sizeof scsi_inquiry_data);
    CSW.bCSWStatus = MSC_CSW_STATUS_PASSED;
    break;
  case SCSI_READ_FORMAT_CAPACITIES:
    buf[8]  = scsi_read_format_capacities(&nblocks, &secsize);
    buf[0]  = buf[1] = buf[2] = 0;
    buf[3]  = 8;
    buf[4]  = (uint8_t)(nblocks >> 24);
    buf[5]  = (uint8_t)(nblocks >> 16);
    buf[6]  = (uint8_t)(nblocks >> 8);
    buf[7]  = (uint8_t)(nblocks >> 0);
    buf[9]  = (uint8_t)(secsize >> 16);
    buf[10] = (uint8_t)(secsize >> 8);
    buf[11] = (uint8_t)(secsize >> 0);
    msc_transmit(usbp, buf, 12);
    CSW.bCSWStatus = MSC_CSW_STATUS_PASSED;
    break;
  default:
    return TRUE;
  }
  return FALSE;
}

/*===========================================================================*/
/* Driver exported functions.                                                */
/*===========================================================================*/

/**
 * @brief   Default requests hook.
 * @details The application must use this function as callback for the
 *          messages hook.
 *          The following requests are emulated:
 *          - MSC_GET_MAX_LUN_COMMAND.
 *          - MSC_MASS_STORAGE_RESET_COMMAND.
 *          .
 *
 * @param[in] usbp      pointer to the @p USBDriver object
 * @return              The hook status.
 * @retval TRUE         Message handled internally.
 * @retval FALSE        Message not handled.
 */
bool_t mscRequestsHook(USBDriver *usbp) {

  if ((usbp->setup[0] & (USB_RTYPE_TYPE_MASK | USB_RTYPE_RECIPIENT_MASK)) ==
       (USB_RTYPE_TYPE_CLASS | USB_RTYPE_RECIPIENT_INTERFACE)) {
    switch (usbp->setup[1]) {
    case MSC_GET_MAX_LUN_COMMAND:
      usbSetupTransfer(usbp, (uint8_t *)zerobuf, 1, NULL);
      return TRUE;
    case MSC_MASS_STORAGE_RESET_COMMAND:
      msc_reset(usbp);
      usbSetupTransfer(usbp, NULL, 0, NULL);
      return TRUE;
    default:
      return FALSE;
    }
  }
  return FALSE;
}

/**
 * @brief   Default data transmitted callback.
 * @details The application must use this function as callback for the IN
 *          data endpoint.
 *
 * @param[in] usbp      pointer to the @p USBDriver object
 * @param[in] ep        endpoint number
 */
void mscDataTransmitted(USBDriver *usbp, usbep_t ep) {

  switch (msc_state) {
  case MSC_DATA_IN:
    CSW.dCSWSignature = MSC_CSW_SIGNATURE;
    CSW.dCSWTag = CBW.dCBWTag;
    chSysLockFromIsr();
    usbStartTransmitI(usbp, ep, (uint8_t *)&CSW, sizeof CSW);
    chSysUnlockFromIsr();
    msc_state = MSC_SENDING_CSW;
    break;
  case MSC_SENDING_CSW:
    chSysLockFromIsr();
    usbStartReceiveI(usbp, MSC_DATA_OUT_EP, (uint8_t *)&CBW, sizeof CBW);
    chSysUnlockFromIsr();
    msc_state = MSC_IDLE;
    break;
  default:
    ;
  }
}

/**
 * @brief   Default data received callback.
 * @details The application must use this function as callback for the OUT
 *          data endpoint.
 *
 * @param[in] usbp      pointer to the @p USBDriver object
 * @param[in] ep        endpoint number
 */
void mscDataReceived(USBDriver *usbp, usbep_t ep) {
  size_t n;

  n = usbGetReceiveTransactionSizeI(usbp, ep);
  switch (msc_state) {
  case MSC_IDLE:
    if ((n != sizeof(msccbw_t)) || (CBW.dCBWSignature != MSC_CBW_SIGNATURE))
      goto stall_out; /* 6.6.1 */

    /* Decoding SCSI command.*/
    if (msc_decode(usbp)) {
      if (CBW.dCBWDataTransferLength == 0) {
        CSW.bCSWStatus = MSC_CSW_STATUS_FAILED;
        CSW.dCSWDataResidue = 0;
        msc_sendstatus(usbp);
        return;
      }
      goto stall_both;
    }

    /* Commands with zero transfer length, 5.1.*/
    if (CBW.dCBWDataTransferLength == 0) {
      msc_sendstatus(usbp);
      return;
    }

    /* Transfer direction.*/
    if (CBW.bmCBWFlags & 0x80) {
      /* IN, Device to Host.*/
      msc_state = MSC_DATA_IN;
    }
    else {
      /* OUT, Host to Device.*/
      msc_state = MSC_DATA_OUT;
    }
    break;
  case MSC_DATA_OUT:
    break;
  default:
    ;
  }
  return;
stall_out:
  msc_state = MSC_ERROR;
  chSysLockFromIsr();
  usbStallReceiveI(usbp, ep);
  chSysUnlockFromIsr();
  return;
stall_both:
  msc_state = MSC_ERROR;
  chSysLockFromIsr();
  usbStallTransmitI(usbp, ep);
  usbStallReceiveI(usbp, ep);
  chSysUnlockFromIsr();
  return;
}

/** @} */