/*
  Heltec WiFi Kit32 V3 (ESP32-S3) BLE Remote for "FLL-Umbrella-Env"

  What this remote does:
  - Scans + connects over BLE to your sensor (server)
  - Lets you switch MODE: LIVE / SIM
  - In SIM mode, lets you edit: Temp (°C), RH (%), Pressure (hPa), IAQ (auto or manual)
  - Writes the same binary packets your sensor expects:
      CMD 0x01: [0]=0x01, [1]=mode(0/1)
      CMD 0x02: [0]=0x02, then LE fields:
                temp_x100 (int16), rh_x100 (uint16), press_x10 (uint16),
                optional iaq (uint16)

  Libraries to install (Arduino Library Manager):
  - NimBLE-Arduino (by h2zero)
  - U8g2 (by olikraus)

  Wiring assumptions (CHANGE IF NEEDED):
  - OLED I2C (0.98" SSD1306/SH1106):
      SDA = GPIO17
      SCL = GPIO18
      RST = GPIO21  (optional; set to -1 if your display has no reset pin)
  - Encoder:
      A   = GPIO5
      B   = GPIO6
      BTN = GPIO7  (to GND, uses internal pullup)

  Notes:
  - If your Heltec board’s OLED pins differ, change OLED_SDA/OLED_SCL/OLED_RST below.
  - If your encoder pins differ, change ENC_A/ENC_B/ENC_BTN below.
*/

#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>

#include <NimBLEDevice.h>

// ===================== BLE IDs (must match your sensor code) =====================
static const char* BLE_DEV_NAME_TARGET = "FLL-Umbrella-Env";
static const char* UUID_SVC_CTRL = "12345678-1234-5678-1234-56789abcdef0";
static const char* UUID_CHR_CTRL = "12345678-1234-5678-1234-56789abcdef1";

// ===================== OLED (I2C) pins =====================
// Common on Heltec WiFi Kit32 V3: SDA=17, SCL=18, RST=21 (verify on your board)
static const int OLED_SDA = 17;
static const int OLED_SCL = 18;
static const int OLED_RST = 21;   // set -1 if not used by your display

// If your display is SSD1306 128x64 I2C:
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ OLED_RST, /* clock=*/ OLED_SCL, /* data=*/ OLED_SDA);

// ===================== Encoder pins =====================
static const int ENC_A   = 5;
static const int ENC_B   = 6;
static const int ENC_BTN = 7;

// ===================== Simple encoder + button handling =====================
static volatile int32_t enc_delta = 0;
static volatile uint8_t enc_last = 0;

static uint32_t btn_down_ms = 0;
static bool btn_last = true; // pullup => idle HIGH

static inline bool readBtn() { return digitalRead(ENC_BTN) == LOW; } // pressed = LOW

IRAM_ATTR void isrEnc() {
  // 2-bit Gray decoding on A/B
  uint8_t a = (uint8_t)digitalRead(ENC_A);
  uint8_t b = (uint8_t)digitalRead(ENC_B);
  uint8_t s = (a << 1) | b;

  // transition table (old->new) using 4-bit index
  // produces +1/-1 per valid step, 0 otherwise
  static const int8_t tbl[16] = {
    0, -1, +1, 0,
    +1, 0, 0, -1,
    -1, 0, 0, +1,
    0, +1, -1, 0
  };
  uint8_t idx = (enc_last << 2) | s;
  enc_last = s;
  enc_delta += tbl[idx];
}

// ===================== App state =====================
enum Mode : uint8_t { MODE_LIVE = 0, MODE_SIM = 1 };

struct SimValues {
  int16_t  temp_x100  = 2500;   // 25.00 C
  uint16_t rh_x100    = 5000;   // 50.00 %
  uint16_t press_x10  = 10132;  // 1013.2 hPa
  uint16_t iaq        = 25;     // 0..500
  bool     iaq_manual = false;  // false => omit IAQ field, sensor computes heuristic
};

static Mode mode = MODE_LIVE;
static SimValues sim;

// ===================== BLE client handles =====================
static NimBLEAdvertisedDevice* advFound = nullptr;
static NimBLEClient* client = nullptr;
static NimBLERemoteCharacteristic* chrCtrl = nullptr;

static bool bleConnected = false;
static bool bleBusy = false;
static int8_t bleRssi = 0;

// ===================== UI =====================
enum Screen {
  SCR_HOME = 0,
  SCR_MENU,
  SCR_EDIT
};

static Screen screen = SCR_HOME;

static const char* menuItems[] = {
  "Connect / Reconnect",
  "Mode: LIVE/SIM",
  "Temp (C)",
  "RH (%)",
  "Press (hPa)",
  "IAQ: Auto/Manual",
  "IAQ Value (0..500)",
  "Send SIM now",
  "Disconnect"
};
static const int MENU_COUNT = (int)(sizeof(menuItems) / sizeof(menuItems[0]));
static int menuIdx = 0;

// Edit state
enum EditField { EF_NONE, EF_MODE, EF_TEMP, EF_RH, EF_PRESS, EF_IAQ_MANUAL, EF_IAQ };
static EditField editField = EF_NONE;
static int32_t editValue = 0;

// ===================== Helpers =====================
static inline uint16_t clampU16(int32_t v, uint16_t lo, uint16_t hi) {
  if (v < (int32_t)lo) return lo;
  if (v > (int32_t)hi) return hi;
  return (uint16_t)v;
}
static inline int16_t clampI16(int32_t v, int16_t lo, int16_t hi) {
  if (v < (int32_t)lo) return lo;
  if (v > (int32_t)hi) return hi;
  return (int16_t)v;
}

static void oledClear() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tf);
}

static void oledDrawCentered(const char* s, int y) {
  int w = u8g2.getStrWidth(s);
  int x = (128 - w) / 2;
  u8g2.drawStr(x < 0 ? 0 : x, y, s);
}

// ===================== BLE write packets =====================
static bool bleWrite(const uint8_t* data, size_t len) {
  if (!bleConnected || !chrCtrl) return false;
  // write without response preferred (your server supports WRITE | WRITE_NR)
  return chrCtrl->writeValue(data, len, false);
}

static bool sendMode(Mode m) {
  uint8_t pkt[2] = { 0x01, (uint8_t)(m == MODE_SIM ? 1 : 0) };
  return bleWrite(pkt, sizeof(pkt));
}

static bool sendSim(const SimValues& v) {
  uint8_t pkt[1 + 2 + 2 + 2 + 2]; // with IAQ (max)
  size_t len = 0;

  pkt[len++] = 0x02;

  auto putU16LE = [&](uint16_t x) {
    pkt[len++] = (uint8_t)(x & 0xFF);
    pkt[len++] = (uint8_t)((x >> 8) & 0xFF);
  };
  auto putI16LE = [&](int16_t x) { putU16LE((uint16_t)x); };

  putI16LE(v.temp_x100);
  putU16LE(v.rh_x100);
  putU16LE(v.press_x10);

  if (v.iaq_manual) {
    putU16LE(v.iaq);
  }
  return bleWrite(pkt, len);
}

// ===================== BLE scanning/connection =====================
class AdvCB : public NimBLEAdvertisedDeviceCallbacks {
  void onResult(NimBLEAdvertisedDevice* d) override {
    // Match by name OR service UUID (either is fine)
    if (d->haveName() && d->getName() == BLE_DEV_NAME_TARGET) {
      advFound = d;
      NimBLEDevice::getScan()->stop();
      return;
    }
    if (d->isAdvertisingService(NimBLEUUID(UUID_SVC_CTRL))) {
      advFound = d;
      NimBLEDevice::getScan()->stop();
      return;
    }
  }
};

static void bleDisconnect() {
  bleBusy = true;
  if (client && client->isConnected()) client->disconnect();
  chrCtrl = nullptr;
  bleConnected = false;
  bleBusy = false;
}

static bool bleConnectToFound() {
  if (!advFound) return false;

  bleBusy = true;

  if (!client) client = NimBLEDevice::createClient();

  if (client->isConnected()) client->disconnect();

  if (!client->connect(advFound)) {
    bleBusy = false;
    return false;
  }

  NimBLERemoteService* svc = client->getService(UUID_SVC_CTRL);
  if (!svc) {
    client->disconnect();
    bleBusy = false;
    return false;
  }

  chrCtrl = svc->getCharacteristic(UUID_CHR_CTRL);
  if (!chrCtrl) {
    client->disconnect();
    bleBusy = false;
    return false;
  }

  bleConnected = true;
  bleBusy = false;

  // Immediately push current mode + (if SIM) sim values so remote and sensor stay aligned
  sendMode(mode);
  if (mode == MODE_SIM) sendSim(sim);

  return true;
}

static bool bleScanAndConnect(uint32_t ms = 5000) {
  bleBusy = true;
  advFound = nullptr;

  NimBLEScan* scan = NimBLEDevice::getScan();
  scan->setAdvertisedDeviceCallbacks(new AdvCB(), true);
  scan->setActiveScan(true);
  scan->setInterval(45);
  scan->setWindow(30);

  scan->start(ms / 1000, false); // blocking in NimBLE-Arduino (seconds)
  bleBusy = false;

  if (!advFound) return false;
  return bleConnectToFound();
}

// ===================== UI drawing =====================
static void drawHome() {
  oledClear();

  u8g2.setFont(u8g2_font_6x12_tf);
  u8g2.drawStr(0, 12, "Env Sensor BLE Remote");

  char line[32];

  snprintf(line, sizeof(line), "BLE: %s", bleConnected ? "CONNECTED" : "DISCONNECTED");
  u8g2.drawStr(0, 28, line);

  snprintf(line, sizeof(line), "Mode: %s", (mode == MODE_SIM) ? "SIM" : "LIVE");
  u8g2.drawStr(0, 40, line);

  // display sim values in friendly units
  float t = sim.temp_x100 / 100.0f;
  float rh = sim.rh_x100 / 100.0f;
  float p = sim.press_x10 / 10.0f;

  snprintf(line, sizeof(line), "T %.2fC RH %.2f%%", t, rh);
  u8g2.drawStr(0, 52, line);

  snprintf(line, sizeof(line), "P %.1fhPa IAQ %s",
           p, sim.iaq_manual ? String(sim.iaq).c_str() : "AUTO");
  u8g2.drawStr(0, 64, line);

  u8g2.sendBuffer();
}

static void drawMenu() {
  oledClear();
  u8g2.drawStr(0, 12, "Menu (press=select)");

  // Show 4 lines windowed
  int top = menuIdx - 1;
  if (top < 0) top = 0;
  if (top > MENU_COUNT - 4) top = MENU_COUNT - 4;
  if (top < 0) top = 0;

  for (int i = 0; i < 4; i++) {
    int idx = top + i;
    if (idx >= MENU_COUNT) break;

    int y = 28 + i * 12;
    if (idx == menuIdx) {
      u8g2.drawBox(0, y - 10, 128, 12);
      u8g2.setDrawColor(0);
      u8g2.drawStr(2, y, menuItems[idx]);
      u8g2.setDrawColor(1);
    } else {
      u8g2.drawStr(2, y, menuItems[idx]);
    }
  }

  // Status footer
  char s[32];
  snprintf(s, sizeof(s), "%s | %s",
           bleBusy ? "BUSY" : (bleConnected ? "BLE OK" : "NO BLE"),
           (mode == MODE_SIM) ? "SIM" : "LIVE");
  u8g2.drawStr(0, 64, s);

  u8g2.sendBuffer();
}

static void drawEdit() {
  oledClear();
  u8g2.drawStr(0, 12, "Edit (rot=chg)");

  char line1[32], line2[32], line3[32];

  switch (editField) {
    case EF_MODE:
      snprintf(line1, sizeof(line1), "Mode:");
      snprintf(line2, sizeof(line2), "%s", (editValue ? "SIM" : "LIVE"));
      break;
    case EF_TEMP:
      snprintf(line1, sizeof(line1), "Temp (C):");
      snprintf(line2, sizeof(line2), "%.2f", editValue / 100.0f);
      snprintf(line3, sizeof(line3), "Step: 0.25C");
      break;
    case EF_RH:
      snprintf(line1, sizeof(line1), "RH (%):");
      snprintf(line2, sizeof(line2), "%.2f", editValue / 100.0f);
      snprintf(line3, sizeof(line3), "Step: 0.50%%");
      break;
    case EF_PRESS:
      snprintf(line1, sizeof(line1), "Pressure (hPa):");
      snprintf(line2, sizeof(line2), "%.1f", editValue / 10.0f);
      snprintf(line3, sizeof(line3), "Step: 0.5hPa");
      break;
    case EF_IAQ_MANUAL:
      snprintf(line1, sizeof(line1), "IAQ Mode:");
      snprintf(line2, sizeof(line2), "%s", (editValue ? "MANUAL" : "AUTO"));
      break;
    case EF_IAQ:
      snprintf(line1, sizeof(line1), "IAQ (0..500):");
      snprintf(line2, sizeof(line2), "%ld", (long)editValue);
      snprintf(line3, sizeof(line3), "Step: 5");
      break;
    default:
      snprintf(line1, sizeof(line1), "Unknown edit");
      break;
  }

  u8g2.drawStr(0, 28, line1);
  u8g2.setFont(u8g2_font_9x15_tf);
  u8g2.drawStr(0, 48, line2);
  u8g2.setFont(u8g2_font_6x12_tf);
  if (line3[0]) u8g2.drawStr(0, 64, line3);

  u8g2.sendBuffer();
}

// ===================== Button events =====================
static bool btnShortPress = false;
static bool btnLongPress  = false;

static void pollButton() {
  bool pressed = readBtn();
  uint32_t now = millis();

  btnShortPress = false;
  btnLongPress = false;

  if (pressed && btn_last == false) {
    // still held
    if (btn_down_ms && (now - btn_down_ms) > 600) {
      // long press fire once
      btnLongPress = true;
      btn_down_ms = 0; // prevent repeat
    }
  } else if (pressed && btn_last == true) {
    // just pressed
    btn_down_ms = now;
  } else if (!pressed && btn_last == false) {
    // just released
    if (btn_down_ms) {
      // released before long threshold => short press
      btnShortPress = true;
    }
    btn_down_ms = 0;
  }

  btn_last = pressed ? false : true; // track "idle high"
}

// ===================== Encoder delta =====================
static int32_t consumeEncSteps() {
  noInterrupts();
  int32_t d = enc_delta;
  enc_delta = 0;
  interrupts();
  // Many encoders generate 2/4 edges per detent; tune divisor if too sensitive.
  // Keep it simple: divide by 2 to calm it down.
  return d / 2;
}

// ===================== Menu actions =====================
static void enterEdit(EditField f, int32_t initial) {
  editField = f;
  editValue = initial;
  screen = SCR_EDIT;
}

static void applyEdit() {
  // Apply editValue to the active field, clamp where needed.
  switch (editField) {
    case EF_MODE:
      mode = (editValue ? MODE_SIM : MODE_LIVE);
      if (bleConnected) sendMode(mode);
      // If switching into SIM, push current sim immediately
      if (bleConnected && mode == MODE_SIM) sendSim(sim);
      break;

    case EF_TEMP:
      sim.temp_x100 = clampI16(editValue, (int16_t)-4000, (int16_t)8500); // -40.00..85.00
      if (bleConnected && mode == MODE_SIM) sendSim(sim);
      break;

    case EF_RH:
      sim.rh_x100 = clampU16(editValue, 0, 10000); // 0..100.00%
      if (bleConnected && mode == MODE_SIM) sendSim(sim);
      break;

    case EF_PRESS:
      sim.press_x10 = clampU16(editValue, 8000, 12000); // 800.0..1200.0 hPa
      if (bleConnected && mode == MODE_SIM) sendSim(sim);
      break;

    case EF_IAQ_MANUAL:
      sim.iaq_manual = (editValue != 0);
      if (bleConnected && mode == MODE_SIM) sendSim(sim); // will include/omit IAQ field
      break;

    case EF_IAQ:
      sim.iaq = clampU16(editValue, 0, 500);
      if (bleConnected && mode == MODE_SIM && sim.iaq_manual) sendSim(sim);
      break;

    default:
      break;
  }

  screen = SCR_MENU;
  editField = EF_NONE;
}

static void doMenuSelect() {
  switch (menuIdx) {
    case 0: // Connect
      bleScanAndConnect(6000);
      break;

    case 1: // Mode
      enterEdit(EF_MODE, (mode == MODE_SIM) ? 1 : 0);
      break;

    case 2: // Temp
      enterEdit(EF_TEMP, sim.temp_x100);
      break;

    case 3: // RH
      enterEdit(EF_RH, sim.rh_x100);
      break;

    case 4: // Pressure
      enterEdit(EF_PRESS, sim.press_x10);
      break;

    case 5: // IAQ Auto/Manual
      enterEdit(EF_IAQ_MANUAL, sim.iaq_manual ? 1 : 0);
      break;

    case 6: // IAQ value
      enterEdit(EF_IAQ, sim.iaq);
      break;

    case 7: // Send SIM now
      if (bleConnected) {
        // force SIM mode on device if you're in SIM locally
        sendMode(mode);
        if (mode == MODE_SIM) sendSim(sim);
      }
      break;

    case 8: // Disconnect
      bleDisconnect();
      break;
  }
}

// ===================== Setup / Loop =====================
void setup() {
  Serial.begin(115200);
  delay(200);

  // Encoder pins
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  pinMode(ENC_BTN, INPUT_PULLUP);

  // Seed encoder state + attach interrupts
  enc_last = ((uint8_t)digitalRead(ENC_A) << 1) | (uint8_t)digitalRead(ENC_B);
  attachInterrupt(digitalPinToInterrupt(ENC_A), isrEnc, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_B), isrEnc, CHANGE);

  // OLED
  u8g2.begin();
  u8g2.setContrast(255);

  // BLE init
  NimBLEDevice::init("Env-Remote");
  NimBLEDevice::setPower(ESP_PWR_LVL_P6);

  screen = SCR_HOME;

  oledClear();
  oledDrawCentered("BLE Env Remote", 28);
  oledDrawCentered("Press to open menu", 48);
  u8g2.sendBuffer();
  delay(800);
}

void loop() {
  pollButton();
  int32_t steps = consumeEncSteps();

  // (optional) RSSI if connected
  if (bleConnected && client && client->isConnected()) {
    int r = client->getRssi();
    if (r != 0) bleRssi = (int8_t)r;
  } else {
    bleConnected = false;
  }

  // Navigation logic
  if (screen == SCR_HOME) {
    if (btnShortPress) screen = SCR_MENU;
    if (steps != 0) screen = SCR_MENU;
    drawHome();
    delay(50);
    return;
  }

  if (screen == SCR_MENU) {
    if (steps != 0) {
      menuIdx += (steps > 0) ? 1 : -1;
      if (menuIdx < 0) menuIdx = 0;
      if (menuIdx >= MENU_COUNT) menuIdx = MENU_COUNT - 1;
    }

    if (btnShortPress) {
      doMenuSelect();
    }

    if (btnLongPress) {
      screen = SCR_HOME;
    }

    drawMenu();
    delay(30);
    return;
  }

  if (screen == SCR_EDIT) {
    // Change edit value based on field
    if (steps != 0) {
      switch (editField) {
        case EF_MODE:
        case EF_IAQ_MANUAL:
          editValue = (editValue == 0) ? 1 : 0;
          break;

        case EF_TEMP: {
          // step 0.25C => 25 in x100
          editValue += (steps > 0 ? 25 : -25);
          editValue = clampI16(editValue, -4000, 8500);
        } break;

        case EF_RH: {
          // step 0.50% => 50 in x100
          editValue += (steps > 0 ? 50 : -50);
          editValue = clampU16(editValue, 0, 10000);
        } break;

        case EF_PRESS: {
          // step 0.5hPa => 5 in x10
          editValue += (steps > 0 ? 5 : -5);
          editValue = clampU16(editValue, 8000, 12000);
        } break;

        case EF_IAQ: {
          // step 5
          editValue += (steps > 0 ? 5 : -5);
          editValue = clampU16(editValue, 0, 500);
        } break;

        default:
          break;
      }
    }

    if (btnShortPress) {
      applyEdit(); // accept
    }
    if (btnLongPress) {
      // cancel
      screen = SCR_MENU;
      editField = EF_NONE;
    }

    drawEdit();
    delay(30);
    return;
  }
}