// theme — pure implementation. See theme.h. NO REAPER / SWELL / LICE / vendor.

#include "theme.h"

#include <algorithm>
#include <cmath>

namespace reasampler {

namespace {

// ===========================================================================
// THE ONE DIRECTION CONSTANTS BLOCK (DS-2 revised: B "Neon Console" REAPER-grey
// neutrals + three-accent pastel system + C pastel spectral).
//
// This is the SINGLE POINT OF CHANGE. Every role color below is one of these
// constants; roleColor() is a pure switch over them. To re-pick the visual
// direction (§4: A Studio Rack / B Neon Console / C full spectral), edit THIS
// block — no shell, no other module, names a color. Values are locked against
// each WCAG floor (proven by test_theme.cpp): text/dim is lifted to the lightest
// grey that still clears AA 4.5:1 body on the greyest surface it draws on; each
// pastel accent is the softest tint that still clears the 3:1 indicator floor on
// bg/cell ("punch from the soft side" — DS-2 revised §2.1 grey re-read).
// ===========================================================================

// REAPER-theme mid-grey elevation stack (DS-2 revised — NOT near-black). Matches
// Daniel's REAPER theme so the dock reads as part of REAPER: base = window chrome
// grey, panel/cell one step lighter each. The elevation-ladder discipline is
// unchanged (base < panel < cell by a few %, micro-gradient + inner highlight/
// shadow carry elevation, not hard borders); only the VALUES moved up into grey.
constexpr KitColor kDirBgBase     {43, 43, 43, 255};   // #2b2b2b — REAPER chrome grey
constexpr KitColor kDirBgPanel    {51, 51, 51, 255};   // #333333 — one step lighter
constexpr KitColor kDirBgCell     {58, 58, 58, 255};   // #3a3a3a — REAPER track bg
constexpr KitColor kDirHairline   {74, 74, 74, 255};   // #4a4a4a — subtle step above cell

// Text: REAPER body light-grey primary (#dcdcdc, clears ~8:1 on bg/cell) + a dimmer
// grey secondary. The greyer surfaces shrank the dim cushion (mid-grey-on-mid-grey
// is the classic AA failure): the spec-start #a0a0a0 lands ~4.35:1 on bg/cell, UNDER
// the AA 4.5 body floor — lifted to #a8a8a8 (~4.78:1 on bg/cell), the lightest grey
// that still reads dim while clearing AA 4.5 body on the greyest surface it draws
// body text on. Locked by test_theme.cpp.
constexpr KitColor kDirTextPrimary{220, 220, 220, 255};  // #dcdcdc
constexpr KitColor kDirTextDim    {168, 168, 168, 255};  // #a8a8a8 (lifted from #a0a0a0)

// The three-accent pastel system (DS-2 revised — replaces the single electric cyan).
// primary = pastel lime (the live/active/selected signal, the eye-magnet); secondary
// = pastel teal, tertiary = pastel purple (CATEGORICAL distinctions — a KIND, never
// intensity). accent/hot is a brighter tint OF the primary for hover/live/drag. On the
// greyer bg/cell the pastels clear the 3:1 indicator floor comfortably (primary ~7.6,
// secondary ~6.8, tertiary ~5.5) at the spec-start values, so no per-hue nudge was
// needed — the hues stay pastel lime/teal/purple. warn is a reserved red/amber for
// byte-deleting states only.
constexpr KitColor kDirAccentPrimary  {176, 224, 152, 255};  // #B0E098 — pastel lime
constexpr KitColor kDirAccentSecondary{132, 214, 208, 255};  // #84D6D0 — pastel teal
constexpr KitColor kDirAccentTertiary {194, 170, 232, 255};  // #C2AAE8 — pastel purple
constexpr KitColor kDirAccentHot      {200, 236, 178, 255};  // #C8ECB2 — lighter pastel lime
constexpr KitColor kDirWarn           {235, 120, 90, 255};   // #eb785a — destructive only

// Direction C pastel spectral ramp (DS-2 revised): a three-stop sweep through the
// accents — pastel lime (low) -> pastel teal (mid) -> pastel purple (high) — so the
// signature keyboard strip reads as an extension of the accent system, not a neon
// flourish. Endpoints/midpoint ARE the three accent constants (single source).
constexpr KitColor kDirSpectralLo  = kDirAccentPrimary;    // low notes: pastel lime
constexpr KitColor kDirSpectralMid = kDirAccentSecondary;  // mid notes: pastel teal
constexpr KitColor kDirSpectralHi  = kDirAccentTertiary;   // high notes: pastel purple

// --- state transform helpers -------------------------------------------------

std::uint8_t clamp8(int v) {
    return static_cast<std::uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
}

// Linear blend from a toward b by t in [0, 1] (alpha carried from a — a state
// tint changes hue/brightness, not opacity; disabled handles alpha separately).
KitColor mix(const KitColor& a, const KitColor& b, double t) {
    return KitColor{
        clamp8(static_cast<int>(std::lround(a.r + (b.r - a.r) * t))),
        clamp8(static_cast<int>(std::lround(a.g + (b.g - a.g) * t))),
        clamp8(static_cast<int>(std::lround(a.b + (b.b - a.b) * t))),
        a.a,
    };
}

// Scale RGB by factor (brightness up/down), alpha untouched.
KitColor scale(const KitColor& c, double factor) {
    return KitColor{
        clamp8(static_cast<int>(std::lround(c.r * factor))),
        clamp8(static_cast<int>(std::lround(c.g * factor))),
        clamp8(static_cast<int>(std::lround(c.b * factor))),
        c.a,
    };
}

// Desaturate toward the color's own luminance-gray by amount in [0, 1].
KitColor desaturate(const KitColor& c, double amount) {
    // 8-bit gray from the perceptual weights (same weighting family as luminance).
    const int gray = clamp8(static_cast<int>(
        std::lround(0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b)));
    const KitColor g{static_cast<std::uint8_t>(gray),
                     static_cast<std::uint8_t>(gray),
                     static_cast<std::uint8_t>(gray), c.a};
    return mix(c, g, amount);
}

double linearizeChannel(std::uint8_t v) {
    const double s = v / 255.0;
    return s <= 0.03928 ? s / 12.92 : std::pow((s + 0.055) / 1.055, 2.4);
}

} // namespace

KitColor roleColor(Role role) {
    switch (role) {
        case Role::BgBase:       return kDirBgBase;
        case Role::BgPanel:      return kDirBgPanel;
        case Role::BgCell:       return kDirBgCell;
        case Role::LineHairline: return kDirHairline;
        case Role::TextPrimary:  return kDirTextPrimary;
        case Role::TextDim:      return kDirTextDim;
        case Role::AccentPrimary:   return kDirAccentPrimary;
        case Role::AccentSecondary: return kDirAccentSecondary;
        case Role::AccentTertiary:  return kDirAccentTertiary;
        case Role::AccentHot:    return kDirAccentHot;
        case Role::Warn:         return kDirWarn;
    }
    return kDirBgBase;  // unreachable; keeps non-void control flow total
}

KitColor roleColorState(Role role, InteractionState state) {
    const KitColor base = roleColor(role);
    switch (state) {
        case InteractionState::Rest:
            return base;
        case InteractionState::Hover:
            // Lighten the surface toward the hot accent (~10%) — the "alive" cue.
            return mix(base, roleColor(Role::AccentHot), 0.10);
        case InteractionState::Active:
            // The selected/active layer carries the PRIMARY accent — "this is live"
            // is always the primary hue (DS-2 revised: primary leads state; secondary/
            // tertiary are categorical, never intensity).
            return roleColor(Role::AccentPrimary);
        case InteractionState::Pressed:
            // The surface "pushes in": darken.
            return scale(base, 0.82);
        case InteractionState::Dragging:
            // A live-drag element reads as active-but-lighter (primary -> hot).
            return mix(roleColor(Role::AccentPrimary), roleColor(Role::AccentHot), 0.30);
        case InteractionState::Focus:
            // Focus keeps the surface but is drawn with a text/primary ring by the
            // shell; the fill nudges toward the primary accent so focus reads pre-ring.
            return mix(base, roleColor(Role::AccentPrimary), 0.08);
        case InteractionState::Disabled: {
            // Desaturate and drop alpha to 40% (§3.3).
            KitColor d = desaturate(base, 0.6);
            d.a = static_cast<std::uint8_t>(std::lround(base.a * 0.4));
            return d;
        }
    }
    return base;
}

KitColor spectralColor(double t) {
    if (t < 0.0) t = 0.0;
    if (t > 1.0) t = 1.0;
    // Three-stop pastel sweep anchored on the accent trio (DS-2 revised Direction C):
    // lime (low) -> teal (mid, t=0.5) -> purple (high). A single Lo->Hi lerp would skip
    // the teal midpoint and drift the ramp off the accent family; interpolate each half
    // so the midpoint IS the secondary accent and every stop stays in the pastel band.
    if (t <= 0.5) {
        return mix(kDirSpectralLo, kDirSpectralMid, t / 0.5);
    }
    return mix(kDirSpectralMid, kDirSpectralHi, (t - 0.5) / 0.5);
}

double relativeLuminance(const KitColor& c) {
    return 0.2126 * linearizeChannel(c.r) +
           0.7152 * linearizeChannel(c.g) +
           0.0722 * linearizeChannel(c.b);
}

double contrastRatio(const KitColor& a, const KitColor& b) {
    const double la = relativeLuminance(a);
    const double lb = relativeLuminance(b);
    const double lighter = std::max(la, lb);
    const double darker = std::min(la, lb);
    return (lighter + 0.05) / (darker + 0.05);
}

double textFloor(TextClass cls) {
    return cls == TextClass::Body ? 4.5 : 3.0;
}

} // namespace reasampler
