text: /*
   * Copyright (C) 2020 Ben Smith
   *
   * This software may be modified and distributed under the terms
   * of the MIT license.  See the LICENSE file for details.
   */
  "use strict";
  
  const RESULT_OK = 0;
  const RESULT_ERROR = 1;
  const SCREEN_WIDTH = 160;
  const SCREEN_HEIGHT = 144;
  const SGB_SCREEN_WIDTH = 256;
  const SGB_SCREEN_HEIGHT = 224;
  const SGB_SCREEN_LEFT = (SGB_SCREEN_WIDTH - SCREEN_WIDTH) >> 1;
  const SGB_SCREEN_RIGHT = (SGB_SCREEN_WIDTH + SCREEN_WIDTH) >> 1;
  const SGB_SCREEN_TOP = (SGB_SCREEN_HEIGHT - SCREEN_HEIGHT) >> 1;
  const SGB_SCREEN_BOTTOM = (SGB_SCREEN_HEIGHT + SCREEN_HEIGHT) >> 1;
  const AUDIO_FRAMES = 4096;
  const AUDIO_LATENCY_SEC = 0.1;
  const MAX_UPDATE_SEC = 5 / 60;
  const CPU_TICKS_PER_SECOND = 4194304;
  const EVENT_NEW_FRAME = 1;
  const EVENT_AUDIO_BUFFER_FULL = 2;
  const EVENT_UNTIL_TICKS = 4;
  const REWIND_FRAMES_PER_BASE_STATE = 45;
  const REWIND_BUFFER_CAPACITY = 4 * 1024 * 1024;
  const REWIND_FACTOR = 1.5;
  const REWIND_UPDATE_MS = 16;
  const BUILTIN_PALETTES = 83;  // See builtin-palettes.def.
  const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4; // When activated, poll for gamepad input about ~4 times per gameboy frame (~240 times second)
  const GAMEPAD_KEYMAP_STANDARD_STR = "standard"; // Try to use "standard" HTML5 mapping config if available
  
  const $ = document.querySelector.bind(document);
  let emulator = null;
  let customVolume = 0.8;
  
  const binjgbPromise = Binjgb();
  
  // Extract stuff from the vue.js implementation in demo.js.
  class VM {
    constructor() {
      this.ticks = 0;
      this.extRamUpdated = false;
      this.paused_ = false;
      this.volume = customVolume;
      this.pal = document.getElementById('palette').value;
      this.rewind = {
        minTicks: 0,
        maxTicks: 0,
      };
      setInterval(() => {
        if (this.extRamUpdated) {
          this.updateExtRam();
          this.extRamUpdated = false;
        }
      }, 1000);
    }
  
    get paused() { return this.paused_; }
    set paused(newPaused) {
      let oldPaused = this.paused_;
      this.paused_ = newPaused;
      if (!emulator) return;
      if (newPaused == oldPaused) return;
      if (newPaused) {
        emulator.pause();
        this.ticks = emulator.ticks;
        this.rewind.minTicks = emulator.rewind.oldestTicks;
        this.rewind.maxTicks = emulator.rewind.newestTicks;
      } else {
        emulator.resume();
      }
    }
  
    togglePause() {
      this.paused = !this.paused;
    }
  
    updateExtRam() {
      if (!emulator) return;
      const extram = emulator.getExtRam();
      localStorage.setItem('extram', JSON.stringify(Array.from(extram)));
    }
  };
  
  const vm = new VM();
  
  // // Load a ROM.
  // (async function go() {
  //   let response = await fetch('./rom/game.gb');
  //   let romBuffer = await response.arrayBuffer();
  //   const extRam = new Uint8Array(JSON.parse(localStorage.getItem('extram')));
  //   Emulator.start(await binjgbPromise, romBuffer, extRam);
  //   emulator.setBuiltinPalette(vm.pal);
  // })();
  
  
  // Copied from demo.js
  function makeWasmBuffer(module, ptr, size) {
    return new Uint8Array(module.HEAP8.buffer, ptr, size);
  }
  
  class Emulator {
    static start(module, romBuffer, extRamBuffer) {
      Emulator.stop();
      emulator = new Emulator(module, romBuffer, extRamBuffer);
      emulator.run();
    }
  
    static stop() {
      if (emulator) {
        emulator.destroy();
        emulator = null;
      }
    }
  
    constructor(module, romBuffer, extRamBuffer) {
      this.module = module;
      this.romDataPtr = this.module._malloc(romBuffer.byteLength);
      makeWasmBuffer(this.module, this.romDataPtr, romBuffer.byteLength)
          .set(new Uint8Array(romBuffer));
      this.e = this.module._emulator_new_simple(
          this.romDataPtr, romBuffer.byteLength, Audio.ctx.sampleRate,
          AUDIO_FRAMES);
      if (this.e == 0) {
        throw new Error('Invalid ROM.');
      }
  
      this.audio = new Audio(module, this.e);
      this.video = new Video(module, this.e, $('canvas'));
      this.rewind = new Rewind(module, this.e);
      this.rewindIntervalId = 0;
  
      this.lastRafSec = 0;
      this.leftoverTicks = 0;
      this.fps = 60;
  
      if (extRamBuffer) {
        this.loadExtRam(extRamBuffer);
      }
  
      this.bindKeys();
    }
  
    destroy() {
      this.unbindKeys();
      this.cancelAnimationFrame();
      clearInterval(this.rewindIntervalId);
      this.rewind.destroy();
      this.module._emulator_delete(this.e);
      this.module._free(this.romDataPtr);
    }
  
    withNewFileData(cb) {
      const fileDataPtr = this.module._ext_ram_file_data_new(this.e);
      const buffer = makeWasmBuffer(
          this.module, this.module._get_file_data_ptr(fileDataPtr),
          this.module._get_file_data_size(fileDataPtr));
      const result = cb(fileDataPtr, buffer);
      this.module._file_data_delete(fileDataPtr);
      return result;
    }
  
    loadExtRam(extRamBuffer) {
      this.withNewFileData((fileDataPtr, buffer) => {
        if (buffer.byteLength === extRamBuffer.byteLength) {
          buffer.set(new Uint8Array(extRamBuffer));
          this.module._emulator_read_ext_ram(this.e, fileDataPtr);
        }
      });
    }
  
    getExtRam() {
      return this.withNewFileData((fileDataPtr, buffer) => {
        this.module._emulator_write_ext_ram(this.e, fileDataPtr);
        return new Uint8Array(buffer);
      });
    }
  
    get isPaused() {
      return this.rafCancelToken === null;
    }
  
    pause() {
      if (!this.isPaused) {
        this.cancelAnimationFrame();
        this.audio.pause();
        this.beginRewind();
      }
    }
  
    resume() {
      if (this.isPaused) {
        this.endRewind();
        this.requestAnimationFrame();
        this.audio.resume();
      }
    }
  
    setBuiltinPalette(pal) {
      this.module._emulator_set_builtin_palette(this.e, pal);
    }
  
    get isRewinding() {
      return this.rewind.isRewinding;
    }
  
    beginRewind() {
      this.rewind.beginRewind();
    }
  
    rewindToTicks(ticks) {
      if (this.rewind.rewindToTicks(ticks)) {
        this.runUntil(ticks);
        this.video.renderTexture();
      }
    }
  
    endRewind() {
      this.rewind.endRewind();
      this.lastRafSec = 0;
      this.leftoverTicks = 0;
      this.audio.startSec = 0;
    }
  
    set autoRewind(enabled) {
      if (enabled) {
        this.rewindIntervalId = setInterval(() => {
          const oldest = this.rewind.oldestTicks;
          const start = this.ticks;
          const delta =
              REWIND_FACTOR * REWIND_UPDATE_MS / 1000 * CPU_TICKS_PER_SECOND;
          const rewindTo = Math.max(oldest, start - delta);
          this.rewindToTicks(rewindTo);
          vm.ticks = emulator.ticks;
        }, REWIND_UPDATE_MS);
      } else {
        clearInterval(this.rewindIntervalId);
        this.rewindIntervalId = 0;
      }
    }
  
    requestAnimationFrame() {
      this.rafCancelToken = requestAnimationFrame(this.rafCallback.bind(this));
    }
  
    cancelAnimationFrame() {
      cancelAnimationFrame(this.rafCancelToken);
      this.rafCancelToken = null;
    }
  
    run() {
      this.requestAnimationFrame();
    }
  
    get ticks() {
      return this.module._emulator_get_ticks_f64(this.e);
    }
  
    runUntil(ticks) {
      while (true) {
        const event = this.module._emulator_run_until_f64(this.e, ticks);
        if (event & EVENT_NEW_FRAME) {
          this.rewind.pushBuffer();
          this.video.uploadTexture();
        }
        if ((event & EVENT_AUDIO_BUFFER_FULL) && !this.isRewinding) {
          this.audio.pushBuffer();
        }
        if (event & EVENT_UNTIL_TICKS) {
          break;
        }
      }
      if (this.module._emulator_was_ext_ram_updated(this.e)) {
        vm.extRamUpdated = true;
      }
    }
  
    rafCallback(startMs) {
      this.requestAnimationFrame();
      let deltaSec = 0;
      if (!this.isRewinding) {
        const startSec = startMs / 1000;
        deltaSec = Math.max(startSec - (this.lastRafSec || startSec), 0);
        const startTicks = this.ticks;
        const deltaTicks =
            Math.min(deltaSec, MAX_UPDATE_SEC) * CPU_TICKS_PER_SECOND;
        const runUntilTicks = (startTicks + deltaTicks - this.leftoverTicks);
        this.runUntil(runUntilTicks);
        this.leftoverTicks = (this.ticks - runUntilTicks) | 0;
        this.lastRafSec = startSec;
      }
      const lerp = (from, to, alpha) => (alpha * from) + (1 - alpha) * to;
      this.fps = lerp(this.fps, Math.min(1 / deltaSec, 10000), 0.3);
      this.video.renderTexture();
    }
  
    bindKeys() {
      this.keyFuncs = {
        'ArrowDown': this.module._set_joyp_down.bind(null, this.e),
        'ArrowLeft': this.module._set_joyp_left.bind(null, this.e),
        'ArrowRight': this.module._set_joyp_right.bind(null, this.e),
        'ArrowUp': this.module._set_joyp_up.bind(null, this.e),
        'KeyZ': this.module._set_joyp_B.bind(null, this.e),
        'KeyX': this.module._set_joyp_A.bind(null, this.e),
        'Enter': this.module._set_joyp_start.bind(null, this.e),
        'Tab': this.module._set_joyp_select.bind(null, this.e)
        //'Backspace': this.keyRewind.bind(this)
        //'Space': this.keyPause.bind(this),
      };
      this.boundKeyDown = this.keyDown.bind(this);
      this.boundKeyUp = this.keyUp.bind(this);
  
      window.addEventListener('keydown', this.boundKeyDown);
      window.addEventListener('keyup', this.boundKeyUp);
    }
  
    unbindKeys() {
      window.removeEventListener('keydown', this.boundKeyDown);
      window.removeEventListener('keyup', this.boundKeyUp);
    }
  
    keyDown(event) {
      this.module._set_joyp_start(null, this.e);
      if (event.code in this.keyFuncs) {
        this.keyFuncs[event.code](true);
        event.preventDefault();
      }
    }
  
    keyUp(event) {
      if (event.code in this.keyFuncs) {
        this.keyFuncs[event.code](false);
        event.preventDefault();
      }
    }
  
    keyRewind(isKeyDown) {
      if (this.isRewinding !== isKeyDown) {
        if (isKeyDown) {
          vm.paused = true;
          this.autoRewind = true;
        } else {
          this.autoRewind = false;
          vm.paused = false;
        }
      }
    }
  
    keyPause(isKeyDown) {
      if (isKeyDown) vm.togglePause();
    }
  }
  
  class Audio {
    constructor(module, e) {
      this.module = module;
      this.buffer = makeWasmBuffer(
          this.module, this.module._get_audio_buffer_ptr(e),
          this.module._get_audio_buffer_capacity(e));
      this.startSec = 0;
      this.resume();
    }
  
    get sampleRate() { return Audio.ctx.sampleRate; }
  
    pushBuffer() {
      const nowSec = Audio.ctx.currentTime;
      const nowPlusLatency = nowSec + AUDIO_LATENCY_SEC;
      const volume = vm.volume;
      this.startSec = (this.startSec || nowPlusLatency);
      if (this.startSec >= nowSec || customVolume !== vm.volume) {
        vm.volume = customVolume;
        const buffer = Audio.ctx.createBuffer(2, AUDIO_FRAMES, this.sampleRate);
        const channel0 = buffer.getChannelData(0);
        const channel1 = buffer.getChannelData(1);
        for (let i = 0; i < AUDIO_FRAMES; i++) {
          channel0[i] = this.buffer[2 * i] * volume / 255;
          channel1[i] = this.buffer[2 * i + 1] * volume / 255;
        }
        const bufferSource = Audio.ctx.createBufferSource();
        bufferSource.buffer = buffer;
        bufferSource.connect(Audio.ctx.destination);
        bufferSource.start(this.startSec);
        const bufferSec = AUDIO_FRAMES / this.sampleRate;
        this.startSec += bufferSec;
      } else {
        console.log(
            'Resetting audio (' + this.startSec.toFixed(2) + ' < ' +
            nowSec.toFixed(2) + ')');
        this.startSec = nowPlusLatency;
      }
    }
  
    pause() {
      Audio.ctx.suspend();
    }
  
    resume() {
      Audio.ctx.resume();
    }
  }
  
  class Video {
    constructor(module, e, el) {
      this.module = module;
      try {
        this.renderer = new WebGLRenderer(el);
      } catch (error) {
        console.log(`Error creating WebGLRenderer: ${error}`);
        this.renderer = new Canvas2DRenderer(el);
      }
      this.buffer = makeWasmBuffer(
          this.module, this.module._get_frame_buffer_ptr(e),
          this.module._get_frame_buffer_size(e));
    }
  
    uploadTexture() {
      this.renderer.uploadTexture(this.buffer);
    }
  
    renderTexture() {
      this.renderer.renderTexture();
    }
  }
  
  class Canvas2DRenderer {
    constructor(el) {
      this.ctx = el.getContext('2d');
      this.imageData = this.ctx.createImageData(el.width, el.height);
    }
  
    renderTexture() {
      this.ctx.putImageData(this.imageData, 0, 0);
    }
  
    uploadTexture(buffer) {
      this.imageData.data.set(buffer);
    }
  }
  
  class WebGLRenderer {
    constructor(el) {
      const gl = this.gl = el.getContext('webgl', {preserveDrawingBuffer: true});
      if (gl === null) {
        throw new Error('unable to create webgl context');
      }
  
      const w = SCREEN_WIDTH / 256;
      const h = SCREEN_HEIGHT / 256;
      const buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1, -1,  0, h,
        +1, -1,  w, h,
        -1, +1,  0, 0,
        +1, +1,  w, 0,
      ]), gl.STATIC_DRAW);
  
      const texture = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.texImage2D(
          gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  
      function compileShader(type, source) {
        const shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          throw new Error(`compileShader failed: ${gl.getShaderInfoLog(shader)}`);
        }
        return shader;
      }
  
      const vertexShader = compileShader(gl.VERTEX_SHADER,
         `attribute vec2 aPos;
          attribute vec2 aTexCoord;
          varying highp vec2 vTexCoord;
          void main(void) {
            gl_Position = vec4(aPos, 0.0, 1.0);
            vTexCoord = aTexCoord;
          }`);
      const fragmentShader = compileShader(gl.FRAGMENT_SHADER,
         `varying highp vec2 vTexCoord;
          uniform sampler2D uSampler;
          void main(void) {
            gl_FragColor = texture2D(uSampler, vTexCoord);
          }`);
  
      const program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw new Error(`program link failed: ${gl.getProgramInfoLog(program)}`);
      }
      gl.useProgram(program);
  
      const aPos = gl.getAttribLocation(program, 'aPos');
      const aTexCoord = gl.getAttribLocation(program, 'aTexCoord');
      const uSampler = gl.getUniformLocation(program, 'uSampler');
  
      gl.enableVertexAttribArray(aPos);
      gl.enableVertexAttribArray(aTexCoord);
      gl.vertexAttribPointer(aPos, 2, gl.FLOAT, gl.FALSE, 16, 0);
      gl.vertexAttribPointer(aTexCoord, 2, gl.FLOAT, gl.FALSE, 16, 8);
      gl.uniform1i(uSampler, 0);
    }
  
    renderTexture() {
      this.gl.clearColor(0.5, 0.5, 0.5, 1.0);
      this.gl.clear(this.gl.COLOR_BUFFER_BIT);
      this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
    }
  
    uploadTexture(buffer) {
      this.gl.texSubImage2D(
          this.gl.TEXTURE_2D, 0, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, this.gl.RGBA,
          this.gl.UNSIGNED_BYTE, buffer);
    }
  }
  
  class Rewind {
    constructor(module, e) {
      this.module = module;
      this.e = e;
      this.joypadBufferPtr = this.module._joypad_new();
      this.statePtr = 0;
      this.bufferPtr = this.module._rewind_new_simple(
          e, REWIND_FRAMES_PER_BASE_STATE, REWIND_BUFFER_CAPACITY);
      this.module._emulator_set_default_joypad_callback(e, this.joypadBufferPtr);
    }
  
    destroy() {
      this.module._rewind_delete(this.bufferPtr);
      this.module._joypad_delete(this.joypadBufferPtr);
    }
  
    get oldestTicks() {
      return this.module._rewind_get_oldest_ticks_f64(this.bufferPtr);
    }
  
    get newestTicks() {
      return this.module._rewind_get_newest_ticks_f64(this.bufferPtr);
    }
  
    pushBuffer() {
      if (!this.isRewinding) {
        this.module._rewind_append(this.bufferPtr, this.e);
      }
    }
  
    get isRewinding() {
      return this.statePtr !== 0;
    }
  
    beginRewind() {
      if (this.isRewinding) return;
      this.statePtr =
          this.module._rewind_begin(this.e, this.bufferPtr, this.joypadBufferPtr);
    }
  
    rewindToTicks(ticks) {
      if (!this.isRewinding) return;
      return this.module._rewind_to_ticks_wrapper(this.statePtr, ticks) ===
          RESULT_OK;
    }
  
    endRewind() {
      if (!this.isRewinding) return;
      this.module._emulator_set_default_joypad_callback(
          this.e, this.joypadBufferPtr);
      this.module._rewind_end(this.statePtr);
      this.statePtr = 0;
    }
  }
  
  document.getElementById('palette').addEventListener('change', function (event) {
    emulator.setBuiltinPalette(this.value);
  })
  
  document.getElementById('volume').addEventListener('change', function (event) {
    customVolume = this.value;
  })
  
  // document.getElementById('fullscreenMode').addEventListener('click', function (event) {
  //   event.preventDefault();
  //   if (document.fullscreenElement) {
  //     document.exitFullscreen()
  //         .then(() => console.log(
  //
  //         ))
  //         .catch((err) => console.error(err))
  //   } else {
  //     document.documentElement.requestFullscreen();
  //   }
  // })
  
  const control = document.getElementById('controls');
  const ked = new KeyboardEvent('keydown', {
    bubbles: true, cancelable: true, keyCode: 13, isTrusted: true, key: "Enter", code: "Enter"
  });
  const keup = new KeyboardEvent('keyup', {
    bubbles: true, cancelable: true, keyCode: 13, isTrusted: true, key: "Enter", code: "Enter"
  });
  
  control.addEventListener('click', function(event) {
    event.preventDefault();
    if (emulator && event.target?.dataset?.bind) {
      console.log(event.target?.dataset?.bind);
      emulator.module[event.target?.dataset?.bind](emulator.e, true);
      setTimeout(() => {
        emulator.module[event.target?.dataset?.bind](emulator.e, false);
      }, 50);
    } else if (!emulator){
      initEmulator();
    }
    return;
  });
  
  window.addEventListener('touchstart', initEmulator);
  window.addEventListener('click', initEmulator);
  window.addEventListener('keydown', initEmulator);
  
  function initEmulator() {
    Audio.ctx = new AudioContext;
    window.removeEventListener('keydown', initEmulator);
    window.removeEventListener('touchstart', initEmulator);
    window.removeEventListener('click', initEmulator);
    const preloader = document.getElementById('preloader');
    const light = document.getElementById('switch-light');
    preloader.parentNode.removeChild(preloader);
    light.classList.add('active');
    (async function go() {
      let response = await fetch('./rom/game.gb');
      let romBuffer = await response.arrayBuffer();
      const extRam = new Uint8Array(JSON.parse(localStorage.getItem('extram')));
      Emulator.start(await binjgbPromise, romBuffer, extRam);
      emulator.setBuiltinPalette(vm.pal);
    })();
  }
  
  