/**
 * @file example.cpp
 * @brief I3C Hot-Join + IBI Translator Example (Blocking C++ API)
 *
 * This example demonstrates I3C Hot-Join and In-Band Interrupt workflows using:
 * - Binho Supernova
 * - Microchip PIC18F16Q20 Curiosity Nano
 * - Binho PIC18F16Q20 Curiosity Nano I3C Target Demo Board
 * - Adafruit I2C FRAM breakout board
 * - On-board W25Q64JV SPI flash (on the Binho demo board)
 *
 * Firmware context:
 * The PIC firmware acts as an I3C-to-I2C/SPI translator. After I3C controller
 * initialization, power up the PIC so it performs Hot-Join.
 *
 * Workflow:
 * 1. Connect to the adapter and initialize I3C.
 * 2. Connect the PIC and wait for its Hot-Join notification to capture the dynamic address.
 * 3. Issue I3C SDR commands that request downstream I2C transfers (write/read).
 * 4. Receive read results through IBI notifications.
 * 5. Issue I3C SDR commands that request downstream SPI flash transfers
 *    (JEDEC ID read, status register write/read), and receive SPI-read data via IBI.
 *
 * Notes:
 * 1. The Supernova IBI payload is limited to 8 bytes total (MDB + data), therefore
 * read requests in this example are limited to <= 7 data bytes.
 * 2. Due to slow performance of the PIC firmware and the blocking nature of this example, fixed delays are used after
 * certain operations to ensure the PIC is ready for the next command.
 * 3. The MPLABX IDE project to build and flash the PIC firmware used in this example is delivered in the "assets" folder.
 */

#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <iostream>
#include <memory>
#include <mutex>
#include <vector>

#include "CosmicSDK.hpp"

struct SyncState {
  std::mutex mutex;
  std::condition_variable condition;
  bool hot_join_received = false;
  uint8_t dynamic_address = 0;
  uint32_t ibi_count = 0;
  std::vector<uint8_t> last_ibi_payload;
};

static bool checkResult(uint16_t code, const char* operation) {
  if (code != SUCCESS) {
    std::cerr << operation << " failed. ERROR CODE: " << code << std::endl;
    return false;
  }
  return true;
}

int main() {
  SyncState sync;
  uint32_t expected_ibi_count = 0;

  // Step 1: Create SDK instance and register I3C notifications.
  auto device = std::make_unique<CosmicSDK>(2000);
  device->onNotification([&](const Notification& notification) {
    if (const I3cHotJoinNotification* hot_join = notification.i3cHotJoin()) {
      {
        std::lock_guard<std::mutex> lock(sync.mutex);
        sync.hot_join_received = true;
        sync.dynamic_address = hot_join->dynamicAddress;
      }
      sync.condition.notify_all();
      std::cout << "Hot Join Notification: dynamic address 0x" << std::hex
                << static_cast<int>(hot_join->dynamicAddress) << std::dec << std::endl;
      return;
    }

    if (const I3cIbiNotification* ibi = notification.i3cIbi()) {
      {
        std::lock_guard<std::mutex> lock(sync.mutex);
        ++sync.ibi_count;
        sync.last_ibi_payload.assign(ibi->payload, ibi->payload + ibi->payloadLength);
      }
      sync.condition.notify_all();
      std::cout << "IBI Notification: address 0x" << std::hex << static_cast<int>(ibi->address)
                << " payloadLength " << std::dec << ibi->payloadLength << std::endl;
      return;
    }
  });

  // Step 2: Connect and print adapter information.
  if (!device->connect()) {
    std::cerr << "Failed to connect to a Binho Device!" << std::endl;
    return 1;
  }

  CosmicSDK::DeviceInfo device_information;
  if (device->getDeviceInfo(device_information) != SUCCESS) {
    std::cerr << "Failed to read device information!" << std::endl;
    device->disconnect();
    return 1;
  }
  std::cout << "Manufacturer: " << device_information.manufacturer << std::endl;
  std::cout << "Product name: " << device_information.productName << std::endl;
  std::cout << "Serial number: " << device_information.serialNumber << std::endl;
  std::cout << "FW version: " << device_information.fwVersion << std::endl;
  std::cout << "HW version: " << device_information.hwVersion << std::endl;

  // Step 3: Set I3C bus voltage and initialize controller/bus.
  uint16_t result_code = device->i3cSetBusVoltage(1200);
  if (!checkResult(result_code, "i3cSetBusVoltage")) {
    device->disconnect();
    return 1;
  }

  result_code = device->i3cControllerInit(I3cPushPullRate::PUSH_PULL_10_MHz_DC_40,
                                          I3cOpenDrainTransferRate::OPEN_DRAIN_400_KHz,
                                          I2cTransferRate::FAST_MODE, I3cDriveStrength::FAST);
  if (result_code != SUCCESS && result_code != FW_INTERFACE_ALREADY_INITIALIZED) {
    if (!checkResult(result_code, "i3cControllerInit")) {
      device->disconnect();
      return 1;
    }
  }

  result_code = device->i3cInitBus();
  if (result_code != SUCCESS && result_code != FW_I3C_BUS_INIT_NACK_RSTDAA) {
    if (!checkResult(result_code, "i3cInitBus")) {
      device->disconnect();
      return 1;
    }
  }

  // Step 4: Power the PIC after bus init and wait for Hot-Join.
  std::cout << "Power the PIC18F16Q20 target board now, waiting for Hot-Join..." << std::endl;
  {
    std::unique_lock<std::mutex> lock(sync.mutex);
    const bool got_hot_join = sync.condition.wait_for(lock, std::chrono::seconds(30), [&]() {
      return sync.hot_join_received;
    });
    if (!got_hot_join) {
      std::cerr << "Timed out waiting for Hot-Join notification" << std::endl;
      device->disconnect();
      return 1;
    }
  }

  const uint8_t dynamic_address = [&]() {
    std::lock_guard<std::mutex> lock(sync.mutex);
    return sync.dynamic_address;
  }();
  std::cout << "PIC dynamic address: 0x" << std::hex << static_cast<int>(dynamic_address)
            << std::dec << std::endl;

  // Pause briefly to ensure the PIC has completed its Hot-Join processing and is ready for transactions.
  std::this_thread::sleep_for(std::chrono::milliseconds(250));
  
  // Step 5: Request downstream I2C write/read transactions via translator.
  std::vector<uint8_t> i2c_write_payload = {0x40, 0xA0, 0x00, 0x00, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5};
  result_code = device->i3cSdrWrite(dynamic_address, {}, i2c_write_payload, I3cTransferFlags::I3C_XFER_NONE);
  if (!checkResult(result_code, "i3cSdrWrite I2C write")) {
    device->disconnect();
    return 1;
  }

  // Pause briefly to ensure the PIC has completed the I2C write before issuing the new request.
  std::this_thread::sleep_for(std::chrono::milliseconds(250));

  std::vector<uint8_t> i2c_reset_pointer = {0x40, 0xA0, 0x00, 0x00};
  result_code = device->i3cSdrWrite(dynamic_address, {}, i2c_reset_pointer, I3cTransferFlags::I3C_XFER_NONE);
  if (!checkResult(result_code, "i3cSdrWrite I2C pointer reset")) {
    device->disconnect();
    return 1;
  }

  // Pause briefly to ensure the PIC has completed the I2C pointer reset before issuing the read request.
  std::this_thread::sleep_for(std::chrono::milliseconds(250));

  std::vector<uint8_t> i2c_read_request = {0x20, 0xA0, 0x05};
  result_code = device->i3cSdrWrite(dynamic_address, {}, i2c_read_request, I3cTransferFlags::I3C_XFER_NONE);
  if (!checkResult(result_code, "i3cSdrWrite I2C read request")) {
    device->disconnect();
    return 1;
  }

  ++expected_ibi_count;
  {
    std::unique_lock<std::mutex> lock(sync.mutex);
    const bool got_ibi = sync.condition.wait_for(lock, std::chrono::seconds(5), [&]() {
      return sync.ibi_count >= expected_ibi_count;
    });
    if (!got_ibi) {
      std::cerr << "Timed out waiting for I2C-read IBI response" << std::endl;
      device->disconnect();
      return 1;
    }
    std::cout << "Last IBI payload: ";
    for (uint8_t byte : sync.last_ibi_payload) {
      std::cout << "0x" << std::hex << static_cast<int>(byte) << " ";
    }
    std::cout << std::dec << std::endl;
  }

  // Pause briefly to ensure the PIC is not busy.
  std::this_thread::sleep_for(std::chrono::milliseconds(250));

  // Step 6: Request downstream SPI transactions via translator.
  std::vector<uint8_t> spi_jedec_read = {0x60, 0x03, 0x9F};
  result_code = device->i3cSdrWrite(dynamic_address, {}, spi_jedec_read, I3cTransferFlags::I3C_XFER_NONE);
  if (!checkResult(result_code, "i3cSdrWrite SPI JEDEC read request")) {
    device->disconnect();
    return 1;
  }

  ++expected_ibi_count;
  {
    std::unique_lock<std::mutex> lock(sync.mutex);
    const bool got_ibi = sync.condition.wait_for(lock, std::chrono::seconds(5), [&]() {
      return sync.ibi_count >= expected_ibi_count;
    });
    if (!got_ibi) {
      std::cerr << "Timed out waiting for SPI JEDEC IBI response" << std::endl;
      device->disconnect();
      return 1;
    }
    std::cout << "Last IBI payload: ";
    for (uint8_t byte : sync.last_ibi_payload) {
      std::cout << "0x" << std::hex << static_cast<int>(byte) << " ";
    }
    std::cout << std::dec << std::endl;
  }

  device->disconnect();
  return 0;
}
