#!/usr/bin/env node
// Adom Studio — Dark 2026, re-grounded on the Adom brand.
//
// NAMING (John 2026-07-27): this design started life as "Adom Studio v2"; once
// approved it TOOK OVER the "Adom Studio" name and the old template-projected
// Studio was abandoned. Keeping the name means every existing reference — the
// v21 golden image's baked default, HD's scheme mapping, every user's
// settings.json — resolves to the new design with zero migration.
//
// John 2026-07-27: "Dark 2026 is a perfect theme. ... take Dark 2026 and just
// shift all the grays to be more to the dark teal ... the most important thing
// is the main background color that Adom Studio uses right now matches the main
// adom brand and when a vscode panel is next to a wiki panel, they look like the
// same brand. and the foreground text color we picked for adom studio is the
// right base text color. ... stay true to the color differences that Dark 2026
// setup cuz it did an excellent job so we don't want to ruin it."
//
// So this file changes NO relationships. It reads the resolved Dark 2026 theme
// (tools/reference-2026-dark.json, extracted verbatim from code-server's
// theme-defaults) and applies one mathematical re-grounding:
//
//   NEUTRALS  every gray is re-issued on the Adom slate hue (215°, the hue of
//             #161b22) at a luminance produced by a two-anchor map in
//             log(L + 0.05) space:  2026 bg #121314 -> #161b22 (Adom ground)
//                                   2026 fg #BBBEBF -> #c9d1d9 (Adom body text)
//             Contrast ratios are functions of (L + 0.05), so a log-space affine
//             map preserves 2026's contrast ARCHITECTURE while landing both
//             anchors exactly. Between/beyond the anchors the scale interpolates,
//             which keeps every 2026 step visibly the same step.
//
//   ACCENT    the 2026 product-blue family (#3994BC etc., hue 180-240 at real
//             saturation) moves to the Adom teal (hue 177), same luminance map —
//             focus rings, badges, buttons keep their exact prominence, in brand.
//
//   SEMANTIC  reds/greens/yellows/purples (errors, git, warnings) and the whole
//             syntax palette are UNTOUCHED. Color-as-meaning is 2026's design;
//             re-tinting it would "ruin it with color shifting".
//
//   node tools/gen-adom-studio-v2.mjs [outDir]

import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const outDir = resolve(process.argv[2] ?? join(root, 'vscode-themes'));
const ref = JSON.parse(readFileSync(join(root, 'tools/reference-2026-dark.json'), 'utf8'));

// ── anchors ──────────────────────────────────────────────────────────────────
const BG_OLD = '#121314', BG_NEW = '#161b22';   // Adom brand ground (wiki-adjacent)
const FG_OLD = '#bbbebf', FG_NEW = '#c9d1d9';   // Adom Studio base text (John: "the right base text color")
const NEUTRAL_HUE = 215;    // hue of #161b22 — the Adom slate cast
const NEUTRAL_SAT = 0.16;   // in-family with #161b22 (.21) / #30363d (.12) / #8b949e (.09)
const TEAL_HUE = 177;       // Adom accent #00b8b1

// ── color math ───────────────────────────────────────────────────────────────
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
function hexToRgb(h) { return [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)); }
function rgbToHex(r) { return '#' + r.map((c) => clamp(Math.round(c), 0, 255).toString(16).padStart(2, '0')).join(''); }
function srgbToLin(c) { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }
function relLum(h) { const [r, g, b] = hexToRgb(h); return 0.2126 * srgbToLin(r) + 0.7152 * srgbToLin(g) + 0.0722 * srgbToLin(b); }
function rgbToHsl(h) {
    let [r, g, b] = hexToRgb(h).map((c) => c / 255);
    const mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2;
    if (mx === mn) return [0, 0, l];
    const d = mx - mn;
    const s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
    let hh = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
    return [hh * 60, s, l];
}
function hslToHex(h, s, l) {
    h = ((h % 360) + 360) % 360 / 360;
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q;
    const f = (t) => {
        t = ((t % 1) + 1) % 1;
        if (t < 1 / 6) return p + (q - p) * 6 * t;
        if (t < 1 / 2) return q;
        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
        return p;
    };
    return rgbToHex([f(h + 1 / 3), f(h), f(h - 1 / 3)].map((c) => c * 255));
}
/** Color at (hue, sat) whose relative luminance ≈ target, solved by bisection on HSL lightness. */
function atLuminance(hue, sat, target) {
    let lo = 0, hi = 1;
    for (let i = 0; i < 40; i++) {
        const mid = (lo + hi) / 2;
        relLum(hslToHex(hue, sat, mid)) < target ? (lo = mid) : (hi = mid);
    }
    return hslToHex(hue, sat, (lo + hi) / 2);
}

// Two-anchor luminance map in log(L+0.05) space: bg->bg and fg->fg land exactly,
// everything else interpolates/extrapolates linearly, preserving step ORDER and
// (piecewise) the multiplicative structure contrast ratios live in. x0/x1 are the
// OLD (Dark 2026) anchors, shared by every re-grounding; only the ground (y0) moves.
const x0 = Math.log(relLum(BG_OLD) + 0.05), x1 = Math.log(relLum(FG_OLD) + 0.05);

// buildTheme re-grounds Dark 2026 onto ONE Adom ground. Adom Studio and Adom Studio
// Dark are the SAME brand-accurate treatment (same base text, same slate hue, same
// teal re-issue, same contrast-preserving map); they differ ONLY in the ground
// luminance. John 2026-08-01: "for Adom Studio Dark ... we use a lot of the mods we
// made for Adom Studio ... but we stay true to the teals ... based on Dark 2026." So
// Studio Dark stops being a template projection and becomes Dark 2026 re-grounded on
// the deeper #0d1117, exactly the way the flagship is on #161b22.
//
// fgNew moves the TEXT anchor the same way bgNew moves the ground. Adom Studio Bright
// (John 2026-08-01) is "Studio Dark with brighter text": same #0d1117 ground, but the
// whole neutral text scale lifted by re-anchoring fg onto #e6edf3. One anchor move
// brightens primary AND secondary text proportionally, so the hierarchy 2026 designed
// (active > primary > secondary) survives instead of flattening.
//
// syntaxMinRatio, when set, lifts code colours WITHIN their hue to at least that
// contrast ratio against the ground -- the 2026-07-25 keyword ruling (7:1 AAA, raise
// lightness only, never swap hue: "the colour IS the meaning").
function buildTheme({ bgNew, name, file, fgNew = FG_NEW, syntaxMinRatio = 0 }) {
    const y0 = Math.log(relLum(bgNew) + 0.05), y1 = Math.log(relLum(fgNew) + 0.05);
    const mapLum = (L) => {
        const x = Math.log(L + 0.05);
        const t = (x - x0) / (x1 - x0);
        return clamp(Math.exp(y0 + t * (y1 - y0)) - 0.05, 0, 1);
    };
    const transformBase = (base) => {
        const [h, s] = rgbToHsl(base);
        const Lnew = mapLum(relLum(base));
        // Near-neutral: re-issue on the Adom slate hue. Saturation eases off toward
        // white/black extremes so near-black and near-white don't look dyed.
        if (s < 0.22) {
            const ease = clamp(1.6 * Math.min(Lnew, 1 - Lnew) + 0.55, 0, 1);
            return atLuminance(NEUTRAL_HUE, NEUTRAL_SAT * ease, Lnew);
        }
        // 2026 product blue -> Adom teal, same luminance treatment, same saturation.
        if (h >= 180 && h <= 245) return atLuminance(TEAL_HUE, s, Lnew);
        // Semantic color (red/green/yellow/purple/orange): untouched.
        return base;
    };
    const cache = new Map();
    const transform = (value) => {
        if (typeof value !== 'string' || !/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(value)) return value;
        const base = value.slice(0, 7).toLowerCase(), alpha = value.slice(7);
        // Anchor equality is architecture: any surface 2026 made IDENTICAL to the
        // ground/text anchors stays identical to ours (active tab == editor, etc.).
        if (base === BG_OLD.toLowerCase()) return bgNew + alpha;
        if (base === FG_OLD.toLowerCase()) return fgNew + alpha;
        if (!cache.has(base)) cache.set(base, transformBase(base));
        return cache.get(base) + alpha;
    };

    const colors = {};
    for (const [k, v] of Object.entries(ref.colors)) colors[k] = transform(v);
    // The anchors must land EXACTLY (float bisection could be one digit off).
    colors['editor.background'] = bgNew;
    colors['editor.foreground'] = fgNew;

    // THE ONE-GROUND CONTRACT (John 2026-08-02, superseding the same-day tab
    // contract): Claude chats paint sideBar.background in history-resumed mode and
    // editor.background in fullEditor mode, so the ONLY way the active tab can flow
    // into EVERY chat (and every file editor) is to unify the two grounds:
    // sideBar takes the editor ground and text. The active tab keeps 2026's anchor
    // equality (== editor ground == both chat modes == file bodies), and inactive
    // tabs/strip keep their natural RAISED 2026 chrome tone, so the active tab
    // reads as the sunken/merged one - exactly Dark 2026's architecture. The
    // sidebar/editor boundary is carried by sideBar.border, not a fill change.
    colors['sideBar.background'] = bgNew;
    colors['sideBar.foreground'] = fgNew;

    // Syntax lift (Bright only): raise lightness WITHIN the hue until the colour clears
    // syntaxMinRatio against the ground. Hue and saturation are untouched -- semantics
    // keep their colour, they just read better. 0 = verbatim (flagship + Dark).
    const liftSyntax = (hex) => {
        if (!syntaxMinRatio || !/^#[0-9a-fA-F]{6}$/.test(hex)) return hex;
        const bgL = relLum(bgNew);
        if ((relLum(hex) + 0.05) / (bgL + 0.05) >= syntaxMinRatio) return hex;
        const [h, s] = rgbToHsl(hex);
        // 2% overshoot so hex quantization can't land the result a hair UNDER the floor.
        return atLuminance(h, s, syntaxMinRatio * 1.02 * (bgL + 0.05) - 0.05);
    };
    const tokenColors = !syntaxMinRatio ? ref.tokenColors : ref.tokenColors.map((tc) => {
        const fg = tc.settings?.foreground;
        if (typeof fg !== 'string') return tc;
        return { ...tc, settings: { ...tc.settings, foreground: liftSyntax(fg) } };
    });

    const theme = {
        name,
        semanticHighlighting: ref.semanticHighlighting ?? true,
        colors,
        // Syntax stays 2026's verbatim (colour-as-meaning, not gray ambience), except
        // the within-hue legibility lift when syntaxMinRatio asks for it.
        tokenColors,
        semanticTokenColors: ref.semanticTokenColors
    };
    writeFileSync(join(outDir, 'themes', file), JSON.stringify(theme, null, 2) + '\n');

    // Sanity: the relationships John cares about survived.
    const cr = (a, b) => { const [x, y] = [relLum(a.slice(0, 7)) + 0.05, relLum(b.slice(0, 7)) + 0.05].sort((p, q) => q - p); return x / y; };
    const rel = (k1, k2) => `${k1}/${k2}: 2026=${cr(ref.colors[k1], ref.colors[k2]).toFixed(3)} new=${cr(colors[k1], colors[k2]).toFixed(3)}`;
    console.log(`${name} <- Dark 2026, re-grounded onto ${bgNew}`);
    console.log('  ' + rel('tab.inactiveBackground', 'editor.background'));
    console.log('  ' + rel('editorWidget.background', 'editor.background'));
    console.log('  ' + rel('input.background', 'editor.background'));
    console.log('  one ground (activeTab==sideBar==editor):', colors['tab.activeBackground'] === colors['editor.background'] && colors['sideBar.background'] === colors['editor.background'], ' inactive raised + distinct:', colors['tab.inactiveBackground'] !== colors['tab.activeBackground']);
    console.log('  chrome sample:', colors['tab.inactiveBackground'], ' overlay sample:', colors['editorWidget.background'], ' accent sample:', colors['tab.activeBorderTop']);
}

// -- emit both --
buildTheme({ bgNew: BG_NEW, name: 'Adom Studio', file: 'adom-studio-color-theme.json' });
// Adom Studio Dark: the SAME re-grounding, onto the deeper base ground (#0d1117).
// Was a plain template projection before; now brand-accurate like the flagship, just
// darker (John 2026-08-01: keep #0d1117, just make it brand-true).
buildTheme({ bgNew: '#0d1117', name: 'Adom Studio Dark', file: 'adom-studio-dark-color-theme.json' });
// 'Adom Studio Dark (Brighter Text)' (label renamed 2026-08-02, was 'Adom Studio Bright'; slug studio-bright is the
// frozen storage key and never changes): Studio Dark's ground with the text scale lifted (John 2026-08-01:
// "Adom Studio Dark as the basis ... just making the text brighter, even more contrast of
// the foreground text against the same background colors"). fg anchor #c9d1d9 -> #e6edf3
// lifts every neutral proportionally; syntax clears 7:1 AAA within-hue (2026-07-25 ruling).
buildTheme({ bgNew: '#0d1117', fgNew: '#e6edf3', syntaxMinRatio: 7.0, name: 'Adom Studio Dark (Brighter Text)', file: 'adom-studio-bright-color-theme.json' });
