// ==UserScript==
// @name         DetectiveChiptunizer Random Chiptunes
// @version      9004
// @description  Plays random Sonic-Sanic Robotnik and Mario chiptunes on streamed generation
// @match        *://yuntian-deng-chatgpt.hf.space/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  const DEBOUNCE_MS = 700;
  const PROGRESS_STEP = 5;
  const MAX_PROGRESS = 95;

  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);

  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;
      this.stepTimerId = null;

      this.bpm = 160;
      this.stepDurationMs = (60000 / this.bpm) / 4;

      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;

      // Define multiple melodies: array of {melody: [], bassline: [], bpm: number}
      this.melodies = [
        // Robotnik style (fast, groovy)
        {
          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,
          ]
        },
        // Mario Bros Theme snippet — simplified
        {
          bpm: 120,
          melody: [
            this.notes.E5, 0, this.notes.E5, 0,
            0, this.notes.C5, this.notes.E5, 0,
            this.notes.G5.bind(this.notes), 0, 0, 0,
            this.notes.G4, 0, 0, 0,
          ],
          bassline: [
            this.notes.C3, 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,
          ]
        },
        // Sanic style — faster repetitive plucky triad melody
        {
          bpm: 220,
          melody: [
            this.notes.G4, this.notes.B4, this.notes.D5, this.notes.G5.bind(this.notes),
            this.notes.D5, this.notes.B4, this.notes.G4, 0,
            this.notes.G4, this.notes.B4, this.notes.D5, this.notes.G5.bind(this.notes),
            this.notes.D5, this.notes.B4, this.notes.G4, 0,
          ],
          bassline: [
            this.notes.C3, 0, this.notes.C3, 0,
            this.notes.C3, 0, this.notes.C3, 0,
            this.notes.C3, 0, this.notes.C3, 0,
            this.notes.C3, 0, this.notes.C3, 0,
          ],
        },
      ];

      this.currentMelodySet = null;
    }

    playNote(freq, duration = this.noteDuration, volume = 0.12, pan = 0) {
      if (!freq || freq === 0) return;
      if (this.audioCtx.state === 'suspended') this.audioCtx.resume();

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

      let frequency = typeof freq === 'function' ? freq() : freq;
      osc.type = 'square';
      osc.frequency.setValueAtTime(frequency, 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) {
      if (!this.currentMelodySet) return;

      const m = this.currentMelodySet.melody;
      const b = this.currentMelodySet.bassline;

      const noteMelody = (step < m.length) ? m[step] : 0;
      const noteBass = (step < b.length) ? b[step] : 0;

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

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

    start() {
      if (this.isPlaying) return;
      this.setMelodyRandomly();
      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.currentMelodySet.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;
  }

  // INIT
  const intervalId = setInterval(() => {
    if (init()) clearInterval(intervalId);
  }, 250);

})();