#pragma once
#include <vector>
#include <deque>
#include <string>
#include "model/PlacedPiece.h"
#include "model/PlacedScenery.h"
#include "model/RocrailPlan.h"
#include "geometry/GridQuantizer.h"
#include "geometry/Vec2.h"

namespace MC {

// ── Tunnel ────────────────────────────────────────────────────────────────────
// A Tunnel records that two scenery portals form a tunnel over a specific
// list of track pieces. Recorded explicitly at creation time and dissolved
// when the user runs "Remove Tunnel" -- not derived from geometry every frame.
//
// Invariants once a Tunnel is in `state.tunnels`:
//   * Both portal ids reference valid PlacedScenery entries in state.scenery.
//   * Every coveredPieceId references a valid PlacedPiece in state.pieces.
//   * No covered piece may be moved, rotated, or deleted while the Tunnel
//     exists (the user must "Remove Tunnel" first).
//   * Portal scenery may not be deleted ad-hoc -- only via "Remove Tunnel".
struct Tunnel {
    int              id           = -1;
    int              portalIdA    = -1;
    int              portalIdB    = -1;
    int              anchorPieceA = -1;   // piece portalA sits inside
    int              anchorPieceB = -1;   // piece portalB sits inside
    std::vector<int> coveredPieceIds;
};

// ── Helix ─────────────────────────────────────────────────────────────────────
// A pre-built two-track vertical spiral. The user drops it onto the canvas as
// a single object and may move, rotate, or delete it as one unit. Individual
// curves inside the helix are NOT placed in state.pieces -- they are only
// rendered visually. The 4 stub pieces (2 bottom + 2 top) are real
// PlacedPieces tagged with helixId so external track can snap to them.
//
// The actual geometry (turns, rise, radii, catalog pieces) is looked up
// from the variantId via MC::FindHelixVariant. See model/HelixVariant.h.
// Defaulting variantId to "proses_2_5_r1r2" keeps pre-variant save files
// working unchanged.
struct Helix {
    int         id      = -1;
    Vec2        center;                    // world position of helix center (mm)
    double      heading = 0.0;             // rotation around vertical axis (degrees)
    std::string variantId = "proses_2_5_r1r2";  // key into HelixVariant registry
    // The 4 stub piece IDs (real PlacedPieces in state.pieces).
    int    bottomStubR1 = -1;
    int    bottomStubR2 = -1;
    int    topStubR1    = -1;
    int    topStubR2    = -1;
};

// ── Canvas label ──────────────────────────────────────────────────────────────
struct CanvasLabel {
    int         id       = -1;
    std::string text;
    Vec2        pos;           // world position (mm)
    bool        selected = false;
};

// ── Measurement ───────────────────────────────────────────────────────────────
// A measuring-tape annotation: two endpoints with arrowheads and a length
// label that lives at midpoint by default but can be dragged off the line.
// `labelOffset` is the user-applied offset from the midpoint (zero = on it).
struct Measurement {
    int   id           = -1;
    Vec2  a;                // world mm
    Vec2  b;                // world mm
    Vec2  labelOffset {0,0};
    bool  selected     = false;
};

// ── Free-form polygon shape ───────────────────────────────────────────────────
// User-drawn polygon that can represent an access hatch, a mountain, a river,
// or any other piece of layout context. Closed (first vertex implicitly joins
// the last). Self-intersection is allowed — even-odd fill applies. Tied to a
// level so per-level visibility hides it. In 3D, it extrudes from the level
// deck (base z = level height) up by heightMm (so a mountain has a nonzero
// height; a flat river or hatch leaves heightMm at 0).
struct CanvasShape {
    int                id         = -1;
    std::vector<Vec2>  vertices;            // world mm, closed
    uint8_t            r = 120, g = 170, b = 110;  // default soft green
    uint8_t            alpha   = 255;       // opaque by default; lower it to see track beneath
    bool               hasOutline = true;
    int                levelId    = 1;
    double             heightMm   = 0.0;    // extrusion height in 3D
    std::string        label;               // optional caption shown in centre
    bool               selected   = false;
};

// ── Layout level (layer) ──────────────────────────────────────────────────────
struct Level {
    int         id      = 1;
    std::string name    = "Level 1";
    bool        visible = true;
    // Physical elevation of this level above the ground (mm). Track and
    // scenery assigned to the level inherit this as their z.
    double      heightMm = 0.0;
    // Track color for pieces on this level. Only applied when hasTrackColor
    // is true; a piece's own custom color (PlacedPiece::hasColor) still wins.
    bool        hasTrackColor = false;
    uint8_t     r = 180, g = 180, b = 180;  // default grey
    // Optional deck shape (board outline) for this level, in world mm.
    // Empty = no deck assigned (the user may forgo a shape for any level).
    // Replaces the old single state.baseboard, now per-level.
    std::vector<Vec2> shape;
};

// ── Edit command (for undo/redo) ──────────────────────────────────────────────
struct GradeRunSnapshot {
    double gradeDeg    = 0.0;
    int    anchorId    = -1;
    int    connIdx     = 1;
};

struct EditSnapshot {
    std::vector<PlacedPiece>      pieces;
    std::vector<PlacedScenery>    scenery;
    std::vector<CanvasLabel>      labels;
    std::vector<Measurement>      measurements;
    std::vector<CanvasShape>      shapes;
    std::vector<Tunnel>           tunnels;
    std::vector<Helix>            helices;
    std::vector<GradeRunSnapshot> gradeRuns;
    bool                          gradeActive   = false;
    double                        gradeDeg      = 0.0;
    int                           gradeAnchorId = -1;
    int                           gradeConnIdx  = 1;
};

// ── Tool modes ────────────────────────────────────────────────────────────────
enum class ToolMode {
    Select,
    Place,
    Pan,
    Measure,        // placing a measuring-tape annotation
    Polygon,        // placing a free-form polygon shape
    BaseboardDraw,  // drawing the baseboard outline (same gesture as Polygon)
};

// ── The shared canvas model ───────────────────────────────────────────────────
struct CanvasState {
    // ── Placed pieces ────────────────────────────────────────────────────
    std::vector<PlacedPiece>   pieces;
    std::vector<PlacedScenery> scenery;
    std::vector<CanvasLabel>   labels;
    std::vector<Measurement>   measurements;
    std::vector<CanvasShape>   shapes;
    std::vector<Tunnel>        tunnels;
    std::vector<Helix>         helices;
    int                        nextId = 1;

    // ── Helix helpers ────────────────────────────────────────────────────
    const Helix* HelixById(int helixId) const {
        for (const auto& h : helices) if (h.id == helixId) return &h;
        return nullptr;
    }
    bool IsPieceInHelix(int pieceId) const {
        for (const auto& p : pieces) if (p.id == pieceId) return p.helixId != -1;
        return false;
    }

    // ── Tunnel helpers ───────────────────────────────────────────────────
    // Returns the Tunnel containing pieceId, or nullptr if none does.
    const Tunnel* TunnelContainingPiece(int pieceId) const {
        for (const auto& t : tunnels)
            for (int id : t.coveredPieceIds)
                if (id == pieceId) return &t;
        return nullptr;
    }
    bool IsPieceInTunnel(int pieceId) const {
        return TunnelContainingPiece(pieceId) != nullptr;
    }
    // Returns the Tunnel a given portal scenery item belongs to, or nullptr.
    const Tunnel* TunnelContainingPortal(int portalSceneryId) const {
        for (const auto& t : tunnels)
            if (t.portalIdA == portalSceneryId || t.portalIdB == portalSceneryId)
                return &t;
        return nullptr;
    }
    bool IsPortalScenery(int sceneryId) const {
        return TunnelContainingPortal(sceneryId) != nullptr;
    }

    // ── Levels ───────────────────────────────────────────────────────────
    std::vector<Level> levels;
    int                activeLevelId = 1;  // new pieces go on this level

    void EnsureDefaultLevel() {
        if (levels.empty()) {
            Level l; l.id=1; l.name="Level 1";
            l.r=180; l.g=200; l.b=220;
            levels.push_back(l);
        }
    }

    Level* FindLevel(int id) {
        for (auto& l : levels) if (l.id == id) return &l;
        return nullptr;
    }

    const Level* FindLevel(int id) const {
        for (auto& l : levels) if (l.id == id) return &l;
        return nullptr;
    }

    // Physical height (mm) of a level, or 0 if unknown.
    double LevelHeight(int id) const {
        const Level* l = FindLevel(id);
        return l ? l->heightMm : 0.0;
    }

    int NextLevelId() const {
        int mx = 0;
        for (auto& l : levels) mx = std::max(mx, l.id);
        return mx + 1;
    }

    bool IsLevelVisible(int id) const {
        for (auto& l : levels) if (l.id == id) return l.visible;
        return true;  // unknown level = visible
    }

    // ── Tool state ───────────────────────────────────────────────────────
    ToolMode    toolMode      = ToolMode::Select;
    std::string placingCat;    // catalog number of piece being dragged
    std::string placingScenery; // scenery item id being dragged

    // ── Baseboard shape ───────────────────────────────────────────────────
    // World coordinates (mm). Empty = no baseboard defined.
    std::vector<MC::Vec2> baseboard;
    bool showBaseboard = true;

    // Last-used Layout Shape dialog settings, so reopening the dialog
    // restores the user's previous choice and dimensions instead of
    // resetting to defaults.
    struct LastShapeSpec {
        int    shapeId = 0;     // 0=rect, 1=L, 2=U, 3=T, 4=custom, 5=clear
        double w_mm   = 2400.0;
        double h_mm   = 1200.0;
        double w2_mm  = 1200.0;
        double h2_mm  = 1200.0;
    } lastShape;

    // ── View options ─────────────────────────────────────────────────────
    bool snapEnabled = true;
    bool showGrid    = true;
    bool showLabels  = true;
    bool showRails   = true;
    int  units       = 0;  // 0=mm, 1=cm, 2=inches

    // ── Active elevation runs ─────────────────────────────────────────────
    struct GradeRun {
        double gradeDeg    = 0.0;
        int    anchorId    = -1;
        int    connIdx     = 1;
    };
    std::vector<GradeRun> gradeRuns;

    // Convenience accessors for the "current" run (last in list, if any)
    bool   gradeActive   = false;  // true when at least one run is active
    double gradeDeg      = 0.0;    // current run's degree
    int    gradeAnchorId = -1;     // current run's anchor
    int    gradeConnIdx  = 1;      // current run's conn

    void SyncGradeFromRuns() {
        if (gradeRuns.empty()) {
            gradeActive = false; gradeDeg = 0.0; gradeAnchorId = -1; gradeConnIdx = 1;
        } else {
            auto& r = gradeRuns.back();
            gradeActive = true; gradeDeg = r.gradeDeg;
            gradeAnchorId = r.anchorId; gradeConnIdx = r.connIdx;
        }
    }

    void StartGradeRun(double deg, int anchorId, int connIdx) {
        // If there's already a run with this anchorId, update it
        for (auto& r : gradeRuns)
            if (r.anchorId == anchorId) { r.gradeDeg = deg; r.connIdx = connIdx; SyncGradeFromRuns(); return; }
        GradeRun r; r.gradeDeg = deg; r.anchorId = anchorId; r.connIdx = connIdx;
        gradeRuns.push_back(r);
        SyncGradeFromRuns();
    }

    void StopGradeRun(int anchorId) {
        gradeRuns.erase(std::remove_if(gradeRuns.begin(), gradeRuns.end(),
            [anchorId](const GradeRun& r){ return r.anchorId == anchorId; }), gradeRuns.end());
        SyncGradeFromRuns();
    }

    void StopAllGradeRuns() {
        gradeRuns.clear();
        SyncGradeFromRuns();
    }

    // Find the grade run whose anchor is the given piece, or nullptr
    GradeRun* FindGradeRun(int anchorId) {
        for (auto& r : gradeRuns) if (r.anchorId == anchorId) return &r;
        return nullptr;
    }

    // Find ANY active grade run that has this piece as anchor
    GradeRun* ActiveRunForAnchor(int anchorId) {
        return FindGradeRun(anchorId);
    }

    // ── Grid quantizer ────────────────────────────────────────────────────
    GridQuantizer quantizer{ 32.0 };

    // ── Undo / Redo ──────────────────────────────────────────────────────
    static constexpr int kMaxUndo = 50;
    std::deque<EditSnapshot> undoStack;
    std::deque<EditSnapshot> redoStack;

    void PushUndo() {
        EditSnapshot snap;
        snap.pieces       = pieces;
        snap.scenery      = scenery;
        snap.labels       = labels;
        snap.measurements = measurements;
        snap.shapes       = shapes;
        snap.tunnels      = tunnels;
        snap.helices      = helices;
        snap.gradeActive  = gradeActive;
        snap.gradeDeg     = gradeDeg;
        snap.gradeAnchorId = gradeAnchorId;
        snap.gradeConnIdx  = gradeConnIdx;
        for (auto& r : gradeRuns)
            snap.gradeRuns.push_back({r.gradeDeg, r.anchorId, r.connIdx});
        undoStack.push_back(std::move(snap));
        if ((int)undoStack.size() > kMaxUndo) undoStack.pop_front();
        redoStack.clear();
    }

    void RestoreSnapshot(const EditSnapshot& snap) {
        pieces        = snap.pieces;
        scenery       = snap.scenery;
        labels        = snap.labels;
        measurements  = snap.measurements;
        shapes        = snap.shapes;
        tunnels       = snap.tunnels;
        helices       = snap.helices;
        gradeActive   = snap.gradeActive;
        gradeDeg      = snap.gradeDeg;
        gradeAnchorId = snap.gradeAnchorId;
        gradeConnIdx  = snap.gradeConnIdx;
        gradeRuns.clear();
        for (auto& r : snap.gradeRuns)
            gradeRuns.push_back({r.gradeDeg, r.anchorId, r.connIdx});
    }

    void Undo() {
        if (undoStack.empty()) return;
        EditSnapshot cur;
        cur.pieces = pieces; cur.scenery = scenery;
        cur.gradeActive = gradeActive; cur.gradeDeg = gradeDeg;
        cur.gradeAnchorId = gradeAnchorId; cur.gradeConnIdx = gradeConnIdx;
        for (auto& r : gradeRuns) cur.gradeRuns.push_back({r.gradeDeg, r.anchorId, r.connIdx});
        redoStack.push_back(std::move(cur));
        RestoreSnapshot(undoStack.back());
        undoStack.pop_back();
    }

    void Redo() {
        if (redoStack.empty()) return;
        EditSnapshot cur;
        cur.pieces = pieces; cur.scenery = scenery;
        cur.gradeActive = gradeActive; cur.gradeDeg = gradeDeg;
        cur.gradeAnchorId = gradeAnchorId; cur.gradeConnIdx = gradeConnIdx;
        for (auto& r : gradeRuns) cur.gradeRuns.push_back({r.gradeDeg, r.anchorId, r.connIdx});
        undoStack.push_back(std::move(cur));
        RestoreSnapshot(redoStack.back());
        redoStack.pop_back();
    }

    bool CanUndo() const { return !undoStack.empty(); }
    bool CanRedo() const { return !redoStack.empty(); }

    // ── Selection ────────────────────────────────────────────────────────
    bool HasSelection() const {
        for (const auto& p : pieces)  if (p.selected) return true;
        for (const auto& s : scenery) if (s.selected) return true;
        for (const auto& s : shapes)  if (s.selected) return true;
        return false;
    }

    // Selected polygon shapes. Shapes rotate like track and scenery do, so the
    // rotation code needs to reach them the same way.
    std::vector<CanvasShape*> SelectedShapes() {
        std::vector<CanvasShape*> out;
        for (auto& s : shapes) if (s.selected) out.push_back(&s);
        return out;
    }

    // Centre of a shape -- the average of its vertices. Used as the rotation
    // pivot: a rectangle should spin about its middle, not a corner.
    static Vec2 ShapeCentroid(const CanvasShape& s) {
        if (s.vertices.empty()) return {0, 0};
        double sx = 0, sy = 0;
        for (const auto& v : s.vertices) { sx += v.x; sy += v.y; }
        double n = (double)s.vertices.size();
        return { sx / n, sy / n };
    }

    std::vector<PlacedPiece*> Selected() {
        std::vector<PlacedPiece*> result;
        for (auto& p : pieces) if (p.selected) result.push_back(&p);
        return result;
    }

    std::vector<PlacedScenery*> SelectedScenery() {
        std::vector<PlacedScenery*> result;
        for (auto& s : scenery) if (s.selected) result.push_back(&s);
        return result;
    }

    void SelectAll() {
        for (auto& p : pieces)  p.selected = true;
        for (auto& s : scenery) s.selected = true;
    }

    void ClearSelection() {
        for (auto& p : pieces)       p.selected = false;
        for (auto& s : scenery)      s.selected = false;
        for (auto& l : labels)       l.selected = false;
        for (auto& m : measurements) m.selected = false;
        for (auto& s : shapes)       s.selected = false;
    }

    void DeleteSelected() {
        PushUndo();
        // Cascade: if any 74861 turntable is being deleted, mark every
        // piece joined to one of its spokes as selected too. That covers
        // the 6 free-bundled 74871 stubs and any extras the user added.
        // Without this, deleting the turntable would leave orphaned stubs
        // floating where the turntable used to be.
        for (auto& p : pieces) {
            if (!p.selected) continue;
            if (p.catalogNumber != "74861") continue;
            for (const auto& j : p.joints) {
                if (j.pieceId < 0) continue;
                if (PlacedPiece* nb = FindById(j.pieceId))
                    nb->selected = true;
            }
        }

        std::vector<int> deletedIds;
        for (const auto& p : pieces)
            if (p.selected) deletedIds.push_back(p.id);

        // If the grade anchor is being deleted, stop the grade run entirely.
        // For each grade run whose anchor is being deleted, walk back to predecessor
        for (auto& run : gradeRuns) {
            if (std::find(deletedIds.begin(), deletedIds.end(), run.anchorId) == deletedIds.end())
                continue;
            int newAnchor = -1;
            int newConnIdx = 1;
            for (const auto& p : pieces) {
                if (p.selected) continue;
                for (int ci = 0; ci < (int)p.joints.size(); ++ci) {
                    if (std::find(deletedIds.begin(), deletedIds.end(),
                                  p.joints[ci].pieceId) != deletedIds.end()) {
                        newAnchor  = p.id;
                        newConnIdx = (ci == 0) ? 1 : 0;
                        break;
                    }
                }
                if (newAnchor >= 0) break;
            }
            run.anchorId = newAnchor;
            run.connIdx  = newConnIdx;
        }
        // Remove runs with no surviving anchor
        gradeRuns.erase(std::remove_if(gradeRuns.begin(), gradeRuns.end(),
            [](const GradeRun& r){ return r.anchorId < 0; }), gradeRuns.end());
        SyncGradeFromRuns();

        for (auto& p : pieces)
            for (auto& j : p.joints)
                for (int did : deletedIds)
                    if (j.pieceId == did) { j.pieceId = -1; j.connIdx = -1; }

        pieces.erase(std::remove_if(pieces.begin(), pieces.end(),
            [](const PlacedPiece& p){ return p.selected; }), pieces.end());

        // If any of a helix's stub pieces was just deleted, the helix is
        // no longer renderable -- drop the Helix record so the ghost
        // curves stop being drawn and the part-count tally is correct.
        if (!helices.empty() && !deletedIds.empty()) {
            auto wasDeleted = [&](int id) {
                return std::find(deletedIds.begin(), deletedIds.end(), id)
                       != deletedIds.end();
            };
            helices.erase(std::remove_if(helices.begin(), helices.end(),
                [&](const Helix& h) {
                    return wasDeleted(h.bottomStubR1)
                        || wasDeleted(h.bottomStubR2)
                        || wasDeleted(h.topStubR1)
                        || wasDeleted(h.topStubR2);
                }), helices.end());
        }

        scenery.erase(std::remove_if(scenery.begin(), scenery.end(),
            [](const PlacedScenery& s){ return s.selected; }), scenery.end());

        labels.erase(std::remove_if(labels.begin(), labels.end(),
            [](const CanvasLabel& l){ return l.selected; }), labels.end());

        measurements.erase(std::remove_if(measurements.begin(), measurements.end(),
            [](const Measurement& m){ return m.selected; }), measurements.end());

        shapes.erase(std::remove_if(shapes.begin(), shapes.end(),
            [](const CanvasShape& s){ return s.selected; }), shapes.end());
    }

    void Clear() {
        PushUndo();
        pieces.clear();
        scenery.clear();
        labels.clear();
        measurements.clear();
        shapes.clear();
        tunnels.clear();
        helices.clear();
        nextId = 1;
        gradeRuns.clear();
        SyncGradeFromRuns();
    }

    // ── Piece lookup ──────────────────────────────────────────────────────
    PlacedPiece* FindById(int id) {
        for (auto& p : pieces) if (p.id == id) return &p;
        return nullptr;
    }

    PlacedScenery* FindSceneryById(int id) {
        for (auto& s : scenery) if (s.id == id) return &s;
        return nullptr;
    }

    int AddPiece(PlacedPiece piece) {
        PushUndo();
        piece.id = nextId++;
        pieces.push_back(std::move(piece));
        return pieces.back().id;
    }

    int AddScenery(PlacedScenery item) {
        PushUndo();
        item.id = nextId++;
        scenery.push_back(std::move(item));
        return scenery.back().id;
    }

    // ── Export ────────────────────────────────────────────────────────────
    RocrailPlan BuildPlan() const;
};

} // namespace MC
