// ==UserScript==
// @name         DetectiveChiptunizer
// @version      9002
// @description  Shows pussy with Sonic-Sanic Robotnik-style chiptune while streaming generation
// @match        *://yuntian-deng-chatgpt.hf.space/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // ========== CONFIG ==========
  const DEBOUNCE_MS = 700;  // ms to wait after last token before stopping
  const PROGRESS_STEP = 5;  // progress bar % increment per token batch
  const MAX_PROGRESS = 95;  // max % progress bar before finishing

  // ========== PROGRESS BAR SETUP ==========
  const barContainer = document.createElement('div');
  Object.assign(barContainer.style, {
    position: 'fixed',
    top: '0',
    left: '0',
    width: '100%',
    height: '4px',
    backgroundColor: 'rgba(0,0,0,0.1)',
    zIndex: 9999,
    opacity: '0',
    transition: 'opacity 0.3s ease',
  });

  const bar = document.createElement('div');
  Object.assign(bar.style, {
    height: '100%',
    width: '0%',
    backgroundColor: '#39ff14',
    transition: 'width 0.3s ease',
  });

  barContainer.appendChild(bar);
  document.body.appendChild(barContainer);

  // ========== AUDIO CONTEXT & CHIPTUNE SYSTEM ==========

  class ChiptunePlayer {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.isPlaying = false;
      this.currentStep = 0;
      this.stepIntervalId = null;

      this.bpm = 160;
      this.stepDuration = (60 / this.bpm) * 250; // milliseconds per step

      this.notes = {
        C3: 130.81,
        Cs3: 138.59,
        D3: 146.83,
        Ds3: 155.56,
        E3: 164.81,
        F3: 174.61,
        Fs3: 185.00,
        G3: 196.00,
        Gs3: 207.65,
        A3: 220.00,
        As3: 233.08,
        B3: 246.94,
        C4: 261.63,
        Cs4: 277.18,
        D4: 293.66,
        Ds4: 311.13,
        E4: 329.63,
        F4: 349.23,
        Fs4: 369.99,
        G4: 392.00,
        Gs4: 415.30,
        A4: 440.00,
        As4: 466.16,
        B4: 493.88,
        C5: 523.25,
        Cs5: 554.37,
        D5: 587.33,
        Ds5: 622.25,
        E5: 659.25,
      };

      // Richer "Robotnik-esque" melody pattern (two parts: melody + bass)
      // Length: 32 steps (16th notes at 160bpm)
      // Each step: note frequency or 0 for rest
      this.melody = [
        this.notes.E5, 0, this.notes.E5, 0,
        this.notes.E5, this.notes.C5, this.notes.E5, 0,
        this.notes.G5(this.notes), 0, 0, 0,
        this.notes.G4, 0, 0, 0,

        this.notes.C5, 0, this.notes.G4, 0,
        this.notes.E4, 0, this.notes.A4, 0,
        this.notes.B4, 0, this.notes.A4, this.notes.G4,
        this.notes.E5, 0, this.notes.G5(this.notes), 0,
      ];

      // Some helper for frequencies not defined, e.g. G5 (one octave above G4)
      this.notes.G5 = () => this.notes.G4 * 2;

      this.bassline = [
        this.notes.C3, 0, 0, 0,
        this.notes.G3, 0, 0, 0,
        this.notes.A3, 0, 0, 0,
        this.notes.B3, 0, 0, 0,

        this.notes.C4, 0, 0, 0,
        this.notes.G3, 0, 0, 0,
        this.notes.A3, 0, 0, 0,
        this.notes.B3, 0, 0, 0,
      ];
    }

    playNote(freq, duration = 0.12, volume = 0.12, pan = 0) {
      if (freq === 0) return; // rest

      if (this.audioCtx.state === 'suspended') this.audioCtx.resume();

      const osc = this.audioCtx.createOscillator();
      const gainNode = this.audioCtx.createGain();
      const panner = this.audioCtx.createStereoPanner();

      osc.type = 'square';
      osc.frequency.setValueAtTime(freq, this.audioCtx.currentTime);

      gainNode.gain.setValueAtTime(volume, this.audioCtx.currentTime);

      panner.pan.setValueAtTime(pan, this.audioCtx.currentTime);

      osc.connect(gainNode);
      gainNode.connect(panner);
      panner.connect(this.audioCtx.destination);

      osc.start();
      osc.stop(this.audioCtx.currentTime + duration);

      osc.onended = () => {
        osc.disconnect();
        gainNode.disconnect();
        panner.disconnect();
      };
    }

    playStep(step) {
      // Safely fetch notes, 0 if out of range
      let noteMelody = 0;
      let noteBass = 0;

      if (step < this.melody.length) {
        const mVal = this.melody[step];
        noteMelody = typeof mVal === 'function' ? mVal() : mVal;
      }

      if (step < this.bassline.length) {
        const bVal = this.bassline[step];
        noteBass = typeof bVal === 'function' ? bVal() : bVal;
      }

      // Pan melody right, bass left
      this.playNote(noteMelody, 0.12, 0.12, 0.4);
      this.playNote(noteBass, 0.12, 0.15, -0.6);
    }

    start() {
      if (this.isPlaying) return;
      this.isPlaying = true;
      this.currentStep = 0;
      if (this.audioCtx.state === 'suspended') this.audioCtx.resume();

      this.stepIntervalId = setInterval(() => {
        this.playStep(this.currentStep);
        this.currentStep = (this.currentStep + 1) % this.melody.length;
      }, this.stepDuration);
    }

    stop() {
      if (!this.isPlaying) return;
      clearInterval(this.stepIntervalId);
      this.stepIntervalId = null;
      this.isPlaying = false;
    }
  }

  // Instantiate AudioContext & player once
  const audioCtx = new AudioContext();
  const chiptunePlayer = new ChiptunePlayer(audioCtx);

  // ========== PROGRESS & TOKEN STREAM HANDLING ==========

  let progress = 0;
  let active = false;
  let stopTimeout = null;

  function startProgress() {
    if (!active) {
      active = true;
      progress = 0;
      bar.style.width = '0%';
      barContainer.style.opacity = '1';
      chiptunePlayer.start();
    }
    incrementProgress();
    deferStop();
  }

  function incrementProgress() {
    if (progress < MAX_PROGRESS) {
      progress = Math.min(progress + PROGRESS_STEP, MAX_PROGRESS);
      bar.style.width = progress + '%';
    }
  }

  function finishProgress() {
    progress = 100;
    bar.style.width = '100%';
    setTimeout(() => {
      barContainer.style.opacity = '0';
      bar.style.width = '0%';
      active = false;
      chiptunePlayer.stop();
    }, 500);
  }

  function deferStop() {
    if (stopTimeout) clearTimeout(stopTimeout);
    stopTimeout = setTimeout(() => {
      finishProgress();
      stopTimeout = null;
    }, DEBOUNCE_MS);
  }

  // ========== OBSERVING STREAMED TOKENS ==========

  const CHAT_SELECTOR = '.bubble-wrap.svelte-gjtrl6';

  function init() {
    const chatContainer = document.querySelector(CHAT_SELECTOR);
    if (!chatContainer) return false;

    const observer = new MutationObserver(mutations => {
      let tokensDetected = false;
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (!(node instanceof HTMLElement)) continue;
          if (node.textContent.trim().length > 0) {
            tokensDetected = true;
            break;
          }
        }
        if (tokensDetected) break;
      }
      if (tokensDetected) startProgress();
    });

    observer.observe(chatContainer, { childList: true, subtree: true });
    return true;
  }

  // ========== INIT LOOP ==========

  const intervalId = setInterval(() => {
    if (init()) clearInterval(intervalId);
  }, 300);

})();