/* 
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Ha Thach (tinyusb.org)
 *
 * 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.
 *
 */

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

#include "../../cdc_hid/src/usb_descriptors.h"
// #include "bsp/board.h"
#include "tusb.h"
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#include "hardware/uart.h"

#include "mcp2221.h"

// #define USE_LED_BLINKING_TASK   1
#define DEBUG_MAIN  1
#define DEBUG_CDC   0
#define DEBUG_HID   1

/* This example demonstrate HID Generic raw Input & Output.
 * It will receive data from Host (In endpoint) and echo back (Out endpoint).
 * HID Report descriptor use vendor for usage page (using template TUD_HID_REPORT_DESC_GENERIC_INOUT)
 *
 * There are 2 ways to test the sketch
 * 1. Using nodejs
 * - Install nodejs and npm to your PC
 *
 * - Install excellent node-hid (https://github.com/node-hid/node-hid) by
 *   $ npm install node-hid
 *
 * - Run provided hid test script
 *   $ node hid_test.js
 *
 * 2. Using python
 * - Install `hid` package (https://pypi.org/project/hid/) by
 *   $ pip install hid
 *
 * - hid package replies on hidapi (https://github.com/libusb/hidapi) for backend,
 *   which already available in Linux. However on windows, you may need to download its dlls from their release page and
 *   copy it over to folder where python is installed.
 *
 * - Run provided hid test script to send and receive data to this device.
 *   $ python3 hid_test.py
 */

//--------------------------------------------------------------------+
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+

/* Blink pattern
 * - 250 ms  : device not mounted
 * - 1000 ms : device mounted
 * - 2500 ms : device is suspended
 */
enum  {
  BLINK_NOT_MOUNTED = 250,
  BLINK_MOUNTED = 1000,
  BLINK_SUSPENDED = 2500,
};

static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;

#if USE_LED_BLINKING_TASK
void led_blinking_task(void);
#endif
#if CFG_TUD_CDC
void cdc_task(void);
#endif
#if CFG_TUD_HID
// void hid_task(void);
#endif

extern char const* string_desc_arr [];

#define BAUD_RATE 115200
#define UART_TX_PIN 8
#define UART_RX_PIN 9

#define I2C_SDA_PIN 6
#define I2C_SCL_PIN 7

#define MCP2221_USE_TIMEOUT     0
#define MCP2221_SN_DESC         "123456"
#define MCP2221_CHIP_SN         "123456"
#define MCP2221_RESPONSE_SIZE   64
#define MCP2221_DATA_MAX        60
#define MCP2221_BUF_SIZE        0x10000
#define MCP2221_CMD_SUCCESS     0x00
#define MCP2221_CMD_BUSY        0x01
#define MCP2221_TIMEOUT         50000   // 0.05s

static uart_inst_t *uart = (uart_inst_t *)uart1; // ch=1 for GP8,GP9

static void pico_uart_init(void) {
    //Initialize UART port
    uart_init(uart, BAUD_RATE);
    // Initialize UART pins
    gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
    gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);
}

static i2c_inst_t *i2c = (i2c_inst_t *)i2c1;    // ch=1 for GP6,GP7
static uint8_t m_response[MCP2221_RESPONSE_SIZE];
static uint8_t m_i2c_buf[MCP2221_BUF_SIZE];
static uint16_t m_i2c_buf_write_index = 0;
static bool m_i2c_read_start = false;
static uint16_t m_i2c_buf_stored_size = 0;
static uint16_t m_i2c_buf_read_index = 0;

static void pico_i2c_init(void) {
    m_i2c_buf_write_index = 0;
    m_i2c_read_start = false;
    m_i2c_buf_stored_size = 0;
    m_i2c_buf_read_index = 0;
    //Initialize I2C port at 400 kHz
    i2c_init(i2c, 400 * 1000);
    // Initialize I2C pins
    gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
}

/*------------- MAIN -------------*/
int main(void)
{
#if USE_LED_BLINKING_TASK
  board_init();
#endif
  stdio_init_all();
#if DEBUG_MAIN
  printf("hid_cdc: start\r\n");
#endif
  pico_uart_init();
  pico_i2c_init();
  tud_init(BOARD_TUD_RHPORT);

  while (1)
  {
    tud_task(); // tinyusb device task
#if USE_LED_BLINKING_TASK
    led_blinking_task();
#endif

#if CFG_TUD_CDC
    cdc_task();
#endif
#if CFG_TUD_HID
    // hid_task();
#endif
  }

  return 0;
}

#if CFG_TUD_CDC

//--------------------------------------------------------------------+
// USB CDC
//--------------------------------------------------------------------+
#define TIMEOUT         1000
void cdc_task(void)
{
  // connected() check for DTR bit
  // Most but not all terminal client set this when making connection
  // if ( tud_cdc_connected() )
  {
    // connected and there are data available
    if ( tud_cdc_available() )
    {
      char rbuf[64];
      uint32_t rcount = tud_cdc_read(rbuf, sizeof(rbuf));

      uart_write_blocking(uart, (const uint8_t *)&rbuf, rcount);
#if DEBUG_CDC
      if (rcount > 0)
      {
        printf("<-%.*s\n", rcount, rbuf);
      }
#endif
    }
    if (uart_is_readable(uart))
    {
      char wbuf[64];
      uint32_t wcount = 0;
      while (uart_is_readable(uart)) {
          uart_read_blocking(uart, (uint8_t *)&wbuf[wcount], (size_t)1);
          wcount ++;
          if (wcount == 64) {
              break;
          }
      }
      // int timeout = 0;
      while (wcount > tud_cdc_write_available()) {
          // busy_wait_us_32(1000);
          // timeout ++;
          // if (timeout >= TIMEOUT) {
          //     break;
          // }
      }
      tud_cdc_write((const char *)&wbuf, wcount);
      tud_cdc_write_flush();
#if DEBUG_CDC
      if (wcount > 0) {
        printf("->%.*s\n", wcount, wbuf);
      }
#endif
    }
  }
}

// Invoked when cdc when line state changed e.g connected/disconnected
void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts)
{
  (void) itf;
  (void) rts;

  // TODO set some indicator
  if ( dtr )
  {
    // Terminal connected
  }else
  {
    // Terminal disconnected
  }
}

// Invoked when CDC interface received data from host
void tud_cdc_rx_cb(uint8_t itf)
{
  (void) itf;
}
#endif

#if CFG_TUD_HID
//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
  // TODO not Implemented
  (void) itf;
  (void) report_id;
  (void) report_type;
  (void) buffer;
  (void) reqlen;

  return 0;
}

static void store_unicode_str(uint8_t *dst, uint8_t *src, size_t size) {
    while (size-- > 0) {
        *dst++ = *src++;
        *dst++ = 0;
    }
    *dst++ = 0;
    *dst++ = 0;
}

static void handle_read_flash_data(uint8_t sub_cmd, uint8_t *res) {
    uint8_t len;
    res[0] = (uint8_t)MCP2221_RFD;
    res[1] = 0;
    switch (sub_cmd) {
    case MCP2221_RFD_READ_CHIP_SETTINGS:
        break;
    case MCP2221_RFD_READ_GP_SETTINGS:
        break;
    case MCP2221_RFD_READ_MF_DESC_STR:
        len = (uint8_t)strlen((const char *)string_desc_arr[1]);
        res[2] = len * 2 + 2;
        res[3] = 0x03;
        store_unicode_str((uint8_t *)&res[4], (uint8_t *)string_desc_arr[1], (size_t)len);
        break;
    case MCP2221_RFD_READ_PR_DESC_STR:
        len = (uint8_t)strlen((const char *)string_desc_arr[2]);
        res[2] = len * 2 + 2;
        res[3] = 0x03;
        store_unicode_str((uint8_t *)&res[4], (uint8_t *)string_desc_arr[2], (size_t)len);
        break;
    case MCP2221_RFD_READ_SN_DESC_STR:
        len = (uint8_t)strlen((const char *)MCP2221_SN_DESC);
        res[2] =  len * 2 + 2;
        res[3] = 0x03;
        store_unicode_str((uint8_t *)&res[4], (uint8_t *)MCP2221_SN_DESC, (size_t)len);
        break;
    case MCP2221_RFD_READ_CHIP_SN:
        len = (uint8_t)strlen((const char *)MCP2221_CHIP_SN);
        res[2] = len;
        memcpy((void *)&res[4], (const void *)MCP2221_CHIP_SN, (size_t)len);
        break;
    default:
        break;
    }
}

static void handle_i2c(uint8_t const *buf, uint8_t *res) {
    int flag = 0;
    uint16_t size = (uint16_t)buf[1] + (((uint16_t)buf[2]) << 8);
    uint16_t len = size;
    uint8_t addr = buf[3] >> 1;
    res[0] = buf[0];
    switch (buf[0]) {
    case MCP2221_I2C_WD:
        if (size > MCP2221_DATA_MAX) {
            len = MCP2221_DATA_MAX;
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            res[1] = MCP2221_CMD_BUSY;
            m_i2c_buf_write_index += len;
        } else {
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            m_i2c_buf_write_index += len;
            i2c->restart_on_next = false;
            if (len == 0) {
                uint8_t dummy = 0;
#if MCP2221_USE_TIMEOUT
                flag =  i2c_write_timeout_us(i2c, addr, (const uint8_t *)&dummy, (size_t)1, false, MCP2221_TIMEOUT);
#else
                flag = i2c_write_blocking(i2c, addr, (const uint8_t *)&dummy, (size_t)1, false);
#endif
            } else {
                flag = i2c_write_blocking(i2c, addr, (const uint8_t *)&m_i2c_buf[0], (size_t)m_i2c_buf_write_index, false);
            }
            if (flag < 0) {
                res[1] = MCP2221_CMD_BUSY;
            } else {
                res[1] = MCP2221_CMD_SUCCESS;
            }
            m_i2c_buf_write_index = 0;
        }
        break;
    case MCP2221_I2C_WD_RS:
        if (size > MCP2221_DATA_MAX) {
            len = MCP2221_DATA_MAX;
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            res[1] = MCP2221_CMD_BUSY;
            res[2] = 0x10;
            m_i2c_buf_write_index += len;
        } else {
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            m_i2c_buf_write_index += len;
            i2c->restart_on_next = true;
            flag - i2c_write_blocking(i2c, addr, (const uint8_t *)&m_i2c_buf[0], (size_t)m_i2c_buf_write_index, false);
            if (flag < 0) {
                res[1] = MCP2221_CMD_BUSY;
            } else {
                res[1] = MCP2221_CMD_SUCCESS;
                res[2] = 0x40;
            }
            m_i2c_buf_write_index = 0;
        }
        break;
    case MCP2221_I2C_WD_NS:
        if (size > MCP2221_DATA_MAX) {
            len = MCP2221_DATA_MAX;
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            res[1] = MCP2221_CMD_BUSY;
            m_i2c_buf_write_index += len;
        } else {
            memcpy((void *)&m_i2c_buf[m_i2c_buf_write_index], (const void *)&buf[4], (size_t)len);
            m_i2c_buf_write_index += len;
            i2c->restart_on_next = false;
            flag = i2c_write_blocking(i2c, addr, (const uint8_t *)&m_i2c_buf[0], (size_t)m_i2c_buf_write_index, true);
            if (flag < 0) {
                res[1] = MCP2221_CMD_BUSY;
            } else {
                res[1] = MCP2221_CMD_SUCCESS;
            }
            m_i2c_buf_write_index = 0;
        }
        break;
    case MCP2221_I2C_RD:
        if (!m_i2c_read_start) {
            m_i2c_read_start = true;
            i2c->restart_on_next = false;
#if MCP2221_USE_TIMEOUT
            flag = i2c_read_timeout_us(i2c, addr, (uint8_t *)&m_i2c_buf[0], (size_t)size, false, MCP2221_TIMEOUT);
#else
            flag = i2c_read_blocking(i2c, addr, (uint8_t *)&m_i2c_buf[0], (size_t)size, false);
#endif
            if (flag < 0) {
                res[1] = MCP2221_CMD_BUSY;
                m_i2c_read_start = false;
                break;
            }
            m_i2c_buf_read_index = 0;
            m_i2c_buf_stored_size = size;
        }
        if (size > MCP2221_DATA_MAX) {
            res[1] = MCP2221_CMD_BUSY;
        } else {
            res[1] = MCP2221_CMD_SUCCESS;
            m_i2c_read_start = false;
        }
        break;
    case MCP2221_I2C_RD_RS:
        if (!m_i2c_read_start) {
            m_i2c_read_start = true;
            i2c->restart_on_next = false;
            flag = i2c_read_blocking(i2c, addr, (uint8_t *)&m_i2c_buf[0], (size_t)size, false);
            if (flag < 0) {
                res[1] = MCP2221_CMD_BUSY;
                break;
            }
        }
        if (size > MCP2221_DATA_MAX) {
            res[1] = MCP2221_CMD_BUSY;
        } else {
            res[1] = MCP2221_CMD_SUCCESS;
            m_i2c_read_start = false;
        }
        break;
    case MCP2221_I2C_RD_GET_I2C_DATA:
        len = m_i2c_buf_stored_size - m_i2c_buf_read_index;
        if (len == 0) {
            res[1] = 0x41;
            res[3] = 0x00;
            break;
        }
        if (len > MCP2221_DATA_MAX) {
            len = MCP2221_DATA_MAX;
        }
        memcpy((void *)&res[4], (const void *)&m_i2c_buf[m_i2c_buf_read_index], (size_t)len);
        res[2] = 0x00;
        res[1] = MCP2221_CMD_SUCCESS;
        res[3] = len;
        m_i2c_buf_read_index += len;
        break;
    default:
        break;
    }
}

static void handle_default(uint8_t const *buf, uint8_t *res) {
    res[0] = buf[0];
    res[1] = MCP2221_CMD_SUCCESS;
}

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize)
{
  // This example doesn't use multiple report and report ID
  (void) itf;
  (void) report_id;
  (void) report_type;

#if DEBUG_HID
  printf("buf:%02x %02x %02x %02x %02x %02x %02x %02x\n",
          buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7]);
#endif
  memset((void *)m_response, 0, (size_t)MCP2221_RESPONSE_SIZE);
  switch (buffer[0]) {
  case MCP2221_RFD:
      handle_read_flash_data(buffer[1], (uint8_t *)m_response);
      break;
  case MCP2221_I2C_WD:
  case MCP2221_I2C_WD_RS:
  case MCP2221_I2C_WD_NS:
  case MCP2221_I2C_RD:
  case MCP2221_I2C_RD_RS:
  case MCP2221_I2C_RD_GET_I2C_DATA:
      handle_i2c(buffer, (uint8_t *)m_response);
      break;
  default:
      handle_default(buffer, (uint8_t *)m_response);
      break;
  }
#if DEBUG_HID
  printf("res:%02x %02x %02x %02x %02x %02x %02x %02x\n",
          m_response[0], m_response[1], m_response[2], m_response[3], m_response[4], m_response[5], m_response[6], m_response[7]);
#endif
  // echo back anything we received from host
  tud_hid_report(0, m_response, sizeof(m_response));
}
#endif

#if USE_LED_BLINKING_TASK
//--------------------------------------------------------------------+
// BLINKING TASK
//--------------------------------------------------------------------+
void led_blinking_task(void)
{
  static uint32_t start_ms = 0;
  static bool led_state = false;

  // Blink every interval ms
  if ( board_millis() - start_ms < blink_interval_ms) return; // not enough time
  start_ms += blink_interval_ms;

  board_led_write(led_state);
  led_state = 1 - led_state; // toggle
}
#endif
