#include "lora_transport.h"
#include "hil_trace.h"
#include <LoRa.h>
#include <SPI.h>
#include <math.h>

static RxRejectReason g_last_rx_reject_reason = RxRejectReason::None;
static uint32_t g_last_rx_reject_log_ms = 0;

static void note_reject(RxRejectReason reason) {
  g_last_rx_reject_reason = reason;
  if (SERIAL_DEBUG_MODE) {
    uint32_t now_ms = millis();
    if (now_ms - g_last_rx_reject_log_ms >= 1000) {
      g_last_rx_reject_log_ms = now_ms;
      Serial.printf("lora_rx: reject reason=%s\n", rx_reject_reason_text(reason));
      hil_trace_eventf("lora_reject", "\"reason\":\"%s\"", rx_reject_reason_text(reason));
    }
  }
}

RxRejectReason lora_get_last_rx_reject_reason() {
  RxRejectReason reason = g_last_rx_reject_reason;
  g_last_rx_reject_reason = RxRejectReason::None;
  return reason;
}

static uint16_t crc16_ccitt(const uint8_t *data, size_t len) {
  uint16_t crc = 0xFFFF;
  for (size_t i = 0; i < len; ++i) {
    crc ^= static_cast<uint16_t>(data[i]) << 8;
    for (uint8_t b = 0; b < 8; ++b) {
      if (crc & 0x8000) {
        crc = (crc << 1) ^ 0x1021;
      } else {
        crc <<= 1;
      }
    }
  }
  return crc;
}

void lora_init() {
  SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS);
  LoRa.setPins(PIN_LORA_NSS, PIN_LORA_RST, PIN_LORA_DIO0);
  LoRa.begin(LORA_FREQUENCY);
  LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR);
  LoRa.setSignalBandwidth(LORA_BANDWIDTH);
  LoRa.setCodingRate4(LORA_CODING_RATE);
  LoRa.enableCrc();
  LoRa.setSyncWord(LORA_SYNC_WORD);
}

bool lora_send(const LoraPacket &pkt) {
  if (LORA_SEND_BYPASS) {
    return true;
  }
  uint32_t t0 = 0;
  uint32_t t1 = 0;
  uint32_t t2 = 0;
  uint32_t t3 = 0;
  uint32_t t4 = 0;
  if (SERIAL_DEBUG_MODE) {
    t0 = millis();
  }
  LoRa.idle();
  if (SERIAL_DEBUG_MODE) {
    t1 = millis();
  }
  uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2];
  size_t idx = 0;
  buffer[idx++] = pkt.protocol_version;
  buffer[idx++] = static_cast<uint8_t>(pkt.role);
  buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short >> 8);
  buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short & 0xFF);
  buffer[idx++] = static_cast<uint8_t>(pkt.payload_type);

  if (pkt.payload_len > LORA_MAX_PAYLOAD) {
    return false;
  }

  memcpy(&buffer[idx], pkt.payload, pkt.payload_len);
  idx += pkt.payload_len;

  uint16_t crc = crc16_ccitt(buffer, idx);
  buffer[idx++] = static_cast<uint8_t>(crc >> 8);
  buffer[idx++] = static_cast<uint8_t>(crc & 0xFF);

  if (pkt.payload_type == PayloadType::MeterBatch && hil_fault_take(HilFault::CorruptChunk) && idx > 7) {
    // Flip a payload byte after calculating the frame CRC so the receiver's
    // production CRC path rejects this one HIL frame.
    buffer[5 + (pkt.payload_len > 6 ? 6 : 0)] ^= 0x01;
    hil_trace_event("fault_injected", "\"fault\":\"corrupt_chunk\"");
  }

  LoRa.beginPacket();
  if (SERIAL_DEBUG_MODE) {
    t2 = millis();
  }
  LoRa.write(buffer, idx);
  if (SERIAL_DEBUG_MODE) {
    t3 = millis();
  }
  int result = LoRa.endPacket(false);
  if (SERIAL_DEBUG_MODE) {
    t4 = millis();
    Serial.printf("lora_tx: idle=%lums begin=%lums write=%lums end=%lums total=%lums len=%u\n",
                  static_cast<unsigned long>(t1 - t0),
                  static_cast<unsigned long>(t2 - t1),
                  static_cast<unsigned long>(t3 - t2),
                  static_cast<unsigned long>(t4 - t3),
                  static_cast<unsigned long>(t4 - t0),
                  static_cast<unsigned>(idx));
  }
  return result == 1;
}

bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
  uint32_t start = millis();
  while (true) {
    int packet_size = LoRa.parsePacket();
    if (packet_size > 0) {
      if (packet_size < 7) {
        while (LoRa.available()) {
          LoRa.read();
        }
        note_reject(RxRejectReason::LengthMismatch);
        return false;
      }

      uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2];
      size_t len = 0;
      while (LoRa.available() && len < sizeof(buffer)) {
        buffer[len++] = LoRa.read();
      }

      if (len < 7) {
        note_reject(RxRejectReason::LengthMismatch);
        return false;
      }

      uint16_t crc_calc = crc16_ccitt(buffer, len - 2);
      uint16_t crc_rx = static_cast<uint16_t>(buffer[len - 2] << 8) | buffer[len - 1];
      if (crc_calc != crc_rx) {
        note_reject(RxRejectReason::CrcFail);
        return false;
      }
      if (buffer[0] != PROTOCOL_VERSION) {
        note_reject(RxRejectReason::BadProtocol);
        return false;
      }

      pkt.protocol_version = buffer[0];
      pkt.role = static_cast<DeviceRole>(buffer[1]);
      pkt.device_id_short = static_cast<uint16_t>(buffer[2] << 8) | buffer[3];
      pkt.payload_type = static_cast<PayloadType>(buffer[4]);
      pkt.payload_len = len - 7;
      if (pkt.payload_len > LORA_MAX_PAYLOAD) {
        note_reject(RxRejectReason::LengthMismatch);
        return false;
      }
      memcpy(pkt.payload, &buffer[5], pkt.payload_len);
      pkt.rssi_dbm = static_cast<int16_t>(LoRa.packetRssi());
      pkt.snr_db = LoRa.packetSnr();
      return true;
    }

    if (timeout_ms == 0 || millis() - start >= timeout_ms) {
      return false;
    }
    delay(5);
  }
}

void lora_idle() {
  LoRa.idle();
}

void lora_sleep() {
  LoRa.sleep();
}

void lora_receive_continuous() {
  LoRa.receive();
}

bool lora_receive_window(LoraPacket &pkt, uint32_t timeout_ms) {
  if (timeout_ms == 0) {
    return false;
  }
  LoRa.receive();
  bool got = lora_receive(pkt, timeout_ms);
  LoRa.sleep();
  return got;
}

uint32_t lora_airtime_ms(size_t packet_len) {
  if (packet_len == 0) {
    return 0;
  }

  const double bw = static_cast<double>(LORA_BANDWIDTH);
  const double sf = static_cast<double>(LORA_SPREADING_FACTOR);
  const double cr = static_cast<double>(LORA_CODING_RATE - 4); // coding rate denominator: 4/(4+cr)
  const double tsym = (1 << LORA_SPREADING_FACTOR) / bw;
  const double t_preamble = (static_cast<double>(LORA_PREAMBLE_LEN) + 4.25) * tsym;

  const bool low_data_rate_opt = (LORA_SPREADING_FACTOR >= 11) && (LORA_BANDWIDTH <= 125000);
  const double de = low_data_rate_opt ? 1.0 : 0.0;
  const double ih = 0.0;
  const double crc = 1.0;

  const double payload_symb_nb = 8.0 + max(
    ceil((8.0 * packet_len - 4.0 * sf + 28.0 + 16.0 * crc - 20.0 * ih) / (4.0 * (sf - 2.0 * de))) * (cr + 4.0),
    0.0);
  const double t_payload = payload_symb_nb * tsym;
  const double t_packet = t_preamble + t_payload;

  return static_cast<uint32_t>(ceil(t_packet * 1000.0));
}
