"use strict";

const crypto = require("node:crypto");
const fs = require("node:fs");
const net = require("node:net");
const os = require("node:os");
const path = require("node:path");
const { execFile, spawn } = require("node:child_process");

const MEDIA_HELPER = path.join(__dirname, "media-helper.ps1");
const MEDIA_PY_HELPER = path.join(__dirname, "media-winrt-helper.py");
const MEDIA_PY_VENDOR = path.join(__dirname, "vendor");
const FONT_STACK = "'Microsoft YaHei','Microsoft JhengHei','Yu Gothic','Meiryo','Malgun Gothic','Segoe UI',Arial,sans-serif";
const STATIC_DECK_STATES_ONLY = true;
const STATIC_STATE_IMAGE_REFS = loadStaticStateImageRefs();
const STATIC_STATE_IMAGE_CACHE = new Map();

const DEFAULTS = {
  auraColor: "#58a9ff",
  auraAccent: "#fddab0",
  sensitivity: 5,
  auraSpeed: 5,
  auraBrightness: 82,
  auraMode: "pulse",
  responseRateMs: 240,
  gradientLeft: "#ff4757",
  gradientMiddle: "#ffa502",
  gradientRight: "#00bcd4",
  innerGradLeft: "#aa00ff",
  innerGradMiddle: "#58a9ff",
  innerGradRight: "#ffd5c2",
  coverTextVisible: true,
  coverSongVisible: true,
  coverArtistVisible: true,
  coverTextMode: "still",
  coverFontSize: "large",
  coverDisplayMode: "barsText",
  coverWalkingText: false,
  coverWaveColor: "#ff4757",
  coverSensitivity: 5,
  coverResponseRateMs: 240,
  coverPressAction: "changeRhythm",
  coverHoldAction: "none",
  coverBarsVisible: true,
  coverMode: "pulse",
  keyMode: "splitBars",
  keyHoldAction: "none"
};
const MEDIA_KEYS = { playpause: 179, next: 176, previous: 177, mute: 173, volumeup: 175, volumedown: 174 };
const PALETTES = [
  ["#58a9ff", "#fddab0"],
  ["#7cf0c6", "#fddab0"],
  ["#8da2ff", "#ffb86b"],
  ["#f0c06a", "#58a9ff"],
  ["#d8f0ff", "#7cf0c6"]
];
const MODES = ["bars", "pulse", "wave", "blocks", "comet", "arc", "sparkline", "prism"];
const KEY_MODES = ["splitBars", "riseDots", "mirrorWave", "centerBloom", "beatSteps", "softComet"];
const COVER_MODES = ["pulse", "frame", "dots", "bars", "clean"];
const RESPONSE_RATES = [30, 60, 90, 120, 240];
const MEDIA_POLL_MS = 5000;
const MEDIA_FAILURE_COOLDOWN_MS = 20000;
const TOUCH_SLOT_WIDTH = 180;
const TOUCH_HEIGHT = 66;
const TOUCH_SLOTS = 4;
const TOUCH_WIDTH = TOUCH_SLOT_WIDTH * TOUCH_SLOTS;
const TOUCH_SEGMENTS = 20;
const TOUCH_BAR_COUNT = 80;

const state = {
  socket: null,
  port: 0,
  pluginUUID: "",
  registerEvent: "",
  contexts: new Map(),
  settings: Object.assign({}, DEFAULTS),
  touchPanelSettings: { auraColor: "#ff4757", sensitivity: 5, auraMode: "bars", responseRateMs: 240 },
  keyBarSettings: { auraColor: "#ff4757", sensitivity: 5, keyMode: "splitBars", responseRateMs: 240, keyHoldAction: "none" },
  touchPanelReady: false,
  playing: false,
  tick: 0,
  timer: null,
  responseRateMs: 240,
  coverTextVisible: true,
  coverSongVisible: true,
  coverArtistVisible: true,
  coverTextMode: "still",
  coverFontSize: "large",
  coverDisplayMode: "barsText",
  coverWalkingText: false,
  coverWaveColor: "#ff4757",
  coverSensitivity: 5,
  coverResponseRateMs: 240,
  coverPressAction: "changeRhythm",
  coverHoldAction: "none",
  coverBarsVisible: true,
  coverMode: "pulse",
  media: {
    pollBusy: false,
    title: "",
    artist: "",
    album: "",
    playbackStatus: "Unknown",
    positionMs: 0,
    endMs: 180000,
    thumbnailDataUrl: "",
    coverKeyDataUrl: "",
    visibleCoverDataUrl: "",
    covDataUrl: "",
    covBusy: false,
    covKey: "",
    covLastAt: 0,
    coverPngCache: new Map(),
    coverPngBusy: new Set(),
    coverLastDataUrl: "",
    updatedAt: 0,
    lastPollAt: 0,
    lastPollFailAt: 0
  },
  audio: {
    capture: null,
    loaded: false,
    started: false,
    failed: false,
    active: false,
    level: 0,
    peak: 0,
    freqPeak: 0,
    slotLevels: Array.from({ length: TOUCH_SLOTS }, () => 0),
    segmentLevels: Array.from({ length: TOUCH_SEGMENTS }, () => 0),
    bandPeaks: Array.from({ length: TOUCH_BAR_COUNT }, () => 0),
    bandAverages: Array.from({ length: TOUCH_BAR_COUNT }, () => 0),
    bars: Array.from({ length: TOUCH_BAR_COUNT }, () => 0)
  }
};

class WebSocketClient {
  constructor(host, port) {
    this.host = host;
    this.port = port;
    this.socket = null;
    this.handshakeComplete = false;
    this.handshakeBuffer = Buffer.alloc(0);
    this.frameBuffer = Buffer.alloc(0);
    this.onopen = null;
    this.onmessage = null;
    this.onclose = null;
  }

  connect() {
    return new Promise((resolve, reject) => {
      const key = crypto.randomBytes(16).toString("base64");
      const socket = net.createConnection({ host: this.host, port: this.port });
      this.socket = socket;
      let resolved = false;

      socket.once("error", (error) => {
        if (!resolved) {
          resolved = true;
          reject(error);
        }
      });

      socket.once("connect", () => {
        socket.write([
          "GET / HTTP/1.1",
          "Host: " + this.host + ":" + this.port,
          "Upgrade: websocket",
          "Connection: Upgrade",
          "Sec-WebSocket-Key: " + key,
          "Sec-WebSocket-Version: 13",
          "",
          ""
        ].join("\r\n"));
      });

      socket.on("data", (chunk) => this.handleData(chunk, () => {
        if (!resolved) {
          resolved = true;
          resolve();
          if (this.onopen) this.onopen();
        }
      }));
      socket.on("close", () => {
        if (this.onclose) this.onclose();
      });
    });
  }

  handleData(chunk, opened) {
    if (!this.handshakeComplete) {
      this.handshakeBuffer = Buffer.concat([this.handshakeBuffer, chunk]);
      const boundary = this.handshakeBuffer.indexOf("\r\n\r\n");
      if (boundary === -1) return;
      this.handshakeComplete = true;
      const rest = this.handshakeBuffer.slice(boundary + 4);
      this.handshakeBuffer = Buffer.alloc(0);
      opened();
      if (rest.length) this.handleData(rest, () => {});
      return;
    }

    this.frameBuffer = Buffer.concat([this.frameBuffer, chunk]);
    while (this.frameBuffer.length >= 2) {
      const first = this.frameBuffer[0];
      const second = this.frameBuffer[1];
      let length = second & 127;
      let offset = 2;
      if (length === 126) {
        if (this.frameBuffer.length < 4) return;
        length = this.frameBuffer.readUInt16BE(2);
        offset = 4;
      } else if (length === 127) {
        if (this.frameBuffer.length < 10) return;
        length = Number(this.frameBuffer.readBigUInt64BE(2));
        offset = 10;
      }

      const masked = (second & 128) !== 0;
      let mask;
      if (masked) {
        if (this.frameBuffer.length < offset + 4) return;
        mask = this.frameBuffer.slice(offset, offset + 4);
        offset += 4;
      }
      if (this.frameBuffer.length < offset + length) return;

      let payload = this.frameBuffer.slice(offset, offset + length);
      this.frameBuffer = this.frameBuffer.slice(offset + length);
      if (masked) {
        const output = Buffer.alloc(length);
        for (let index = 0; index < length; index += 1) output[index] = payload[index] ^ mask[index % 4];
        payload = output;
      }
      const op = first & 15;
      if (op === 1 && this.onmessage) this.onmessage(payload.toString("utf8"));
      if (op === 8) {
        this.close();
        return;
      }
    }
  }

  send(payload) {
    if (!this.socket || this.socket.destroyed) return;
    const body = Buffer.from(JSON.stringify(payload));
    let header;
    if (body.length < 126) {
      header = Buffer.alloc(2);
      header[1] = body.length;
    } else {
      header = Buffer.alloc(4);
      header[1] = 126;
      header.writeUInt16BE(body.length, 2);
    }
    header[0] = 0x81;
    this.socket.write(Buffer.concat([header, body]));
  }

  close() {
    try {
      if (this.socket) this.socket.end();
    } catch (_error) {}
  }
}

function parseArgs(argv) {
  const parsed = {};
  for (let index = 2; index < argv.length; index += 2) {
    const key = argv[index];
    if (key && key.startsWith("-")) parsed[key.slice(1)] = argv[index + 1];
  }
  return parsed;
}

function send(payload) {
  if (state.socket) state.socket.send(payload);
}

function clamp(value, min, max) {
  const number = Number(value);
  return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
}

function actionSlug(action) {
  return String(action || "").split(".").pop();
}

function merged(settings) {
  return Object.assign({}, DEFAULTS, state.settings, settings || {});
}

function getContext(context) {
  if (!state.contexts.has(context)) {
    state.contexts.set(context, { context, action: "", settings: {}, visible: false });
  }
  return state.contexts.get(context);
}

function loadStaticStateImageRefs() {
  const refs = new Map();
  try {
    const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8").replace(/^\uFEFF/, ""));
    for (const action of manifest.Actions || []) {
      const image = action && action.States && action.States[0] && action.States[0].Image;
      if (action && action.UUID && image) refs.set(action.UUID, image);
    }
  } catch (_error) {}
  return refs;
}

function resolveStaticImagePath(ref) {
  const cleanRef = String(ref || "").replace(/^[\\/]+/, "");
  if (!cleanRef || cleanRef.includes("..")) return "";
  const relativeRefs = path.extname(cleanRef) ? [cleanRef] : [`${cleanRef}.png`, `${cleanRef}@2x.png`];
  for (const relativeRef of relativeRefs) {
    const imagePath = path.resolve(__dirname, "..", relativeRef);
    if (fs.existsSync(imagePath)) return imagePath;
  }
  return "";
}

function staticStateImageForAction(action) {
  const ref = STATIC_STATE_IMAGE_REFS.get(String(action || ""));
  if (!ref) return "";
  if (STATIC_STATE_IMAGE_CACHE.has(ref)) return STATIC_STATE_IMAGE_CACHE.get(ref);
  const imagePath = resolveStaticImagePath(ref);
  if (!imagePath) return "";
  try {
    const dataUrl = "data:image/png;base64," + fs.readFileSync(imagePath).toString("base64");
    STATIC_STATE_IMAGE_CACHE.set(ref, dataUrl);
    return dataUrl;
  } catch (_error) {
    return "";
  }
}

function sendWindowsMediaKey(vkey) {
  if (os.platform() !== "win32") return;
  const command = `$c='[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);'; Add-Type -MemberDefinition $c -Name NativeMedia -Namespace Bravent; [Bravent.NativeMedia]::keybd_event(${vkey},0,0,0); Start-Sleep -Milliseconds 18; [Bravent.NativeMedia]::keybd_event(${vkey},0,2,0)`;
  execFile("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], { windowsHide: true, timeout: 2200 }, () => {});
}

function runPowerShellMediaCommand(command, done) {
  if (os.platform() !== "win32" || !fs.existsSync(MEDIA_HELPER)) {
    done(new Error("powershell media helper unavailable"));
    return;
  }
  execFile("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", MEDIA_HELPER, "-Command", command], { windowsHide: true, timeout: 4200 }, (error, stdout) => {
    if (error) return done(error);
    try {
      const data = JSON.parse(String(stdout || "{}").replace(/^\uFEFF/, ""));
      if (!data || data.ok === false) return done(new Error(data && data.error || "powershell media command failed"));
      done(null, data);
    } catch (parseError) {
      done(parseError);
    }
  });
}

function media(command) {
  const vkey = MEDIA_KEYS[command];
  if (["playpause", "next", "previous"].includes(command)) {
    runPowerShellMediaCommand(command, (error) => {
      if (error && vkey) sendWindowsMediaKey(vkey);
      refreshMediaSoon(650);
    });
  } else if (vkey) {
    sendWindowsMediaKey(vkey);
  }
  if (command === "playpause") state.playing = !state.playing;
  renderAll();
}

function refreshMediaSoon(delayMs) {
  setTimeout(() => {
    state.media.lastPollAt = 0;
    pollMediaTitle();
  }, Math.max(0, Number(delayMs) || 0));
}

function setImage(context, image) {
  send({ event: "setImage", context, payload: { target: 0, image } });
}

function setSettings(context, settings) {
  send({ event: "setSettings", context, payload: settings });
}

function initAudioCapture() {
  if (state.audio.loaded || state.audio.failed || os.platform() !== "win32") return;
  try {
    const { AudioCaptureWrapper } = require("./win_audio_capture.node");
    state.audio.capture = new AudioCaptureWrapper();
    state.audio.capture.start();
    state.audio.loaded = true;
    state.audio.started = true;
  } catch (_error) {
    state.audio.failed = true;
  }
}

function silentAudioFrame() {
  state.audio.active = false;
  state.audio.level = 0;
  state.audio.peak *= .82;
  state.audio.freqPeak *= .84;
  state.audio.slotLevels = state.audio.slotLevels.map((value) => value * .36);
  state.audio.segmentLevels = state.audio.segmentLevels.map((value) => value * .36);
  state.audio.bandPeaks = state.audio.bandPeaks.map((value) => value * .985);
  state.audio.bandAverages = state.audio.bandAverages.map((value) => value * .94);
  state.audio.bars = state.audio.bars.map((value) => value * .28);
}

function dftMagnitude(samples, bin) {
  const count = samples.length;
  if (!count || bin <= 0) return 0;
  const step = (Math.PI * 2 * bin) / count;
  let real = 0;
  let imag = 0;
  for (let index = 0; index < count; index += 1) {
    const window = .5 - .5 * Math.cos((Math.PI * 2 * index) / Math.max(1, count - 1));
    const sample = samples[index] * window;
    const angle = step * index;
    real += sample * Math.cos(angle);
    imag -= sample * Math.sin(angle);
  }
  return Math.sqrt(real * real + imag * imag) / count;
}

function frequencyBars(samples, count) {
  const source = samples.slice(-1024);
  const length = source.length;
  const maxBin = Math.max(4, Math.min(132, Math.floor(length / 2) - 2));
  const minBin = 2;
  if (!Array.isArray(state.audio.bandPeaks) || state.audio.bandPeaks.length !== count) {
    state.audio.bandPeaks = Array.from({ length: count }, () => 0);
  }
  if (!Array.isArray(state.audio.bandAverages) || state.audio.bandAverages.length !== count) {
    state.audio.bandAverages = Array.from({ length: count }, () => 0);
  }
  const mags = [];
  for (let index = 0; index < count; index += 1) {
    const position = index / Math.max(1, count - 1);
    const centerDistance = Math.abs(position - .5) * 2;
    const ratio = Math.pow(centerDistance, 1.42);
    const bin = Math.round(minBin * Math.pow(maxBin / minBin, ratio));
    const mag = dftMagnitude(source, bin) * .55 + dftMagnitude(source, Math.min(maxBin, bin + 1)) * .3 + dftMagnitude(source, Math.min(maxBin, bin + 3)) * .15;
    mags.push(mag);
  }
  const maxMag = Math.max(...mags, 0);
  state.audio.freqPeak = Math.max(maxMag, (state.audio.freqPeak || maxMag) * .9);
  const floor = Math.max(0.0000001, state.audio.freqPeak * .035);
  const range = Math.max(floor, state.audio.freqPeak - floor);
  return mags.map((mag, index) => {
    const position = index / Math.max(1, count - 1);
    const centerWeight = 1 - Math.abs(position - .5) * 2;
    const sideWeight = 1 - centerWeight;
    const previousAverage = state.audio.bandAverages[index] || mag;
    const average = previousAverage * .94 + mag * .06;
    state.audio.bandAverages[index] = average;
    const previousPeak = state.audio.bandPeaks[index] || mag;
    const peak = Math.max(mag, previousPeak * .972);
    state.audio.bandPeaks[index] = peak;
    const adaptiveFloor = Math.max(floor, average * .72);
    const adaptiveRange = Math.max(floor * 12, peak - adaptiveFloor);
    const local = clamp((mag - adaptiveFloor) / adaptiveRange, 0, 1);
    const global = clamp((mag - floor) / range, 0, 1);
    const centerTrim = 1 - centerWeight * .18;
    const sideLift = 1 + sideWeight * .1;
    const cap = .92 - centerWeight * .14;
    const balanced = Math.pow(local * .82 + global * .18, .72) * centerTrim * sideLift;
    return clamp(Math.min(cap, balanced), 0, .94);
  });
}

function updateAudioFrame() {
  initAudioCapture();
  const capture = state.audio.capture;
  if (!capture || state.audio.failed) {
    silentAudioFrame();
    return;
  }
  try {
    if (!capture.isCapturing()) capture.start();
    const raw = capture.getBuffer();
    if (!raw || !raw.length) {
      silentAudioFrame();
      return;
    }
    const source = Array.from(raw).map(Number).filter((value) => Number.isFinite(value));
    const samples = source.slice(-2048);
    if (!samples.length) {
      silentAudioFrame();
      return;
    }
    let max = 0;
    let sumSq = 0;
    for (const sample of samples) {
      const abs = Math.abs(sample);
      max = Math.max(max, abs);
      sumSq += sample * sample;
    }
    if (max < 0.00001) {
      silentAudioFrame();
      return;
    }
    const globalRms = Math.sqrt(sumSq / Math.max(1, samples.length));
    state.audio.peak = Math.max(globalRms, (state.audio.peak || globalRms) * .88);
    const floor = Math.max(0.00001, state.audio.peak * .025);
    const range = Math.max(floor, state.audio.peak - floor);

    const count = state.audio.bars.length;
    const next = frequencyBars(samples, count);
    const level = clamp((globalRms - floor) / range, 0, 1);
    state.audio.active = level > .025 || max > floor * 5;
    state.audio.level = state.audio.active ? level : 0;
    state.audio.bars = next.map((value, index) => {
      const previous = state.audio.bars[index] || 0;
      return value > previous ? previous * .16 + value * .84 : previous * .58 + value * .42;
    });
    state.audio.slotLevels = Array.from({ length: TOUCH_SLOTS }, (_item, slot) => {
      const start = Math.floor(slot * count / TOUCH_SLOTS);
      const end = Math.floor((slot + 1) * count / TOUCH_SLOTS);
      const values = state.audio.bars.slice(start, end);
      return values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
    });
    state.audio.segmentLevels = Array.from({ length: TOUCH_SEGMENTS }, (_item, segment) => {
      const start = Math.floor(segment * count / TOUCH_SEGMENTS);
      const end = Math.floor((segment + 1) * count / TOUCH_SEGMENTS);
      const values = state.audio.bars.slice(start, end);
      return values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
    });
  } catch (_error) {
    silentAudioFrame();
  }
}

function applyMediaData(data) {
  const previousTitle = state.media.title;
  const incomingTitle = String(data.title || "").trim();
  const incomingStatus = String(data.playbackStatus || "");
  const hasSession = incomingTitle && incomingTitle !== "Bravent Music - G" && incomingStatus.toLowerCase() !== "nosession";
  state.media.title = incomingTitle;
  state.media.artist = String(data.artist || "").trim();
  state.media.album = String(data.album || "").trim();
  state.media.playbackStatus = incomingStatus;
  state.media.positionMs = Number(data.positionMs || 0);
  state.media.endMs = Number(data.endMs || 180000);
  if (!hasSession) {
    state.media.thumbnailDataUrl = "";
    state.media.coverKeyDataUrl = "";
    state.media.visibleCoverDataUrl = "";
    state.media.covDataUrl = "";
    state.media.covKey = "";
    state.media.coverPngCache.clear();
    state.media.coverPngBusy.clear();
    state.media.coverLastDataUrl = "";
    state.media.updatedAt = Date.now();
    return;
  }
  state.media.thumbnailDataUrl = String(data.thumbnailDataUrl || state.media.thumbnailDataUrl || "").trim();
  state.media.coverKeyDataUrl = String(data.coverKeyDataUrl || state.media.coverKeyDataUrl || "").trim();
  state.media.visibleCoverDataUrl = String(data.thumbnailDataUrl || state.media.thumbnailDataUrl || data.coverKeyDataUrl || state.media.coverKeyDataUrl || state.media.visibleCoverDataUrl || "").trim();
  if (previousTitle && previousTitle !== state.media.title) {
    state.media.covDataUrl = "";
    state.media.covKey = "";
    state.media.coverPngCache.clear();
    state.media.coverPngBusy.clear();
  }
  state.media.updatedAt = Date.now();
}

function runPythonMediaState(done) {
  if (os.platform() !== "win32" || !fs.existsSync(MEDIA_PY_HELPER) || !fs.existsSync(MEDIA_PY_VENDOR)) {
    done(new Error("python media helper unavailable"));
    return;
  }
  const env = Object.assign({}, process.env);
  env.PYTHONPATH = MEDIA_PY_VENDOR + (env.PYTHONPATH ? path.delimiter + env.PYTHONPATH : "");
  execFile("python", [MEDIA_PY_HELPER, "state"], { cwd: path.resolve(__dirname, ".."), env, windowsHide: true, timeout: 5200 }, (error, stdout) => {
    if (error) return done(error);
    try {
      const data = JSON.parse(String(stdout || "{}").replace(/^\uFEFF/, ""));
      if (!data || data.ok === false) return done(new Error(data && data.error || "python media helper failed"));
      done(null, data);
    } catch (parseError) {
      done(parseError);
    }
  });
}

function runPowerShellMediaState(done) {
  if (os.platform() !== "win32" || !fs.existsSync(MEDIA_HELPER)) {
    done(new Error("powershell media helper unavailable"));
    return;
  }
  execFile("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", MEDIA_HELPER, "-Command", "state"], { windowsHide: true, timeout: 4200 }, (error, stdout) => {
    if (error) return done(error);
    try {
      done(null, JSON.parse(String(stdout || "{}").replace(/^\uFEFF/, "")));
    } catch (parseError) {
      done(parseError);
    }
  });
}

function coverVariant(action) {
  const match = String(action || "").match(/\.cover\.test([1-5])$/);
  return match ? `test${match[1]}` : "";
}

function requestCovComposite(settings, action) {
  const coverDataUrl = String(state.media.coverKeyDataUrl || state.media.thumbnailDataUrl || "").trim();
  if (!coverDataUrl || state.media.covBusy || os.platform() !== "win32" || !fs.existsSync(MEDIA_PY_HELPER)) return;
  const variant = coverVariant(action);
  const coverSize = variant === "test2" ? 116 : variant === "test3" ? 96 : variant === "test4" ? 84 : 104;
  const key = [variant || "live", coverDataUrl.slice(0, 90), state.tick, Math.round((state.audio.level || 0) * 100), settings.coverWaveColor, settings.coverSensitivity, coverSize].join("|");
  if (state.media.covKey === key && state.media.covDataUrl) return;
  if (Date.now() - state.media.covLastAt < 90) return;
  state.media.covBusy = true;
  state.media.covLastAt = Date.now();
  const env = Object.assign({}, process.env);
  env.PYTHONPATH = MEDIA_PY_VENDOR + (env.PYTHONPATH ? path.delimiter + env.PYTHONPATH : "");
  const child = spawn("python", [MEDIA_PY_HELPER, "cov"], { cwd: path.resolve(__dirname, ".."), env, windowsHide: true });
  let stdout = "";
  child.stdout.on("data", (chunk) => { stdout += String(chunk); });
  child.on("close", () => {
    state.media.covBusy = false;
    try {
      const data = JSON.parse(String(stdout || "{}").replace(/^\uFEFF/, ""));
      if (data && data.ok && data.covDataUrl) {
        state.media.covDataUrl = data.covDataUrl;
        state.media.covKey = key;
        renderAll();
      }
    } catch (_error) {}
  });
  child.stdin.end(JSON.stringify({
    coverDataUrl,
    level: state.audio.active ? responsiveAudioLevel(state.audio.level || 0, { sensitivity: settings.coverSensitivity || 5 }) : 0,
    tick: state.tick,
    color: settings.coverWaveColor || "#ff4757",
    sensitivity: settings.coverSensitivity || 5,
    coverSize
  }));
}

function coverRenderKey(settings, coverDataUrl, title, artist, variant, scrollPhase) {
  return crypto.createHash("sha1").update(JSON.stringify({
    coverDataUrl,
    title,
    artist,
    variant,
    coverTextVisible: settings.coverTextVisible,
    coverSongVisible: settings.coverSongVisible,
    coverArtistVisible: settings.coverArtistVisible,
    coverBarsVisible: settings.coverBarsVisible,
    coverTextMode: settings.coverTextMode,
    coverFontSize: settings.coverFontSize,
    coverDisplayMode: settings.coverDisplayMode,
    coverWalkingText: settings.coverWalkingText,
    scrollPhase
  })).digest("hex");
}

function requestCoverRender(settings, action, coverDataUrl, title, artist, variant) {
  if (!coverDataUrl || os.platform() !== "win32" || !fs.existsSync(MEDIA_PY_HELPER)) return "";
  const visualSettings = coverVisualSettings(settings);
  const scrollPhase = visualSettings.coverTextMode === "scroll" ? (Math.floor(state.tick / 12) % 16) / 15 : 0;
  const key = coverRenderKey(visualSettings, coverDataUrl, title, artist, variant, scrollPhase);
  if (state.media.coverPngCache.has(key)) {
    state.media.coverLastDataUrl = state.media.coverPngCache.get(key);
    return state.media.coverLastDataUrl;
  }
  if (state.media.coverPngBusy.has(key)) return state.media.coverLastDataUrl || "";
  state.media.coverPngBusy.add(key);
  const env = Object.assign({}, process.env);
  env.PYTHONPATH = MEDIA_PY_VENDOR + (env.PYTHONPATH ? path.delimiter + env.PYTHONPATH : "");
  const child = spawn("python", [MEDIA_PY_HELPER, "cover"], { cwd: path.resolve(__dirname, ".."), env, windowsHide: true });
  let stdout = "";
  child.stdout.on("data", (chunk) => { stdout += String(chunk); });
  child.on("close", () => {
    state.media.coverPngBusy.delete(key);
    try {
      const data = JSON.parse(String(stdout || "{}").replace(/^\uFEFF/, ""));
      if (data && data.ok && data.coverDataUrl) {
        state.media.coverPngCache.set(key, data.coverDataUrl);
        state.media.coverLastDataUrl = data.coverDataUrl;
        if (state.media.coverPngCache.size > 120) {
          const first = state.media.coverPngCache.keys().next().value;
          if (first) state.media.coverPngCache.delete(first);
        }
        renderAll();
      }
    } catch (_error) {}
  });
  child.stdin.end(JSON.stringify({
    coverDataUrl,
    title,
    artist,
    variant,
    coverTextVisible: visualSettings.coverTextVisible,
    coverSongVisible: visualSettings.coverSongVisible,
    coverArtistVisible: visualSettings.coverArtistVisible,
    coverBarsVisible: visualSettings.coverBarsVisible,
    coverTextMode: visualSettings.coverTextMode,
    coverFontSize: visualSettings.coverFontSize,
    coverDisplayMode: visualSettings.coverDisplayMode,
    coverWalkingText: visualSettings.coverWalkingText,
    scrollPhase
  }));
  return "";
}

function pollMediaTitle() {
  if (state.media.pollBusy || os.platform() !== "win32") return;
  const now = Date.now();
  const cooldown = state.media.lastPollFailAt && now - state.media.lastPollFailAt < MEDIA_FAILURE_COOLDOWN_MS
    ? MEDIA_FAILURE_COOLDOWN_MS
    : MEDIA_POLL_MS;
  if (now - state.media.lastPollAt < cooldown) return;
  state.media.lastPollAt = now;
  state.media.pollBusy = true;
  runPythonMediaState((pythonError, pythonData) => {
    if (!pythonError && pythonData && pythonData.ok) {
      applyMediaData(pythonData);
      state.media.lastPollFailAt = 0;
      state.media.pollBusy = false;
      return;
    }
    state.media.lastPollFailAt = Date.now();
    state.media.pollBusy = false;
  });
}

function esc(value) {
  return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" }[char]));
}

function shortText(value, max) {
  const text = String(value || "").trim();
  return text.length > max ? text.slice(0, Math.max(1, max - 1)) + "..." : text;
}

function normalizeMode(mode) {
  const value = String(mode || "bars");
  if (value === "ribbon") return "wave";
  if (value === "classic") return "bars";
  return MODES.includes(value) ? value : "bars";
}

function normalizeKeyMode(mode) {
  return KEY_MODES.includes(String(mode || "")) ? String(mode) : "splitBars";
}

function normalizeResponseRate(value) {
  const ms = Number(value);
  return RESPONSE_RATES.includes(ms) ? ms : 240;
}

function normalizeCoverTextMode(value) {
  return String(value || "still") === "scroll" ? "scroll" : "still";
}

function startRhythmLoop(ms) {
  const nextMs = normalizeResponseRate(ms);
  if (state.timer && state.responseRateMs === nextMs) return;
  if (state.timer) clearInterval(state.timer);
  state.responseRateMs = nextMs;
  state.timer = setInterval(() => {
    state.tick += 1;
    updateAudioFrame();
    pollMediaTitle();
    renderAll();
  }, nextMs);
}

function touchPanelHeader() {
  const title = String(state.media.title || "").trim();
  const artist = String(state.media.artist || "").trim();
  const status = String(state.media.playbackStatus || "").toLowerCase();
  if (state.audio.active && title && title !== "Bravent Music - G" && status !== "nosession") {
    return { title: shortText(title, 28), artist: shortText(artist, 30) };
  }
  return { title: "Rhythm Basic", artist: "" };
}

function boolSetting(value, fallback) {
  if (value === true || value === "true" || value === 1 || value === "1") return true;
  if (value === false || value === "false" || value === 0 || value === "0") return false;
  return fallback;
}

function mediaProgressRatio() {
  const end = Math.max(1, Number(state.media.endMs || 180000));
  let position = Math.max(0, Number(state.media.positionMs || 0));
  if (String(state.media.playbackStatus || "").toLowerCase() === "playing" && state.media.updatedAt) {
    position += Math.max(0, Date.now() - state.media.updatedAt);
  }
  return clamp(position / end, 0, 1);
}

function deckIconDataUrl() {
  const iconPath = path.join(__dirname, "..", "images", "music-g-state.png");
  try {
    return "data:image/png;base64," + fs.readFileSync(iconPath).toString("base64");
  } catch (_err) {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144"><rect width="144" height="144" rx="18" fill="#10182d"/><path d="M20 92 C42 75 63 106 88 77 C105 58 123 63 144 72" fill="none" stroke="#58a9ff" stroke-width="6" stroke-linecap="round"/><text x="72" y="126" text-anchor="middle" font-family="${FONT_STACK}" font-size="14" font-weight="900" fill="#fddab0">Rhythm Basic</text></svg>`);
  }
}

function metadataText(value, id, x, y, width, fontSize, fill, weight, charWidth, mode) {
  const text = String(value || "").trim();
  const approxWidth = Math.max(20, text.length * charWidth);
  if (mode !== "scroll") {
    return `<text x="${x + width / 2}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" text-anchor="middle" fill="${fill}">${esc(shortText(text, Math.max(8, Math.floor(width / charWidth))))}</text>`;
  }
  const gap = Math.max(34, Math.round(width * .18));
  const loop = approxWidth + gap;
  const drift = (state.tick * 9) % loop;
  const x1 = x - drift;
  const x2 = x1 + approxWidth + gap;
  return `<clipPath id="${id}Clip"><rect x="${x}" y="${y - fontSize - 4}" width="${width}" height="${fontSize + 10}"/></clipPath>
  <g clip-path="url(#${id}Clip)">
    <text x="${x1}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${esc(text)}</text>
    <text x="${x2}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${esc(text)}</text>
  </g>`;
}

function displayCharWidth(value, fallback) {
  return /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff\uac00-\ud7af]/.test(String(value || "")) ? fallback + 2.8 : fallback;
}

function metadataTextRight(value, id, x, y, width, fontSize, fill, weight, charWidth, mode) {
  const text = String(value || "").trim();
  const safeCharWidth = displayCharWidth(text, charWidth);
  const approxWidth = Math.max(20, text.length * safeCharWidth);
  if (mode !== "scroll") {
    return `<text x="${x + width / 2}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" text-anchor="middle" fill="${fill}">${esc(shortText(text, Math.max(6, Math.floor(width / safeCharWidth))))}</text>`;
  }
  const gap = Math.max(40, Math.round(width * .24));
  const loop = approxWidth + gap;
  const drift = (state.tick * 3) % loop;
  const x1 = x + width - approxWidth + drift;
  const x2 = x1 - approxWidth - gap;
  return `<clipPath id="${id}Clip"><rect x="${x}" y="${y - fontSize - 4}" width="${width}" height="${fontSize + 10}"/></clipPath>
  <g clip-path="url(#${id}Clip)">
    <text x="${x1}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${esc(text)}</text>
    <text x="${x2}" y="${y}" font-family="${FONT_STACK}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${esc(text)}</text>
  </g>`;
}

function normalizeCoverMode(mode) {
  return COVER_MODES.includes(String(mode || "")) ? String(mode) : "pulse";
}

function normalizeCoverDisplayMode(value) {
  const mode = String(value || "custom");
  return ["custom", "barsText", "cleanText", "songOnly", "artistOnly", "coverOnly"].includes(mode) ? mode : "custom";
}

function coverVisualSettings(settings) {
  const mergedSettings = Object.assign({}, settings || {});
  const coverDisplayMode = "barsText";
  let coverTextVisible = boolSetting(mergedSettings.coverTextVisible, state.coverTextVisible);
  let coverSongVisible = boolSetting(mergedSettings.coverSongVisible, state.coverSongVisible);
  let coverArtistVisible = boolSetting(mergedSettings.coverArtistVisible, state.coverArtistVisible);
  let coverBarsVisible = boolSetting(mergedSettings.coverBarsVisible, state.coverBarsVisible);
  let coverWalkingText = boolSetting(mergedSettings.coverWalkingText, state.coverWalkingText);
  if (coverDisplayMode === "barsText") {
    coverTextVisible = true;
    coverSongVisible = true;
    coverArtistVisible = true;
    coverBarsVisible = true;
  } else if (coverDisplayMode === "cleanText") {
    coverTextVisible = true;
    coverSongVisible = true;
    coverArtistVisible = true;
    coverBarsVisible = false;
  } else if (coverDisplayMode === "songOnly") {
    coverTextVisible = true;
    coverSongVisible = true;
    coverArtistVisible = false;
    coverBarsVisible = false;
  } else if (coverDisplayMode === "artistOnly") {
    coverTextVisible = true;
    coverSongVisible = false;
    coverArtistVisible = true;
    coverBarsVisible = false;
  } else if (coverDisplayMode === "coverOnly") {
    coverTextVisible = false;
    coverSongVisible = false;
    coverArtistVisible = false;
    coverBarsVisible = false;
    coverWalkingText = false;
  }
  if (!coverSongVisible && !coverArtistVisible) coverTextVisible = false;
  return {
    coverTextVisible,
    coverSongVisible,
    coverArtistVisible,
    coverTextMode: coverWalkingText ? "scroll" : "still",
    coverFontSize: "large",
    coverDisplayMode,
    coverWalkingText,
    coverWaveColor: mergedSettings.coverWaveColor || state.coverWaveColor || "#ff4757",
    coverSensitivity: clamp(Number(mergedSettings.coverSensitivity || state.coverSensitivity || 5), 1, 10),
    coverResponseRateMs: normalizeResponseRate(mergedSettings.coverResponseRateMs || state.coverResponseRateMs || 240),
    coverPressAction: String(mergedSettings.coverPressAction || state.coverPressAction || "changeRhythm"),
    coverHoldAction: String(mergedSettings.coverHoldAction || state.coverHoldAction || "none"),
    coverBarsVisible,
    coverMode: normalizeCoverMode(mergedSettings.coverMode || state.coverMode)
  };
}

function coverWaveSvg(settings) {
  const level = state.audio.active ? responsiveAudioLevel(state.audio.level || 0, { sensitivity: settings.coverSensitivity || 5 }) : 0;
  const color = settings.coverWaveColor || "#ff4757";
  const pulse = (state.tick % 18) / 18;
  const rings = [0, 1, 2].map((item) => {
    const phase = (pulse + item / 3) % 1;
    const size = 84 + phase * 66 + level * 26;
    const x = 72 - size / 2;
    const opacity = state.audio.active ? (.34 + level * .46) * (1 - phase) : 0;
    const stroke = 2.5 + level * 5;
    return `<rect x="${x}" y="${x}" width="${size}" height="${size}" rx="${18 + level * 10}" fill="none" stroke="${color}" stroke-width="${stroke}" opacity="${opacity}"/>`;
  }).join("");
  const blocks = Array.from({ length: 16 }, (_item, index) => {
    const value = state.audio.active ? responsiveAudioLevel((state.audio.bars || [])[Math.floor(index / 16 * (state.audio.bars.length || 1))] || 0, { sensitivity: settings.coverSensitivity || 5 }) : 0;
    const size = 2 + value * 8;
    const angle = index / 16 * Math.PI * 2 + state.tick * .06;
    const radius = 54 + value * 22;
    const x = 72 + Math.cos(angle) * radius - size / 2;
    const y = 72 + Math.sin(angle) * radius - size / 2;
    return `<rect x="${x}" y="${y}" width="${size}" height="${size}" rx="2" fill="${spectrumColor(index / 15, { auraColor: color })}" opacity="${state.audio.active ? .22 + value * .58 : 0}"/>`;
  }).join("");
  return `<g>${rings}${blocks}</g>`;
}

function miniCovSvg(visualSettings, coverDataUrl, mode) {
  const safeCover = esc(coverDataUrl || "");
  const base = `<rect width="144" height="144" rx="18" fill="#10182d"/>${coverWaveSvg(visualSettings)}`;
  const clip = `<clipPath id="miniCovClip"><rect x="24" y="24" width="96" height="96" rx="16"/></clipPath>`;
  if (!safeCover || mode === "label") {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">${base}<rect x="24" y="24" width="96" height="96" rx="16" fill="#111827" opacity=".86"/><text x="72" y="76" text-anchor="middle" font-family="${FONT_STACK}" font-size="15" font-weight="900" fill="#fddab0">COV</text></svg>`);
  }
  if (mode === "xlink") {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="144" height="144" viewBox="0 0 144 144">${base}${clip}<image x="24" y="24" width="96" height="96" xlink:href="${safeCover}" clip-path="url(#miniCovClip)" preserveAspectRatio="xMidYMid slice"/></svg>`);
  }
  if (mode === "fullXlink") {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="144" height="144" viewBox="0 0 144 144"><clipPath id="fullCovClip"><rect width="144" height="144" rx="18"/></clipPath><g clip-path="url(#fullCovClip)"><image width="144" height="144" xlink:href="${safeCover}" preserveAspectRatio="xMidYMid slice"/><rect width="144" height="144" fill="#000000" opacity=".18"/>${coverWaveSvg(visualSettings)}</g></svg>`);
  }
  if (mode === "pattern") {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144"><defs><pattern id="covPattern" patternUnits="objectBoundingBox" width="1" height="1"><image width="96" height="96" href="${safeCover}" preserveAspectRatio="xMidYMid slice"/></pattern></defs>${base}<rect x="24" y="24" width="96" height="96" rx="16" fill="url(#covPattern)"/></svg>`);
  }
  if (mode === "foreign") {
    return svgData(`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">${base}<foreignObject x="24" y="24" width="96" height="96"><img xmlns="http://www.w3.org/1999/xhtml" src="${safeCover}" style="width:96px;height:96px;object-fit:cover;border-radius:16px;"/></foreignObject></svg>`);
  }
  return svgData(`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">${base}${clip}<image x="24" y="24" width="96" height="96" href="${safeCover}" clip-path="url(#miniCovClip)" preserveAspectRatio="xMidYMid slice"/></svg>`);
}

function coverKeySvg(settings, knobMode, action) {
  const visualSettings = coverVisualSettings(settings);
  const accent = settings.auraAccent || "#fddab0";
  const status = String(state.media.playbackStatus || "").toLowerCase();
  const rawTitle = String(state.media.title || "").trim();
  const rawArtist = String(state.media.artist || "").trim();
  const hasSession = rawTitle && rawTitle !== "Bravent Music - G" && status !== "nosession";
  const title = hasSession ? rawTitle : (knobMode ? "Cover Knob" : "Live Cover");
  const artist = hasSession ? (rawArtist || (knobMode ? "Cover Knob" : "Live Cover")) : "Waiting for music";
  const thumb = String(state.media.thumbnailDataUrl || "").trim();
  const coverKey = String(state.media.coverKeyDataUrl || "").trim();
  const coverImage = hasSession ? (thumb || String(state.media.visibleCoverDataUrl || "").trim() || coverKey) : "";
  const renderedPng = requestCoverRender(visualSettings, action, coverImage, title, artist, coverVariant(action) || "v1a");
  if (renderedPng) return renderedPng;
  if (hasSession && state.media.coverLastDataUrl) return state.media.coverLastDataUrl;
  if (hasSession && coverKey) return coverKey;
  const barsVisible = visualSettings.coverBarsVisible !== false;
  const titleSize = visualSettings.coverFontSize === "large" ? 14 : 13;
  const artistSize = visualSettings.coverFontSize === "large" ? 13 : 12;
  const showMetadata = hasSession && visualSettings.coverTextVisible;
  const topBar = showMetadata && visualSettings.coverSongVisible;
  const bottomBar = showMetadata && visualSettings.coverArtistVisible;
  const waveLayer = "";
  const text = showMetadata
    ? `${barsVisible && topBar ? `<rect x="0" y="0" width="144" height="38" fill="#000000" opacity=".58"/>` : ""}
  ${barsVisible && bottomBar ? `<rect x="0" y="106" width="144" height="38" fill="#000000" opacity=".70"/>` : ""}
  ${topBar ? metadataTextRight(title, "coverTitle", 8, 25, 128, titleSize, "#f8f2e8", 900, 7.4, visualSettings.coverTextMode) : ""}
  ${bottomBar ? metadataTextRight(artist, "coverArtist", 8, 131, 128, artistSize, accent, 900, 8.2, visualSettings.coverTextMode) : ""}`
    : "";
  const coverLayer = coverImage
    ? `<image width="144" height="144" href="${esc(coverImage)}" xlink:href="${esc(coverImage)}" preserveAspectRatio="xMidYMid slice"/>`
    : `<rect width="144" height="144" fill="#10182d"/>
    <circle cx="72" cy="70" r="39" fill="none" stroke="${visualSettings.coverWaveColor}" stroke-width="7" opacity=".76"/>
    <path d="M55 49 L55 92 L92 70 Z" fill="#fddab0" opacity=".92"/>
    <path d="M24 102 C42 88 56 115 75 94 C91 76 108 84 124 73" fill="none" stroke="#58a9ff" stroke-width="5" stroke-linecap="round" opacity=".82"/>`;
  return svgData(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="144" height="144" viewBox="0 0 144 144">
  ${coverImage ? "" : `<rect width="144" height="144" fill="#10182d"/>`}
  <clipPath id="coverClip"><rect width="144" height="144" rx="18"/></clipPath>
  <g clip-path="url(#coverClip)">
    <rect width="144" height="144" fill="#10182d"/>
    ${coverLayer}
    <rect width="144" height="144" fill="#000000" opacity=".10"/>
    ${waveLayer}
    ${text}
  </g>
</svg>`);
}

function svgData(svg) {
  return "data:image/svg+xml;base64," + Buffer.from(svg, "utf8").toString("base64");
}

function hexToRgb(hex) {
  const clean = String(hex || "#ffffff").replace("#", "");
  const full = clean.length === 3 ? clean.split("").map((x) => x + x).join("") : clean.padEnd(6, "f").slice(0, 6);
  return {
    r: parseInt(full.slice(0, 2), 16),
    g: parseInt(full.slice(2, 4), 16),
    b: parseInt(full.slice(4, 6), 16)
  };
}

function rgbToHex(r, g, b) {
  return "#" + [r, g, b].map((v) => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, "0")).join("");
}

function mixColor(left, right, ratio) {
  const a = hexToRgb(left);
  const b = hexToRgb(right);
  const t = clamp(ratio, 0, 1);
  return rgbToHex(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t);
}

function rgbToHsl(r, g, b) {
  const rn = r / 255;
  const gn = g / 255;
  const bn = b / 255;
  const max = Math.max(rn, gn, bn);
  const min = Math.min(rn, gn, bn);
  let h = 0;
  let s = 0;
  const l = (max + min) / 2;
  if (max !== min) {
    const d = max - min;
    s = l > .5 ? d / (2 - max - min) : d / (max + min);
    if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);
    else if (max === gn) h = (bn - rn) / d + 2;
    else h = (rn - gn) / d + 4;
    h /= 6;
  }
  return { h, s, l };
}

function hslToHex(h, s, l) {
  const hue2rgb = (p, q, t) => {
    let tn = t;
    if (tn < 0) tn += 1;
    if (tn > 1) tn -= 1;
    if (tn < 1 / 6) return p + (q - p) * 6 * tn;
    if (tn < 1 / 2) return q;
    if (tn < 2 / 3) return p + (q - p) * (2 / 3 - tn) * 6;
    return p;
  };
  let r;
  let g;
  let b;
  if (s === 0) {
    r = l;
    g = l;
    b = l;
  } else {
    const q = l < .5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }
  return rgbToHex(r * 255, g * 255, b * 255);
}

function hueShift(base, degrees, satBoost = .1, lightBoost = 0) {
  const rgb = hexToRgb(base);
  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
  const h = (hsl.h + degrees / 360 + 1) % 1;
  const s = clamp(hsl.s + satBoost, .42, .96);
  const l = clamp(hsl.l + lightBoost, .34, .76);
  return hslToHex(h, s, l);
}

function spectrumColor(position, settings) {
  const base = settings.auraColor || "#ff4757";
  const colors = [
    hueShift(base, -34, .18, .03),
    hueShift(base, -8, .2, .02),
    hueShift(base, 24, .16, .04),
    hueShift(base, 60, .12, .02),
    hueShift(base, 112, .18, .05),
    hueShift(base, 172, .14, .02),
    hueShift(base, 224, .2, .04)
  ];
  const p = clamp(position, 0, 1) * (colors.length - 1);
  const index = Math.min(colors.length - 2, Math.floor(p));
  return mixColor(colors[index], colors[index + 1], p - index);
}

function responsiveAudioLevel(value, settings) {
  const sensitivity = clamp(Number(settings.sensitivity || 5), 1, 10);
  const gain = .28 + Math.pow(sensitivity / 10, 1.55) * .95;
  const curve = clamp(.88 - sensitivity * .025, .56, .86);
  return clamp(Math.pow(clamp(Number(value) || 0, 0, 1) * gain, curve) * .94, 0, 1);
}

function touchSlotSvg(slot, settings, power, audioBars, audioActive) {
  const safeSlot = clamp(slot, 1, TOUCH_SLOTS);
  const viewX = (safeSlot - 1) * TOUCH_SLOT_WIDTH;
  const accent = spectrumColor(.36, settings);
  const rawMode = normalizeMode(settings.auraMode);
  const header = touchPanelHeader();
  const count = TOUCH_BAR_COUNT;
  const barWidth = TOUCH_WIDTH / count;
  const speedBase = 5.2;
  const bottom = TOUCH_HEIGHT - 5;
  let visual = "";

  if (rawMode === "pulse") {
    const dotCount = 48;
    const rawLevels = Array.from({ length: dotCount }, (_item, index) => {
      return audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / dotCount * audioBars.length)], settings) : 0;
    });
    const minLevel = Math.min(...rawLevels);
    const maxLevel = Math.max(...rawLevels);
    visual = rawLevels.map((raw, index) => {
      const spread = maxLevel - minLevel > .08 ? (raw - minLevel) / (maxLevel - minLevel) : raw;
      const level = clamp(raw * .5 + spread * .58, 0, 1);
      const x = Math.round(index * (TOUCH_WIDTH / dotCount) + 6);
      const height = Math.round(8 + level * 48);
      const y = bottom - height;
      const r = Math.max(2.4, 2.7 + level * 4.4);
      const color = spectrumColor(index / Math.max(1, dotCount - 1), settings);
      return `<line x1="${x}" y1="${bottom - 2}" x2="${x}" y2="${y}" stroke="${color}" stroke-width="${2.2 + level * 2.2}" stroke-linecap="round" opacity="${.18 + level * .48}"/><circle cx="${x}" cy="${y}" r="${r}" fill="${color}" opacity="${.42 + level * .54}"/>`;
    }).join("");
  } else if (rawMode === "wave") {
    const level = audioActive ? state.audio.level : 0;
    visual = [0, 1, 2].map((row) => {
      const yBase = bottom - 6 - row * 9;
      const points = Array.from({ length: 18 }, (_item, index) => {
        const x = Math.round(index * (TOUCH_WIDTH / 17));
        const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 17 * (audioBars.length - 1))] || level, settings) : 0;
        const y = Math.round(yBase - audio * (19 + row * 5) * Math.max(.22, power));
        return `${index === 0 ? "M" : "L"}${x} ${y}`;
      }).join(" ");
      return `<path d="${points}" fill="none" stroke="${spectrumColor(.18 + row * .28, settings)}" stroke-width="${2.2 + power * 2.2}" stroke-linecap="round" stroke-linejoin="round" opacity="${.24 + (audioActive ? power * (.42 + row * .14) : 0)}"/>`;
    }).join("");
  } else if (rawMode === "blocks") {
    const segmentLevels = state.audio.segmentLevels || [];
    const segmentWidth = TOUCH_WIDTH / TOUCH_SEGMENTS;
    visual = Array.from({ length: TOUCH_SEGMENTS }, (_item, segmentIndex) => {
      const segmentLevel = audioActive ? responsiveAudioLevel(segmentLevels[segmentIndex], settings) : 0;
      return Array.from({ length: 5 }, (_block, blockIndex) => {
        const on = segmentLevel > blockIndex / 6;
        const x = Math.round(segmentIndex * segmentWidth + 5);
        const width = Math.max(8, Math.round(segmentWidth - 10));
        const height = 4 + Math.round(segmentLevel * 8);
        const y = bottom - 5 - blockIndex * 7;
        const color = spectrumColor(segmentIndex / Math.max(1, TOUCH_SEGMENTS - 1), settings);
        return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="3" fill="${color}" opacity="${on ? .28 + segmentLevel * .6 : .07}"/>`;
      }).join("");
    }).join("");
  } else if (rawMode === "comet") {
    visual = Array.from({ length: 36 }, (_item, index) => {
      const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 36 * audioBars.length)], settings) : 0;
      const x = Math.round(index * (TOUCH_WIDTH / 36) + 8);
      const y = bottom - Math.round(5 + audio * 45 * Math.max(.22, power));
      const color = spectrumColor(index / 35, settings);
      return `<line x1="${x}" y1="${bottom}" x2="${x}" y2="${y + 4}" stroke="${color}" stroke-width="2" opacity="${.08 + audio * .34}"/><circle cx="${x}" cy="${y}" r="${2 + audio * 4.5 * power}" fill="${color}" opacity="${.22 + audio * .68 * power}"/>`;
    }).join("");
  } else if (rawMode === "arc") {
    visual = Array.from({ length: 30 }, (_item, index) => {
      const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 30 * audioBars.length)], settings) : 0;
      const x = Math.round(index * (TOUCH_WIDTH / 30) + 8);
      const height = 10 + Math.round(audio * 34);
      const y = bottom - height;
      const color = spectrumColor(index / 29, settings);
      return `<path d="M${x - 5} ${bottom - 1} Q${x} ${y - 5} ${x + 5} ${bottom - 1}" fill="none" stroke="${color}" stroke-width="${2.2 + audio * 4}" stroke-linecap="round" opacity="${.2 + audio * .68}"/>`;
    }).join("");
  } else if (rawMode === "sparkline") {
    const lines = [0, 1, 2].map((row) => {
      const points = Array.from({ length: 34 }, (_item, index) => {
        const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 34 * audioBars.length)], settings) : 0;
        const x = Math.round(index * (TOUCH_WIDTH / 33));
        const y = Math.round(bottom - 7 - row * 10 - audio * (16 + row * 5));
        return `${index === 0 ? "M" : "L"}${x} ${y}`;
      }).join(" ");
      return `<path d="${points}" fill="none" stroke="${spectrumColor(.12 + row * .34, settings)}" stroke-width="${1.8 + row * .7}" stroke-linecap="round" stroke-linejoin="round" opacity="${.28 + power * .42}"/>`;
    }).join("");
    const dots = Array.from({ length: 14 }, (_item, index) => {
      const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 14 * audioBars.length)], settings) : 0;
      const x = Math.round(index * (TOUCH_WIDTH / 14) + 12);
      const y = Math.round(bottom - 11 - audio * 39);
      const color = spectrumColor(index / 13, settings);
      return `<circle cx="${x}" cy="${y}" r="${1.8 + audio * 3.2}" fill="${color}" opacity="${.25 + audio * .62}"/>`;
    }).join("");
    visual = lines + dots;
  } else if (rawMode === "prism") {
    visual = Array.from({ length: 24 }, (_item, index) => {
      const audio = audioActive ? responsiveAudioLevel(audioBars[Math.floor(index / 24 * audioBars.length)], settings) : 0;
      const x = Math.round(index * (TOUCH_WIDTH / 24) + 5);
      const width = Math.round(TOUCH_WIDTH / 24 - 5);
      const height = 7 + Math.round(audio * 45);
      const y = bottom - height;
      const color = spectrumColor(index / 23, settings);
      return `<path d="M${x} ${bottom} L${x + width / 2} ${y} L${x + width} ${bottom} Z" fill="${color}" opacity="${.14 + audio * .72}"/>`;
    }).join("");
  } else {
    visual = Array.from({ length: count }, (_item, index) => {
      const level = audioActive ? responsiveAudioLevel(audioBars[index], settings) : 0;
      const height = Math.round(3 + level * 47 * Math.max(.22, power));
      const x = Math.round(index * barWidth + 2);
      const y = bottom - height;
      const color = spectrumColor(index / Math.max(1, count - 1), settings);
      return `<rect x="${x}" y="${y}" width="${Math.max(3, Math.floor(barWidth - 4))}" height="${height}" rx="2" fill="${color}" opacity="${.22 + Math.min(.72, level * power)}"/>`;
    }).join("");
  }

  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${TOUCH_SLOT_WIDTH}" height="${TOUCH_HEIGHT}" viewBox="${viewX} 0 ${TOUCH_SLOT_WIDTH} ${TOUCH_HEIGHT}">
  <defs><filter id="touchGlow"><feGaussianBlur stdDeviation="${1.4 + power * 2}" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
  <rect x="0" y="0" width="${TOUCH_WIDTH}" height="${TOUCH_HEIGHT}" fill="#000000" opacity=".001"/>
  <g filter="url(#touchGlow)">${visual}</g>
  <text x="14" y="13" font-family="${FONT_STACK}" font-size="9.5" font-weight="800" fill="#f8f2e8" opacity=".95">${esc(header.title)}</text>
  ${header.artist ? `<text x="14" y="25" font-family="${FONT_STACK}" font-size="8" font-weight="650" fill="#fddab0" opacity=".86">${esc(header.artist)}</text>` : ""}
  <rect x="0" y="${bottom}" width="${TOUCH_WIDTH}" height="2" fill="${accent}" opacity="${.38 + power * .4}"/>
</svg>`;
  return svgData(svg);
}

function keyBarSvg(slot, settings, audioBars, audioActive) {
  const safeSlot = clamp(slot, 1, 5);
  const mode = normalizeKeyMode(settings.keyMode);
  const count = 16;
  const start = Math.floor((safeSlot - 1) * audioBars.length / 5);
  const end = Math.floor(safeSlot * audioBars.length / 5);
  const source = audioBars.slice(start, end);
  const bottom = 122;
  const slotGain = [0, .9, 1.34, .76, 1.34, .9][safeSlot] || 1;
  const values = Array.from({ length: count }, (_item, index) => {
    const sourceIndex = Math.floor(index / count * Math.max(1, source.length));
    const raw = audioActive ? responsiveAudioLevel(source[sourceIndex] || 0, settings) : 0;
    return clamp((raw * .88 + (state.audio.level || 0) * .08) * slotGain, 0, 1);
  });
  const bars = values.map((level, index) => {
    const height = Math.round(7 + level * 92);
    const x = 9 + index * 8;
    const y = bottom - height;
    const color = spectrumColor(((safeSlot - 1) * count + index) / Math.max(1, count * 5 - 1), settings);
    if (mode === "riseDots") return `<line x1="${x + 2}" y1="${bottom}" x2="${x + 2}" y2="${y}" stroke="${color}" stroke-width="2.2" stroke-linecap="round" opacity="${audioActive ? .2 + level * .5 : .08}"/><circle cx="${x + 2}" cy="${y}" r="${2 + level * 4.2}" fill="${color}" opacity="${audioActive ? .38 + level * .56 : .1}"/>`;
    if (mode === "beatSteps") return `<rect x="${x}" y="${bottom - Math.round((index % 4 + 1) * 8 + level * 52)}" width="5" height="${Math.round(5 + level * 24)}" rx="2" fill="${color}" opacity="${audioActive ? .24 + level * .62 : .08}"/>`;
    if (mode === "softComet") return `<circle cx="${x + 2}" cy="${bottom - 9 - level * 84}" r="${1.7 + level * 3.6}" fill="${color}" opacity="${audioActive ? .18 + level * .72 : .08}"/>`;
    if (mode === "centerBloom") {
      const center = 1 - Math.abs(index / Math.max(1, count - 1) - .5) * 2;
      return `<rect x="${x}" y="${bottom - Math.round(8 + level * 72 * (.55 + center * .45))}" width="5" height="${Math.round(8 + level * 72 * (.55 + center * .45))}" rx="3" fill="${color}" opacity="${audioActive ? .2 + level * .62 : .08}"/>`;
    }
    return `<rect x="${x}" y="${y}" width="5" height="${height}" rx="3" fill="${color}" opacity="${audioActive ? .28 + level * .68 : .08}"/>`;
  }).join("");
  const linePoints = values.map((level, index) => {
    const x = 11 + index * 8;
    const y = mode === "mirrorWave" ? Math.round(72 + Math.sin(index / 2) * 8 - level * 46) : Math.round(bottom - 5 - level * 96);
    return `${index === 0 ? "M" : "L"}${x} ${y}`;
  }).join(" ");
  const wave = mode === "mirrorWave" || mode === "softComet"
    ? `<path d="${linePoints}" fill="none" stroke="${settings.auraColor || "#58a9ff"}" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" opacity="${audioActive ? .62 : .12}"/>`
    : "";
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">
  <defs><filter id="keyGlow"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
  <rect width="144" height="144" fill="#000000" opacity=".001"/>
  <g filter="url(#keyGlow)">${bars}${wave}</g>
</svg>`;
  return svgData(svg);
}

function render(contextInfo) {
  const settings = merged(contextInfo.settings);
  if (isCoverSurfaceAction(contextInfo.action)) {
    setImage(contextInfo.context, coverKeySvg(settings, isCoverKnobAction(contextInfo.action), contextInfo.action));
    return;
  }
  if (STATIC_DECK_STATES_ONLY) {
    const staticImage = staticStateImageForAction(contextInfo.action);
    if (staticImage) setImage(contextInfo.context, staticImage);
    return;
  }
  const slug = actionSlug(contextInfo.action);
  const primary = settings.auraColor;
  const accent = settings.auraAccent;
  const brightness = clamp(settings.auraBrightness, 20, 100) / 100;
  const sensitivity = clamp(settings.sensitivity, 1, 10);
  const speed = clamp(settings.auraSpeed, 1, 10);
  const motion = Math.abs(Math.sin(state.tick / Math.max(2.4, 11 - speed)));
  const pulse = .18 + motion * (state.playing ? .82 : .62);
  const power = clamp((pulse * (sensitivity / 5)) * brightness, .04, 1);
  const mode = settings.auraMode || "pulse";
  const label = slug.replace(/-/g, " ").toUpperCase();
  let visual = "";

  const touchMatch = contextInfo.action.match(/\.touch\.slot([1-4])$/);
  if (touchMatch) {
    const audioActive = state.audio.active;
    const touchPower = audioActive ? clamp((.16 + state.audio.level * 1.05) * (.65 + sensitivity / 14), .05, .92) : .05;
    setImage(contextInfo.context, touchSlotSvg(Number(touchMatch[1]), settings, touchPower, state.audio.bars, audioActive));
    return;
  }

  const barMatch = slug.match(/^bar([1-5])$/);
  if (barMatch) {
    setImage(contextInfo.context, keyBarSvg(Number(barMatch[1]), Object.assign({}, settings, state.keyBarSettings, contextInfo.settings), state.audio.bars, state.audio.active));
    return;
  } else if (mode === "bars" || slug.includes("volume")) {
    for (let index = 0; index < 7; index += 1) {
      const height = 18 + Math.round(70 * Math.abs(Math.sin((state.tick + index * 1.7) / 4)) * power);
      visual += `<rect x="${20 + index * 15}" y="${112 - height}" width="9" height="${height}" rx="5" fill="${index % 2 ? accent : primary}" opacity="${.48 + power * .45}"/>`;
    }
  } else if (mode === "ribbon") {
    visual = `<path d="M10 90 C35 ${40 + 30 * pulse}, 54 ${126 - 40 * pulse}, 78 78 S116 ${34 + 55 * pulse}, 134 70" fill="none" stroke="${primary}" stroke-width="10" stroke-linecap="round" opacity="${.45 + power * .4}"/><path d="M16 102 C46 ${66 - 25 * pulse}, 72 ${110 - 48 * pulse}, 128 84" fill="none" stroke="${accent}" stroke-width="5" stroke-linecap="round" opacity=".82"/>`;
  } else {
    const radius = 24 + Math.round(28 * power);
    visual = `<circle cx="72" cy="72" r="${radius}" fill="none" stroke="${primary}" stroke-width="10" opacity="${.35 + power * .5}"/><circle cx="72" cy="72" r="${Math.max(10, radius - 18)}" fill="${accent}" opacity="${.22 + power * .2}"/>`;
  }

  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#14183d"/><stop offset=".58" stop-color="#202d64"/><stop offset="1" stop-color="#07111f"/></linearGradient><filter id="glow"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="144" height="144" rx="18" fill="url(#g)"/><g filter="url(#glow)">${visual}</g><text x="72" y="21" font-family="${FONT_STACK}" font-size="11" font-weight="800" text-anchor="middle" fill="rgba(255,255,255,.78)">${state.playing ? "PLAYING" : "READY"}</text><text x="72" y="132" font-family="${FONT_STACK}" font-size="12" font-weight="900" text-anchor="middle" fill="#fff8ef">${label.slice(0, 16)}</text></svg>`;
  setImage(contextInfo.context, svgData(svg));
}

function renderAll() {
  for (const contextInfo of state.contexts.values()) {
    if (contextInfo.visible) render(contextInfo);
  }
}

function isGTouchAction(action) {
  return /\.touch\.slot[1-4]$/.test(String(action || ""));
}

function isKeyBarAction(action) {
  return /\.keybar\.bar[1-5]$/.test(String(action || ""));
}

function isAuraSurfaceAction(action) {
  return isGTouchAction(action) || isKeyBarAction(action);
}

function isCoverAction(action) {
  const id = String(action || "");
  return id.includes(".cover.live") || id.includes(".cover.classic");
}

function isCoverKnobAction(action) {
  return String(action || "").includes(".cover.knob");
}

function isCoverSurfaceAction(action) {
  return isCoverAction(action) || isCoverKnobAction(action);
}

function onlyTouchPanelVisual(settings) {
  const mergedSettings = Object.assign({}, state.touchPanelSettings, settings || {});
  return {
    auraColor: mergedSettings.auraColor || "#ff4757",
    sensitivity: clamp(mergedSettings.sensitivity, 1, 10),
    auraMode: normalizeMode(mergedSettings.auraMode),
    responseRateMs: normalizeResponseRate(mergedSettings.responseRateMs)
  };
}

function onlyKeyBarVisual(settings) {
  const mergedSettings = Object.assign({}, state.keyBarSettings, settings || {});
  return {
    auraColor: mergedSettings.auraColor || "#ff4757",
    sensitivity: clamp(mergedSettings.sensitivity, 1, 10),
    keyMode: normalizeKeyMode(mergedSettings.keyMode),
    responseRateMs: normalizeResponseRate(mergedSettings.responseRateMs),
    keyHoldAction: String(mergedSettings.keyHoldAction || "none")
  };
}

function updateSettings(delta) {
  state.settings = Object.assign({}, state.settings, delta);
  for (const contextInfo of state.contexts.values()) contextInfo.settings = Object.assign({}, contextInfo.settings, delta);
  renderAll();
}

function updateGTouchSettings(delta, persist) {
  state.touchPanelSettings = onlyTouchPanelVisual(delta);
  state.touchPanelReady = true;
  state.settings = Object.assign({}, state.settings, state.touchPanelSettings);
  startRhythmLoop(state.touchPanelSettings.responseRateMs);
  for (const contextInfo of state.contexts.values()) {
    if (!isGTouchAction(contextInfo.action)) continue;
    contextInfo.settings = Object.assign({}, contextInfo.settings, state.touchPanelSettings);
    if (persist) setSettings(contextInfo.context, contextInfo.settings);
  }
  renderAll();
}

function updateKeyBarSettings(delta, persist) {
  state.keyBarSettings = onlyKeyBarVisual(delta);
  startRhythmLoop(state.keyBarSettings.responseRateMs);
  for (const contextInfo of state.contexts.values()) {
    if (!isKeyBarAction(contextInfo.action)) continue;
    contextInfo.settings = Object.assign({}, contextInfo.settings, state.keyBarSettings);
    if (persist) setSettings(contextInfo.context, contextInfo.settings);
  }
  renderAll();
}

function updateCoverSettings(delta, persist) {
  const next = coverVisualSettings(delta || {});
  state.coverTextVisible = next.coverTextVisible;
  state.coverTextMode = next.coverTextMode;
  state.coverSongVisible = next.coverSongVisible;
  state.coverArtistVisible = next.coverArtistVisible;
  state.coverFontSize = next.coverFontSize;
  state.coverDisplayMode = next.coverDisplayMode;
  state.coverWalkingText = next.coverWalkingText;
  state.coverWaveColor = next.coverWaveColor;
  state.coverSensitivity = next.coverSensitivity;
  state.coverResponseRateMs = next.coverResponseRateMs;
  state.coverPressAction = next.coverPressAction;
  state.coverHoldAction = next.coverHoldAction;
  state.coverBarsVisible = next.coverBarsVisible;
  state.coverMode = next.coverMode;
  startRhythmLoop(next.coverResponseRateMs);
  for (const contextInfo of state.contexts.values()) {
    if (!isCoverSurfaceAction(contextInfo.action)) continue;
    contextInfo.settings = Object.assign({}, contextInfo.settings, next);
    if (persist) setSettings(contextInfo.context, contextInfo.settings);
  }
  renderAll();
}

function toggleCoverText() {
  updateCoverSettings({ coverTextVisible: !state.coverTextVisible }, true);
}

function cycleCoverTextMode(step) {
  const modes = ["scroll", "still"];
  const current = modes.indexOf(normalizeCoverTextMode(state.coverTextMode));
  updateCoverSettings({ coverTextMode: modes[(current + step + modes.length) % modes.length] }, true);
}

function cycleCoverMode(step) {
  const index = COVER_MODES.indexOf(normalizeCoverMode(state.coverMode));
  updateCoverSettings({ coverMode: COVER_MODES[(index + step + COVER_MODES.length) % COVER_MODES.length] }, true);
}

function choosePalette(step) {
  const index = Math.abs(Math.round((Date.now() / 1000) + step)) % PALETTES.length;
  updateSettings({ auraColor: PALETTES[index][0], auraAccent: PALETTES[index][1] });
}

function cycleMode(step) {
  const index = MODES.indexOf(normalizeMode(state.settings.auraMode));
  updateSettings({ auraMode: MODES[(index + step + MODES.length) % MODES.length] });
}

function cycleTouchPanelMode(step) {
  const index = MODES.indexOf(normalizeMode(state.touchPanelSettings.auraMode));
  updateGTouchSettings({ auraMode: MODES[(index + step + MODES.length) % MODES.length] }, true);
}

function cycleKeyBarMode(step, contextInfo) {
  const currentMode = normalizeKeyMode((contextInfo && contextInfo.settings && contextInfo.settings.keyMode) || state.keyBarSettings.keyMode);
  const index = KEY_MODES.indexOf(currentMode);
  const keyMode = KEY_MODES[(index + step + KEY_MODES.length) % KEY_MODES.length];
  const next = Object.assign({}, state.keyBarSettings, { keyMode });
  if (contextInfo && isKeyBarAction(contextInfo.action)) {
    contextInfo.settings = Object.assign({}, contextInfo.settings, next);
    setSettings(contextInfo.context, contextInfo.settings);
  }
  updateKeyBarSettings(next, true);
}

function setTouchPanelAction(contextInfo, touchAction) {
  if (!contextInfo || !isGTouchAction(contextInfo.action)) return;
  contextInfo.settings = Object.assign({}, contextInfo.settings, { touchAction: touchAction || "changeRhythm" });
  setSettings(contextInfo.context, contextInfo.settings);
}

function setKeyHoldAction(contextInfo, holdAction) {
  if (!contextInfo || !isKeyBarAction(contextInfo.action)) return;
  contextInfo.settings = Object.assign({}, contextInfo.settings, { keyHoldAction: holdAction || "none" });
  setSettings(contextInfo.context, contextInfo.settings);
}

function actionCommand(action) {
  const slug = actionSlug(action);
  if (slug === "playpause") return "playpause";
  if (slug === "next") return "next";
  if (slug === "previous") return "previous";
  if (slug === "up") return "volumeup";
  if (slug === "down") return "volumedown";
  if (slug === "mute") return "mute";
  return "";
}

function handleTouchPanelPress(contextInfo) {
  const action = String(contextInfo.settings.touchAction || "changeRhythm");
  if (action === "changeRhythm") return cycleTouchPanelMode(1);
  if (action === "playpause") return media("playpause");
  if (action === "next") return media("next");
  if (action === "previous") return media("previous");
  if (action === "mute") return media("mute");
  if (action === "volumeup") return media("volumeup");
  if (action === "volumedown") return media("volumedown");
  return renderAll();
}

function runConfiguredAction(action) {
  if (action === "changeRhythm") return cycleCoverMode(1);
  if (action === "playpause") return media("playpause");
  if (action === "next") return media("next");
  if (action === "previous") return media("previous");
  if (action === "mute") return media("mute");
  if (action === "volumeup") return media("volumeup");
  if (action === "volumedown") return media("volumedown");
  return renderAll();
}

function coverHoldStart(contextInfo) {
  if (!contextInfo || !isCoverSurfaceAction(contextInfo.action)) return;
  if (contextInfo.holdTimer) clearTimeout(contextInfo.holdTimer);
  contextInfo.holdFired = false;
  contextInfo.holdTimer = setTimeout(() => {
    contextInfo.holdFired = true;
    runConfiguredAction(contextInfo.settings.coverHoldAction || state.coverHoldAction || "none");
  }, 520);
}

function coverHoldEnd(contextInfo) {
  if (!contextInfo || !isCoverSurfaceAction(contextInfo.action)) return false;
  if (contextInfo.holdTimer) clearTimeout(contextInfo.holdTimer);
  const fired = !!contextInfo.holdFired;
  contextInfo.holdTimer = null;
  contextInfo.holdFired = false;
  return fired;
}

function keyBarHoldStart(contextInfo) {
  if (!contextInfo || !isKeyBarAction(contextInfo.action)) return;
  if (contextInfo.holdTimer) clearTimeout(contextInfo.holdTimer);
  contextInfo.holdFired = false;
  contextInfo.holdTimer = setTimeout(() => {
    contextInfo.holdFired = true;
    runConfiguredAction(contextInfo.settings.keyHoldAction || state.keyBarSettings.keyHoldAction || "none");
  }, 520);
}

function keyBarHoldEnd(contextInfo) {
  if (!contextInfo || !isKeyBarAction(contextInfo.action)) return false;
  if (contextInfo.holdTimer) clearTimeout(contextInfo.holdTimer);
  const fired = !!contextInfo.holdFired;
  contextInfo.holdTimer = null;
  contextInfo.holdFired = false;
  return fired;
}

function press(contextInfo) {
  const action = contextInfo.action;
  const command = actionCommand(action);
  if (command) return media(command);
  if (isKeyBarAction(action)) return cycleKeyBarMode(1, contextInfo);
  if (action.includes("volume.knob")) return media("mute");
  if (action.includes("aura.display")) return media("playpause");
  if (action.includes("aura.color")) return choosePalette(1);
  if (action.includes("aura.sensitivity")) return updateSettings({ sensitivity: 5 });
  if (action.includes("aura.speed")) return updateSettings({ auraSpeed: 5 });
  if (action.includes("aura.brightness")) return updateSettings({ auraBrightness: 82 });
  if (action.includes("aura.mode")) return cycleMode(1);
  if (isCoverSurfaceAction(action)) return runConfiguredAction(contextInfo.settings.coverPressAction || state.coverPressAction || "changeRhythm");
  if (isGTouchAction(action)) return handleTouchPanelPress(contextInfo);
  if (action.includes("aura.reset")) return updateSettings(DEFAULTS);
  return renderAll();
}

function rotate(contextInfo, ticks) {
  const amount = Number(ticks) || 0;
  if (contextInfo.action.includes("volume.knob")) return media(amount > 0 ? "volumeup" : "volumedown");
  if (contextInfo.action.includes("aura.color")) return choosePalette(amount);
  if (contextInfo.action.includes("aura.sensitivity")) return updateSettings({ sensitivity: clamp((state.settings.sensitivity || 5) + amount, 1, 10) });
  if (contextInfo.action.includes("aura.speed")) return updateSettings({ auraSpeed: clamp((state.settings.auraSpeed || 5) + amount, 1, 10) });
  if (contextInfo.action.includes("aura.brightness")) return updateSettings({ auraBrightness: clamp((state.settings.auraBrightness || 82) + amount * 4, 20, 100) });
  if (contextInfo.action.includes("aura.mode")) return cycleMode(amount > 0 ? 1 : -1);
  if (isCoverKnobAction(contextInfo.action)) return cycleCoverTextMode(amount > 0 ? 1 : -1);
  return renderAll();
}

function parseTicks(payload) {
  if (!payload) return 0;
  if (Number.isFinite(Number(payload.ticks))) return Number(payload.ticks);
  if (Number.isFinite(Number(payload.delta))) return Number(payload.delta);
  if (Number.isFinite(Number(payload.value))) return Number(payload.value);
  return 0;
}

function handleEvent(message) {
  let contextInfo;
  switch (message.event) {
    case "willAppear":
      contextInfo = getContext(message.context);
      contextInfo.visible = true;
      contextInfo.action = message.action || "";
      contextInfo.settings = merged(message.payload && message.payload.settings);
      if (isGTouchAction(contextInfo.action)) {
        if (!state.touchPanelReady) {
          state.touchPanelSettings = onlyTouchPanelVisual(contextInfo.settings);
          state.touchPanelReady = true;
        }
        contextInfo.settings = Object.assign({}, contextInfo.settings, state.touchPanelSettings);
        setSettings(contextInfo.context, contextInfo.settings);
      }
      if (isKeyBarAction(contextInfo.action)) {
        contextInfo.settings = Object.assign({}, contextInfo.settings, onlyKeyBarVisual(contextInfo.settings));
        setSettings(contextInfo.context, contextInfo.settings);
      }
      if (isCoverSurfaceAction(contextInfo.action)) {
        updateCoverSettings(contextInfo.settings, false);
        contextInfo.settings = Object.assign({}, contextInfo.settings, coverVisualSettings(contextInfo.settings));
      }
      state.settings = merged(contextInfo.settings);
      render(contextInfo);
      break;
    case "willDisappear":
      getContext(message.context).visible = false;
      break;
    case "didReceiveSettings":
      contextInfo = getContext(message.context);
      contextInfo.settings = merged(message.payload && message.payload.settings);
      if (isGTouchAction(contextInfo.action)) {
        updateGTouchSettings(onlyTouchPanelVisual(contextInfo.settings), false);
        contextInfo.settings = Object.assign({}, contextInfo.settings, { touchAction: contextInfo.settings.touchAction || "changeRhythm" });
        break;
      }
      if (isKeyBarAction(contextInfo.action)) {
        updateKeyBarSettings(onlyKeyBarVisual(contextInfo.settings), false);
        break;
      }
      if (isCoverSurfaceAction(contextInfo.action)) {
        updateCoverSettings(contextInfo.settings, false);
        break;
      }
      state.settings = merged(contextInfo.settings);
      renderAll();
      break;
    case "propertyInspectorDidAppear":
      contextInfo = getContext(message.context);
      send({ event: "sendToPropertyInspector", context: message.context, payload: { settings: isGTouchAction(contextInfo.action) ? state.touchPanelSettings : isKeyBarAction(contextInfo.action) ? state.keyBarSettings : isCoverSurfaceAction(contextInfo.action) ? coverVisualSettings(contextInfo.settings) : state.settings, playing: state.playing } });
      break;
    case "keyDown":
      contextInfo = getContext(message.context);
      if (isCoverSurfaceAction(contextInfo.action)) coverHoldStart(contextInfo);
      else keyBarHoldStart(contextInfo);
      break;
    case "keyUp":
      contextInfo = getContext(message.context);
      if (isCoverSurfaceAction(contextInfo.action)) {
        if (!coverHoldEnd(contextInfo)) press(contextInfo);
      } else if (!keyBarHoldEnd(contextInfo)) press(contextInfo);
      break;
    case "dialDown":
    case "dialPress":
    case "encoderPress":
      press(getContext(message.context));
      break;
    case "touchTap":
      contextInfo = getContext(message.context);
      if (contextInfo.action.includes("volume.knob")) media("playpause");
      else press(contextInfo);
      break;
    case "dialRotate":
    case "encoderRotate":
      rotate(getContext(message.context), parseTicks(message.payload));
      break;
    case "sendToPlugin":
      if (message.payload && message.payload.command === "resetAura") updateSettings(DEFAULTS);
      if (message.payload && message.payload.command === "applySettings") updateSettings(message.payload.settings || {});
      if (message.payload && message.payload.command === "applyGTouchSettings") updateGTouchSettings(message.payload.settings || {}, false);
      if (message.payload && message.payload.command === "saveGTouchSettings") updateGTouchSettings(message.payload.settings || {}, true);
      if (message.payload && message.payload.command === "applyKeyBarSettings") updateKeyBarSettings(message.payload.settings || {}, false);
      if (message.payload && message.payload.command === "saveKeyBarSettings") updateKeyBarSettings(message.payload.settings || {}, true);
      if (message.payload && message.payload.command === "applyCoverSettings") updateCoverSettings(message.payload.settings || {}, false);
      if (message.payload && message.payload.command === "saveCoverSettings") updateCoverSettings(message.payload.settings || {}, true);
      if (message.payload && message.payload.command === "setTouchAction") setTouchPanelAction(getContext(message.context), message.payload.touchAction || "changeRhythm");
      if (message.payload && message.payload.command === "setKeyHoldAction") setKeyHoldAction(getContext(message.context), message.payload.keyHoldAction || "none");
      break;
    default:
      break;
  }
}

async function main() {
  const args = parseArgs(process.argv);
  state.port = Number(args.port || 0);
  state.pluginUUID = String(args.pluginUUID || "");
  state.registerEvent = String(args.registerEvent || "registerPlugin");
  const socket = new WebSocketClient("127.0.0.1", state.port);
  state.socket = socket;
  socket.onopen = () => send({ event: state.registerEvent, uuid: state.pluginUUID });
  socket.onmessage = (raw) => {
    try {
      handleEvent(JSON.parse(raw));
    } catch (_error) {}
  };
  socket.onclose = () => process.exit(0);
  await socket.connect();
  initAudioCapture();
  startRhythmLoop(state.touchPanelSettings.responseRateMs);
}

main().catch(() => process.exit(1));

