// Copyright 2015-2020 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "USBCDC.h"

#if SOC_USB_OTG_SUPPORTED
#include "USB.h"
#if CONFIG_TINYUSB_CDC_ENABLED

#include "esp32-hal-tinyusb.h"
#include "rom/ets_sys.h"

ESP_EVENT_DEFINE_BASE(ARDUINO_USB_CDC_EVENTS);
esp_err_t arduino_usb_event_post(esp_event_base_t event_base, int32_t event_id, void *event_data, size_t event_data_size, TickType_t ticks_to_wait);
esp_err_t arduino_usb_event_handler_register_with(esp_event_base_t event_base, int32_t event_id, esp_event_handler_t event_handler, void *event_handler_arg);

USBCDC *devices[CFG_TUD_CDC];

static uint16_t load_cdc_descriptor(uint8_t *dst, uint8_t *itf) {
  uint8_t str_index = tinyusb_add_string_descriptor("TinyUSB CDC");
  uint8_t descriptor[TUD_CDC_DESC_LEN] = {// Interface number, string index, EP notification address and size, EP data address (out, in) and size.
                                          TUD_CDC_DESCRIPTOR(*itf, str_index, 0x85, CFG_TUD_ENDOINT_SIZE, 0x03, 0x84, CFG_TUD_ENDOINT_SIZE)
  };
  *itf += 2;
  memcpy(dst, descriptor, TUD_CDC_DESC_LEN);
  return TUD_CDC_DESC_LEN;
}

static uint16_t load_cdc_descriptor2(uint8_t *dst, uint8_t *itf) {
  uint8_t str_index = tinyusb_add_string_descriptor("TinyUSB CDC2");
  uint8_t ep_ntfy = tinyusb_get_free_in_endpoint();
  TU_VERIFY(ep_ntfy != 0);
  uint8_t ep_in = tinyusb_get_free_in_endpoint();
  TU_VERIFY(ep_in != 0);
  uint8_t ep_out = tinyusb_get_free_out_endpoint();
  TU_VERIFY(ep_out != 0);
  uint8_t descriptor[TUD_CDC_DESC_LEN] = {
    // Interface number, string index, EP notification address and size, EP data address (out, in) and size.
    TUD_CDC_DESCRIPTOR(*itf, str_index, (uint8_t)(0x80 | ep_ntfy), CFG_TUD_ENDOINT_SIZE, ep_out, (uint8_t)(0x80 | ep_in), CFG_TUD_ENDOINT_SIZE)
  };
  *itf += 2;
  memcpy(dst, descriptor, TUD_CDC_DESC_LEN);
  return TUD_CDC_DESC_LEN;
}

// Invoked when line state DTR & RTS are changed via SET_CONTROL_LINE_STATE
void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) {
  //log_v("ITF: %u, DTR: %u, RTS: %u", itf, dtr, rts);
  if (itf < CFG_TUD_CDC && devices[itf] != NULL) {
    devices[itf]->_onLineState(dtr, rts);
  }
}

// Invoked when line coding is change via SET_LINE_CODING
void tud_cdc_line_coding_cb(uint8_t itf, cdc_line_coding_t const *p_line_coding) {
  //log_v("ITF: %u, BITRATE: %lu, STOP_BITS: %u, PARITY: %u, DATA_BITS: %u", itf, p_line_coding->bit_rate, p_line_coding->stop_bits, p_line_coding->parity, p_line_coding->data_bits);
  if (itf < CFG_TUD_CDC && devices[itf] != NULL) {
    devices[itf]->_onLineCoding(p_line_coding->bit_rate, p_line_coding->stop_bits, p_line_coding->parity, p_line_coding->data_bits);
  }
}

// Invoked when received new data
void tud_cdc_rx_cb(uint8_t itf) {
  //log_v("ITF: %u", itf);
  if (itf < CFG_TUD_CDC && devices[itf] != NULL) {
    devices[itf]->_onRX();
  }
}

// Invoked when received send break
void tud_cdc_send_break_cb(uint8_t itf, uint16_t duration_ms) {
  //log_v("itf: %u, duration_ms: %u", itf, duration_ms);
}

// Invoked when space becomes available in TX buffer
void tud_cdc_tx_complete_cb(uint8_t itf) {
  if (itf < CFG_TUD_CDC && devices[itf] != NULL) {
    devices[itf]->_onTX();
  }
}

static void ARDUINO_ISR_ATTR cdc0_write_char(char c) {
  if (CFG_TUD_CDC && devices[0] != NULL) {
    tud_cdc_n_write_char(0, c);
  }
}

static void usb_unplugged_cb(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
  ((USBCDC *)arg)->_onUnplugged();
}

USBCDC::USBCDC(uint8_t itfn)
  : itf(itfn), bit_rate(0), stop_bits(0), parity(0), data_bits(0), dtr(false), rts(false), connected(false), reboot_enable(true), rx_queue(NULL), tx_lock(NULL),
    tx_timeout_ms(250) {
  if (itf < CFG_TUD_CDC) {
    if (itf == 0) {
      tinyusb_enable_interface(USB_INTERFACE_CDC, TUD_CDC_DESC_LEN, load_cdc_descriptor);
    } else {
      tinyusb_enable_interface(USB_INTERFACE_CDC2, TUD_CDC_DESC_LEN, load_cdc_descriptor2);
    }
    arduino_usb_event_handler_register_with(ARDUINO_USB_EVENTS, ARDUINO_USB_STOPPED_EVENT, usb_unplugged_cb, this);
  } else {
    log_e("Maximum of %u CDC devices are supported", CFG_TUD_CDC);
  }
}

USBCDC::~USBCDC() {
  end();
}

void USBCDC::onEvent(esp_event_handler_t callback) {
  onEvent(ARDUINO_USB_CDC_ANY_EVENT, callback);
}
void USBCDC::onEvent(arduino_usb_cdc_event_t event, esp_event_handler_t callback) {
  arduino_usb_event_handler_register_with(ARDUINO_USB_CDC_EVENTS, event, callback, this);
}

size_t USBCDC::setRxBufferSize(size_t rx_queue_len) {
  size_t currentQueueSize = rx_queue ? uxQueueSpacesAvailable(rx_queue) + uxQueueMessagesWaiting(rx_queue) : 0;

  if (rx_queue_len != currentQueueSize) {
    QueueHandle_t new_rx_queue = NULL;
    if (rx_queue_len) {
      new_rx_queue = xQueueCreate(rx_queue_len, sizeof(uint8_t));
      if (!new_rx_queue) {
        log_e("CDC Queue creation failed.");
        return 0;
      }
      if (rx_queue) {
        size_t copySize = uxQueueMessagesWaiting(rx_queue);
        if (copySize > 0) {
          for (size_t i = 0; i < copySize; i++) {
            uint8_t ch = 0;
            xQueueReceive(rx_queue, &ch, 0);
            if (!xQueueSend(new_rx_queue, &ch, 0)) {
              arduino_usb_cdc_event_data_t p;
              p.rx_overflow.dropped_bytes = copySize - i;
              arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_RX_OVERFLOW_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
              log_e("CDC RX Overflow.");
              break;
            }
          }
        }
        vQueueDelete(rx_queue);
      }
      rx_queue = new_rx_queue;
      return rx_queue_len;
    } else {
      if (rx_queue) {
        vQueueDelete(rx_queue);
        rx_queue = NULL;
      }
    }
  }
  return rx_queue_len;
}

void USBCDC::begin(unsigned long baud) {
  if (itf >= CFG_TUD_CDC) {
    return;
  }
  if (tx_lock == NULL) {
    tx_lock = xSemaphoreCreateMutex();
  }
  // if rx_queue was set before begin(), keep it
  if (!rx_queue) {
    setRxBufferSize(256);  //default if not preset
  }
  devices[itf] = this;
}

void USBCDC::end() {
  if (itf >= CFG_TUD_CDC) {
    return;
  }
  connected = false;
  devices[itf] = NULL;
  setRxBufferSize(0);
  if (tx_lock != NULL) {
    vSemaphoreDelete(tx_lock);
    tx_lock = NULL;
  }
}

void USBCDC::setTxTimeoutMs(uint32_t timeout) {
  tx_timeout_ms = timeout;
}

void USBCDC::_onUnplugged(void) {
  if (connected) {
    connected = false;
    dtr = false;
    rts = false;
    arduino_usb_cdc_event_data_t p;
    arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_DISCONNECTED_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
  }
}

enum {
  CDC_LINE_IDLE,
  CDC_LINE_1,
  CDC_LINE_2,
  CDC_LINE_3
};
void USBCDC::_onLineState(bool _dtr, bool _rts) {
  static uint8_t lineState = CDC_LINE_IDLE;

  if (dtr == _dtr && rts == _rts) {
    return;  // Skip duplicate events
  }

  dtr = _dtr;
  rts = _rts;

  if (reboot_enable) {
    if (!dtr && rts) {
      if (lineState == CDC_LINE_IDLE) {
        lineState++;
        if (connected) {
          connected = false;
          arduino_usb_cdc_event_data_t p;
          arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_DISCONNECTED_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
        }
        // } else if(lineState == CDC_LINE_2){//esptool.js
        //     lineState++;
      } else {
        lineState = CDC_LINE_IDLE;
      }
    } else if (dtr && rts) {
      if (lineState == CDC_LINE_1) {
        lineState++;
      } else {
        lineState = CDC_LINE_IDLE;
      }
    } else if (dtr && !rts) {
      if (lineState == CDC_LINE_2) {
        lineState++;
        // } else if(lineState == CDC_LINE_IDLE){//esptool.js
        //     lineState++;
      } else {
        lineState = CDC_LINE_IDLE;
      }
    } else if (!dtr && !rts) {
      if (lineState == CDC_LINE_3) {
        usb_persist_restart(RESTART_BOOTLOADER);
      } else {
        lineState = CDC_LINE_IDLE;
      }
    }
  }

  if (lineState == CDC_LINE_IDLE) {
    if (dtr && rts && !connected) {
      connected = true;
      arduino_usb_cdc_event_data_t p;
      arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_CONNECTED_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
    } else if (!dtr && connected) {
      connected = false;
      arduino_usb_cdc_event_data_t p;
      arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_DISCONNECTED_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
    }
    arduino_usb_cdc_event_data_t l;
    l.line_state.dtr = dtr;
    l.line_state.rts = rts;
    arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_LINE_STATE_EVENT, &l, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
  }
}

void USBCDC::_onLineCoding(uint32_t _bit_rate, uint8_t _stop_bits, uint8_t _parity, uint8_t _data_bits) {
  if (bit_rate != _bit_rate || data_bits != _data_bits || stop_bits != _stop_bits || parity != _parity) {
    // ArduinoIDE sends LineCoding with 1200bps baud to reset the device
    if (reboot_enable && _bit_rate == 1200) {
      usb_persist_restart(RESTART_BOOTLOADER);
    } else {
      bit_rate = _bit_rate;
      data_bits = _data_bits;
      stop_bits = _stop_bits;
      parity = _parity;
      arduino_usb_cdc_event_data_t p;
      p.line_coding.bit_rate = bit_rate;
      p.line_coding.data_bits = data_bits;
      p.line_coding.stop_bits = stop_bits;
      p.line_coding.parity = parity;
      arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_LINE_CODING_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
    }
  }
}

void USBCDC::_onRX() {
  arduino_usb_cdc_event_data_t p;
  uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];
  uint32_t count = tud_cdc_n_read(itf, buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE);
  for (uint32_t i = 0; i < count; i++) {
    if (rx_queue == NULL || !xQueueSend(rx_queue, buf + i, 10)) {
      p.rx_overflow.dropped_bytes = count - i;
      arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_RX_OVERFLOW_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
      log_e("CDC RX Overflow.");
      count = i;
      break;
    }
  }
  if (count) {
    p.rx.len = count;
    arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_RX_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
  }
}

void USBCDC::_onTX() {
  arduino_usb_cdc_event_data_t p;
  arduino_usb_event_post(ARDUINO_USB_CDC_EVENTS, ARDUINO_USB_CDC_TX_EVENT, &p, sizeof(arduino_usb_cdc_event_data_t), portMAX_DELAY);
}

void USBCDC::enableReboot(bool enable) {
  reboot_enable = enable;
}
bool USBCDC::rebootEnabled(void) {
  return reboot_enable;
}

int USBCDC::available(void) {
  if (itf >= CFG_TUD_CDC || rx_queue == NULL) {
    return -1;
  }
  return uxQueueMessagesWaiting(rx_queue);
}

int USBCDC::peek(void) {
  if (itf >= CFG_TUD_CDC || rx_queue == NULL) {
    return -1;
  }
  uint8_t c;
  if (xQueuePeek(rx_queue, &c, 0)) {
    return c;
  }
  return -1;
}

int USBCDC::read(void) {
  if (itf >= CFG_TUD_CDC || rx_queue == NULL) {
    return -1;
  }
  uint8_t c = 0;
  if (xQueueReceive(rx_queue, &c, 0)) {
    return c;
  }
  return -1;
}

size_t USBCDC::read(uint8_t *buffer, size_t size) {
  if (itf >= CFG_TUD_CDC || rx_queue == NULL) {
    return -1;
  }
  uint8_t c = 0;
  size_t count = 0;
  while (count < size && xQueueReceive(rx_queue, &c, 0)) {
    buffer[count++] = c;
  }
  return count;
}

void USBCDC::flush(void) {
  if (itf >= CFG_TUD_CDC || tx_lock == NULL || !tud_cdc_n_connected(itf)) {
    return;
  }
  if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) {
    return;
  }
  tud_cdc_n_write_flush(itf);
  xSemaphoreGive(tx_lock);
}

int USBCDC::availableForWrite(void) {
  if (itf >= CFG_TUD_CDC || tx_lock == NULL || !tud_cdc_n_connected(itf)) {
    return 0;
  }
  if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) {
    return 0;
  }
  size_t a = tud_cdc_n_write_available(itf);
  xSemaphoreGive(tx_lock);
  return a;
}

size_t USBCDC::write(const uint8_t *buffer, size_t size) {
  if (itf >= CFG_TUD_CDC || tx_lock == NULL || buffer == NULL || size == 0 || !tud_cdc_n_connected(itf)) {
    return 0;
  }
  if (xPortInIsrContext()) {
    BaseType_t taskWoken = false;
    if (xSemaphoreTakeFromISR(tx_lock, &taskWoken) != pdPASS) {
      return 0;
    }
  } else if (xSemaphoreTake(tx_lock, tx_timeout_ms / portTICK_PERIOD_MS) != pdPASS) {
    return 0;
  }
  size_t to_send = size, so_far = 0;
  // writeTimeout will prevent that TinyUSB failure locks the while(to_send) loop
  uint32_t writeTimeout = millis() + tx_timeout_ms;
  while (to_send) {
    if (!tud_cdc_n_connected(itf) || (int32_t)(millis() - writeTimeout) >= 0) {
      log_e("USB is disconnected or CDC writing has timed out.");
      size = so_far;
      break;
    }
    size_t space = tud_cdc_n_write_available(itf);
    if (!space) {
      tud_cdc_n_write_flush(itf);
      continue;
    }
    if (space > to_send) {
      space = to_send;
    }
    size_t sent = tud_cdc_n_write(itf, buffer + so_far, space);
    if (sent) {
      so_far += sent;
      to_send -= sent;
      tud_cdc_n_write_flush(itf);
    } else {
      size = so_far;
      break;
    }
  }
  if (xPortInIsrContext()) {
    BaseType_t taskWoken = false;
    xSemaphoreGiveFromISR(tx_lock, &taskWoken);
  } else {
    xSemaphoreGive(tx_lock);
  }
  return size;
}

size_t USBCDC::write(uint8_t c) {
  return write(&c, 1);
}

uint32_t USBCDC::baudRate() {
  return bit_rate;
}

void USBCDC::setDebugOutput(bool en) {
  if (itf) {
    return;
  }
  if (en) {
    uartSetDebug(NULL);
    ets_install_putc2((void (*)(char)) & cdc0_write_char);
  } else {
    ets_install_putc2(NULL);
  }
  ets_install_putc1(NULL);  // closes UART log output
}

USBCDC::operator bool() const {
  if (itf >= CFG_TUD_CDC) {
    return false;
  }
  return connected;
}

#if !ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT  // Native USB CDC selected
// USBSerial is always available to be used
USBCDC USBSerial(0);
#endif

#endif /* CONFIG_TINYUSB_CDC_ENABLED */
#endif /* SOC_USB_OTG_SUPPORTED */
