#include "LayoutPrintout.h"
#include "canvas/Renderer.h"
#include "io/CatalogLoader.h"
#include <wx/dcmemory.h>
#include <wx/image.h>
#include <cmath>
#include <algorithm>

LayoutPrintout::LayoutPrintout(const wxString& title,
                                MC::CanvasState* state,
                                const PrintOptionsDialog::Options& opts)
    : wxPrintout(title)
    , m_state(state)
    , m_opts(opts)
{}

void LayoutPrintout::ComputeLayoutBounds(double& minX, double& minY,
                                         double& maxX, double& maxY) const {
    minX = 1e9; minY = 1e9; maxX = -1e9; maxY = -1e9;
    auto expand = [&](double x, double y) {
        if (x < minX) minX = x;
        if (y < minY) minY = y;
        if (x > maxX) maxX = x;
        if (y > maxY) maxY = y;
    };
    auto& catalog = MC::GetCatalog();
    for (const auto& p : m_state->pieces) {
        // Skip pieces on invisible levels unless we're compositing everything.
        if (m_opts.levels == PrintOptionsDialog::LevelMode::ActiveOnly &&
            p.levelId != m_state->activeLevelId) continue;
        const MC::TrackPiece* def = catalog.Find(p.catalogNumber);
        if (!def) continue;
        double rad = p.transform.heading * M_PI / 180.0;
        MC::Vec2 p0 = p.transform.pos;
        MC::Vec2 p1 = p0 + MC::Vec2{std::cos(rad), std::sin(rad)} * def->lengthMm;
        expand(p0.x, p0.y);
        expand(p1.x, p1.y);
    }
    for (const auto& s : m_state->scenery) {
        expand(s.transform.pos.x, s.transform.pos.y);
    }
    if (m_opts.showBaseboard) {
        for (const auto& v : m_state->baseboard) expand(v.x, v.y);
    }
    if (minX > maxX) {
        // Empty layout -- pick a sensible 1m × 1m placeholder
        minX = 0; minY = 0; maxX = 1000; maxY = 1000;
    }
}

void LayoutPrintout::ResolveScale(int pageW, int pageH) {
    double W = std::max(1.0, m_layMaxX - m_layMinX);
    double H = std::max(1.0, m_layMaxY - m_layMinY);

    // Reserve a small margin so content doesn't kiss the page edge.
    const double marginFrac = 0.04;
    int usableW = (int)(pageW * (1.0 - 2 * marginFrac));
    int usableH = (int)(pageH * (1.0 - 2 * marginFrac));
    if (usableW < 1) usableW = 1;
    if (usableH < 1) usableH = 1;

    switch (m_opts.scale) {
    case PrintOptionsDialog::ScaleMode::FitToPage: {
        double sX = usableW / W;
        double sY = usableH / H;
        m_scale = std::min(sX, sY);
        m_tilesX = 1; m_tilesY = 1;
        break;
    }
    case PrintOptionsDialog::ScaleMode::ActualScale: {
        // 1:N -- N mm of layout becomes 1 mm of paper.
        // pageW is in device pixels; GetPPIPrinter gives DPI; 1 inch = 25.4 mm.
        int ppiX, ppiY;
        GetPPIPrinter(&ppiX, &ppiY);
        if (ppiX <= 0) ppiX = 300;
        if (ppiY <= 0) ppiY = 300;
        double pxPerMmX = ppiX / 25.4;
        // Page DCs may use a mapping mode that differs from raw printer pixels;
        // best-effort: use printer PPI for both axes.
        double denom = std::max(1, m_opts.scaleDenom);
        m_scale = pxPerMmX / denom;  // page-px per layout-mm
        // How many pages to tile?
        double layPxW = W * m_scale;
        double layPxH = H * m_scale;
        m_tilesX = std::max(1, (int)std::ceil(layPxW / usableW));
        m_tilesY = std::max(1, (int)std::ceil(layPxH / usableH));
        break;
    }
    case PrintOptionsDialog::ScaleMode::CustomPercent: {
        // pct% of fit-to-page scale.
        double fitS = std::min(usableW / W, usableH / H);
        m_scale = fitS * (m_opts.scalePercent / 100.0);
        double layPxW = W * m_scale;
        double layPxH = H * m_scale;
        m_tilesX = std::max(1, (int)std::ceil(layPxW / usableW));
        m_tilesY = std::max(1, (int)std::ceil(layPxH / usableH));
        break;
    }
    }
}

void LayoutPrintout::OnPreparePrinting() {
    // Apply level-visibility override if user asked for all levels.
    m_savedLevelVis.clear();
    if (m_opts.levels == PrintOptionsDialog::LevelMode::AllLevels) {
        for (auto& l : m_state->levels) {
            m_savedLevelVis.emplace_back(l.id, l.visible);
            l.visible = true;
        }
    } else {
        // ActiveOnly: hide every level except the active one.
        for (auto& l : m_state->levels) {
            m_savedLevelVis.emplace_back(l.id, l.visible);
            l.visible = (l.id == m_state->activeLevelId);
        }
    }

    ComputeLayoutBounds(m_layMinX, m_layMinY, m_layMaxX, m_layMaxY);
    int pageW, pageH;
    GetPageSizePixels(&pageW, &pageH);
    m_pageW = pageW;
    m_pageH = pageH;
    ResolveScale(pageW, pageH);
}

bool LayoutPrintout::HasPage(int page) {
    return page >= 1 && page <= m_tilesX * m_tilesY;
}

void LayoutPrintout::GetPageInfo(int* minPage, int* maxPage,
                                  int* pageFrom, int* pageTo) {
    *minPage  = 1;
    *maxPage  = std::max(1, m_tilesX * m_tilesY);
    *pageFrom = 1;
    *pageTo   = *maxPage;
}

bool LayoutPrintout::OnPrintPage(int page) {
    wxDC* dc = GetDC();
    if (!dc) return false;

    // Recompute scale once we actually have a DC (preview vs printer
    // dimensions can differ).
    int pageW, pageH;
    GetPageSizePixels(&pageW, &pageH);
    if (pageW != m_pageW || pageH != m_pageH) {
        m_pageW = pageW; m_pageH = pageH;
        ResolveScale(pageW, pageH);
    }

    // Map logical→device so we draw in logical pixel space.
    FitThisSizeToPage(wxSize(pageW, pageH));

    DrawPageContent(*dc, page, pageW, pageH);
    return true;
}

void LayoutPrintout::DrawPageContent(wxDC& dc, int page, int pageW, int pageH) {
    int tileIdx = page - 1;
    int tx = tileIdx % std::max(1, m_tilesX);
    int ty = tileIdx / std::max(1, m_tilesX);

    const double marginFrac = 0.04;
    int marginX = (int)(pageW * marginFrac);
    int marginY = (int)(pageH * marginFrac);
    int usableW = pageW - 2 * marginX;
    int usableH = pageH - 2 * marginY;

    double W = std::max(1.0, m_layMaxX - m_layMinX);
    double H = std::max(1.0, m_layMaxY - m_layMinY);

    // Build ViewTransform that puts (m_layMinX + tx*tileMmW, ...) at the
    // top-left of the usable page area, with m_scale page-px per layout-mm.
    MC::ViewTransform view;
    view.scale = m_scale;

    if (m_tilesX == 1 && m_tilesY == 1) {
        // Single page: center within usable area.
        double layPxW = W * m_scale;
        double layPxH = H * m_scale;
        double offsetX = marginX + (usableW - layPxW) * 0.5;
        double offsetY = marginY + (usableH - layPxH) * 0.5;
        view.panX = offsetX - m_layMinX * m_scale;
        view.panY = offsetY - m_layMinY * m_scale;
    } else {
        // Tiled: each page covers a usableW × usableH chunk in page-px,
        // which is (usableW/m_scale) × (usableH/m_scale) in layout-mm.
        double tileMmW = usableW / m_scale;
        double tileMmH = usableH / m_scale;
        double tileMinX = m_layMinX + tx * tileMmW;
        double tileMinY = m_layMinY + ty * tileMmH;
        view.panX = marginX - tileMinX * m_scale;
        view.panY = marginY - tileMinY * m_scale;
    }

    // Hide grid if user didn't want it.
    bool savedShowGrid = m_state->showGrid;
    if (!m_opts.showGrid) m_state->showGrid = false;
    bool savedShowBase = m_state->showBaseboard;
    if (!m_opts.showBaseboard) m_state->showBaseboard = false;

    // Render. For grayscale, draw to an offscreen bitmap, desaturate, blit.
    if (m_opts.color == PrintOptionsDialog::ColorMode::Grayscale) {
        wxBitmap bmp(pageW, pageH, 24);
        {
            wxMemoryDC mem(bmp);
            mem.SetBackground(*wxWHITE_BRUSH);
            mem.Clear();
            MC::Renderer r;
            r.Draw(mem, *m_state, view, wxSize(pageW, pageH));
        }
        wxImage img = bmp.ConvertToImage();
        img = img.ConvertToGreyscale();
        wxBitmap gray(img);
        dc.DrawBitmap(gray, 0, 0, false);
    } else {
        MC::Renderer r;
        r.Draw(dc, *m_state, view, wxSize(pageW, pageH));
    }

    m_state->showGrid     = savedShowGrid;
    m_state->showBaseboard = savedShowBase;

    // Restore level visibility on the final page so we don't leave the state
    // mutated. (We restore on every page just in case the job is cancelled.)
    if (page == m_tilesX * m_tilesY) {
        for (auto& kv : m_savedLevelVis) {
            for (auto& l : m_state->levels) {
                if (l.id == kv.first) { l.visible = kv.second; break; }
            }
        }
        m_savedLevelVis.clear();
    }
}
