// Lumi Control Monitor (KL-63): 8 read-only sliders that mirror
// director params with a "ghost touch" animation when Lumi changes a
// value. Cyan inner-track + purple outer glow fade in, the thumb
// glides smoothly old→new, then everything fades back out. Tells the
// visual story of Lumi reaching in and turning a knob.
//
// Extracted from show.js per KL-109. main.js calls updateLumiSliders()
// on the 10Hz state push.

const SLIDER_CONFIGS = [
    {id: 'ctrl-strobe',    wrapId: 'slider-strobe',    param: 'strobe_multiplier',   scale: 100,   decimals: 2},
    {id: 'ctrl-movement',  wrapId: 'slider-movement',  param: 'movement_multiplier', scale: 100,   decimals: 2},
    {id: 'ctrl-release',   wrapId: 'slider-release',   param: 'release_speed',       scale: 100,   decimals: 2},
    {id: 'ctrl-superbeat', wrapId: 'slider-superbeat', param: 'superbeat_threshold', scale: 100,   decimals: 2},
    {id: 'ctrl-hue-bass',  wrapId: 'slider-hue-bass',  param: 'hue_offset_bass',     scale: 100,   decimals: 2},
    {id: 'ctrl-hue-mids',  wrapId: 'slider-hue-mids',  param: 'hue_offset_mids',     scale: 100,   decimals: 2},
    {id: 'ctrl-hue-highs', wrapId: 'slider-hue-highs', param: 'hue_offset_highs',    scale: 100,   decimals: 2},
    {id: 'ctrl-par-wash',  wrapId: 'slider-par-wash',  param: 'par_wash_speed',      scale: 10000, decimals: 4},
    // KL-136: optical knobs from OFL ingestion. Hidden from view on
    // installs whose rig has no zoom/iris/frost/CCT channels would be
    // a future polish — for now they always show, just with their
    // ghost-touch animation idle.
    {id: 'ctrl-zoom',      wrapId: 'slider-zoom',      param: 'zoom_pulse_amount',   scale: 100,   decimals: 2},
    {id: 'ctrl-iris',      wrapId: 'slider-iris',      param: 'iris_response',       scale: 100,   decimals: 2},
    {id: 'ctrl-frost',     wrapId: 'slider-frost',     param: 'frost_amount',        scale: 100,   decimals: 2},
    {id: 'ctrl-cct',       wrapId: 'slider-cct',       param: 'cct_warm_cool_bias',  scale: 100,   decimals: 2},
];

const _lastLumiValues = new Map();
const _activeAnimations = new Map();

function _changed(config, oldVal, newVal) {
    if (oldVal === undefined) return false;
    const oldScaled = Math.round(oldVal * config.scale);
    const newScaled = Math.round(newVal * config.scale);
    return oldScaled !== newScaled;
}

function _easeInOutCubic(t) {
    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

const GHOST_FADE_IN_MS = 150;
const GHOST_GLIDE_MS = 1100;
const GHOST_HOLD_MS = 100;
const GHOST_STAGGER_MS = 250;

function ghostTouchSlider(config, fromVal, toVal) {
    const slider = document.getElementById(config.id);
    const wrap = document.getElementById(config.wrapId);
    const valSpan = document.getElementById(config.id.replace('ctrl-', 'val-'));
    if (!slider || !wrap) return;

    const prev = _activeAnimations.get(config.id);
    if (prev) cancelAnimationFrame(prev.rafId);

    wrap.classList.add('ghost-touching');

    const startedAt = performance.now();
    const fromSliderVal = parseFloat(slider.value);
    const toSliderVal = toVal * config.scale;

    const tick = (now) => {
        const elapsed = now - startedAt;
        if (elapsed < GHOST_FADE_IN_MS) {
            const handle = {rafId: requestAnimationFrame(tick)};
            _activeAnimations.set(config.id, handle);
            return;
        }
        const glideElapsed = elapsed - GHOST_FADE_IN_MS;
        const t = Math.min(glideElapsed / GHOST_GLIDE_MS, 1);
        const eased = _easeInOutCubic(t);
        const currentSliderVal = fromSliderVal + (toSliderVal - fromSliderVal) * eased;
        slider.value = currentSliderVal;
        if (valSpan) {
            const currentReal = currentSliderVal / config.scale;
            valSpan.textContent = currentReal.toFixed(config.decimals);
        }
        if (t < 1) {
            const handle = {rafId: requestAnimationFrame(tick)};
            _activeAnimations.set(config.id, handle);
        } else {
            slider.value = toSliderVal;
            if (valSpan) valSpan.textContent = toVal.toFixed(config.decimals);
            const timeoutId = setTimeout(() => {
                wrap.classList.remove('ghost-touching');
                _activeAnimations.delete(config.id);
            }, GHOST_HOLD_MS);
            _activeAnimations.set(config.id, {rafId: 0, timeoutId});
        }
    };
    const handle = {rafId: requestAnimationFrame(tick)};
    _activeAnimations.set(config.id, handle);
}

function _snapSlider(config, val) {
    const slider = document.getElementById(config.id);
    const valSpan = document.getElementById(config.id.replace('ctrl-', 'val-'));
    if (!slider) return;
    slider.value = val * config.scale;
    if (valSpan) valSpan.textContent = val.toFixed(config.decimals);
}

export function updateLumiSliders(directorParams) {
    if (!directorParams) return;
    const changed = [];
    for (const config of SLIDER_CONFIGS) {
        const newVal = directorParams[config.param];
        if (typeof newVal !== 'number') continue;
        const oldVal = _lastLumiValues.get(config.param);
        if (_changed(config, oldVal, newVal)) {
            changed.push({config, oldVal, newVal});
        } else if (oldVal === undefined) {
            _snapSlider(config, newVal);
        }
        _lastLumiValues.set(config.param, newVal);
    }
    for (let i = changed.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [changed[i], changed[j]] = [changed[j], changed[i]];
    }
    changed.forEach((change, i) => {
        const delay = i * GHOST_STAGGER_MS;
        if (delay === 0) {
            ghostTouchSlider(change.config, change.oldVal, change.newVal);
        } else {
            setTimeout(
                () => ghostTouchSlider(change.config, change.oldVal, change.newVal),
                delay,
            );
        }
    });
}

export function initSliders() {
    // No event listeners — the sliders are pure read-only displays.
    // initSliders is kept exported so main.js's existing init order
    // still works without churn; if/when we need pre-state-push setup
    // (e.g. tooltips) it lands here.
}
