// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_uart.h"
#include "esphome/core/log.h"
#include "esphome/components/uart/uart_debugger.h"

#include <cinttypes>

namespace esphome {
namespace usb_uart {

/**
 *
 * Given a configuration, look for the required interfaces defining a CDC-ACM device
 * @param config_desc The configuration descriptor
 * @param intf_idx The index of the interface to be examined
 * @return
 */
static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
  int conf_offset, ep_offset;
  const usb_ep_desc_t *notify_ep{}, *in_ep{}, *out_ep{};
  uint8_t interface_number = 0;
  // look for an interface with one interrupt endpoint (notify), and an interface with two bulk endpoints (data in/out)
  for (;;) {
    auto intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
    if (!intf_desc) {
      ESP_LOGE(TAG, "usb_parse_interface_descriptor failed");
      return nullopt;
    }
    if (intf_desc->bNumEndpoints == 1) {
      ep_offset = conf_offset;
      notify_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
      if (!notify_ep) {
        ESP_LOGE(TAG, "notify_ep: usb_parse_endpoint_descriptor_by_index failed");
        return nullopt;
      }
      if (notify_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_INT)
        notify_ep = nullptr;
    } else if (USB_CLASS_CDC_DATA && intf_desc->bNumEndpoints == 2) {
      interface_number = intf_desc->bInterfaceNumber;
      ep_offset = conf_offset;
      out_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
      if (!out_ep) {
        ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed");
        return nullopt;
      }
      if (out_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
        out_ep = nullptr;
      ep_offset = conf_offset;
      in_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 1, config_desc->wTotalLength, &ep_offset);
      if (!in_ep) {
        ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
        return nullopt;
      }
      if (in_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
        in_ep = nullptr;
    }
    if (in_ep != nullptr && out_ep != nullptr && notify_ep != nullptr)
      break;
  }
  if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN)
    return CdcEps{notify_ep, in_ep, out_ep, interface_number};
  return CdcEps{notify_ep, out_ep, in_ep, interface_number};
}

std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t dev_hdl) {
  const usb_config_desc_t *config_desc;
  const usb_device_desc_t *device_desc;
  int desc_offset = 0;
  std::vector<CdcEps> cdc_devs{};

  // Get required descriptors
  if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
    ESP_LOGE(TAG, "get_device_descriptor failed");
    return {};
  }
  if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
    ESP_LOGE(TAG, "get_active_config_descriptor failed");
    return {};
  }
  if (device_desc->bDeviceClass == USB_CLASS_COMM) {
    // single CDC-ACM device
    if (auto eps = get_cdc(config_desc, 0)) {
      ESP_LOGV(TAG, "Found CDC-ACM device");
      cdc_devs.push_back(*eps);
    }
    return cdc_devs;
  }
  if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) &&
       (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) ||
      ((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) &&
       (device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) {
    // This is a composite device, that uses Interface Association Descriptor
    const auto *this_desc = reinterpret_cast<const usb_standard_desc_t *>(config_desc);
    for (;;) {
      this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength,
                                                    USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset);
      if (!this_desc)
        break;
      const auto *iad_desc = reinterpret_cast<const usb_iad_desc_t *>(this_desc);

      if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) {
        ESP_LOGV(TAG, "Found CDC-ACM device in composite device");
        if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface))
          cdc_devs.push_back(*eps);
      }
    }
  }
  return cdc_devs;
}

void RingBuffer::push(uint8_t item) {
  this->buffer_[this->insert_pos_] = item;
  this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
}
void RingBuffer::push(const uint8_t *data, size_t len) {
  for (size_t i = 0; i != len; i++) {
    this->buffer_[this->insert_pos_] = *data++;
    this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
  }
}

uint8_t RingBuffer::pop() {
  uint8_t item = this->buffer_[this->read_pos_];
  this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
  return item;
}
size_t RingBuffer::pop(uint8_t *data, size_t len) {
  len = std::min(len, this->get_available());
  for (size_t i = 0; i != len; i++) {
    *data++ = this->buffer_[this->read_pos_];
    this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
  }
  return len;
}
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
  if (!this->initialised_) {
    ESP_LOGV(TAG, "Channel not initialised - write ignored");
    return;
  }
  while (this->output_buffer_.get_free_space() != 0 && len-- != 0) {
    this->output_buffer_.push(*data++);
  }
  len++;
  if (len > 0) {
    ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len);
  }
  this->parent_->start_output(this);
}

bool USBUartChannel::peek_byte(uint8_t *data) {
  if (this->input_buffer_.is_empty()) {
    return false;
  }
  *data = this->input_buffer_.peek();
  return true;
}
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
  if (!this->initialised_) {
    ESP_LOGV(TAG, "Channel not initialised - read ignored");
    return false;
  }
  auto available = this->available();
  bool status = true;
  if (len > available) {
    ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available);
    len = available;
    status = false;
  }
  for (size_t i = 0; i != len; i++) {
    *data++ = this->input_buffer_.pop();
  }
  this->parent_->start_input(this);
  return status;
}
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() { USBClient::loop(); }
void USBUartComponent::dump_config() {
  USBClient::dump_config();
  for (auto &channel : this->channels_) {
    ESP_LOGCONFIG(TAG,
                  "  UART Channel %d\n"
                  "    Baud Rate: %" PRIu32 " baud\n"
                  "    Data Bits: %u\n"
                  "    Parity: %s\n"
                  "    Stop bits: %s\n"
                  "    Debug: %s\n"
                  "    Dummy receiver: %s",
                  channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_],
                  STOP_BITS_NAMES[channel->stop_bits_], YESNO(channel->debug_), YESNO(channel->dummy_receiver_));
  }
}
void USBUartComponent::start_input(USBUartChannel *channel) {
  if (!channel->initialised_ || channel->input_started_ ||
      channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
    return;
  auto ep = channel->cdc_dev_.in_ep;
  auto callback = [this, channel](const usb_host::TransferStatus &status) {
    ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
    if (!status.success) {
      ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
      return;
    }
#ifdef USE_UART_DEBUGGER
    if (channel->debug_) {
      uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
                               std::vector<uint8_t>(status.data, status.data + status.data_len), ',');  // NOLINT()
    }
#endif
    channel->input_started_ = false;
    if (!channel->dummy_receiver_) {
      for (size_t i = 0; i != status.data_len; i++) {
        channel->input_buffer_.push(status.data[i]);
      }
    }
    if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
      this->defer([this, channel] { this->start_input(channel); });
    }
  };
  channel->input_started_ = true;
  this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
}

void USBUartComponent::start_output(USBUartChannel *channel) {
  if (channel->output_started_)
    return;
  if (channel->output_buffer_.is_empty()) {
    return;
  }
  auto ep = channel->cdc_dev_.out_ep;
  auto callback = [this, channel](const usb_host::TransferStatus &status) {
    ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
    channel->output_started_ = false;
    this->defer([this, channel] { this->start_output(channel); });
  };
  channel->output_started_ = true;
  uint8_t data[ep->wMaxPacketSize];
  auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
  this->transfer_out(ep->bEndpointAddress, callback, data, len);
#ifdef USE_UART_DEBUGGER
  if (channel->debug_) {
    uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector<uint8_t>(data, data + len), ',');  // NOLINT()
  }
#endif
  ESP_LOGV(TAG, "Output %d bytes started", len);
}

/**
 * Hacky fix for some devices that report incorrect MPS values
 * @param ep The endpoint descriptor
 */
static void fix_mps(const usb_ep_desc_t *ep) {
  if (ep != nullptr) {
    auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep);
    if (ep->wMaxPacketSize > 64) {
      ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize);
      ep_mutable->wMaxPacketSize = 64;
    }
  }
}
void USBUartTypeCdcAcm::on_connected() {
  auto cdc_devs = this->parse_descriptors_(this->device_handle_);
  if (cdc_devs.empty()) {
    this->status_set_error("No CDC-ACM device found");
    this->disconnect();
    return;
  }
  ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size());
  auto i = 0;
  for (auto channel : this->channels_) {
    if (i == cdc_devs.size()) {
      ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
      this->status_set_warning("No configuration found for channel");
      break;
    }
    channel->cdc_dev_ = cdc_devs[i++];
    fix_mps(channel->cdc_dev_.in_ep);
    fix_mps(channel->cdc_dev_.out_ep);
    channel->initialised_ = true;
    auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number, 0);
    if (err != ESP_OK) {
      ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
               channel->cdc_dev_.interface_number);
      this->status_set_error("usb_host_interface_claim failed");
      this->disconnect();
      return;
    }
  }
  this->enable_channels();
}

void USBUartTypeCdcAcm::on_disconnected() {
  for (auto channel : this->channels_) {
    if (channel->cdc_dev_.in_ep != nullptr) {
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
    }
    if (channel->cdc_dev_.out_ep != nullptr) {
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
    }
    if (channel->cdc_dev_.notify_ep != nullptr) {
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
    }
    usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number);
    channel->initialised_ = false;
    channel->input_started_ = false;
    channel->output_started_ = false;
    channel->input_buffer_.clear();
    channel->output_buffer_.clear();
  }
  USBClient::on_disconnected();
}

void USBUartTypeCdcAcm::enable_channels() {
  for (auto channel : this->channels_) {
    if (!channel->initialised_)
      continue;
    channel->input_started_ = false;
    channel->output_started_ = false;
    this->start_input(channel);
  }
}

}  // namespace usb_uart
}  // namespace esphome
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
