#include "TBeamSupremeBleInterface.h"
#include "DebugStats.h"
#include "SelectedText.h"
#include "TBeamDisplay.h"

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

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

#ifndef FILE_TRANSFER_CHUNK_SIZE
#define FILE_TRANSFER_CHUNK_SIZE 32
#endif

#ifndef FILE_TRANSFER_CHUNK_INTERVAL_MS
#define FILE_TRANSFER_CHUNK_INTERVAL_MS 500
#endif

#ifndef FILE_TRANSFER_REPEAT_INTERVAL_MS
#define FILE_TRANSFER_REPEAT_INTERVAL_MS 10000
#endif

static constexpr size_t TRANSFER_CHUNK_SIZE = FILE_TRANSFER_CHUNK_SIZE;
static constexpr uint32_t TRANSFER_CHUNK_INTERVAL_MS = FILE_TRANSFER_CHUNK_INTERVAL_MS;
static constexpr uint32_t TRANSFER_REPEAT_INTERVAL_MS = FILE_TRANSFER_REPEAT_INTERVAL_MS;
static constexpr uint32_t FNV1A_OFFSET = 2166136261UL;
static constexpr uint32_t FNV1A_PRIME = 16777619UL;
static constexpr uint8_t OLED_TEXT_LINES = 8;
static constexpr uint8_t OLED_TEXT_COLS = 25;

static RNS::Reticulum reticulum({RNS::Type::NONE});
static RNS::Interface ble_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::Link active_link({RNS::Type::NONE});
static RNS::Link pending_link({RNS::Type::NONE});
static RNS::Bytes peer_hash;
static String peer_label;
static bool have_peer = false;
static bool link_active = false;
static bool link_attempted = false;
static TBeamSupremeBleInterface* ble_impl = nullptr;
static String node_label;
static tbeam::TBeamDisplay oled_display;
static char oled_lines[OLED_TEXT_LINES][OLED_TEXT_COLS + 1] = {};
static uint8_t oled_line = 0;
static uint8_t oled_col = 0;

struct RxTransferState {
  bool active = false;
  String sender;
  String file_name;
  size_t expected_size = 0;
  uint32_t expected_chunks = 0;
  uint32_t expected_crc = 0;
  uint32_t received_chunks = 0;
  size_t received_size = 0;
  uint32_t crc = FNV1A_OFFSET;
};

static RxTransferState rx_transfer;

struct TxTransferState {
  bool active = false;
  bool complete = false;
  size_t offset = 0;
  uint32_t sequence = 0;
  uint32_t total_chunks = 0;
  uint32_t crc = 0;
  uint32_t next_start_ms = 0;
  uint32_t round = 0;
};

static TxTransferState tx_transfer;

static String default_node_label() {
  uint64_t mac = ESP.getEfuseMac();
  char label[24];
  snprintf(label, sizeof(label), "Node-%012llX", (unsigned long long)(mac & 0xFFFFFFFFFFFFULL));
  return String(label);
}

static String board_name_from_node_label(const String& label) {
  if (label == "Node-68BF5B43CA48") return "AMY";
  if (label == "Node-DC935A43CA48") return "BOB";
  if (label == "Node-44915A43CA48") return "CY";
  if (label == "Node-A0935A43CA48") return "DAN";
  if (label == "Node-D0905A43CA48") return "ED";
  if (label == "Node-748C5B43CA48") return "FLO";
  if (label == "Node-E0405A43CA48") return "GUY";
  return label;
}

static bool should_initiate_link_to(const String& label) {
  return strcmp(node_label.c_str(), label.c_str()) < 0;
}

static uint32_t fnv1a_update(uint32_t crc, uint8_t byte) {
  crc ^= byte;
  crc *= FNV1A_PRIME;
  return crc;
}

static uint32_t selected_text_crc() {
  uint32_t crc = FNV1A_OFFSET;
  for (size_t i = 0; i < SELECTED_TEXT_SIZE; ++i) {
    crc = fnv1a_update(crc, read_selected_text_byte(i));
  }
  return crc;
}

static String hex32(uint32_t value) {
  char buffer[9];
  snprintf(buffer, sizeof(buffer), "%08lX", (unsigned long)value);
  return String(buffer);
}

static String link_id_hex(const RNS::Link& link) {
  if (!link) {
    return String("none");
  }
  return String(link.link_id().toHex().c_str());
}

static uint32_t bytes_crc32(const RNS::Bytes& bytes) {
  return DebugStats::bytes_crc32(bytes);
}

static String preview_payload(const String& payload) {
  String preview;
  for (size_t i = 0; i < payload.length() && preview.length() < 56; ++i) {
    char c = payload.charAt(i);
    if (c == '\n') {
      preview += "\\n";
    } else if (c == '\r') {
      preview += "\\r";
    } else if ((uint8_t)c < 32) {
      preview += '?';
    } else {
      preview += c;
    }
  }
  return preview;
}

static void render_oled_text() {
  if (!oled_display.ready()) {
    return;
  }

  U8G2& oled = oled_display.raw();
  oled.clearBuffer();
  oled.setFont(u8g2_font_5x8_tf);
  for (uint8_t i = 0; i < OLED_TEXT_LINES; ++i) {
    if (oled_lines[i][0] != '\0') {
      oled.drawUTF8(0, 8 + (i * 8), oled_lines[i]);
    }
  }
  oled.sendBuffer();
}

static void clear_oled_text() {
  for (uint8_t i = 0; i < OLED_TEXT_LINES; ++i) {
    oled_lines[i][0] = '\0';
  }
  oled_line = 0;
  oled_col = 0;
  render_oled_text();
}

static void show_startup_splash() {
  if (!oled_display.ready()) {
    return;
  }

  U8G2& oled = oled_display.raw();
  oled.clearBuffer();
  oled.setFont(u8g2_font_5x8_tf);
  oled.drawUTF8(0, 8, "Exercise 306");
  oled.drawUTF8(0, 16, "Commencing...");
  oled.drawUTF8(0, 32, "pairing has not");
  oled.drawUTF8(0, 40, "been completed");
  oled.drawUTF8(0, 48, "and when completed,");
  oled.drawUTF8(0, 56, "text will appear.");
  oled.sendBuffer();
}

static void advance_oled_line() {
  if (oled_line < OLED_TEXT_LINES - 1) {
    oled_line++;
  } else {
    for (uint8_t i = 0; i < OLED_TEXT_LINES - 1; ++i) {
      strlcpy(oled_lines[i], oled_lines[i + 1], sizeof(oled_lines[i]));
    }
  }
  oled_lines[oled_line][0] = '\0';
  oled_col = 0;
}

static void append_oled_text(const String& text) {
  for (size_t i = 0; i < text.length(); ++i) {
    char c = text.charAt(i);
    if (c == '\r') {
      continue;
    }
    if (c == '\n') {
      advance_oled_line();
      continue;
    }
    if ((uint8_t)c < 32) {
      c = ' ';
    }
    if (oled_col >= OLED_TEXT_COLS) {
      advance_oled_line();
    }
    oled_lines[oled_line][oled_col++] = c;
    oled_lines[oled_line][oled_col] = '\0';
  }
  render_oled_text();
}

static void handle_file_begin(const String& sender,
                              const String& file_name,
                              size_t size,
                              uint32_t chunks,
                              uint32_t crc) {
  rx_transfer.active = true;
  rx_transfer.sender = sender;
  rx_transfer.file_name = file_name;
  rx_transfer.expected_size = size;
  rx_transfer.expected_chunks = chunks;
  rx_transfer.expected_crc = crc;
  rx_transfer.received_chunks = 0;
  rx_transfer.received_size = 0;
  rx_transfer.crc = FNV1A_OFFSET;

  Serial.printf("RX FILE BEGIN: from=%s file=%s bytes=%u chunks=%lu crc=%s\r\n",
                sender.c_str(),
                file_name.c_str(),
                (unsigned)size,
                (unsigned long)chunks,
                hex32(crc).c_str());
  clear_oled_text();
}

static void handle_file_data(const String& sender,
                             uint32_t sequence,
                             uint32_t total,
                             const String& payload) {
  if (!rx_transfer.active || rx_transfer.sender != sender) {
    Serial.printf("RX FILE DATA ignored: sender=%s seq=%lu no active transfer\r\n",
                  sender.c_str(), (unsigned long)sequence);
    return;
  }

  for (size_t i = 0; i < payload.length(); ++i) {
    rx_transfer.crc = fnv1a_update(rx_transfer.crc, (uint8_t)payload.charAt(i));
  }
  rx_transfer.received_chunks++;
  rx_transfer.received_size += payload.length();

  Serial.printf("RX FILE DATA: from=%s seq=%lu/%lu bytes=%u preview=\"%s\"\r\n",
                sender.c_str(),
                (unsigned long)sequence,
                (unsigned long)total,
                (unsigned)payload.length(),
                preview_payload(payload).c_str());
  append_oled_text(payload);
}

static void handle_file_end(const String& sender,
                            const String& file_name,
                            size_t size,
                            uint32_t chunks,
                            uint32_t crc) {
  bool ok = rx_transfer.active &&
            rx_transfer.sender == sender &&
            rx_transfer.file_name == file_name &&
            rx_transfer.expected_size == size &&
            rx_transfer.expected_chunks == chunks &&
            rx_transfer.expected_crc == crc &&
            rx_transfer.received_size == size &&
            rx_transfer.received_chunks == chunks &&
            rx_transfer.crc == crc;

  Serial.printf("RX FILE END: from=%s file=%s received=%u/%u chunks=%lu/%lu crc=%s status=%s\r\n",
                sender.c_str(),
                file_name.c_str(),
                (unsigned)rx_transfer.received_size,
                (unsigned)size,
                (unsigned long)rx_transfer.received_chunks,
                (unsigned long)chunks,
                hex32(rx_transfer.crc).c_str(),
                ok ? "OK" : "VERIFY_FAIL");
  rx_transfer.active = false;
}

static void parse_file_transfer_packet(const String& message) {
  int p1 = message.indexOf('|');
  int p2 = message.indexOf('|', p1 + 1);
  if (p1 < 0 || p2 < 0) {
    Serial.printf("RX LINK BLE: %s\r\n", message.c_str());
    append_oled_text(message);
    return;
  }

  String type = message.substring(0, p1);
  String sender = message.substring(p1 + 1, p2);

  if (type == "FTB") {
    int p3 = message.indexOf('|', p2 + 1);
    int p4 = message.indexOf('|', p3 + 1);
    int p5 = message.indexOf('|', p4 + 1);
    if (p3 < 0 || p4 < 0 || p5 < 0) {
      Serial.printf("RX FILE malformed BEGIN: %s\r\n", message.c_str());
      return;
    }
    handle_file_begin(sender,
                      message.substring(p2 + 1, p3),
                      (size_t)message.substring(p3 + 1, p4).toInt(),
                      (uint32_t)message.substring(p4 + 1, p5).toInt(),
                      (uint32_t)strtoul(message.substring(p5 + 1).c_str(), nullptr, 16));
    return;
  }

  if (type == "FTD") {
    int p3 = message.indexOf('|', p2 + 1);
    int p4 = message.indexOf('|', p3 + 1);
    if (p3 < 0 || p4 < 0) {
      Serial.printf("RX FILE malformed DATA: %s\r\n", message.c_str());
      return;
    }
    handle_file_data(sender,
                     (uint32_t)message.substring(p2 + 1, p3).toInt(),
                     (uint32_t)message.substring(p3 + 1, p4).toInt(),
                     message.substring(p4 + 1));
    return;
  }

  if (type == "FTE") {
    int p3 = message.indexOf('|', p2 + 1);
    int p4 = message.indexOf('|', p3 + 1);
    int p5 = message.indexOf('|', p4 + 1);
    if (p3 < 0 || p4 < 0 || p5 < 0) {
      Serial.printf("RX FILE malformed END: %s\r\n", message.c_str());
      return;
    }
    handle_file_end(sender,
                    message.substring(p2 + 1, p3),
                    (size_t)message.substring(p3 + 1, p4).toInt(),
                    (uint32_t)message.substring(p4 + 1, p5).toInt(),
                    (uint32_t)strtoul(message.substring(p5 + 1).c_str(), nullptr, 16));
    return;
  }

  Serial.printf("RX LINK BLE: %s\r\n", message.c_str());
}

static void on_link_packet(const RNS::Bytes& data, const RNS::Packet& packet) {
  (void)packet;
  parse_file_transfer_packet(String(data.toString().c_str()));
}

static void on_link_closed(RNS::Link& link) {
  Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_closed peer=%s link_id=%s link_obj=%s\r\n",
                (unsigned long)millis(),
                DebugStats::board(),
                DebugStats::role(),
                peer_label.length() ? peer_label.c_str() : "unknown",
                link_id_hex(link).c_str(),
                link.toString().c_str());
  Serial.println("LINK CLOSED");
  active_link = {RNS::Type::NONE};
  pending_link = {RNS::Type::NONE};
  link_active = false;
  link_attempted = false;
}

static void on_outbound_link_established(RNS::Link& link) {
  active_link = link;
  active_link.set_packet_callback(on_link_packet);
  active_link.set_link_closed_callback(on_link_closed);
  link_active = true;
  Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_established direction=outbound peer=%s link_id=%s link_hash_crc32=%08lX peer_dest_crc32=%08lX sign_key_crc32=unavailable enc_key_crc32=unavailable link_obj=%s\r\n",
                (unsigned long)millis(),
                DebugStats::board(),
                DebugStats::role(),
                peer_label.length() ? peer_label.c_str() : "unknown",
                link_id_hex(active_link).c_str(),
                (unsigned long)bytes_crc32(active_link.hash()),
                (unsigned long)bytes_crc32(peer_hash),
                active_link.toString().c_str());
  Serial.println("LINK ACTIVE: initiator link established");
}

static void on_inbound_link_established(RNS::Link& link) {
  active_link = link;
  active_link.set_packet_callback(on_link_packet);
  active_link.set_link_closed_callback(on_link_closed);
  link_active = true;
  link_attempted = true;
  Serial.printf("RNSLINK ms=%lu board=%s role=%s event=link_established direction=inbound peer=%s link_id=%s link_hash_crc32=%08lX peer_dest_crc32=%08lX sign_key_crc32=unavailable enc_key_crc32=unavailable link_obj=%s\r\n",
                (unsigned long)millis(),
                DebugStats::board(),
                DebugStats::role(),
                peer_label.length() ? peer_label.c_str() : "unknown",
                link_id_hex(active_link).c_str(),
                (unsigned long)bytes_crc32(active_link.hash()),
                (unsigned long)bytes_crc32(peer_hash),
                active_link.toString().c_str());
  Serial.println("RX LINK: inbound link established");
}

class FileAnnounceHandler : public RNS::AnnounceHandler {
public:
  FileAnnounceHandler() : 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("RNSLINK ms=%lu board=%s role=%s event=announce_rx peer=%s peer_dest=%s peer_dest_crc32=%08lX\r\n",
                  (unsigned long)millis(),
                  DebugStats::board(),
                  DebugStats::role(),
                  peer_label.c_str(),
                  peer_hash.toHex().c_str(),
                  (unsigned long)bytes_crc32(peer_hash));
    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 FileAnnounceHandler());

static void print_config() {
  Serial.printf("Node=%s\r\n", node_label.c_str());
  Serial.printf("BLE role=%s service=37145b00-442d-4a94-917f-8f42c5da28e3\r\n",
                ble_impl ? ble_impl->role_name() : "?");
  Serial.printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\r\n",
                SELECTED_TEXT_NAME,
                (unsigned)SELECTED_TEXT_SIZE,
                (unsigned)TRANSFER_CHUNK_SIZE,
                (unsigned long)TRANSFER_CHUNK_INTERVAL_MS,
                (unsigned long)TRANSFER_REPEAT_INTERVAL_MS);
}

static void send_announce() {
  if (!inbound_destination) {
    return;
  }
  Serial.printf("RNSLINK ms=%lu board=%s role=%s event=announce_tx peer=all local_dest=%s local_dest_crc32=%08lX\r\n",
                (unsigned long)millis(),
                DebugStats::board(),
                DebugStats::role(),
                inbound_destination.hash().toHex().c_str(),
                (unsigned long)bytes_crc32(inbound_destination.hash()));
  Serial.printf("TX ANNOUNCE: %s\r\n", node_label.c_str());
  inbound_destination.announce(RNS::bytesFromString(node_label.c_str()));
}

static void maybe_open_link() {
  if (!have_peer || link_active || link_attempted || !peer_destination) {
    return;
  }
  if (!should_initiate_link_to(peer_label)) {
    return;
  }

  Serial.printf("TX LINKREQUEST: opening link to %s\r\n", peer_label.c_str());
  pending_link = RNS::Link(peer_destination);
  pending_link.set_packet_callback(on_link_packet);
  pending_link.set_link_established_callback(on_outbound_link_established);
  pending_link.set_link_closed_callback(on_link_closed);
  link_attempted = true;
}

static void send_link_message(const String& message) {
  RNS::Bytes plaintext = RNS::bytesFromString(message.c_str());
  static uint32_t app_tx_seq = 0;
  app_tx_seq++;
  if (DebugStats::should_log_packet(app_tx_seq)) {
    Serial.printf("RNSTX ms=%lu board=%s role=%s seq=%lu stage=link_plain link_id=%s len=%u crc32=%08lX first4=%s last4=%s\r\n",
                  (unsigned long)millis(),
                  DebugStats::board(),
                  DebugStats::role(),
                  (unsigned long)app_tx_seq,
                  link_id_hex(active_link).c_str(),
                  (unsigned)plaintext.size(),
                  (unsigned long)DebugStats::bytes_crc32(plaintext),
                  DebugStats::bytes_first4(plaintext).c_str(),
                  DebugStats::bytes_last4(plaintext).c_str());
  }
  RNS::Packet(active_link, RNS::bytesFromString(message.c_str())).send();
}

static bool send_next_file_packet(uint32_t now) {
  if (tx_transfer.complete && (int32_t)(now - tx_transfer.next_start_ms) < 0) {
    return false;
  }

  if (!tx_transfer.active) {
    tx_transfer.active = true;
    tx_transfer.complete = false;
    tx_transfer.offset = 0;
    tx_transfer.sequence = 0;
    tx_transfer.crc = selected_text_crc();
    tx_transfer.total_chunks = (SELECTED_TEXT_SIZE + TRANSFER_CHUNK_SIZE - 1) / TRANSFER_CHUNK_SIZE;
    tx_transfer.round++;

    String begin = String("FTB|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
                   String((unsigned)SELECTED_TEXT_SIZE) + "|" + String((unsigned long)tx_transfer.total_chunks) +
                   "|" + hex32(tx_transfer.crc);
    Serial.printf("TX FILE BEGIN: round=%lu file=%s bytes=%u chunks=%lu crc=%s\r\n",
                  (unsigned long)tx_transfer.round,
                  SELECTED_TEXT_NAME,
                  (unsigned)SELECTED_TEXT_SIZE,
                  (unsigned long)tx_transfer.total_chunks,
                  hex32(tx_transfer.crc).c_str());
    send_link_message(begin);
    return true;
  }

  if (tx_transfer.offset < SELECTED_TEXT_SIZE) {
    size_t remaining = SELECTED_TEXT_SIZE - tx_transfer.offset;
    size_t count = remaining < TRANSFER_CHUNK_SIZE ? remaining : TRANSFER_CHUNK_SIZE;
    String payload;
    payload.reserve(count);
    for (size_t i = 0; i < count; ++i) {
      payload += (char)read_selected_text_byte(tx_transfer.offset + i);
    }
    tx_transfer.sequence++;
    tx_transfer.offset += count;

    String data = String("FTD|") + node_label + "|" + String((unsigned long)tx_transfer.sequence) +
                  "|" + String((unsigned long)tx_transfer.total_chunks) + "|" + payload;
    Serial.printf("TX FILE DATA: round=%lu seq=%lu/%lu bytes=%u preview=\"%s\"\r\n",
                  (unsigned long)tx_transfer.round,
                  (unsigned long)tx_transfer.sequence,
                  (unsigned long)tx_transfer.total_chunks,
                  (unsigned)count,
                  preview_payload(payload).c_str());
    send_link_message(data);
    return true;
  }

  String end = String("FTE|") + node_label + "|" + SELECTED_TEXT_NAME + "|" +
               String((unsigned)SELECTED_TEXT_SIZE) + "|" + String((unsigned long)tx_transfer.total_chunks) +
               "|" + hex32(tx_transfer.crc);
  Serial.printf("TX FILE END: round=%lu file=%s bytes=%u chunks=%lu crc=%s next_round_in_ms=%lu\r\n",
                (unsigned long)tx_transfer.round,
                SELECTED_TEXT_NAME,
                (unsigned)SELECTED_TEXT_SIZE,
                (unsigned long)tx_transfer.total_chunks,
                hex32(tx_transfer.crc).c_str(),
                (unsigned long)TRANSFER_REPEAT_INTERVAL_MS);
  send_link_message(end);
  tx_transfer.active = false;
  tx_transfer.complete = true;
  tx_transfer.next_start_ms = now + TRANSFER_REPEAT_INTERVAL_MS;
  return true;
}

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

  ble_impl = new TBeamSupremeBleInterface(node_label);
  ble_interface = ble_impl;
  ble_interface.mode(RNS::Type::Interface::MODE_GATEWAY);
  RNS::Transport::register_interface(ble_interface);
  ble_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_link_established_callback(on_inbound_link_established);
  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();
  RNS::loglevel(RNS::LOG_NOTICE);
  node_label = default_node_label();
  DebugStats::set_board(board_name_from_node_label(node_label));
  DebugStats::set_role("boot");
  tbeam::DisplayConfig display_config;
  display_config.powerSave = false;
  oled_display.begin(display_config);
  show_startup_splash();

  setup_reticulum();
  Serial.println("Exercise 306: microReticulum BLE file transfer OLED display");
  print_config();
  Serial.println("microReticulum ready");
}

void loop() {
  reticulum.loop();

  static uint32_t next_announce_ms = 0;
  static uint32_t next_transfer_ms = 0;
  static uint32_t next_wait_log_ms = 0;
  uint32_t now = millis();

  if (ble_impl && !ble_impl->connected()) {
    if (next_wait_log_ms == 0 || (int32_t)(now - next_wait_log_ms) >= 0) {
      next_wait_log_ms = now + 10000;
      Serial.printf("BLE %s waiting for peer\r\n", ble_impl->role_name());
    }
    delay(5);
    return;
  }

  if (next_announce_ms == 0) {
    uint32_t offset = 700 + ((uint32_t)node_label.charAt(node_label.length() - 1) % 5) * 400;
    next_announce_ms = now + offset;
  }
  if (!link_active && (int32_t)(now - next_announce_ms) >= 0) {
    next_announce_ms = now + 15000;
    send_announce();
  }

  maybe_open_link();

  if (link_active && next_transfer_ms == 0) {
    next_transfer_ms = now + (should_initiate_link_to(peer_label) ? 900 : 1200);
  }

  if (link_active && (int32_t)(now - next_transfer_ms) >= 0) {
    next_transfer_ms = now + TRANSFER_CHUNK_INTERVAL_MS;
    send_next_file_packet(now);
  }

  delay(5);
}

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