#include "panels/PalettePanel.h"
#include "panels/CanvasPanel.h"
#include "panels/PropertiesPanel.h"
#include "MainFrame.h"
#include "io/CatalogLoader.h"
#include "io/SceneryCatalog.h"
#include "io/LayoutXml.h"
#include "UserPaths.h"
#include "model/HelixVariant.h"
#include <wx/dir.h>
#include <wx/stdpaths.h>
#include <wx/dcbuffer.h>
#include <map>
#include <set>
#include <cmath>

enum {
    ID_SysChoice  = 3000,
    ID_ListTrack,   // no longer a wxListBox but keep ID for compat
    ID_ListLib,
    ID_ListScenery,
    ID_ListSignals
};

// ── TrackIconGrid ─────────────────────────────────────────────────────────────
// A custom scrolled panel that shows track pieces as 80x80 icon cells with
// a catalog-number label underneath, arranged in 3 columns. Section headers
// span the full width. Replaces the old wxListBox for the Track tab.
// ─────────────────────────────────────────────────────────────────────────────

static const int kCellW    = 80;
static const int kCellH    = 80;
static const int kLabelH   = 16;
static const int kPadX     = 6;
static const int kPadY     = 6;
static const int kHdrH     = 22;
static const int kCols     = 3;   // maximum columns
static const int kMinCellW = 72;  // minimum cell width before reducing columns

static int ComputeCols(int panelW) {
    if (panelW >= kMinCellW * 3 + kPadX * 2) return 3;
    if (panelW >= kMinCellW * 2 + kPadX * 2) return 2;
    return 1;
}

struct GridEntry {
    bool        isHeader  = false;
    bool        isText    = false;      // full-width selectable text row (helix)
    wxString    headerLabel;            // only if isHeader
    wxString    displayName;            // shown for isText rows
    std::string catNumber;              // catalog number or @helix: pseudo-id
    wxBitmap    bitmap;                 // pre-rendered 80x80 (piece cells only)
    int         row = 0;
    int         col = 0;
};

class TrackIconGrid : public wxScrolledWindow {
public:
    TrackIconGrid(wxWindow* parent, PalettePanel* palette)
        : wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
                           wxVSCROLL | wxWANTS_CHARS)
        , m_palette(palette)
    {
        SetBackgroundStyle(wxBG_STYLE_PAINT);
        SetScrollRate(0, 10);
        Bind(wxEVT_PAINT,       &TrackIconGrid::OnPaint,     this);
        Bind(wxEVT_LEFT_DOWN,   &TrackIconGrid::OnLeftDown,  this);
        Bind(wxEVT_LEFT_UP,     &TrackIconGrid::OnLeftUp,    this);
        Bind(wxEVT_LEFT_DCLICK, &TrackIconGrid::OnDblClick,  this);
        Bind(wxEVT_MOTION,      &TrackIconGrid::OnMouseMove, this);
        Bind(wxEVT_LEAVE_WINDOW,&TrackIconGrid::OnLeave,     this);
        Bind(wxEVT_SIZE,        &TrackIconGrid::OnResize,    this);
    }

    void Clear() {
        m_entries.clear();
        m_catMap.clear();
        m_selectedIdx = -1;
        m_hoverIdx    = -1;
        UpdateLayout();
    }

    // Add a full-width section header row.
    void AddHeader(const wxString& label) {
        GridEntry e;
        e.isHeader    = true;
        e.headerLabel = label;
        m_entries.push_back(std::move(e));
        UpdateLayout();
    }

    // Add a full-width selectable text row (used for helix variants).
    void AddTextItem(const std::string& catNum, const wxString& displayName) {
        GridEntry e;
        e.isHeader   = false;
        e.isText     = true;
        e.catNumber  = catNum;
        e.displayName = displayName;
        m_entries.push_back(std::move(e));
        UpdateLayout();
    }

    // Add a piece cell. Returns the entry index in m_entries.
    int AddPiece(const std::string& catNum) {
        const MC::TrackPiece* def = MC::GetCatalog().Find(catNum);
        GridEntry e;
        e.isHeader  = false;
        e.catNumber = catNum;
        e.bitmap    = RenderPiece(def);
        int idx = (int)m_entries.size();
        m_catMap[catNum] = idx;
        m_entries.push_back(std::move(e));
        UpdateLayout();
        return idx;
    }

    // Returns catalog number of currently selected piece (empty if none / header).
    std::string SelectedCat() const {
        if (m_selectedIdx < 0 || m_selectedIdx >= (int)m_entries.size()) return "";
        if (m_entries[m_selectedIdx].isHeader) return "";
        return m_entries[m_selectedIdx].catNumber;
    }

    int SelectedIdx() const { return m_selectedIdx; }

    // Hit-test: returns entry index at unscrolled client point, -1 if none.
    int HitTest(wxPoint client) const {
        // Adjust for scroll offset
        int scrollY = 0; GetViewStart(nullptr, &scrollY);
        int uy = client.y + scrollY * 10;
        int ux = client.x;
        int W = GetClientSize().x;
        int totalCellW = (W - kPadX) / ComputeCols(W);

        int y = kPadY;
        for (int i = 0; i < (int)m_entries.size(); ++i) {
            const auto& e = m_entries[i];
            if (e.isHeader) {
                if (uy >= y && uy < y + kHdrH) return -1; // header, not a piece
                y += kHdrH;
            } else {
                // Determine how many piece cells are in this row
                // Find row start: walk back to first piece after last header on this row
                int col = e.col;
                int rowY = y - col * 0; // we track row starts separately
                // Use pre-computed row/col
                int cx = kPadX + col * totalCellW;
                int cy = m_rowY[i];
                if (ux >= cx && ux < cx + totalCellW &&
                    uy >= cy && uy < cy + kCellH + kLabelH + kPadY)
                    return i;
            }
        }
        return -1;
    }

    wxPoint CellScreenPos(int idx) const {
        if (idx < 0 || idx >= (int)m_entries.size()) return wxDefaultPosition;
        int scrollY = 0; GetViewStart(nullptr, &scrollY);
        int W = GetClientSize().x;
        int totalCellW = (W - kPadX) / ComputeCols(W);
        int cx = kPadX + m_entries[idx].col * totalCellW;
        int cy = m_rowY[idx] - scrollY * 10;
        return ClientToScreen(wxPoint(cx, cy));
    }

private:
    PalettePanel* m_palette;
    std::vector<GridEntry> m_entries;
    std::vector<int>       m_rowY;      // top-y of each entry in unscrolled coords
    std::unordered_map<std::string, int> m_catMap;
    int  m_selectedIdx    = -1;
    int  m_hoverIdx       = -1;
    bool m_justDblClicked = false;
    MC::Renderer m_renderer;

    void UpdateLayout() {
        m_rowY.resize(m_entries.size(), 0);
        int W = GetClientSize().x;
        if (W < 10) W = 240;
        int cols = ComputeCols(W);
        int totalCellW = (W - kPadX) / cols;

        int y = kPadY;
        int col = 0;
        int rowStartY = y;

        for (int i = 0; i < (int)m_entries.size(); ++i) {
            auto& e = m_entries[i];
            if (e.isHeader || e.isText) {
                // Flush any partial row first
                if (col > 0) {
                    y = rowStartY + kCellH + kLabelH + kPadY;
                    col = 0;
                }
                m_rowY[i] = y;
                y += kHdrH;
                rowStartY = y;
            } else {
                e.col = col;
                m_rowY[i] = rowStartY;
                col++;
                if (col >= cols) {
                    y = rowStartY + kCellH + kLabelH + kPadY;
                    rowStartY = y;
                    col = 0;
                }
            }
        }
        if (col > 0) y = rowStartY + kCellH + kLabelH + kPadY;

        SetVirtualSize(W, y + kPadY);
        Refresh();
    }

    wxBitmap RenderPiece(const MC::TrackPiece* def) {
        wxBitmap bmp(kCellW, kCellH);
        wxMemoryDC dc(bmp);
        dc.SetBackground(wxBrush(wxColour(255, 255, 255)));
        dc.Clear();

        if (!def) return bmp;

        // All pieces are rendered at the same horizontal scale, using 24230's
        // chord length (218.75mm = 437.5 * sin(30deg)) as the reference width.
        // This makes a short 24064 and a long 24360 appear at the same width
        // in the cell, giving the grid a uniform, grid-like look.
        static const double kRefWidth = 218.75;
        // Turntable pit (278mm diameter) is much larger than a normal piece;
        // use a bigger reference width so it renders smaller in the cell.
        double refW = (def->type == MC::PieceType::Turntable) ? 600.0 : kRefWidth;
        double pieceH = 40.0;
        if (def->radiusMm > 0 && def->angleDeg != 0) {
            double bow = def->radiusMm *
                (1.0 - std::cos(std::abs(def->angleDeg) * M_PI / 180.0));
            pieceH = std::max(pieceH, bow * 2.5);
        }
        if (def->connections.size() > 2) pieceH = std::max(pieceH, 60.0);

        double margin = 8.0;
        double drawW  = kCellW - 2 * margin;
        double drawH  = kCellH - 2 * margin;
        // Scale horizontally to fill same width as the reference piece;
        // also constrain vertically so tall curves don't overflow.
        double scaleX = drawW / refW * 0.82;
        double scaleY = drawH / pieceH;
        double scale  = std::min(scaleX, scaleY);
        scale = std::max(0.03, std::min(scale, 4.0));

        MC::ViewTransform view;
        view.scale = scale;
        // Centre horizontally using the piece's actual length, not the reference.
        double pieceW = std::max(def->lengthMm, 1.0);
        view.panX  = margin + drawW / 2.0 - (pieceW * scale) / 2.0;
        view.panY  = margin + drawH / 2.0;

        MC::PlacedPiece placed;
        placed.catalogNumber = def->catalogNumber;
        placed.transform.pos = {0, 0};
        placed.transform.heading = 0;
        placed.joints.resize(def->connections.size(), {-1, -1});

        m_renderer.DrawPiece(dc, placed, *def, view, false, false);
        return bmp;
    }

    void OnPaint(wxPaintEvent&) {
        wxAutoBufferedPaintDC dc(this);
        DoPrepareDC(dc);
        dc.SetBackground(wxBrush(wxColour(245, 245, 245)));
        dc.Clear();

        int W = GetClientSize().x;
        int totalCellW = (W - kPadX) / ComputeCols(W);

        // Visible rect for culling
        wxRect visible;
        {
            int sx, sy; GetViewStart(&sx, &sy);
            wxSize sz = GetClientSize();
            visible = wxRect(0, sy * 10, sz.x, sz.GetHeight());
        }

        for (int i = 0; i < (int)m_entries.size(); ++i) {
            const auto& e = m_entries[i];
            int ey = m_rowY[i];

            if (e.isHeader) {
                if (ey + kHdrH < visible.y || ey > visible.GetBottom()) continue;
                dc.SetBrush(wxBrush(wxColour(75, 85, 100)));
                dc.SetPen(*wxTRANSPARENT_PEN);
                dc.DrawRectangle(0, ey, W, kHdrH);
                dc.SetFont(wxFont(9, wxFONTFAMILY_DEFAULT,
                                  wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
                dc.SetTextForeground(wxColour(220, 225, 235));
                dc.DrawText("  " + e.headerLabel, 2, ey + 4);
            } else if (e.isText) {
                if (ey + kHdrH < visible.y || ey > visible.GetBottom()) continue;
                bool sel   = (i == m_selectedIdx);
                bool hover = (i == m_hoverIdx);
                wxColour bg = sel   ? wxColour(200, 220, 255)
                            : hover ? wxColour(225, 235, 250)
                            : wxColour(250, 250, 252);
                dc.SetBrush(wxBrush(bg));
                dc.SetPen(wxPen(wxColour(200, 200, 205), 1));
                dc.DrawRectangle(0, ey, W, kHdrH);
                dc.SetFont(wxFont(9, wxFONTFAMILY_DEFAULT,
                                  wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
                dc.SetTextForeground(wxColour(40, 40, 40));
                dc.SetBackgroundMode(wxTRANSPARENT);
                dc.DrawText("  " + e.displayName, 4, ey + 4);
            } else {
                if (ey + kCellH + kLabelH < visible.y || ey > visible.GetBottom()) continue;
                int cx = kPadX + e.col * totalCellW;

                // Cell background
                bool selected = (i == m_selectedIdx);
                bool hover    = (i == m_hoverIdx);
                wxColour bg = selected ? wxColour(200, 220, 255)
                            : hover    ? wxColour(225, 235, 250)
                            : wxColour(255, 255, 255);
                dc.SetBrush(wxBrush(bg));
                dc.SetPen(wxPen(selected ? wxColour(200, 30, 30)
                               : hover   ? wxColour(150, 170, 200)
                               : wxColour(200, 200, 200), selected ? 2 : 1));
                dc.DrawRectangle(cx, ey, kCellW, kCellH);

                // Bitmap
                if (e.bitmap.IsOk())
                    dc.DrawBitmap(e.bitmap, cx, ey, false);

                // Label
                dc.SetFont(wxFont(8, wxFONTFAMILY_DEFAULT,
                                  wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
                dc.SetTextForeground(wxColour(40, 40, 40));
                dc.SetBackgroundMode(wxTRANSPARENT);
                wxString lbl = wxString::FromUTF8(e.catNumber.c_str());
                wxSize ts = dc.GetTextExtent(lbl);
                int lx = cx + (kCellW - ts.x) / 2;
                int ly = ey + kCellH + 2;
                dc.DrawText(lbl, lx, ly);
            }
        }
    }

    void OnLeftDown(wxMouseEvent& e) {
        SetFocus();
        wxPoint pt = CalcUnscrolledPosition(e.GetPosition());
        int idx = HitTestUnscrolled(pt);
        if (idx >= 0 && !m_entries[idx].isHeader) {
            m_selectedIdx = idx;
            m_palette->NotifySelectionChanged(m_entries[idx].catNumber);
            Refresh();
        }
        e.Skip();
    }

    void OnLeftUp(wxMouseEvent& e) {
        m_justDblClicked = false;
        e.Skip();
    }

    void OnDblClick(wxMouseEvent& e) {
        wxPoint pt = CalcUnscrolledPosition(e.GetPosition());
        int idx = HitTestUnscrolled(pt);
        if (idx >= 0 && !m_entries[idx].isHeader) {
            m_selectedIdx = idx;
            m_justDblClicked = true;
            m_palette->NotifyDoubleClick(m_entries[idx].catNumber);
        }
        // Do NOT e.Skip() -- prevents the event propagating to canvas
        // and starting a spurious drag after the piece is placed.
    }

    void OnMouseMove(wxMouseEvent& e) {
        wxPoint pt = CalcUnscrolledPosition(e.GetPosition());
        int idx = HitTestUnscrolled(pt);

        // Hover highlight
        int newHover = (idx >= 0 && !m_entries[idx].isHeader) ? idx : -1;
        if (newHover != m_hoverIdx) {
            m_hoverIdx = newHover;
            Refresh();
        }

        // Hover preview popup
        m_palette->NotifyHover(newHover >= 0 ? m_entries[newHover].catNumber : "",
                               e.GetPosition(), this);

        // Drag initiation — suppressed for one move cycle after a double-click
        // so the canvas doesn't receive a spurious BeginPieceDrag.
        if (e.LeftIsDown() && e.Dragging() && !m_justDblClicked &&
            m_selectedIdx >= 0 && !m_entries[m_selectedIdx].isHeader) {
            m_palette->NotifyDrag(m_entries[m_selectedIdx].catNumber);
        }
        if (!e.LeftIsDown()) m_justDblClicked = false;

        e.Skip();
    }

    void OnLeave(wxMouseEvent& e) {
        m_hoverIdx = -1;
        m_palette->NotifyHover("", wxDefaultPosition, this);
        Refresh();
        e.Skip();
    }

    void OnResize(wxSizeEvent& e) {
        UpdateLayout();
        e.Skip();
    }

    // Hit-test against unscrolled coordinates.
    int HitTestUnscrolled(wxPoint pu) const {
        int W = GetClientSize().x;
        if (W < 10) W = 240;
        int totalCellW = (W - kPadX) / ComputeCols(W);

        for (int i = 0; i < (int)m_entries.size(); ++i) {
            const auto& e = m_entries[i];
            int ey = m_rowY[i];
            if (e.isHeader) continue;
            if (e.isText) {
                if (pu.x >= 0 && pu.x < W &&
                    pu.y >= ey && pu.y < ey + kHdrH)
                    return i;
                continue;
            }
            int cx = kPadX + e.col * totalCellW;
            if (pu.x >= cx && pu.x < cx + kCellW &&
                pu.y >= ey && pu.y < ey + kCellH + kLabelH + kPadY)
                return i;
        }
        return -1;
    }

    std::unordered_map<std::string, int> m_catMapDummy; // suppress warning
};

// ── PalettePanel event table ──────────────────────────────────────────────────

wxBEGIN_EVENT_TABLE(PalettePanel, wxPanel)
    EVT_CHOICE(ID_SysChoice,         PalettePanel::OnSystemChanged)
    EVT_LISTBOX_DCLICK(ID_ListLib,     PalettePanel::OnLibraryActivated)
    EVT_LISTBOX_DCLICK(ID_ListScenery, PalettePanel::OnSceneryActivated)
    EVT_LISTBOX_DCLICK(ID_ListSignals, PalettePanel::OnSceneryActivated)
wxEND_EVENT_TABLE()

// ── PalettePanel implementation ───────────────────────────────────────────────

PalettePanel::PalettePanel(wxWindow* parent, MainFrame* frame)
    : wxPanel(parent, wxID_ANY), m_frame(frame)
{
    BuildUI();
}

void PalettePanel::BuildUI() {
    wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);

    wxArrayString systems;
    systems.Add("Marklin C-Track");
    systems.Add("Marklin K-Track");
    systems.Add("TRIX C-Track");
    systems.Add("Marklin Z-Track");
    systems.Add("Marklin Gauge 1");
    systems.Add("Marklin M-Track");
    m_sysChoice = new wxChoice(this, ID_SysChoice,
        wxDefaultPosition, wxDefaultSize, systems);
    m_sysChoice->SetSelection(0);
    sizer->Add(m_sysChoice, 0, wxEXPAND | wxALL, 4);

    sizer->Add(new wxStaticText(this, wxID_ANY, "Double-click to place on canvas."),
               0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 4);

    m_notebook = new wxNotebook(this, wxID_ANY);

    // Track tab — icon grid
    m_iconGrid = new TrackIconGrid(m_notebook, this);
    m_notebook->AddPage(m_iconGrid, "Track");

    // Library tab
    m_listLib = new wxListBox(m_notebook, ID_ListLib,
        wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
    m_notebook->AddPage(m_listLib, "Library");
    m_listLib->Bind(wxEVT_CONTEXT_MENU, &PalettePanel::OnLibraryRightClick, this);

    // Scenery tab
    m_listScenery = new wxListBox(m_notebook, ID_ListScenery,
        wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
    m_notebook->AddPage(m_listScenery, "Scenery");
    m_listScenery->Bind(wxEVT_LEFT_DOWN, &PalettePanel::OnSceneryMouseDown, this);
    m_listScenery->Bind(wxEVT_MOTION,    &PalettePanel::OnSceneryMouseMove, this);

    // Signals tab
    m_listSignals = new wxListBox(m_notebook, ID_ListSignals,
        wxDefaultPosition, wxDefaultSize, 0, nullptr, wxLB_SINGLE);
    m_notebook->AddPage(m_listSignals, "Signals");
    m_listSignals->Bind(wxEVT_LEFT_DOWN, &PalettePanel::OnSceneryMouseDown, this);
    m_listSignals->Bind(wxEVT_MOTION,    &PalettePanel::OnSceneryMouseMove, this);

    sizer->Add(m_notebook, 1, wxEXPAND | wxALL, 4);
    SetSizer(sizer);

    m_preview = new PiecePreviewPopup(wxGetTopLevelParent(this));
}

// Called by TrackIconGrid when a cell is selected.
void PalettePanel::NotifySelectionChanged(const std::string& catNum) {
    // Update m_catTrack selection tracking if needed.
    // For now just ensure preview is hidden.
    if (m_preview) m_preview->Hide();
    m_lastHoverIdx = -1;
}

// Called by TrackIconGrid on double-click.
void PalettePanel::NotifyDoubleClick(const std::string& catNum) {
    if (catNum.empty()) return;
    m_justDoubleClicked = true;
    if (m_preview) m_preview->Hide();
    const std::string kHelixPrefix = "@helix:";
    if (catNum.rfind(kHelixPrefix, 0) == 0) {
        m_frame->PlaceHelix(MC::Vec2{500.0, 500.0}, 0.0,
                            catNum.substr(kHelixPrefix.size()));
        return;
    }
    m_frame->Canvas()->PlaceAndJoinToSelected(catNum);
}

// Called by TrackIconGrid on hover — shows display name as tooltip, no icon.
void PalettePanel::NotifyHover(const std::string& catNum,
                                const wxPoint& /*clientPt*/,
                                wxWindow* gridWin) {
    if (m_preview) m_preview->Hide();
    if (catNum.empty()) {
        gridWin->SetToolTip(nullptr);
        return;
    }
    const MC::TrackPiece* piece = MC::GetCatalog().Find(catNum);
    if (!piece) { gridWin->SetToolTip(nullptr); return; }
    gridWin->SetToolTip(wxString::FromUTF8(piece->displayName.c_str()));
}

// Called by TrackIconGrid when a drag is initiated.
void PalettePanel::NotifyDrag(const std::string& catNum) {
    if (catNum.empty()) return;
    m_justDoubleClicked = false;
    if (m_preview) m_preview->Hide();
    const std::string kHelixPrefix = "@helix:";
    if (catNum.rfind(kHelixPrefix, 0) == 0) {
        m_frame->Canvas()->BeginHelixDrag(catNum.substr(kHelixPrefix.size()));
    } else {
        m_frame->Canvas()->BeginPieceDrag(catNum);
    }
}

void PalettePanel::AddSection(wxListBox* /*list*/, std::vector<std::string>& cats,
    const wxString& label, const std::vector<std::string>& catNums)
{
    m_iconGrid->AddHeader(label);
    cats.push_back(""); // header placeholder
    auto& catalog = MC::GetCatalog();
    for (const auto& cat : catNums) {
        const MC::TrackPiece* p = catalog.Find(cat);
        if (!p) continue;
        m_iconGrid->AddPiece(cat);
        cats.push_back(cat);
    }
}

void PalettePanel::AddHelixSection(wxListBox* /*list*/,
    std::vector<std::string>& cats, const wxString& label,
    const std::vector<std::pair<std::string,std::string>>& helixes)
{
    m_iconGrid->AddHeader(label);
    cats.push_back("");
    for (const auto& [varId, dispName] : helixes) {
        std::string pseudo = "@helix:" + varId;
        m_iconGrid->AddTextItem(pseudo, wxString::FromUTF8(dispName.c_str()));
        cats.push_back(pseudo);
    }
}

std::string PalettePanel::SelectedCatalogNumber() const {
    if (!m_iconGrid) return "";
    return m_iconGrid->SelectedCat();
}


// ── Unchanged methods from original PalettePanel ──────────────────────────────

void PalettePanel::Populate() {
    PopulateForSystem(m_sysChoice->GetSelection());
    LoadLibrary();
    PopulateScenery();
    PopulateSignals();
    UpdateSceneryTabVisibility(0);
}

void PalettePanel::UpdateSceneryTabVisibility(int sysIdx) {
    const bool wantAttached = (sysIdx != 3 && sysIdx != 4 && sysIdx != 5);
    if (wantAttached == m_scenerySheetAttached) return;
    if (wantAttached) {
        m_notebook->AddPage(m_listScenery, "Scenery");
        m_scenerySheetAttached = true;
    } else {
        int sceneryPage = -1;
        for (size_t i = 0; i < m_notebook->GetPageCount(); ++i) {
            if (m_notebook->GetPage(i) == m_listScenery) { sceneryPage = (int)i; break; }
        }
        if (sceneryPage < 0) return;
        if (m_notebook->GetSelection() == sceneryPage) m_notebook->SetSelection(0);
        m_notebook->RemovePage(sceneryPage);
        m_scenerySheetAttached = false;
    }
}

void PalettePanel::SelectSystemForLayout(const MC::CanvasState& state) {
    auto& catalog = MC::GetCatalog();
    bool hasMarlinC = false, hasMarlinK = false, hasTrixC = false, hasMarlinZ = false, hasGauge1 = false, hasMarlinM = false;
    for (const auto& p : state.pieces) {
        const MC::TrackPiece* def = catalog.Find(p.catalogNumber);
        if (!def) continue;
        switch (def->series) {
            case MC::TrackSeries::MarlinC: hasMarlinC = true; break;
            case MC::TrackSeries::MarlinK: hasMarlinK = true; break;
            case MC::TrackSeries::TrixC:   hasTrixC   = true; break;
            case MC::TrackSeries::MarlinZ: hasMarlinZ = true; break;
            case MC::TrackSeries::Gauge1:  hasGauge1  = true; break;
            case MC::TrackSeries::MarlinM: hasMarlinM = true; break;
            default: break;
        }
    }
    int sel = 0;
    if (hasMarlinM) sel = 5;
    if (hasGauge1)  sel = 4;
    if (hasMarlinZ) sel = 3;
    if (hasTrixC)   sel = 2;
    if (hasMarlinK) sel = 1;
    if (hasMarlinC) sel = 0;
    m_sysChoice->SetSelection(sel);
    PopulateForSystem(sel);
    LoadLibrary();
    PopulateSignals();
    UpdateSceneryTabVisibility(sel);
}

void PalettePanel::PopulateForSystem(int sysIdx) {
    m_iconGrid->Clear();
    m_catTrack.clear();

    if (sysIdx == 0) {
        AddSection(nullptr, m_catTrack, "Straights", {
            "24064","24071","24077","24094",
            "24172","24188","24229","24236","24360"
        });
        AddSection(nullptr, m_catTrack, "Curves", {
            "24130","24115","24107",
            "24230","24294","24224","24215","24207","24206",
            "24330","24315",
            "24430",
            "24530",
            "24912"
        });
        AddSection(nullptr, m_catTrack, "Switches", {
            "24611","24612",
            "24671","24672",
            "24630"
        });
        AddSection(nullptr, m_catTrack, "Crossings", {
            "24640","24649","24740"
        });
        AddSection(nullptr, m_catTrack, "Double Slip Switches", {
            "24624","24720"
        });
        AddSection(nullptr, m_catTrack, "Slim Switches", {
            "24711","24712",
            "24771","24772"
        });
        AddSection(nullptr, m_catTrack, "Circuit Tracks", {
            "24994","24294"
        });
        AddSection(nullptr, m_catTrack, "Other", {
            "24997","24977","24978","74861","74871"
        });
        AddSection(nullptr, m_catTrack, "Adapter Tracks", {
            "24922","24951"
        });
        AddSection(nullptr, m_catTrack, "Bridges", {
            "74636","74620","74623","74613","74618"
        });
        {
            std::vector<std::pair<std::string,std::string>> helixes;
            for (const auto& v : MC::GetHelixVariants())
                helixes.push_back({ v.id, v.displayName });
            AddHelixSection(nullptr, m_catTrack, "Helix", helixes);
        }
    } else if (sysIdx == 1) {
        AddSection(nullptr, m_catTrack, "Straights", {
            "2200","2201","2202","2203","2204","2205",
            "2206","2207","2208","2209","2293"
        });
        AddSection(nullptr, m_catTrack, "Special Track", {
            "2290","2291","2292","2295","2297","2299"
        });
        AddSection(nullptr, m_catTrack, "Curves R1 (360mm)", { "2210","2229" });
        AddSection(nullptr, m_catTrack, "Curves R2 (424.6mm)", { "2221","2223","2224","2239" });
        AddSection(nullptr, m_catTrack, "Curves R3 (515mm)", { "2231","2232","2233","2234","2235" });
        AddSection(nullptr, m_catTrack, "Curves R4 (573.6mm)", { "2241","2251" });
        AddSection(nullptr, m_catTrack, "Curves R5 (636mm)", { "2274" });
        AddSection(nullptr, m_catTrack, "Crossings", { "2257","2258","2259" });
        AddSection(nullptr, m_catTrack, "Turnouts", {
            "2262","2263","2265","2266","2268","2269","2270","22715","22716"
        });
        AddSection(nullptr, m_catTrack, "Double Slip Switches", { "2260","2275" });
        AddSection(nullptr, m_catTrack, "Bumpers", { "7389","7391" });
        AddSection(nullptr, m_catTrack, "Bridges", { "7263","7262","7569","7267" });
        AddSection(nullptr, m_catTrack, "Turntable", { "7286","7287" });
    } else if (sysIdx == 2) {
        AddSection(nullptr, m_catTrack, "Straights", {
            "62064","62071","62077","62094",
            "62172","62188","62229","62236","62360"
        });
        AddSection(nullptr, m_catTrack, "Curves R1 (360mm)", { "62107","62115","62130" });
        AddSection(nullptr, m_catTrack, "Curves R2 (437.5mm)", { "62206","62207","62215","62224","62230" });
        AddSection(nullptr, m_catTrack, "Curves R3 (515mm)", { "62315","62330" });
        AddSection(nullptr, m_catTrack, "Curves R9 (1114.6mm)", { "62912" });
        AddSection(nullptr, m_catTrack, "Switches", {
            "62611","62612","62671","62672","62630"
        });
        AddSection(nullptr, m_catTrack, "Double Slip Switches", { "62624" });
        AddSection(nullptr, m_catTrack, "Other", { "62977","62997" });
        AddSection(nullptr, m_catTrack, "Bridges", { "62636","62620","62623","62613" });
    } else if (sysIdx == 3) {
        AddSection(nullptr, m_catTrack, "Straights", {
            "8500","8501","8502","8503","8504","8505","8506","8507"
        });
        AddSection(nullptr, m_catTrack, "Curves R1 (145mm)", { "8510","8511" });
        AddSection(nullptr, m_catTrack, "Curves R2 (195mm)", { "8520","8521","8529" });
        AddSection(nullptr, m_catTrack, "Curves R3 (220mm)", { "8530","8531","8539" });
        AddSection(nullptr, m_catTrack, "Turnout Branch Curve", { "8591" });
        AddSection(nullptr, m_catTrack, "Turnouts", { "8562","8563","8565","8566" });
        AddSection(nullptr, m_catTrack, "Curved Turnouts", { "8568","8569" });
        AddSection(nullptr, m_catTrack, "Crossings", { "8559","8560" });
        AddSection(nullptr, m_catTrack, "Other", { "8587","8588","8590","8598","8991" });
        AddSection(nullptr, m_catTrack, "Bridges", { "8975","89759","8976","8977" });
    } else if (sysIdx == 4) {
        // Märklin Gauge 1 (Scale I, 1:32)
        AddSection(nullptr, m_catTrack, "Straights", {
            "59051","59052","59053","59054","59055",
            "59056","59057","59058","59059","59061"
        });
        AddSection(nullptr, m_catTrack, "Curves R1 (1020mm) 22.5°", { "59035" });
        AddSection(nullptr, m_catTrack, "Curves R2 (1176mm) 22.5°", { "59072" });
        AddSection(nullptr, m_catTrack, "Curves R3 (1394mm)", { "59073","59075" });
        AddSection(nullptr, m_catTrack, "Curves R4 (1550mm)", { "59076","59074" });
        AddSection(nullptr, m_catTrack, "Curves R5 (1715mm) 22.5°", { "59077" });
        AddSection(nullptr, m_catTrack, "Curves R6 (2321mm) 10°",   { "59078" });
        AddSection(nullptr, m_catTrack, "Curves R7 (2461mm) 10°",   { "59071" });
        AddSection(nullptr, m_catTrack, "Turnouts", {
            "59087","59088","59083","59084"
        });
        AddSection(nullptr, m_catTrack, "Curved Turnouts", { "59085","59086" });
        AddSection(nullptr, m_catTrack, "Three-Way & Special", {
            "59091","59092","59093","59094"
        });
        AddSection(nullptr, m_catTrack, "Bumpers", { "5602","59099" });
    } else if (sysIdx == 5) {
        // Märklin M-Track (HO vintage tinplate)
        AddSection(nullptr, m_catTrack, "Straights", {
            "5106","5107","5129","5108","5109","5110","5210","5208"
        });
        AddSection(nullptr, m_catTrack, "Contact & Uncoupler", {
            "5105","5145","5112"
        });
        AddSection(nullptr, m_catTrack, "Curves R0 (286mm) 30°", { "5120" });
        AddSection(nullptr, m_catTrack, "Curves R1 (360mm)", { "5100","5101","5102" });
        AddSection(nullptr, m_catTrack, "Curves R2 (437.4mm)", {
            "5200","5201","5206","5205","5208C"
        });
        AddSection(nullptr, m_catTrack, "Turnouts R1 (360mm)", {
            "5117","5118","5119","5121","5122","5123","5137","3600"
        });
        AddSection(nullptr, m_catTrack, "Turnouts R2 (437.4mm)", {
            "5202","5203","5204","5221"
        });
        AddSection(nullptr, m_catTrack, "Curved Turnouts R2/R1", {
            "5142","5143"
        });
        AddSection(nullptr, m_catTrack, "Crossings & Special", {
            "5114","5211","5215","5207","5128","5214"
        });
        AddSection(nullptr, m_catTrack, "Bumpers", { "5190","5191" });
        AddSection(nullptr, m_catTrack, "Adapter Tracks", { "24951" });
    }
}

wxString PalettePanel::CurrentLibraryFolder() const {
    wxString sysName = m_sysChoice->GetStringSelection();
    wxString folderName;
    if      (sysName == "Marklin C-Track") folderName = "Marklin_C-Track";
    else if (sysName == "Marklin K-Track") folderName = "Marklin_K-Track";
    else if (sysName == "TRIX C-Track")    folderName = "TRIX_C-Track";
    else if (sysName == "Marklin Z-Track") folderName = "Marklin_Z-Track";
    else if (sysName == "Marklin Gauge 1") folderName = "Marklin_Gauge1";
    else if (sysName == "Marklin M-Track") folderName = "Marklin_M-Track";
    else if (sysName == "Minitrix")        folderName = "Minitrix-Track";
    else { folderName = sysName; folderName.Replace(" ", "_"); }
    // Library blocks live under the user's documents folder, not beside the
    // exe -- see UserPaths.h.
    return TD::UserLibraryDir() + wxFILE_SEP_PATH + folderName;
}

void PalettePanel::LoadLibrary() {
    if (!m_listLib) return;
    m_listLib->Clear();
    m_libFiles.clear();
    wxString libDir = CurrentLibraryFolder();
    if (!wxDirExists(libDir)) return;
    wxDir dir(libDir);
    if (!dir.IsOpened()) return;
    wxString file;
    bool cont = dir.GetFirst(&file, "*.tdl", wxDIR_FILES);
    while (cont) {
        wxString name = wxFileName(file).GetName();
        m_listLib->Append(name);
        m_libFiles.push_back((libDir + wxFILE_SEP_PATH + file).ToStdString());
        cont = dir.GetNext(&file);
    }
}

void PalettePanel::OnSystemChanged(wxCommandEvent&) {
    int sel = m_sysChoice->GetSelection();
    PopulateForSystem(sel);
    LoadLibrary();
    PopulateSignals();
    UpdateSceneryTabVisibility(sel);
}

void PalettePanel::OnLibraryRightClick(wxContextMenuEvent& e) {
    if (!m_listLib) return;
    wxPoint sp = e.GetPosition();
    int idx = wxNOT_FOUND;
    if (sp == wxDefaultPosition) {
        idx = m_listLib->GetSelection();
    } else {
        wxPoint cp = m_listLib->ScreenToClient(sp);
        idx = m_listLib->HitTest(cp);
        if (idx == wxNOT_FOUND) idx = m_listLib->GetSelection();
    }
    if (idx < 0 || idx >= (int)m_libFiles.size()) return;
    m_listLib->SetSelection(idx);
    wxString stem = m_listLib->GetString(idx);
    wxMenu menu;
    const int kIdDelete = wxID_HIGHEST + 1;
    menu.Append(kIdDelete, "Delete...");
    int picked = m_listLib->GetPopupMenuSelectionFromUser(menu);
    if (picked != kIdDelete) return;
    wxString msg;
    msg.Printf("Delete library block \"%s\"?\n\nThis cannot be undone.", stem);
    if (wxMessageBox(msg, "Delete Library Block",
                     wxYES_NO | wxICON_WARNING | wxNO_DEFAULT, this) != wxYES)
        return;
    wxString path = wxString::FromUTF8(m_libFiles[idx]);
    if (!wxRemoveFile(path)) {
        wxMessageBox("Could not delete:\n" + path, "Delete Failed",
                     wxOK | wxICON_ERROR, this);
        return;
    }
    LoadLibrary();
}

void PalettePanel::OnLibraryActivated(wxCommandEvent&) {
    int sel = m_listLib->GetSelection();
    if (sel < 0 || sel >= (int)m_libFiles.size()) return;

    std::string blockName;
    std::vector<MC::PlacedPiece>   pieces;
    std::vector<MC::PlacedScenery> scenery;
    std::string err = TD::LayoutXml::LoadBlock(m_libFiles[sel], blockName, pieces, scenery);
    if (!err.empty()) {
        wxMessageBox(wxString::FromUTF8(err), "Load Block Failed", wxICON_ERROR | wxOK);
        return;
    }
    if (pieces.empty() && scenery.empty()) return;

    // Compute the block's minimum X/Y across pieces AND scenery so the whole
    // block translates together to the origin.
    double minX = 1e9, minY = 1e9;
    for (auto& p : pieces) {
        minX = std::min(minX, p.transform.pos.x);
        minY = std::min(minY, p.transform.pos.y);
    }
    for (auto& s : scenery) {
        minX = std::min(minX, s.transform.pos.x);
        minY = std::min(minY, s.transform.pos.y);
    }
    if (minX > 1e8) { minX = 0; minY = 0; }

    auto& state = m_frame->State();

    // Ensure any level IDs used by the block exist in state.levels.
    {
        std::set<int> existingIds;
        for (const auto& l : state.levels) existingIds.insert(l.id);
        std::set<int> neededIds;
        for (const auto& p : pieces) neededIds.insert(p.levelId);
        for (const auto& s : scenery) neededIds.insert(s.levelId);
        for (int id : neededIds) {
            if (existingIds.count(id)) continue;
            MC::Level l;
            l.id = id;
            l.name = "Level " + std::to_string(id);
            l.heightMm = 0.0;
            state.levels.push_back(l);
            existingIds.insert(id);
        }
    }

    state.PushUndo();

    // Pieces: remap ids and rewire joints.
    std::map<int,int> idMap;
    for (auto& p : pieces) {
        int newId = state.nextId++;
        idMap[p.id] = newId;
        p.id = newId;
        p.transform.pos.x -= minX;
        p.transform.pos.y -= minY;
    }
    for (auto& p : pieces) {
        for (auto& j : p.joints) {
            if (j.pieceId != -1) {
                auto it = idMap.find(j.pieceId);
                j.pieceId = (it != idMap.end()) ? it->second : -1;
            }
        }
        state.pieces.push_back(p);
    }

    // Scenery: fresh ids, translate by the same origin offset, keep scale.
    for (auto& s : scenery) {
        s.id = state.nextId++;
        s.transform.pos.x -= minX;
        s.transform.pos.y -= minY;
        s.selected = false;
        state.scenery.push_back(s);
    }

    m_frame->MarkDirty();
    m_frame->RefreshTally();
    m_frame->Canvas()->ZoomFit(true);
    m_frame->RefreshCanvas();
}

// Scenery tab
std::string PalettePanel::SelectedSceneryId() const {
    if (!m_notebook) return "";
    wxWindow* page = m_notebook->GetCurrentPage();
    if (page == m_listScenery) {
        int sel = m_listScenery->GetSelection();
        if (sel < 0 || sel >= (int)m_sceneryIds.size()) return "";
        return m_sceneryIds[sel];
    }
    if (page == m_listSignals) {
        int sel = m_listSignals->GetSelection();
        if (sel < 0 || sel >= (int)m_signalIds.size()) return "";
        return m_signalIds[sel];
    }
    return "";
}

void PalettePanel::PopulateScenery() {
    if (!m_listScenery) return;
    m_listScenery->Clear();
    m_sceneryIds.clear();
    auto& cat = MC::GetSceneryCatalog();
    auto isAllowed = [](MC::SceneryCategory c) {
        return c == MC::SceneryCategory::Tree
            || c == MC::SceneryCategory::Platform
            || c == MC::SceneryCategory::Station
            || c == MC::SceneryCategory::Bridge
            || c == MC::SceneryCategory::Misc;
    };
    std::string lastCat;
    for (const auto& item : cat.All()) {
        if (!isAllowed(item.category)) continue;
        std::string catName = item.categoryName();
        if (catName != lastCat) {
            m_listScenery->Append("--- " + wxString::FromUTF8(catName.c_str()));
            m_sceneryIds.push_back("");
            lastCat = catName;
        }
        m_listScenery->Append(wxString::FromUTF8(item.displayName.c_str()));
        m_sceneryIds.push_back(item.id);
    }
}

void PalettePanel::PopulateSignals() {
    if (!m_listSignals) return;
    m_listSignals->Clear();
    m_signalIds.clear();
    int sysIdx = m_sysChoice->GetSelection();
    auto& cat = MC::GetSceneryCatalog();
    for (const auto& item : cat.All()) {
        if (item.category != MC::SceneryCategory::Signal) continue;
        if (sysIdx == 4) continue;  // no signals for Gauge 1
        if (sysIdx == 5) continue;  // no signals for M-Track
        if (sysIdx == 3 && !item.zScale) continue;
        if (sysIdx != 3 &&  item.zScale) continue;
        m_listSignals->Append(wxString::FromUTF8(item.displayName.c_str()));
        m_signalIds.push_back(item.id);
    }
}

void PalettePanel::OnSceneryActivated(wxCommandEvent&) {
    std::string id = SelectedSceneryId();
    if (id.empty()) return;
    m_frame->Canvas()->BeginSceneryDrag(id);
}

void PalettePanel::OnSceneryMouseDown(wxMouseEvent& e) {
    m_justDoubleClicked = false;
    e.Skip();
}

void PalettePanel::OnSceneryMouseMove(wxMouseEvent& e) {
    if (e.LeftIsDown() && e.Dragging() && !m_justDoubleClicked) {
        std::string id = SelectedSceneryId();
        if (!id.empty()) {
            m_frame->Canvas()->BeginSceneryDrag(id);
            return;
        }
    }
    e.Skip();
}

// Stub handlers kept for event table compat (no longer wired to listbox events)
void PalettePanel::OnListMouseDown(wxMouseEvent& e) { e.Skip(); }
void PalettePanel::OnListMouseMove(wxMouseEvent& e) { e.Skip(); }
void PalettePanel::OnListMouseHover(wxMouseEvent& e) { e.Skip(); }
void PalettePanel::OnItemActivated(wxCommandEvent&) {}
