// draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h.
//
// Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that
// touches LICE + SWELL. All colors come from the pure `theme` module; all geometry from
// the pure `component_geometry` module. DAW-verified, not unit-tested.

#include "draw_kit.h"

#include <cstddef>

#include "bank_grid.h"  // compressAmplitudeForDisplay — the shared dB display curve (pure)

// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is
// provided by the host. Mirrors bank_panel.cpp's include discipline.
#ifdef _WIN32
#include <windows.h>
#else
#include "swell/swell.h"
#endif
#include "wdltypes.h"
#include "lice/lice.h"
#include "lice/lice_text.h"

namespace reasampler {

// --- KitColor <-> LICE boundary ----------------------------------------------

// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a)
// (lice.h:57). The theme owns the color; the shell owns the packing. Declared in draw_kit.h
// so shell translation units (bank_panel) can use it without duplicating the LICE_RGBA pack.
LICE_pixel toLice(const KitColor& c) {
    return LICE_RGBA(c.r, c.g, c.b, c.a);
}

namespace {

// The draw alpha the LICE primitives take (0..1), from the KitColor's 8-bit alpha. Used so
// a disabled surface (alpha 0.4) composites at the right opacity — LICE_FillRect etc. take
// a float alpha argument separate from the pixel's own alpha byte.
float drawAlpha(const KitColor& c) { return c.a / 255.0f; }

// --- Font set (owned by the kit) ---------------------------------------------

struct KitFonts {
    LICE_CachedFont title;
    LICE_CachedFont label;
    LICE_CachedFont valueMono;
    LICE_CachedFont micro;
    bool ready = false;
};

KitFonts g_fonts;

// Creates one HFONT and hands it to a cached font with OWNS_HFONT so the cached font frees
// it (lice_text.h:41). Negative lfHeight = point-ish pixel height (Win32 convention). The
// face is chosen here so a change is one line.
void loadFont(LICE_CachedFont& dst, int pxHeight, int weight, const char* face) {
    HFONT hf = CreateFont(-pxHeight, 0, 0, 0, weight, FALSE, FALSE, FALSE,
                          DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                          DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, face);
    if (!hf) return;  // dst stays with no HFONT; DrawText on it renders nothing (safe)
    dst.SetFromHFont(hf, LICE_FONT_FLAG_OWNS_HFONT);
    dst.SetBkMode(TRANSPARENT);
}

LICE_CachedFont* fontFor(Font f) {
    if (!g_fonts.ready) return nullptr;
    switch (f) {
        case Font::Title:     return &g_fonts.title;
        case Font::Label:     return &g_fonts.label;
        case Font::ValueMono: return &g_fonts.valueMono;
        case Font::Micro:     return &g_fonts.micro;
    }
    return nullptr;
}

UINT alignFlag(Align a) {
    switch (a) {
        case Align::Left:   return DT_LEFT;
        case Align::Center: return DT_CENTER;
        case Align::Right:  return DT_RIGHT;
    }
    return DT_LEFT;
}

// A 1px inner highlight on the top edge and shadow on the bottom edge — the vwnd trick
// that gives a flat fill dimension (§2.2). Lightens the top row, darkens the bottom row.
void innerEdges(LICE_IBitmap* bmp, const KitBox& b, float alpha) {
    if (b.width < 2 || b.height < 2) return;
    const LICE_pixel hi = LICE_RGBA(255, 255, 255, 255);
    const LICE_pixel lo = LICE_RGBA(0, 0, 0, 255);
    // Top inner highlight (subtle) and bottom inner shadow (subtle), inset 1px from the
    // vertical edges so corners read clean.
    LICE_Line(bmp, b.x + 1, b.y, b.x + b.width - 2, b.y, hi, 0.10f * alpha, 0, false);
    LICE_Line(bmp, b.x + 1, b.y + b.height - 1, b.x + b.width - 2, b.y + b.height - 1,
              lo, 0.22f * alpha, 0, false);
}

// The kit's core surface fill: a top-down micro-gradient (a few percent lighter at the
// top) + the inner highlight/shadow. Used by fillSurface and the component draws.
void fillGradient(LICE_IBitmap* bmp, const KitBox& b, const KitColor& top,
                  const KitColor& bottom) {
    if (b.empty()) return;
    const float a = drawAlpha(top);
    // LICE_GradRect wants initial R/G/B/A (0..1) and per-axis deltas. Verified signature
    // lice.h:466 — ir..ia are the top-left color; drdy..dady ramp DOWN the height so the
    // bottom row reaches `bottom`. No horizontal ramp (drdx.. = 0).
    const float ir = top.r / 255.0f, ig = top.g / 255.0f, ib = top.b / 255.0f;
    const float dr = (bottom.r - top.r) / 255.0f;
    const float dg = (bottom.g - top.g) / 255.0f;
    const float db = (bottom.b - top.b) / 255.0f;
    const float h = static_cast<float>(b.height);
    LICE_GradRect(bmp, b.x, b.y, b.width, b.height,
                  ir, ig, ib, a,
                  0.0f, 0.0f, 0.0f, 0.0f,          // no per-x ramp
                  dr / h, dg / h, db / h, 0.0f,    // per-y ramp: top -> bottom
                  LICE_BLIT_MODE_COPY);
    innerEdges(bmp, b, a);
}

// A surface color and its gradient partner (a few percent lighter at the top). Elevation
// reads as a subtle top-lightening of the same hue.
void gradientPair(const KitColor& base, KitColor& top, KitColor& bottom) {
    top = base;
    // Lighten the top ~7% (clamped by the theme's own values staying < 255 in practice).
    auto lighten = [](int v) { int r = v + (v * 7) / 100 + 4; return r > 255 ? 255 : r; };
    top.r = static_cast<unsigned char>(lighten(base.r));
    top.g = static_cast<unsigned char>(lighten(base.g));
    top.b = static_cast<unsigned char>(lighten(base.b));
    bottom = base;
}

RECT toRect(const KitBox& b) {
    return RECT{b.x, b.y, b.x + b.width, b.y + b.height};
}

} // namespace

// --- Font lifecycle ----------------------------------------------------------

void kitFontsInit() {
    if (g_fonts.ready) return;  // idempotent
    // §3.1 type scale: title ~15px semibold, label ~12px, value-mono ~12px tabular,
    // micro ~10px. Segoe UI (universal on the Windows target); Consolas for numerics.
    loadFont(g_fonts.title,     15, FW_SEMIBOLD, "Segoe UI");
    loadFont(g_fonts.label,     12, FW_NORMAL,   "Segoe UI");
    loadFont(g_fonts.valueMono, 12, FW_NORMAL,   "Consolas");
    loadFont(g_fonts.micro,     10, FW_NORMAL,   "Segoe UI");
    g_fonts.ready = true;
}

void kitFontsShutdown() {
    if (!g_fonts.ready) return;  // idempotent
    // LICE_CachedFont's destructor frees its OWNS_HFONT HFONT. Re-assigning an empty font
    // via SetFromHFont(nullptr) would leak nothing but also do nothing useful; instead we
    // mark not-ready and let the fonts release their HFONTs when g_fonts is reset. Because
    // g_fonts is a static instance (not re-created), free the HFONTs explicitly by handing
    // each a null font, which OWNS semantics clean up the prior HFONT (lice_text.h:41).
    g_fonts.title.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
    g_fonts.label.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
    g_fonts.valueMono.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
    g_fonts.micro.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
    g_fonts.ready = false;
}

// --- Text --------------------------------------------------------------------

void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
          Font font, const KitColor& color, Align align) {
    if (!bmp || !str || box.empty()) return;
    LICE_CachedFont* f = fontFor(font);
    if (!f) return;  // before init or font-create failed: draw nothing (safe)
    f->SetTextColor(toLice(color));
    f->SetBkMode(TRANSPARENT);
    RECT rc = toRect(box);
    f->DrawText(bmp, str, -1, &rc,
                alignFlag(align) | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}

void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
          Font font, Role role, Align align) {
    text(bmp, box, str, font, roleColor(role), align);
}

// --- Surfaces + components ----------------------------------------------------

void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state) {
    if (!bmp || box.empty()) return;
    const KitColor base = roleColorState(role, state);
    KitColor top, bottom;
    gradientPair(base, top, bottom);
    fillGradient(bmp, box, top, bottom);
}

void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label,
                InteractionState state, bool warn) {
    const KitBox& b = button.box;
    if (!bmp || b.empty()) return;

    const Role surfaceRole = warn ? Role::Warn : Role::BgCell;
    const KitColor base = roleColorState(surfaceRole, state);
    KitColor top, bottom;
    gradientPair(base, top, bottom);

    // Rounded surface: fill the interior gradient, then an AA rounded border. Corner
    // radius scales gently with height, clamped so tiny buttons stay legible.
    fillGradient(bmp, b, top, bottom);
    const int radius = b.height >= 20 ? 5 : (b.height >= 12 ? 3 : 2);
    const KitColor borderCol =
        (state == InteractionState::Active || state == InteractionState::Focus)
            ? roleColor(Role::AccentPrimary)
            : roleColor(Role::LineHairline);
    LICE_RoundRect(bmp, static_cast<float>(b.x), static_cast<float>(b.y),
                   static_cast<float>(b.width - 1), static_cast<float>(b.height - 1),
                   radius, toLice(borderCol), drawAlpha(borderCol), 0, true);

    if (label && *label) {
        // Active fill is the accent — draw its label in the base bg for contrast; else
        // text/primary (disabled dims via the state on the surface, label stays primary
        // but the whole control reads recessed).
        const Role textRole = (state == InteractionState::Active)
                                  ? Role::BgBase
                                  : Role::TextPrimary;
        text(bmp, b, label, Font::Label, textRole, Align::Center);
    }
}

void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state) {
    if (!bmp || geom.track.empty()) return;

    // Track groove: the cell surface, recessed (pressed-ish) so it reads as a channel.
    fillSurface(bmp, geom.track, Role::BgCell, InteractionState::Pressed);

    // Filled portion up to the handle: the accent (hover/dragging brighten it).
    if (!geom.filled.empty()) {
        const InteractionState fillState =
            (state == InteractionState::Hover || state == InteractionState::Dragging)
                ? InteractionState::Hover
                : InteractionState::Active;
        KitColor top, bottom;
        gradientPair(roleColorState(Role::AccentPrimary, fillState), top, bottom);
        fillGradient(bmp, geom.filled, top, bottom);
    }

    // Handle: a raised knob honoring state.
    if (!geom.handle.empty()) {
        const KitButtonBox knob{geom.handle};
        drawButton(bmp, knob, nullptr, state, /*warn=*/false);
    }
}

void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
                 int thumbWidth, InteractionState state) {
    const KitBox& b = row.box;
    if (!bmp || b.empty()) return;

    // Row surface: bg/cell transformed by state (hover lightens, active = accent).
    fillSurface(bmp, b, Role::BgCell, state);

    // Focus ring: a 1px text/primary rectangle, distinct from the accent selection fill.
    if (state == InteractionState::Focus) {
        const KitColor ring = roleColor(Role::TextPrimary);
        LICE_DrawRect(bmp, b.x, b.y, b.width - 1, b.height - 1,
                      toLice(ring), drawAlpha(ring), 0);
    }

    // Label in the width after the reserved thumbnail inset. Active rows draw the label in
    // bg/base for contrast against the accent fill; else text/primary.
    if (label && *label) {
        const int inset = thumbWidth > 0 ? thumbWidth + 6 : 6;
        KitBox labelBox{b.x + inset, b.y, b.width - inset - 6, b.height};
        if (!labelBox.empty()) {
            const Role tr = (state == InteractionState::Active) ? Role::BgBase
                                                               : Role::TextPrimary;
            text(bmp, labelBox, label, Font::Label, tr, Align::Left);
        }
    }
}

void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
    if (!bmp || box.empty()) return;

    const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
    const LICE_pixel waveCol = toLice(roleColor(Role::AccentPrimary));

    if (env.empty()) {
        const int midY = box.y + box.height / 2;
        LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
                  midCol, 1.0f, 0, false);
        return;
    }

    const int channels = static_cast<int>(env.size());
    const int bandH = box.height / channels;
    for (int ch = 0; ch < channels; ++ch) {
        const ChannelEnvelope& bins = env[static_cast<std::size_t>(ch)];
        const int bandTop = box.y + ch * bandH;
        const int midY = bandTop + bandH / 2;
        const double halfSpan = (bandH / 2) - 2;

        LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
                  midCol, 1.0f, 0, false);

        const int nbins = static_cast<int>(bins.size());
        if (nbins <= 0) continue;

        const int innerW = box.width - 4;
        for (int i = 0; i < nbins; ++i) {
            const int x = box.x + 2 +
                          (nbins > 1 ? (i * (innerW - 1)) / (nbins - 1) : 0);
            // Same dB display compression as the panel thumbnail (bank_grid, pure) so a
            // waveform reads identically wherever the kit draws it.
            int yMax = midY - static_cast<int>(
                compressAmplitudeForDisplay(bins[static_cast<std::size_t>(i)].max) * halfSpan);
            int yMin = midY - static_cast<int>(
                compressAmplitudeForDisplay(bins[static_cast<std::size_t>(i)].min) * halfSpan);
            if (yMax < bandTop) yMax = bandTop;
            if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
            LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
        }
    }
}

} // namespace reasampler
