
import { CpuState, EmuState } from "./baseplatform";
import { CPU, SampledAudioSink, ProbeAll, NullProbe } from "./devices";
import { EmuHalt } from "./emu";

// WASM Support
// TODO: detangle from c64

export abstract class BaseWASMMachine {
  prefix : string;
  instance : WebAssembly.Instance;
  exports : any;
  sys : number;
  pixel_dest : Uint32Array;
  pixel_src : Uint32Array;
  stateptr : number;
  statearr : Uint8Array;
  cpustateptr : number;
  cpustatearr : Uint8Array;
  ctrlstateptr : number;
  ctrlstatearr : Uint8Array;
  cpu : CPU;
  romptr : number;
  romlen : number;
  romarr : Uint8Array;
  biosptr : number;
  biosarr : Uint8Array;
  audio : SampledAudioSink;
  audioarr : Float32Array;
  probe : ProbeAll;
  maxROMSize : number = 0x40000;

  abstract getCPUState() : CpuState;
  abstract saveState() : EmuState;
  abstract loadState(state: EmuState);

  constructor(prefix: string) {
    this.prefix = prefix;
    var self = this;
    this.cpu = {
      getPC: self.getPC.bind(self),
      getSP: self.getSP.bind(self),
      isStable: self.isStable.bind(self),
      reset: self.reset.bind(self),
      saveState: () => {
        return self.getCPUState();
      },
      loadState: () => {
        console.log("loadState not implemented")
      },
      connectMemoryBus() {
        console.log("connectMemoryBus not implemented")
      },
    }
  }
  getImports(wmod: WebAssembly.Module) {
    return {};
  }
  async fetchWASM() {
    var wasmResponse = await fetch('res/'+this.prefix+'.wasm');
    if (wasmResponse.status == 200 || (wasmResponse as any as Blob).size) {
      var wasmBinary = await wasmResponse.arrayBuffer();
      var wasmCompiled = await WebAssembly.compile(wasmBinary);
      var wasmResult = await WebAssembly.instantiate(wasmCompiled, this.getImports(wasmCompiled));
      this.instance = wasmResult;
      this.exports = wasmResult.exports;
    } else throw new Error('could not load WASM file');
  }
  allocateBIOS(biosBinary: Uint8Array) {
    this.biosptr = this.exports.malloc(biosBinary.byteLength);
    this.biosarr = new Uint8Array(this.exports.memory.buffer, this.biosptr, biosBinary.byteLength);
}
  async fetchBIOS() {
    var biosResponse = await fetch('res/'+this.prefix+'.bios');
    if (biosResponse.status == 200 || (biosResponse as any as Blob).size) {
      var biosBinary = new Uint8Array(await biosResponse.arrayBuffer());
      this.allocateBIOS(biosBinary);
      this.loadBIOS(biosBinary);
    } else throw new Error('could not load BIOS file');
  }
  async initWASM() {
    // init machine instance
    this.sys = this.exports.machine_init(this.biosptr);
    let statesize = this.exports.machine_get_state_size();
    this.stateptr = this.exports.malloc(statesize);
    let ctrlstatesize = this.exports.machine_get_controls_state_size();
    this.ctrlstateptr = this.exports.malloc(ctrlstatesize);
    let cpustatesize = this.exports.machine_get_cpu_state_size();
    this.cpustateptr = this.exports.malloc(cpustatesize);
    this.romptr = this.exports.malloc(this.maxROMSize);
    // create state buffers
    // must do this after allocating memory (and everytime we grow memory?)
    this.statearr = new Uint8Array(this.exports.memory.buffer, this.stateptr, statesize);
    this.ctrlstatearr = new Uint8Array(this.exports.memory.buffer, this.ctrlstateptr, ctrlstatesize);
    this.cpustatearr = new Uint8Array(this.exports.memory.buffer, this.cpustateptr, cpustatesize);
    // create audio buffer
    let sampbufsize = 4096*4;
    this.audioarr = new Float32Array(this.exports.memory.buffer, this.exports.machine_get_sample_buffer(), sampbufsize);
    // create ROM buffer
    this.romarr = new Uint8Array(this.exports.memory.buffer, this.romptr, this.maxROMSize);
    // enable c64 joystick map to arrow keys (TODO)
    //this.exports.c64_set_joystick_type(this.sys, 1);
    console.log('machine_init', this.sys, statesize, ctrlstatesize, cpustatesize, sampbufsize);
  }
  async loadWASM() {
    await this.fetchWASM();
    this.exports.memory.grow(96); // TODO: need more when probing?
    await this.fetchBIOS();
    await this.initWASM();
  }
  getPC() : number {
    return this.exports.machine_cpu_get_pc(this.sys);
  }
  getSP() : number {
    return this.exports.machine_cpu_get_sp(this.sys);
  }
  isStable() : boolean {
    return this.exports.machine_cpu_is_stable(this.sys);
  }
  loadROM(rom: Uint8Array) {
    if (rom.length > this.maxROMSize) throw new EmuHalt(`Rom size too big: ${rom.length} bytes`);
    this.romarr.set(rom);
    this.romlen = rom.length;
    console.log('load rom', rom.length, 'bytes');
    this.reset(); // TODO?
  }
  // TODO: can't load after machine_init
  loadBIOS(srcArray: Uint8Array) {
    this.biosarr.set(srcArray);
  }
  reset() {
    this.exports.machine_reset(this.sys);
  }
  /* TODO: we don't need this because c64_exec does this?
  pollControls() {
    this.exports.machine_start_frame(this.sys);
  }
  */
  read(address: number) : number {
    return this.exports.machine_mem_read(this.sys, address & 0xffff);
  }
  readConst(address: number) : number {
    return this.exports.machine_mem_read(this.sys, address & 0xffff);
  }
  write(address: number, value: number) : void {
    this.exports.machine_mem_write(this.sys, address & 0xffff, value & 0xff);
  }
  getAudioParams() {
    return {sampleRate:44100, stereo:false};
  }
  videoOffsetBytes = 0;
  connectVideo(pixels:Uint32Array) : void {
    this.pixel_dest = pixels;
    var pixbuf = this.exports.machine_get_pixel_buffer(this.sys); // save video pointer
    console.log('connectVideo', pixbuf, pixels.length);
    this.pixel_src = new Uint32Array(this.exports.memory.buffer, pixbuf+this.videoOffsetBytes, pixels.length);
  }
  syncVideo() {
    if (this.exports.machine_update_video) {
      this.exports.machine_update_video(this.sys);
    }
    if (this.pixel_dest != null) {
      this.pixel_dest.set(this.pixel_src);
    }
  }
  // assume controls buffer is smaller than cpu buffer
  saveControlsState() : any {
    //console.log(1, this.romptr, this.romlen, this.ctrlstateptr, this.romarr.slice(0,4), this.ctrlstatearr.slice(0,4));
    this.exports.machine_save_controls_state(this.sys, this.ctrlstateptr);
    //console.log(2, this.romptr, this.romlen, this.ctrlstateptr, this.romarr.slice(0,4), this.ctrlstatearr.slice(0,4));
    return { controls:this.ctrlstatearr.slice(0) }
  }
  loadControlsState(state) : void {
    this.ctrlstatearr.set(state.controls);
    this.exports.machine_load_controls_state(this.sys, this.ctrlstateptr);
  }
  connectAudio(audio : SampledAudioSink) : void {
    this.audio = audio;
  }
  syncAudio() {
    if (this.audio != null) {
      var n = this.exports.machine_get_sample_count();
      for (var i=0; i<n; i++) {
        this.audio.feedSample(this.audioarr[i], 1);
      }
    }
  }
  // TODO: tick might advance 1 instruction
  advanceFrameClock(trap, cpf:number) : number {
    var i : number;
    if (trap) {
      for (i=0; i<cpf; i++) {
        if (trap()) {
          break;
        }
        this.exports.machine_tick(this.sys);
      }
    } else {
      this.exports.machine_exec(this.sys, cpf);
      i = cpf;
    }
    this.syncVideo();
    this.syncAudio();
    return i;
  }
  copyProbeData() {
    if (this.probe && !(this.probe instanceof NullProbe)) {
      var datalen = this.exports.machine_get_probe_buffer_size();
      var dataaddr = this.exports.machine_get_probe_buffer_address();
      // TODO: more efficient way to put into probe
      var databuf = new Uint32Array(this.exports.memory.buffer, dataaddr, datalen);
      this.probe.logNewFrame(); // TODO: machine should do this
      this.probe.addLogBuffer(databuf);
    }
  }
  connectProbe(probe: ProbeAll): void {
    this.probe = probe;
  }
  getDebugTree() {
    return this.saveState();
  }
}
