// ==UserScript==
// @name         DetectiveChiptunizer Audio Only
// @version      9006
// @description  Plays layered chiptune melodies during streamed token generation on yuntian-deng-chatgpt.hf.space
// @match        *://yuntian-deng-chatgpt.hf.space/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // CONFIG
  const DEBOUNCE_MS = 1200; // time without tokens before stopping audio

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

  class Voice {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.gainNode = audioCtx.createGain();
      this.gainNode.gain.setValueAtTime(1, audioCtx.currentTime);
      this.gainNode.connect(audioCtx.destination);
    }
    playNote(freq, startTime, duration, volume = 0.12, pan = 0) {
      if (!freq || freq === 0) return;
      const oscillator = this.audioCtx.createOscillator();
      const gainNode = this.audioCtx.createGain();
      const panner = this.audioCtx.createStereoPanner();

      oscillator.type = 'square';
      oscillator.frequency.setValueAtTime(freq, startTime);
      gainNode.gain.setValueAtTime(0, startTime);
      // ADSR fade in/out for smooth sound
      const attack = 0.01;
      const release = 0.05;
      gainNode.gain.linearRampToValueAtTime(volume, startTime + attack);
      gainNode.gain.setValueAtTime(volume, startTime + duration - release);
      gainNode.gain.linearRampToValueAtTime(0, startTime + duration);

      panner.pan.setValueAtTime(pan, startTime);

      oscillator.connect(gainNode);
      gainNode.connect(panner);
      panner.connect(this.gainNode);

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

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

  class ChiptunePlayer {
    constructor(audioCtx) {
      this.audioCtx = audioCtx;
      this.isPlaying = false;
      this.currentStep = 0;
      this.nextNoteTime = 0;
      this.scheduleAheadTime = 0.1; // seconds ahead to schedule notes
      this.lookahead = 25; // ms between scheduler runs
      this.timerId = null;

      this.bpm = 160;
      this.stepDuration = (60 / this.bpm) / 4; // 16th note duration in seconds

      this.notes = {
        C3: 130.81, D3: 146.83, E3: 164.81, F3: 174.61, G3: 196.00,
        A3: 220.00, B3: 246.94, C4: 261.63, D4: 293.66, E4: 329.63,
        F4: 349.23, G4: 392.00, A4: 440.00, B4: 493.88, C5: 523.25,
      };
      this.notes.G5 = () => this.notes.G4 * 2;

      // Voices: melody, bass, arpeggio
      this.voices = [
        new Voice(audioCtx),
        new Voice(audioCtx),
        new Voice(audioCtx),
      ];

      // Melody sets defined: two complete patterns
      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.E4, 0,
            this.notes.C5, 0, this.notes.E4, 0,
            this.notes.G5, 0, 0, 0,
            0, 0, 0, 0,
            this.notes.G5, 0, this.notes.E4, 0,
            this.notes.C5, 0, this.notes.E4, 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 i = Math.floor(Math.random() * this.melodies.length);
      this.selectedMelody = this.melodies[i];
      this.bpm = this.selectedMelody.bpm;
      this.stepDuration = (60 / 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);
    }

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

      const m = this.selectedMelody.melody;
      const b = this.selectedMelody.bassline;
      const a = this.selectedMelody.arpeggio || [];

      const len = m.length;

      const noteMel = (step < len) ? m[step] : 0;
      const noteBass = (step < len) ? b[step] : 0;
      const noteArp = (a && step < a.length) ? a[step] : 0;

      this.voices[0].playNote(noteMel, time, this.noteDuration, 0.12, +0.3);
      this.voices[1].playNote(noteBass, time, this.noteDuration, 0.15, -0.6);
      if (noteArp) this.voices[2].playNote(noteArp, time, this.noteDuration * 0.85, 0.08, +0.7);
    }

    advanceStep() {
      this.nextNoteTime += this.stepDuration;
      this.currentStep++;
      if (this.currentStep >= this.selectedMelody.melody.length) this.currentStep = 0;
    }

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

      if (this.audioCtx.state === 'suspended') {
        const resumeAudio = () => {
          this.audioCtx.resume();
          document.body.removeEventListener('click', resumeAudio);
          document.body.removeEventListener('keydown', resumeAudio);
        };
        document.body.addEventListener('click', resumeAudio);
        document.body.addEventListener('keydown', 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);

  // STREAM TOKEN DETECTION

  const CHAT_SELECTOR = '.bubble-wrap.svelte-gjtrl6';
  let stopTimeout = null;
  let isGenerating = false;

  function startPlaying() {
    if (!isGenerating) {
      isGenerating = true;
      chiptunePlayer.start();
    }
    debounceStop();
  }

  function debounceStop() {
    if (stopTimeout) clearTimeout(stopTimeout);
    stopTimeout = setTimeout(() => {
      isGenerating = false;
      chiptunePlayer.stop();
    }, DEBOUNCE_MS);
  }

  function observeTokens() {
    const chat = document.querySelector(CHAT_SELECTOR);
    if (!chat) return false;

    const observer = new MutationObserver(mutations => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node instanceof HTMLElement && node.textContent.trim().length > 0) {
            startPlaying();
            return; // one trigger per batch is enough
          }
        }
      }
    });

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

  const intervalId = setInterval(() => {
    if (observeTokens()) clearInterval(intervalId);
  }, 250);

})();