#pragma once
// Waypoint management UI for Tools › Trail: list / navigate / add-by-coords,
// plus mark-here, rename, delete and send. Extracted from TrailScreen as a
// self-contained component that TrailScreen owns and delegates to while active.
// It needs the TrailStore only for the synthetic "Trail start" backtrack row.
//
// Included by TrailScreen.h (which UITask.cpp pulls in after UITask is fully
// defined, so the inline _task->… calls below see the complete type).

#include "../Waypoint.h"
#include "../GeoUtils.h"
#include "../Trail.h"
#include "KeyboardWidget.h"
#include "PopupMenu.h"
#include "NavView.h"
#include "DigitEditor.h"

class WaypointsView {
  UITask*     _task;
  TrailStore* _store;

  // Sub-modes layered over the trail views. OFF = the component is dormant.
  enum Mode { OFF, LIST, NAV, ADD, AVG, TRACKBACK };
  uint8_t _mode   = OFF;
  int     _sel    = 0;
  int     _scroll = 0;

  // Track-back (Tools › Trail › Track back): retrace the recorded trail in
  // reverse using NavView. _tb_idx is the current target breadcrumb; it walks
  // down to 0 (the start) as each is reached within TB_ARRIVE_M.
  static const int TB_ARRIVE_M = 20;
  int                 _tb_idx = 0;
  navview::EtaTracker _tb_eta;

  // GPS averaging (Tools › Trail › Settings › Mark avg). When enabled, markHere()
  // accumulates fixes for gps_avg_idx seconds and marks the mean position — a
  // steadier mark than one instantaneous fix. Sampling runs in poll() on a 1 s
  // gate, independent of (slow, on e-ink) redraws. int64 sums: 30 samples ×
  // ~180e6 overflows int32.
  long long _avg_sum_lat = 0, _avg_sum_lon = 0;
  uint32_t  _avg_n       = 0;       // fixes accumulated so far
  uint32_t  _avg_end_ms  = 0;       // millis() when the averaging window closes
  uint32_t  _avg_next_ms = 0;       // millis() of the next sample
  uint16_t  _avg_total_s = 0;       // configured window length (for the readout)

  PopupMenu      _ctx;               // Rename / Delete / Send on a selected waypoint
  bool      _kb_active = false;
  int       _kb_rename_idx = -1;     // -1 = marking new, ≥0 = renaming that index
  int32_t   _mark_lat = 0, _mark_lon = 0;   // pending new-mark position
  uint32_t  _mark_ts  = 0;

  // ADD form — enter a waypoint by coordinates. Lat/Lon are numeric, so they use
  // the digit-by-digit scroll editor (same widget as the radio frequency field)
  // rather than the text keyboard: Enter opens the editor on the magnitude,
  // LEFT/RIGHT on the row toggles the hemisphere (N/S, E/W). Only the Label field
  // uses the keyboard.
  float     _add_lat_mag = 0.0f, _add_lon_mag = 0.0f;   // magnitude (degrees)
  char      _add_label[WAYPOINT_LABEL_LEN] = "";
  bool      _add_lat_neg = false;    // true = S
  bool      _add_lon_neg = false;    // true = W
  int       _add_sel  = 0;           // 0=Lat 1=Lon 2=Label 3=Save
  int       _kb_field = -1;          // ADD label edit (>=0) vs mark/rename (-1)
  DigitEditor _add_editor;           // active while editing a Lat/Lon magnitude

  bool useImperial() const { return _task && _task->useImperial(); }
  bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); }

  // The nav list shows a synthetic "Trail start" row (index 0) whenever a trail
  // exists, followed by the saved waypoints. These map the combined row index
  // (_sel) onto that layout.
  bool hasStart() const { return !_store->empty(); }
  int  wpListCount() const { return (hasStart() ? 1 : 0) + _task->waypoints().count(); }
  bool selIsStart() const { return hasStart() && _sel == 0; }
  int  wpIndex() const { return _sel - (hasStart() ? 1 : 0); }  // index into WaypointStore

  // Add a waypoint by coordinates (no GPS fix needed). Opens the ADD form.
  void openAddForm() {
    if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
    _add_lat_mag = _add_lon_mag = 0.0f;
    _add_label[0] = '\0';
    _add_lat_neg = _add_lon_neg = false;
    _add_sel = 0;
    _add_editor.active = false;
    _mode = ADD;
  }

  // Store the just-typed label (the only ADD field that still uses the keyboard).
  void applyAddField(const char* buf) {
    strncpy(_add_label, buf, sizeof(_add_label) - 1);
    _add_label[sizeof(_add_label) - 1] = '\0';
  }

  // Validate the ADD form and save the waypoint.
  void commitAddForm() {
    double la = _add_lat_mag, lo = _add_lon_mag;
    if (_add_lat_neg) la = -la;
    if (_add_lon_neg) lo = -lo;
    if (la < -90.0 || la > 90.0 || lo < -180.0 || lo > 180.0) {
      _task->showAlert("Out of range", 1200); return;
    }
    if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
    int32_t lat = (int32_t)(la * 1e6 + (la < 0 ? -0.5 : 0.5));
    int32_t lon = (int32_t)(lo * 1e6 + (lo < 0 ? -0.5 : 0.5));
    char label[WAYPOINT_LABEL_LEN];
    if (_add_label[0]) { strncpy(label, _add_label, sizeof(label) - 1); label[sizeof(label) - 1] = '\0'; }
    else               snprintf(label, sizeof(label), "WP%d", _task->waypoints().count() + 1);
    if (_task->addWaypoint(lat, lon, label)) { _mode = OFF; }
  }

  // Open the shared keyboard for a literal field. Waypoint labels and the
  // lat/lon magnitudes are never placeholder-expanded, so {loc}/{time} are
  // cleared (same intent as the bot's literal trigger field).
  void openKb(const char* initial, int max) {
    KeyboardWidget& kb = _task->keyboard();
    kb.begin(initial, max);
    kb.clearPlaceholders();
    _kb_active = true;
  }

  // Capture a mark position and open the keyboard for its label. Shared by the
  // instant "Mark here" and the end of GPS averaging. _mode goes OFF: the
  // keyboard takes over the screen, and there's no sub-view to return to once
  // the label is committed (active() then tracks _kb_active alone).
  void beginLabel(int32_t lat, int32_t lon) {
    _mode = OFF;
    _mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
    _kb_rename_idx = -1;
    openKb("", WAYPOINT_LABEL_LEN - 1);
  }

  // Commit a mark-here / rename label from the keyboard.
  void commitLabel(const char* buf) {
    if (_kb_rename_idx >= 0) {
      _task->waypoints().rename(_kb_rename_idx, buf);
      _task->saveWaypoints();
      _kb_rename_idx = -1;
      return;
    }
    char auto_lbl[WAYPOINT_LABEL_LEN];
    if (!buf || !buf[0]) {
      snprintf(auto_lbl, sizeof(auto_lbl), "WP%d", _task->waypoints().count() + 1);
      buf = auto_lbl;
    }
    _task->addWaypoint(_mark_lat, _mark_lon, _mark_ts, buf);
  }

  void renderAddForm(DisplayDriver& display) {
    display.setColor(DisplayDriver::LIGHT);
    display.drawCenteredHeader("ADD WAYPOINT");
    const int top  = display.listStart();
    const int step = display.lineStep();
    for (int i = 0; i < 4; i++) {
      int y = top + i * step;
      bool sel = (i == _add_sel);
      display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
      // While editing a magnitude, draw the row prefix + the digit editor so the
      // place under the cursor is highlighted; otherwise draw the plain value.
      if ((i == 0 || i == 1) && sel && _add_editor.active) {
        char prefix[10];
        if (i == 0) snprintf(prefix, sizeof(prefix), "Lat: %c ", _add_lat_neg ? 'S' : 'N');
        else        snprintf(prefix, sizeof(prefix), "Lon: %c ", _add_lon_neg ? 'W' : 'E');
        display.setCursor(2, y); display.print(prefix);
        _add_editor.render(display, 2 + (int)strlen(prefix) * display.getCharWidth(), y);
      } else {
        char row[28];
        if      (i == 0) snprintf(row, sizeof(row), "Lat: %c %.5f", _add_lat_neg ? 'S' : 'N', _add_lat_mag);
        else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %.5f", _add_lon_neg ? 'W' : 'E', _add_lon_mag);
        else if (i == 2) snprintf(row, sizeof(row), "Label: %s",  _add_label[0] ? _add_label : "(auto)");
        else             snprintf(row, sizeof(row), "[Save]");
        display.setCursor(2, y); display.print(row);
      }
      display.setColor(DisplayDriver::LIGHT);
    }
  }

  // GPS-averaging progress screen. Sampling itself happens in poll(); this only
  // reports remaining time and the running sample count.
  void renderAvg(DisplayDriver& display) {
    display.setColor(DisplayDriver::LIGHT);
    display.drawCenteredHeader("AVERAGING GPS");
    const int top  = display.listStart();
    const int step = display.lineStep();
    int remain = (int)((int32_t)(_avg_end_ms - millis()) / 1000);
    if (remain < 0) remain = 0;
    char line[28];
    snprintf(line, sizeof(line), "%ds left (of %us)", remain, (unsigned)_avg_total_s);
    display.setCursor(2, top);            display.print(line);
    snprintf(line, sizeof(line), "Samples: %u", (unsigned)_avg_n);
    display.setCursor(2, top + step);     display.print(line);
    display.setCursor(2, top + 2 * step); display.print("Cancel to abort");
  }

  void renderWpList(DisplayDriver& display) {
    display.setColor(DisplayDriver::LIGHT);
    char title[24];
    snprintf(title, sizeof(title), "WAYPOINTS %d/%d",
             _task->waypoints().count(), WaypointStore::CAPACITY);
    display.drawCenteredHeader(title);

    int n     = wpListCount();
    int total = n + 1;                    // final row = "+ Add by coords"
    int32_t mylat, mylon; bool have = ownPos(mylat, mylon);

    drawList(display, total, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
      drawRowSelection(display, y, sel, reserve);

      if (row == n) {                     // the synthetic "Add" row
        display.setCursor(2, y); display.print("+ Add by coords");
        display.setColor(DisplayDriver::LIGHT);
        return;
      }

      int32_t tlat, tlon; const char* label;
      if (!rowTarget(row, tlat, tlon, label)) return;
      char dist[12] = ""; int bw = 0;
      if (have) {
        geo::fmtDist(dist, sizeof(dist), geo::haversineKm(mylat, mylon, tlat, tlon), useImperial());
        bw = display.getTextWidth(dist) + 2;
      }
      char nm[24];
      display.translateUTF8ToBlocks(nm, label, sizeof(nm));
      display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, nm);
      if (dist[0]) { display.setCursor(display.width() - bw + 1 - reserve, y); display.print(dist); }
      display.setColor(DisplayDriver::LIGHT);
    });
  }

  void renderWpNav(DisplayDriver& display) {
    int32_t tlat, tlon; const char* label;
    if (!rowTarget(_sel, tlat, tlon, label)) { _mode = LIST; return; }
    int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
    int cog; bool cogv = _task->currentCourse(cog);
    navview::draw(display, have, mylat, mylon, tlat, tlon, label, cogv, cog, useImperial());
  }

  // Navigate to the current track-back breadcrumb. The header doubles as the
  // progress readout: "Trail start" on the last leg, else points still to go.
  void renderTrackBack(DisplayDriver& display) {
    if (_tb_idx < 0 || _tb_idx >= _store->count()) { _mode = OFF; return; }
    const TrailPoint& t = _store->at(_tb_idx);
    int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
    int cog; bool cogv = _task->currentCourse(cog);
    char label[20];
    if (_tb_idx == 0) snprintf(label, sizeof(label), "Trail start");
    else              snprintf(label, sizeof(label), "Back: %d pt", _tb_idx);
    navview::draw(display, have, mylat, mylon, t.lat_1e6, t.lon_1e6,
                  label, cogv, cog, useImperial(), &_tb_eta);
  }

  // Resolve a combined-list row to a nav target. Row 0 is the trail start when
  // a trail exists; the rest are saved waypoints.
  bool rowTarget(int row, int32_t& lat, int32_t& lon, const char*& label) {
    if (hasStart() && row == 0) {
      const TrailPoint& s = _store->first();
      lat = s.lat_1e6; lon = s.lon_1e6; label = "Trail start";
      return true;
    }
    int wi = row - (hasStart() ? 1 : 0);
    WaypointStore& wp = _task->waypoints();
    if (wi >= 0 && wi < wp.count()) {
      const Waypoint& w = wp.at(wi);
      lat = w.lat_1e6; lon = w.lon_1e6;
      label = w.label[0] ? w.label : "(unnamed)";
      return true;
    }
    return false;
  }

public:
  WaypointsView(UITask* task, TrailStore* store) : _task(task), _store(store) {}

  void reset() { _mode = OFF; _ctx.active = false; _kb_active = false; _kb_field = -1; }

  // True while the component owns the screen (a sub-mode or the keyboard is up).
  bool active() const { return _mode != OFF || _kb_active; }

  // Entry points called from TrailScreen's action menu.
  void openList() { _mode = LIST; _sel = 0; _scroll = 0; }
  void markHere() {
    int32_t lat, lon;
    if (!ownPos(lat, lon))         { _task->showAlert("No GPS fix", 1000); return; }
    if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
    NodePrefs* p = _task->getNodePrefs();
    uint8_t avg_idx = p ? p->gps_avg_idx : 0;
    if (avg_idx == 0) { beginLabel(lat, lon); return; }   // instant mark (default)
    // Averaging: seed with the current fix, then sample for N more seconds.
    _avg_total_s = NodePrefs::gpsAvgSecs(avg_idx);
    _avg_sum_lat = lat; _avg_sum_lon = lon; _avg_n = 1;
    uint32_t now = millis();
    _avg_end_ms  = now + (uint32_t)_avg_total_s * 1000;
    _avg_next_ms = now + 1000;
    _mode = AVG;
  }

  // Retrace the recorded trail back to its start. Snaps onto the route at the
  // nearest recorded point, then NavView guides to each earlier breadcrumb in
  // turn (poll() advances the target) until the start is reached.
  void startTrackBack() {
    if (_store->count() < 2) { _task->showAlert("No trail", 1000); return; }
    int idx = _store->count() - 1;        // default: the newest end of the trail
    int32_t lat, lon;
    if (ownPos(lat, lon)) {               // else snap to the nearest recorded point
      float best = 1e30f;
      for (int i = 0; i < _store->count(); i++) {
        float d = geo::haversineKm(lat, lon, _store->at(i).lat_1e6, _store->at(i).lon_1e6);
        if (d < best) { best = d; idx = i; }
      }
    }
    _tb_idx = idx;
    _tb_eta.reset();
    _mode = TRACKBACK;
  }

  // Driven from TrailScreen::poll() so the position-tracking sub-modes tick on
  // the main loop, not on the (slow on e-ink) render cadence.
  void poll() {
    if (_mode == AVG)       pollAvg();
    else if (_mode == TRACKBACK) pollTrackBack();
  }

  // Accumulate GPS fixes while averaging; mark the mean when the window closes.
  void pollAvg() {
    uint32_t now = millis();
    if ((int32_t)(now - _avg_next_ms) >= 0) {
      int32_t lat, lon;
      if (ownPos(lat, lon)) { _avg_sum_lat += lat; _avg_sum_lon += lon; _avg_n++; }
      _avg_next_ms = now + 1000;
    }
    if ((int32_t)(now - _avg_end_ms) >= 0) {       // window closed → mark the mean
      if (_avg_n == 0) { _task->showAlert("No GPS fix", 1000); _mode = OFF; return; }
      int32_t mlat = (int32_t)(_avg_sum_lat / (long long)_avg_n);
      int32_t mlon = (int32_t)(_avg_sum_lon / (long long)_avg_n);
      beginLabel(mlat, mlon);                       // opens the label keyboard
    }
  }

  // Advance the track-back target when the current breadcrumb is reached; once
  // at the start (index 0) and within range, announce arrival and exit.
  void pollTrackBack() {
    int32_t lat, lon;
    if (!ownPos(lat, lon)) return;
    float d_m = geo::haversineKm(lat, lon, _store->at(_tb_idx).lat_1e6,
                                 _store->at(_tb_idx).lon_1e6) * 1000.0f;
    if (d_m > (float)TB_ARRIVE_M) return;
    if (_tb_idx > 0) { _tb_idx--; _tb_eta.reset(); }
    else { _task->showAlert("Back at start", 1500); _mode = OFF; }
  }

  // Only called while active().
  int render(DisplayDriver& display) {
    display.setTextSize(1);
    display.setColor(DisplayDriver::LIGHT);
    if (_kb_active) return _task->keyboard().render(display);  // keyboard owns the screen
    if (_mode == ADD) { renderAddForm(display); return 1000; }
    if (_mode == AVG) { renderAvg(display);     return display.isEink() ? 1000 : 300; }
    if (_mode == TRACKBACK) { renderTrackBack(display); return 1000; }
    if (_mode == NAV) { renderWpNav(display);   return 1000; }
    renderWpList(display);                          // LIST
    if (_ctx.active) _ctx.render(display);
    return 1000;
  }

  // Returns true if the input was consumed (always, while active()).
  bool handleInput(char c) {
    // Label keyboard (mark new / rename / ADD field) takes all input first.
    if (_kb_active) {
      KeyboardWidget& kb = _task->keyboard();
      auto r = kb.handleInput(c);
      if (r == KeyboardWidget::DONE) {
        if (_kb_field >= 0) applyAddField(kb.buf);    // ADD field edit
        else                commitLabel(kb.buf);      // mark / rename label
        _kb_active = false; _kb_field = -1;
      } else if (r == KeyboardWidget::CANCELLED) {
        _kb_active = false; _kb_field = -1;
      }
      return true;
    }

    // Rename/Delete/Send popup — operates on the saved-waypoint index (the
    // synthetic Trail-start row, if any, is never editable).
    if (_ctx.active) {
      auto res = _ctx.handleInput(c);
      if (res == PopupMenu::SELECTED) {
        int sel = _ctx.selectedIndex();
        int wi  = wpIndex();
        if (sel == 0) {                                  // Rename
          if (wi >= 0 && wi < _task->waypoints().count()) {
            _kb_rename_idx = wi;
            openKb(_task->waypoints().at(wi).label, WAYPOINT_LABEL_LEN - 1);
          }
        } else if (sel == 1) {                            // Delete
          if (wi >= 0 && wi < _task->waypoints().count()) {
            const Waypoint& w = _task->waypoints().at(wi);
            _task->clearTargetIfWaypoint(w.lat_1e6, w.lon_1e6);   // don't leave the Locator pointed at a deleted spot
            _task->waypoints().remove(wi);
            _task->saveWaypoints();
            if (_sel >= wpListCount()) _sel = wpListCount() - 1;
            if (_sel < 0) _sel = 0;
          }
        } else if (sel == 2) {                            // Send (share in a message)
          if (wi >= 0 && wi < _task->waypoints().count()) {
            const Waypoint& w = _task->waypoints().at(wi);
            double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
            char text[80];
            if (w.label[0]) snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f %s", lat, lon, w.label);
            else            snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
            _task->shareToMessage(text);   // hands off to the Messages screen
          }
        } else {                                          // Set as target
          if (wi >= 0 && wi < _task->waypoints().count()) {
            const Waypoint& w = _task->waypoints().at(wi);
            _task->setTargetNow(0, nullptr, w.lat_1e6, w.lon_1e6, w.label);
          }
        }
      }
      return true;
    }

    // Averaging window — any cancel aborts the mark; otherwise just wait it out.
    if (_mode == AVG) {
      if (c == KEY_CANCEL) _mode = OFF;
      return true;
    }

    // ADD form: scroll-edit lat/lon (magnitude) + hemisphere toggle + label.
    if (_mode == ADD) {
      // The digit editor, while open, consumes all input (UP/DOWN change the
      // digit, LEFT/RIGHT move the cursor, Enter commits, Cancel aborts).
      if (_add_editor.active) {
        if (_add_editor.handleInput(c) == DigitEditor::DONE) {
          if (_add_sel == 0) _add_lat_mag = _add_editor.value;
          else               _add_lon_mag = _add_editor.value;
        }
        return true;
      }
      if (c == KEY_CANCEL) { _mode = OFF; return true; }
      if (c == KEY_UP)   { _add_sel = (_add_sel > 0) ? _add_sel - 1 : 3; return true; }
      if (c == KEY_DOWN) { _add_sel = (_add_sel < 3) ? _add_sel + 1 : 0; return true; }
      if (keyIsPrev(c) || keyIsNext(c)) {
        if      (_add_sel == 0) _add_lat_neg = !_add_lat_neg;   // N <-> S
        else if (_add_sel == 1) _add_lon_neg = !_add_lon_neg;   // E <-> W
        return true;
      }
      if (c == KEY_ENTER) {
        if      (_add_sel == 0) _add_editor.begin(_add_lat_mag, 0.0f,  90.0f, 2, 5);
        else if (_add_sel == 1) _add_editor.begin(_add_lon_mag, 0.0f, 180.0f, 3, 5);
        else if (_add_sel == 2) { _kb_field = 2; openKb(_add_label, WAYPOINT_LABEL_LEN - 1); }
        else                    { commitAddForm(); }
        return true;
      }
      return true;
    }

    // Navigation view — any nav key returns to the list.
    if (_mode == NAV) {
      if (c == KEY_CANCEL || keyIsPrev(c) || keyIsNext(c)) { _mode = LIST; }
      return true;
    }

    // Track-back — launched from the action menu, so Cancel returns to the
    // trail views (not the waypoint list).
    if (_mode == TRACKBACK) {
      if (c == KEY_CANCEL) _mode = OFF;
      return true;
    }

    // List (row 0 is the synthetic "Trail start" when a trail exists; the final
    // row is "+ Add by coords"). Cancel returns control to the trail views.
    int n = wpListCount();
    int total = n + 1;                       // + the "Add by coords" row
    if (c == KEY_CANCEL) { _mode = OFF; return true; }
    // drawList() reclamps _scroll from _sel every render.
    if (c == KEY_UP)   { _sel = (_sel > 0) ? _sel - 1 : total - 1; return true; }
    if (c == KEY_DOWN) { _sel = (_sel < total - 1) ? _sel + 1 : 0; return true; }
    if (c == KEY_ENTER) {
      if (_sel == n) openAddForm();          // last row → open the add form
      else           _mode = NAV;            // a waypoint / Trail-start row
      return true;
    }
    // Rename/Delete/Send/Locator apply to saved waypoints only — not Trail-start or Add.
    if (c == KEY_CONTEXT_MENU && !selIsStart() && _sel != n &&
        wpIndex() >= 0 && wpIndex() < _task->waypoints().count()) {
      _ctx.begin("Waypoint", 4);
      _ctx.addItem("Rename");
      _ctx.addItem("Delete");
      _ctx.addItem("Send");
      _ctx.addItem("Set as target");
      return true;
    }
    return true;
  }
};
