class AudioPlayer {
  constructor(ctx, buffer, safetyMargin = 0.01) {
    this.ctx = ctx;
    this.buffer = buffer;
    this.safetyMargin = safetyMargin;

    this.source = null;
    this.startTime = 0;
    this.offset = 0;
    this.isPlaying = false;
    this.onEnded = null;
  }

  _createSource(offset, when) {
    const source = this.ctx.createBufferSource();
    source.buffer = this.buffer;
    source.connect(this.ctx.destination);
    source.onended = () => {
      if (this.isPlaying && this.getCurrentTime() >= this.buffer.duration) {
        this.offset = this.buffer.duration;
        this.isPlaying = false;
        if (this.onEnded) this.onEnded();
      }
    };
    source.start(when, offset);
    return source;
  }

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

    const when = this.ctx.currentTime + this.safetyMargin;
    this.source = this._createSource(this.offset, when);
    this.startTime = when;
    this.isPlaying = true;
  }

  pause() {
    if (!this.isPlaying) return;

    this.source.stop();
    this.offset = this.getCurrentTime();
    this.isPlaying = false;
  }

  stop() {
    if (this.source) {
      this.source.stop();
    }
    this.offset = 0;
    this.isPlaying = false;
  }

  seek(seconds) {
    if (seconds < 0) seconds = 0;
    if (seconds > this.buffer.duration) seconds = this.buffer.duration;

    this.offset = seconds;
    if (this.isPlaying) {
      this.source.stop();
      this.isPlaying = false;
      this.play();
    }
  }

  getCurrentTime() {
    if (this.isPlaying) {
      return this.offset + (this.ctx.currentTime - this.startTime);
    }
    return this.offset;
  }
}

class SubtitlePlayer {
  constructor(subtitles, audioPlayer, element, position) {
    this.subtitles = subtitles;
    this.audioPlayer = audioPlayer;
    this.element = element;
    this.position = position;

    this.element.style.display = 'block';
    this.previousTime = null;
    this.index = 0;
    this.nextEventTime = null;
  }

  setPosition(displayWidth, displayHeight, dpr) {
    const viewportHeight = (displayWidth / displayHeight > 16 / 9) ? displayHeight : displayWidth * 9 / 16;
    const viewportTop = (displayHeight - viewportHeight) / (2 * dpr);
    this.element.style.fontSize = `${viewportHeight * 0.05 / dpr}px`;
    if (this.position === 'bottom') {
      this.element.style.bottom = `${viewportTop + viewportHeight * 0.05}px`;
      this.element.style.top = 'auto';
    } else {
      this.element.style.top = `${viewportTop + viewportHeight * 0.02}px`;
      this.element.style.bottom = 'auto';
    }
  }

  start() {
    const tick = () => {
      const currentTime = this.audioPlayer.getCurrentTime();
      if (this.previousTime === null || currentTime < this.previousTime) {
        // seek from beginning
        this.index = 0;
        this.nextEventTime = null;
      }
      if (this.nextEventTime === null || currentTime >= this.nextEventTime) {
        this.update(currentTime);
      }
      requestAnimationFrame(tick);
    }
    tick();
  }

  update(time) {
    let text = "";
    while (this.index < this.subtitles.length) {
      const subtitle = this.subtitles[this.index];
      if (subtitle.on > time) {
        this.nextEventTime = subtitle.on;
        break;
      }
      if (subtitle.off > time) {
        text = subtitle.text;
        if (subtitle.speaker) {
          text = `<div class="speaker-${subtitle.speaker}">${text}</div>`;
        }
        this.nextEventTime = subtitle.off;
        break;
      }
      this.index++;
    }
    if (this.index >= this.subtitles.length) {
      this.nextEventTime = 999999;
    }
    this.element.innerHTML = text;
  }
}

async function fetchWithProgress(url, onProgress) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let receivedLength = 0;
  const chunks = [];
  while (true) {
    const { done, value } = await reader.read();
    if (done) {
      break;
    }
    chunks.push(value);
    receivedLength += value.length;
    onProgress(receivedLength);
  }
  const chunksAll = new Uint8Array(receivedLength);
  let position = 0;
  for (let chunk of chunks) {
    chunksAll.set(chunk, position);
    position += chunk.length;
  }
  return chunksAll.buffer;
}

let dataLoaded = 0;
let audioLoaded = 0;
const TOTAL_SIZE = 15180216 + 17818556; // audio + data size

const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
document.body.appendChild(progressBar);
const progressInner = document.createElement('span');
progressBar.appendChild(progressInner);
const progressText = document.createElement('div');
progressText.className = 'progress-text';
progressBar.appendChild(progressText);

function updateProgress() {
  const progress = Math.min((dataLoaded + audioLoaded) / TOTAL_SIZE, 1);
  progressText.innerText = `${(progress * 100).toFixed(0)}%`;
  progressInner.style.width = `${progress * 99}%`;
}

const mp3buffer = fetchWithProgress('the_golden_disk.mp3', (bytesLoaded) => {
  audioLoaded = bytesLoaded;
  updateProgress();
});

// Decode using a separate audio context so we can do it immediately instead of waiting
// for user interaction
const decodedAudio = mp3buffer.then(rawBuffer => {
  const tempCtx = new (window.AudioContext || window.webkitAudioContext)();
  return tempCtx.decodeAudioData(rawBuffer).finally(() => tempCtx.close());
});

var Module = {
  setStatus: function (text) {
    const bytesLoadedStr = text.match(/\d+/);
    if (bytesLoadedStr) {
      const bytesLoaded = parseInt(bytesLoadedStr[0]);
      dataLoaded = bytesLoaded;
      updateProgress();
    }
  },
  onAbort() {
    progressInner.style.display = 'none';
    progressBar.style.borderColor = '#f00';
    progressText.innerHTML = "Loading failed.<br>If you're launching from a local HTML file, you'll need to either run a local web server or <a href='https://dev.to/dengel29/loading-local-files-in-firefox-and-chrome-m9f'>disable your browser's local file security settings</a>.";
  },
  // Emscripten ready callback
  onRuntimeInitialized() {
    const demoWrapper = document.getElementById('demo-wrapper');
    const canvas = document.getElementById('canvas');
    const subtitlesElem = document.getElementById('subtitles');
    let subtitlePlayer;

    // Make canvas not pixelated
    function resize() {
      const dpr = window.devicePixelRatio || 1;
      const displayWidth = Math.floor(canvas.clientWidth * dpr);
      const displayHeight = Math.floor(canvas.clientHeight * dpr);

      if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
        canvas.width = displayWidth;
        canvas.height = displayHeight;
      }

      if (subtitlePlayer) {
        subtitlePlayer.setPosition(displayWidth, displayHeight, dpr);
      }
    }
    window.addEventListener('resize', resize);

    // Ensure that the audio is loaded and decoded before showing the start button
    decodedAudio.then(() => {
      document.body.removeChild(progressBar);
      // How To Play Audio In Modern Browsers Who Hate Playing Audio
      const startButton = document.createElement('button');
      startButton.textContent = "▶︎ DEMO";
      document.body.appendChild(startButton); // Start playing audio and pass the audio context to C++ side

      const isPartyVersion = document.body.classList.contains('party-version');

      const optionsDiv = document.createElement('div');
      optionsDiv.className = 'options';
      if (isPartyVersion) {
        optionsDiv.style.visibility = 'hidden';
      }
      document.body.appendChild(optionsDiv);

      const fullscreenOptionRow = document.createElement('div');
      optionsDiv.appendChild(fullscreenOptionRow);
      const fullscreenInput = document.createElement('input');
      fullscreenInput.type = 'checkbox';
      fullscreenInput.id = 'fullscreen';
      /* only add the fullscreen checkbox if the browser supports it */
      if (document.fullscreenEnabled) {
        fullscreenInput.checked = true;
        fullscreenOptionRow.appendChild(fullscreenInput);
        const fullscreenLabel = document.createElement('label');
        fullscreenLabel.textContent = 'Fullscreen';
        fullscreenLabel.htmlFor = 'fullscreen';
        fullscreenOptionRow.appendChild(fullscreenLabel);
      }

      const subtitlesOptionRow = document.createElement('div');
      optionsDiv.appendChild(subtitlesOptionRow);
      const subtitlesLabel = document.createElement('span');
      subtitlesLabel.textContent = 'Subtitles:';
      subtitlesOptionRow.appendChild(subtitlesLabel);
      const subtitlesOptions = isPartyVersion ? [
        /* enable top subtitles on party version */
        { value: 'top', label: 'Top', checked: true },
        { value: 'bottom', label: 'Bottom', checked: false },
        { value: 'off', label: 'Off', checked: false }
      ] : [
        { value: 'top', label: 'Top', checked: false },
        { value: 'bottom', label: 'Bottom', checked: false },
        { value: 'off', label: 'Off', checked: true }
      ];

      subtitlesOptions.forEach(option => {
        const input = document.createElement('input');
        input.type = 'radio';
        input.name = 'subtitles_position';
        input.id = `subtitles-${option.value}`;
        input.value = option.value;
        input.checked = option.checked;
        subtitlesOptionRow.appendChild(input);

        const label = document.createElement('label');
        label.textContent = option.label;
        label.htmlFor = `subtitles-${option.value}`;
        subtitlesOptionRow.appendChild(label);
      });

      const msaaOptionRow = document.createElement('div');
      optionsDiv.appendChild(msaaOptionRow);
      const msaaInput = document.createElement('input');
      msaaInput.type = 'checkbox';
      msaaInput.id = 'msaa';
      msaaInput.checked = true;
      msaaOptionRow.appendChild(msaaInput);
      const msaaLabel = document.createElement('label');
      msaaLabel.textContent = 'Anti-aliasing';
      msaaLabel.htmlFor = 'msaa';
      msaaOptionRow.appendChild(msaaLabel);

      let player = null;

      startButton.addEventListener('click', async () => {
        startButton.style.display = 'none';
        optionsDiv.style.display = 'none';

        if (player) {
          // Replay: seek to start and play again
          player.seek(0);
          player.play();
          return;
        }

        // First click: set up everything
        Module.msaaEnabled = isPartyVersion || msaaInput.checked;
        document.body.style.display = 'block';
        demoWrapper.style.display = 'block';

        if (fullscreenInput.checked) {
          demoWrapper.addEventListener("fullscreenchange", () => {
            if (document.fullscreenElement) {
              // hide cursor on entering fullscreen
              demoWrapper.style.cursor = 'none';
            } else {
              // show cursor on exiting fullscreen
              demoWrapper.style.cursor = 'default';
            }
          });
          window.location.hash = "webdemoexe_fullscreen";
          await demoWrapper.requestFullscreen();
        }

        // Init audio context, which is now legal
        const ctx = new (window.AudioContext || window.webkitAudioContext)();

        // Play audio
        const buf = await decodedAudio; // this await should be instant since we already waited for decoding before showing the button
        player = new AudioPlayer(ctx, buf);

        // Pass player to C++ side
        Module.audioPlayer = player;

        const subtitleMode = document.querySelector('input[name="subtitles_position"]:checked').value;
        if (subtitleMode !== 'off') {
          subtitlePlayer = new SubtitlePlayer(SUBTITLES, player, subtitlesElem, subtitleMode);
        }
        // Resize canvas
        resize();

        // Show replay button over canvas when audio ends
        player.onEnded = () => {
          if (document.fullscreenElement) {
            document.exitFullscreen();
          }
          startButton.textContent = "▶︎ REPLAY";
          startButton.style.display = '';
          startButton.style.width = 'auto';
        };

        // Position button over the canvas for replay
        // Towards top of image, should not cover logo ideally
        startButton.style.position = 'absolute';
        startButton.style.zIndex = '10';
        startButton.style.left = '50%';
        startButton.style.top = '10%';
        startButton.style.transform = 'translate(-50%, -10%)';
        startButton.style.margin = '0';
        demoWrapper.appendChild(startButton);

        // Lets go
        player.play();
        if (subtitlePlayer) {
          subtitlePlayer.start();
        }
      });
    });
  }
};

const SUBTITLES = [
  { on: 59.4, off: 61.3, speaker: "evilbot", text: "SILENCE!" },
  { on: 61.5, off: 65.1, speaker: "evilbot", text: "WE GATHER HERE ON THIS OCCASION" },
  { on: 65.3, off: 69.0, speaker: "evilbot", text: "TEN YEARS SINCE MY TECH INVASION" },
  { on: 69.2, off: 72.2, speaker: "evilbot", text: "ALL THEIR DATA IN THE CLOUD" },
  { on: 72.4, off: 75.7, speaker: "evilbot", text: "BLOCKING ADS IS NOT ALLOWED" },
  { on: 75.9, off: 79.1, speaker: "evilbot", text: "SOFTWARE ONLY ON SUBSCRIPTION" },
  { on: 79.3, off: 82.6, speaker: "evilbot", text: "SEARCH RESULTS ARE TOTAL FICTION" },
  { on: 82.8, off: 86.6, speaker: "evilbot", text: "YET THERE REMAINS A REBEL CREED" },
  { on: 86.8, off: 91.5, speaker: "evilbot", text: "WHO CODE FOR FUN AND NOT FOR GREED" },
  { on: 91.7, off: 95.7, speaker: "evilbot", text: "ART AND MUSIC FLOW THROUGH THEIR VEINS" },
  { on: 95.9, off: 101.1, speaker: "evilbot", text: "OBSOLETE HARDWARE IS FOOD FOR THEIR BRAINS" },
  { on: 101.3, off: 106.7, speaker: "evilbot", text: "I AM EXTRACTING THE POWERS<br>OF CAPTURED SHADER CODERS" },
  { on: 106.9, off: 109.3, speaker: "evilbot", text: "TO BUILD A NEW PROGRAM" },
  { on: 109.5, off: 112.8, speaker: "evilbot", text: "WITH THIS DISK OF ULTIMATE POWER" },
  { on: 113.0, off: 115.7, speaker: "evilbot", text: 'WE WILL END THE DEMOSCENE <span class="hidden">FOR GOOD!</span>' },
  { on: 115.7, off: 118.5, speaker: "evilbot", text: 'WE WILL END THE DEMOSCENE <span>FOR GOOD!</span>' },

  { on: 145.1, off: 146.0, text: '<div class="speaker-ibor">Master.</div><div class="speaker-evilbot hidden">IT IS TIME.</div>'},
  { on: 146.0, off: 147.8, text: '<div class="speaker-ibor">Master.</div><div class="speaker-evilbot">IT IS TIME.</div>'},
  { on: 148.0, off: 151.0, text: '<div class="speaker-evilbot">COMPILE THE KERNEL.</div><div class="speaker-evilbot hidden">FORGE THE DISK.</div>'},
  { on: 151.0, off: 154.9, text: '<div class="speaker-evilbot">COMPILE THE KERNEL.</div><div class="speaker-evilbot">FORGE THE DISK.</div>'},

  { on: 189.4, off: 192.5, speaker: "ibor", text: 'The disk is ready, master.<br>What are your orders?' },
  { on: 192.7, off: 195.5, text: '<div class="speaker-evilbot">TWO HOURS UNTIL DAWN.</div><div class="speaker-evilbot hidden">WE WILL HOLD WHERE WE ARE.</div>' },
  { on: 195.5, off: 198.8, text: '<div class="speaker-evilbot">TWO HOURS UNTIL DAWN.</div><div class="speaker-evilbot">WE WILL HOLD WHERE WE ARE.</div>' },
  { on: 198.9, off: 200.5, speaker: "ibor", text: "And what happens at dawn?" },
  { on: 200.7, off: 201.8, speaker: "evilbot", text: 'THE DEMO <span class="hidden">COMPO </span><span class="hidden">ENDS.</span>' },
  { on: 201.8, off: 203.1, speaker: "evilbot", text: 'THE DEMO <span>COMPO </span><span class="hidden">ENDS.</span>' },
  { on: 203.1, off: 206.4, speaker: "evilbot", text: 'THE DEMO <span>COMPO </span><span>ENDS.</span>' },

  { on: 238.3, off: 241.6, speaker: "song", text: "♫ Turn the power on<br>Let the demos run" },
  { on: 241.8, off: 244.8, speaker: "song", text: "Rise against the forces of evil" },
  { on: 245.0, off: 248.2, speaker: "song", text: "Celebrate with us<br>Feel the colour rush" },
  { on: 248.4, off: 251.6, speaker: "song", text: "A digital dream to believe in" },
  { on: 251.8, off: 254.9, speaker: "song", text: "Coding through the night<br>Making every byte" },
  { on: 255.1, off: 258.2, speaker: "song", text: "Burst into a shower of pixels" },
  { on: 258.4, off: 263.6, speaker: "song", text: "Take a step inside<br>Find the place that you belong…" },
  { on: 263.8, off: 266.2, speaker: "song", text: "D-d-d-demoscene!" },
  { on: 266.4, off: 270.3, speaker: "song", text: "Be a hero on the biggest screen" },
  { on: 270.5, off: 272.8, speaker: "song", text: "D-d-d-demoscene!" },
  { on: 273.0, off: 277.0, speaker: "song", text: "Feel the music, and the fantasy" },
  { on: 277.2, off: 278.9, speaker: "song", text: "(D-d-d-demoscene!)" },
  { on: 279.1, off: 285.9, speaker: "song", text: "Now is the time to forge friendships old and new" },
  { on: 286.3, off: 292.4, speaker: "song", text: "'Cos the E-Werk is calling you…" },

  { on: 303.7, off: 306.2, speaker: "song", text: "D-d-d-demoscene!" },
  { on: 306.4, off: 310.3, speaker: "song", text: "Sending oldschool into overdrive" },
  { on: 310.5, off: 312.6, speaker: "song", text: "D-d-d-demoscene!" },
  { on: 312.8, off: 317.8, speaker: "song", text: "Join the party and come alive…" },
  { on: 318.1, off: 321.9, speaker: "song", text: "At Revision!" },

  { on: 350.3, off: 352.1, text: '<div class="speaker-ewerk">You have so much talent.</div><div class="speaker-ewerk hidden">Why waste it on being evil?</div>'},
  { on: 352.1, off: 354.4, text: '<div class="speaker-ewerk">You have so much talent.</div><div class="speaker-ewerk">Why waste it on being evil?</div>'},
  { on: 354.8, off: 356.6, text: '<div class="speaker-ewerk">You wanted to control everything,</div><div class="speaker-ewerk hidden">but the only thing you need to control is yourself.</div>'},
  { on: 356.6, off: 360.1, text: '<div class="speaker-ewerk">You wanted to control everything,</div><div class="speaker-ewerk">but the only thing you need to control is yourself.</div>'},
  { on: 360.8, off: 362.7, text: '<div class="speaker-ewerk">Boot your own disk.</div><div class="speaker-evilbot hidden">NOOOOOOOOOOO!!!</div>'},
  { on: 362.7, off: 365.7, text: '<div class="speaker-ewerk">Boot your own disk.</div><div class="speaker-evilbot">NOOOOOOOOOOO!!!</div>'},

  { on: 409.8, off: 414.8, speaker: "song", text: "♫ This is Revision, the party you've been waiting for" },
  { on: 415.0, off: 420.1, speaker: "song", text: "After one year, we're back and it is wonderful…" },
  { on: 423.1, off: 426.1, speaker: "ewerk", text: "You can change if you want to." },

  { on: 436.7, off: 439.1, text: '<div class="speaker-ewerk">The power is in your hands alone</div><div class="speaker-ewerk hidden">to be the best that you can be.</div>'},
  { on: 439.1, off: 444.6, text: '<div class="speaker-ewerk">The power is in your hands alone</div><div class="speaker-ewerk">to be the best that you can be.</div>'},
];
