#include "TBeamSupremeLoRaInterface.h"

#include <Arduino.h>
#include <Destination.h>
#include <Identity.h>
#include <Log.h>
#include <Packet.h>
#include <Reticulum.h>
#include <Transport.h>
#include <Type.h>
#include <Utilities/OS.h>
#include <microStore/Adapters/UniversalFileSystem.h>
#include <microStore/FileSystem.h>

#ifndef NODE_LABEL
#define NODE_LABEL "?"
#endif

static constexpr const char* APP_NAME = "microreticulum";
static constexpr const char* APP_ASPECT = "singleping";
static constexpr const char* ANNOUNCE_FILTER = "microreticulum.singleping";

static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface lora_interface({RNS::Type::NONE});
static RNS::Identity local_identity({RNS::Type::NONE});
static RNS::Destination inbound_destination({RNS::Type::NONE});
static RNS::Destination peer_destination({RNS::Type::NONE});
static RNS::Bytes peer_hash;
static String peer_label;
static bool have_peer = false;
static TBeamSupremeLoRaInterface* lora_impl = nullptr;

class SingleAnnounceHandler : public RNS::AnnounceHandler {
public:
  SingleAnnounceHandler() : RNS::AnnounceHandler(ANNOUNCE_FILTER) {}

  void received_announce(const RNS::Bytes& destination_hash,
                         const RNS::Identity& announced_identity,
                         const RNS::Bytes& app_data) override {
    String label = app_data ? String(app_data.toString().c_str()) : String("(no label)");
    if (label == NODE_LABEL) {
      return;
    }
    if (!announced_identity) {
      Serial.printf("RX ANNOUNCE ignored: missing identity for hash=%s\r\n",
                    destination_hash.toHex().c_str());
      return;
    }

    peer_hash = destination_hash;
    peer_label = label;
    peer_destination = RNS::Destination(announced_identity,
                                        RNS::Type::Destination::OUT,
                                        RNS::Type::Destination::SINGLE,
                                        destination_hash);
    have_peer = true;

    Serial.printf("RX ANNOUNCE: label=%s hash=%s\r\n",
                  peer_label.c_str(), peer_hash.toHex().c_str());
  }
};

static RNS::HAnnounceHandler announce_handler(new SingleAnnounceHandler());

static void print_config() {
  Serial.printf("Node=%s\r\n", NODE_LABEL);
  Serial.printf("Pins: CS=%d DIO1=%d RST=%d BUSY=%d SCK=%d MISO=%d MOSI=%d\r\n",
                (int)LORA_CS, (int)LORA_DIO1, (int)LORA_RESET, (int)LORA_BUSY,
                (int)LORA_SCK, (int)LORA_MISO, (int)LORA_MOSI);
  Serial.printf("LoRa: freq=%.1f BW=%.1f SF=%d CR=%d sync=0x%02x txp=%d\r\n",
                (double)LORA_FREQ_MHZ, (double)LORA_BW_KHZ, (int)LORA_SF,
                (int)LORA_CR, (int)LORA_SYNC_WORD, (int)LORA_TX_POWER_DBM);
}

static void on_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
  (void)packet;
  float rssi = lora_impl ? lora_impl->last_rssi() : 0.0f;
  float snr = lora_impl ? lora_impl->last_snr() : 0.0f;
  Serial.printf("RX SINGLE: %s | RSSI=%.1f SNR=%.1f\r\n",
                data.toString().c_str(), rssi, snr);
}

static void send_announce() {
  if (!inbound_destination) {
    return;
  }
  Serial.printf("TX ANNOUNCE: %s\r\n", NODE_LABEL);
  inbound_destination.announce(RNS::bytesFromString(NODE_LABEL));
}

static void setup_reticulum() {
  microStore::FileSystem filesystem{microStore::Adapters::UniversalFileSystem()};
  filesystem.init();
  RNS::Utilities::OS::register_filesystem(filesystem);

  lora_impl = new TBeamSupremeLoRaInterface();
  lora_interface = lora_impl;
  lora_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
  RNS::Transport::register_interface(lora_interface);
  lora_interface.start();

  reticulum = RNS::Reticulum();
  reticulum.transport_enabled(false);
  reticulum.probe_destination_enabled(false);
  reticulum.start();

  local_identity = RNS::Identity();
  inbound_destination = RNS::Destination(local_identity,
                                         RNS::Type::Destination::IN,
                                         RNS::Type::Destination::SINGLE,
                                         APP_NAME,
                                         APP_ASPECT);
  inbound_destination.set_packet_callback(on_packet);
  inbound_destination.set_proof_strategy(RNS::Type::Destination::PROVE_NONE);

  RNS::Transport::register_announce_handler(announce_handler);

  Serial.printf("Local SINGLE destination: %s\r\n",
                inbound_destination.hash().toHex().c_str());
}

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 5000) {
    delay(100);
  }
  delay(250);

  Serial.println();
  Serial.println("Exercise 202: microReticulum announce + SINGLE destination ping-pong");
  print_config();

  RNS::loglevel(RNS::LOG_NOTICE);
  setup_reticulum();
  Serial.println("microReticulum ready");
}

void loop() {
  reticulum.loop();

  static uint32_t next_announce_ms = 0;
  static uint32_t next_tx_ms = 0;
  static uint32_t iter = 0;
  uint32_t now = millis();

  if (next_announce_ms == 0) {
    uint32_t offset = (NODE_LABEL[0] == 'A') ? 700 : 1700;
    next_announce_ms = now + offset;
  }
  if ((int32_t)(now - next_announce_ms) >= 0) {
    next_announce_ms = now + 15000;
    send_announce();
  }

  if (have_peer && next_tx_ms == 0) {
    uint32_t offset = (NODE_LABEL[0] == 'A') ? 2500 : 4000;
    next_tx_ms = now + offset;
  }
  if (have_peer && (int32_t)(now - next_tx_ms) >= 0) {
    next_tx_ms = now + 3000;

    String message = String(NODE_LABEL) + " -> " + peer_label + " iter=" + String(iter++);
    Serial.printf("TX SINGLE: %s\r\n", message.c_str());

    RNS::Packet packet(peer_destination,
                       RNS::bytesFromString(message.c_str()),
                       RNS::Type::Packet::DATA,
                       RNS::Type::Packet::CONTEXT_NONE,
                       RNS::Type::Transport::BROADCAST,
                       RNS::Type::Packet::HEADER_1,
                       {RNS::Bytes::NONE},
                       false);
    packet.send();
  }

  delay(5);
}

int _write(int file, char* ptr, int len) {
  (void)file;
  int wrote = Serial.write(ptr, len);
  Serial.flush();
  return wrote;
}
