// ==UserScript==
// @name         DetectiveChiptunizer Pro
// @version      9005
// @description  Smooth chiptune progress with polished Robotnik and Mario motifs during generation in yuntian-deng-chatgpt.hf.space
// @match        *://yuntian-deng-chatgpt.hf.space/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // --- CONFIGURATION ---
  const DEBOUNCE_MS = 1200;       // Consider generation ended after 1.2s no tokens (longer for smoother)
  const PROGRESS_STEP = 4;         // % progress per token batch
  const MAX_PROGRESS = 98;         // Max progress before jump to complete
  const POLYPHONY_VOICES = 3;      // voices for fuller sound

  // --- PROGRESS BAR SETUP ---
  const barContainer = document.createElement('div');
  Object.assign(barContainer.style, {
    position: 'fixed',
    top: '0',
    left: '0',
    width: '100%',
    height: '5px',
    backgroundColor: 'rgba(0,0,0,0.12)',
    zIndex: 2147483647,
    opacity: '0',
    transition: 'opacity 0.4s ease',
    pointerEvents: 'none',
  });
  const bar = document.createElement('div');
  Object.assign(bar.style, {
    height: '100%',
    width: '0%',
    backgroundColor: '#39ff14',
    transition: 'width 0.25s ease-out',
    borderRadius: '0 3px 3px 0',
  });
  barContainer.appendChild(bar);
  document.body.appendChild(barContainer);

  // --- AUDIO SETUP ---
  const AudioContext = window.AudioContext || window.webkitAudioContext;
  const audioCtx = new AudioContext();

  // We'll schedule notes precisely with AudioContext time, no setInterval for pristinity.
  // Pre-calculate step duration and use recursive scheduling with buffer ahead time.

  class Voice {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.oscillator = null;
      this.gainNode = this.audioCtx.createGain();
      this.gainNode.connect(this.audioCtx.destination);
    }

    playNote(freq, startTime, duration) {
      if (!freq || freq === 0) return;

      const osc = this.audioCtx.createOscillator();
      osc.type = 'square';
      osc.frequency.setValueAtTime(typeof freq === 'function' ? freq() : freq, startTime);

      const gain = this.audioCtx.createGain();
      // ADSR envelope - smooth fade in/out for less harsh sound
      const attack = 0.01;
      const release = 0.05;

      gain.gain.setValueAtTime(0, startTime);
      gain.gain.linearRampToValueAtTime(0.13, startTime + attack);
      gain.gain.setValueAtTime(0.13, startTime + duration - release);
      gain.gain.linearRampToValueAtTime(0, startTime + duration);

      osc.connect(gain);
      gain.connect(this.gainNode);

      osc.start(startTime);
      osc.stop(startTime + duration);

      // Cleanup
      osc.onended = () => {
        osc.disconnect();
        gain.disconnect();
      };
    }
  }

  class ChiptunePlayer {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.isPlaying = false;
      this.currentStep = 0;
      this.nextNoteTime = 0;
      this.lookahead = 0.1; // schedule notes 100ms ahead
      this.scheduleAheadTime = 0.1;
      this.timerID = null;

      this.bpm = 160;

      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,
      };
      this.notes.G5 = () => this.notes.G4 * 2;

      // Polyphonic Voices
      this.voices = [];
      for (let i = 0; i < POLYPHONY_VOICES; i++) {
        this.voices.push(new Voice(audioCtx));
      }

      // PREDEFINED MELODIES: both melody and bassline arrays of frequencies (0 = rest)
      this.melodies = [
        {
          name: 'Robotnik',
          bpm: 160,
          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: [
            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,
          ],
          arpeggio: [
            this.notes.G5, 0, this.notes.E5, 0,
            this.notes.C5, 0, this.notes.E5, 0,
            this.notes.G5, 0, 0, 0,
            0, 0, 0, 0,
            this.notes.G5, 0, this.notes.E5, 0,
            this.notes.C5, 0, this.notes.E5, 0,
            this.notes.G5, 0, 0, 0,
            0, 0, 0, 0,
          ],
        },
        {
          name: 'Mario',
          bpm: 130,
          melody: [
            this.notes.E5, this.notes.E5, 0, this.notes.E5,
            0, this.notes.C5, this.notes.E5, 0,
            this.notes.G5, 0, 0, 0,
            this.notes.G4, 0, 0, 0,
          ],
          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,
          ],
          arpeggio: [
            this.notes.G5, 0, this.notes.E5, 0,
            this.notes.C5, 0, this.notes.E5, 0,
            this.notes.G5, 0, 0, 0,
            0, 0, 0, 0,
          ],
        },
      ];

      this.selectedMelody = null;
    }

    selectRandomMelody() {
      const idx = Math.floor(Math.random() * this.melodies.length);
      this.selectedMelody = this.melodies[idx];
      this.bpm = this.selectedMelody.bpm;
      this.stepDurationMs = (60000 / this.bpm) / 4;
    }

    scheduleNotes() {
      while (this.nextNoteTime < this.audioCtx.currentTime + this.scheduleAheadTime) {
        this.scheduleStep(this.currentStep, this.nextNoteTime);
        this.advanceStep();
      }
      this.timerID = setTimeout(() => this.scheduleNotes(), this.lookahead * 1000);
    }

    scheduleStep(step, time) {
      if (!this.selectedMelody) return;

      const melodyNotes = this.selectedMelody.melody;
      const basslineNotes = this.selectedMelody.bassline;
      const arpeggioNotes = this.selectedMelody.arpeggio;

      const len = melodyNotes.length;

      const melNote = (step < len) ? melodyNotes[step] : 0;
      const bassNote = (step < len) ? basslineNotes[step] : 0;
      const arpNote = (arpeggioNotes && step < arpeggioNotes.length) ? arpeggioNotes[step] : 0;

      // Play voices with slight stereo panning
      this.voices[0].playNote(melNote, this.noteDuration, 0.13, +0.35);
      this.voices[1].playNote(bassNote, this.noteDuration, 0.15, -0.7);
      if (arpNote) this.voices[2].playNote(arpNote, this.noteDuration * 0.85, 0.1, +0.8);
    }

    advanceStep() {
      this.nextNoteTime += this.stepDurationMs / 1000;
      this.currentStep++;
      if (!this.selectedMelody) return;
      if (this.currentStep >= this.selectedMelody.melody.length) {
        this.currentStep = 0;
      }
    }

    start() {
      if (this.isPlaying) return;

      if (this.audioCtx.state === 'suspended') {
        // Note: Some browsers require user interaction to unlock AudioContext
        const resumeAudio = () => {
          this.audioCtx.resume();
          document.body.removeEventListener('click', resumeAudio);
        };
        document.body.addEventListener('click', resumeAudio);
      }

      this.selectRandomMelody();
      this.isPlaying = true;
      this.currentStep = 0;
      this.nextNoteTime = this.audioCtx.currentTime + 0.05;
      this.scheduleNotes();
    }

    stop() {
      if (!this.isPlaying) return;
      clearTimeout(this.timerID);
      this.timerID = null;
      this.isPlaying = false;
    }
  }

  const chiptunePlayer = new ChiptunePlayer(audioCtx);

  // --- PROGRESS 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();
    }, 800);
  }

  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;

    let lastMutationTime = 0;
    let debouncedStart = 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;
            lastMutationTime = performance.now();
            break;
          }
        }
        if (tokensDetected) break;
      }
      if (tokensDetected) {
        if (!debouncedStart) {
          startProgress();
          debouncedStart = true;
        }
      }
    });

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

    // Extra fallback check to reset debouncedStart if no mutations for longer than DEBOUNCE_MS
    setInterval(() => {
      if (debouncedStart && performance.now() - lastMutationTime > DEBOUNCE_MS) {
        debouncedStart = false;
      }
    }, 300);

    return true;
  }

  // --- INITIALIZATION WAIT LOOP ---
  const intervalId = setInterval(() => {
    if (init()) clearInterval(intervalId);
  }, 300);
})();