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

(function() {
  'use strict';

  // CONFIG
  const DEBOUNCE_MS = 700;  // wait after last token before stopping progress/audio
  const PROGRESS_STEP = 5;   // progress bar increment % on each token batch
  const MAX_PROGRESS = 95;   // max % progress bar before finish

  // 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: 999999,
    opacity: '0',
    transition: 'opacity 0.3s ease',
    pointerEvents: 'none',
  });

  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 SETUP & CHIPTUNE PLAYBACK
  const AudioContext = window.AudioContext || window.webkitAudioContext;
  const audioCtx = new AudioContext();

  class ChiptunePlayer {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.isPlaying = false;
      this.currentStep = 0;
      this.noteDuration = 0.12; // seconds
      this.stepTimerId = null;

      // Tempo and timing (16th notes = 4 notes per beat * bpm)
      this.bpm = 160;
      this.stepDurationMs = (60000 / this.bpm) / 4;

      // Notes dictionary for frequencies
      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,
      };

      // Helper to get G5 note (octave above G4)
      this.notes.G5 = () => this.notes.G4 * 2;

      // Melody pattern: 32 steps (16th notes pattern)
      this.melody = [
        this.notes.E4, 0, this.notes.E4, 0,
        this.notes.E4, this.notes.C4, this.notes.E4, 0,
        this.notes.G4, 0, 0, 0,
        this.notes.G3, 0, 0, 0,

        this.notes.C4, 0, this.notes.G3, 0,
        this.notes.E3, 0, this.notes.A3, 0,
        this.notes.B3, 0, this.notes.A3, this.notes.G3,
        this.notes.E4, 0, this.notes.G4, 0,
      ];

      // Bassline pattern: 32 steps
      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 = this.noteDuration, volume = 0.12, pan = 0) {
      if (!freq || 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(typeof freq === 'function' ? freq() : 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) {
      let noteMelody = 0;
      let noteBass = 0;

      if (step < this.melody.length) {
        noteMelody = this.melody[step];
      }
      if (step < this.bassline.length) {
        noteBass = this.bassline[step];
      }

      this.playNote(noteMelody, undefined, 0.12, 0.4);
      this.playNote(noteBass, undefined, 0.15, -0.6);
    }

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

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

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

  const chiptunePlayer = new ChiptunePlayer(audioCtx);

  // PROGRESS BAR STATE
  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();
    }, 600);
  }

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

  // STREAM TOKEN DETECTION
  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 mutation of mutations) {
        for (const node of mutation.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;
  }

  // Wait for chat and initiate
  const intervalId = setInterval(() => {
    if (init()) clearInterval(intervalId);
  }, 250);

})();