
export interface SavesState<S> {
  saveState(): S;
  loadState(state: S): void;
}

export interface Bus {
  read(a: number): number;
  write(a: number, v: number): void;
  readConst?(a: number): number;
}

export interface Bus32 {
  read32(a: number): number;
  write32(a: number, v: number): void;
  readConst32?(a: number): number;
}

export interface ClockBased {
  advanceClock(): void;
}

export interface InstructionBased {
  advanceInsn(): number;
}

export type TrapCondition = () => boolean;

export interface FrameBased {
  advanceFrame(trap: TrapCondition): number;
}

export interface VideoSource {
  getVideoParams(): VideoParams;
  connectVideo(pixels: Uint32Array): void;
}

export interface RasterFrameBased extends FrameBased, VideoSource {
  getRasterY(): number;
  getRasterX(): number;
  getRasterCanvasPosition?(): { x: number, y: number };
}

export interface VideoParams {
  width: number;
  height: number;
  overscan?: boolean;
  rotate?: number;
  videoFrequency?: number; // default = 60
  aspect?: number;
}

// TODO: frame buffer optimization (apple2, etc)

export interface SampledAudioParams {
  sampleRate: number;
  stereo: boolean;
}

export interface SampledAudioSink {
  feedSample(value: number, count: number): void;
  //sendAudioFrame(samples:Uint16Array) : void;
}

export interface SampledAudioSource {
  getAudioParams(): SampledAudioParams;
  connectAudio(audio: SampledAudioSink): void;
}

export interface AcceptsROM {
  loadROM(data: Uint8Array, title?: string): void;
}

export interface AcceptsBIOS {
  loadBIOS(data: Uint8Array, title?: string): void;
}

export interface Resettable {
  reset(): void;
}

export interface MemoryBusConnected {
  connectMemoryBus(bus: Bus): void;
}

export interface IOBusConnected {
  connectIOBus(bus: Bus): void;
}

export interface CPU extends MemoryBusConnected, Resettable, SavesState<any> {
  getPC(): number;
  getSP(): number;
  isStable(): boolean;
}

export interface HasCPU extends Resettable {
  cpu: CPU;
}

export interface Interruptable<IT> {
  interrupt(type: IT): void;
}

export interface SavesInputState<CS> {
  loadControlsState(cs: CS): void;
  saveControlsState(): CS;
}

export interface AcceptsKeyInput {
  setKeyInput(key: number, code: number, flags: number): void;
}

export interface AcceptsPaddleInput {
  setPaddleInput(controller: number, value: number): void;
}

// TODO: interface not yet used (setKeyInput() handles joystick)
export interface AcceptsJoyInput {
  setJoyInput(joy: number, bitmask: number): void;
}

// SERIAL I/O

export interface SerialEvent {
  op: 'read' | 'write';
  value: number;
  nbits: number;
}

// TODO: all these needed?
export interface SerialIOInterface {
  // from machine to platform
  clearToSend(): boolean;
  sendByte(b: number);
  // from platform to machine
  byteAvailable(): boolean;
  recvByte(): number;
  // implement these too
  reset(): void;
  advance(clocks: number): void;
  //  refresh() : void;
}

export interface HasSerialIO {
  connectSerialIO(serial: SerialIOInterface);
  serialOut?: SerialEvent[];    // outgoing event log
  serialIn?: SerialEvent[];     // incoming queue
}

/// PROFILER

export interface Probeable {
  connectProbe(probe: ProbeAll): void;
}

export interface ProbeTime {
  logClocks(clocks: number);
  logNewScanline();
  logNewFrame();
}

export interface ProbeCPU {
  logExecute(address: number, SP: number);
  logInterrupt(type: number);
  logIllegal(address: number);
  logWait(address: number);
}

export interface ProbeBus {
  logRead(address: number, value: number);
  logWrite(address: number, value: number);
  logDMARead(address: number, value: number);
  logDMAWrite(address: number, value: number);
}

export interface ProbeIO {
  logIORead(address: number, value: number);
  logIOWrite(address: number, value: number);
}

export interface ProbeVRAM {
  logVRAMRead(address: number, value: number);
  logVRAMWrite(address: number, value: number);
}

export interface ProbeAll extends ProbeTime, ProbeCPU, ProbeBus, ProbeIO, ProbeVRAM {
  logData(data: number); // entire 32 bits
  addLogBuffer(src: Uint32Array);
}

export class NullProbe implements ProbeAll {
  logClocks() { }
  logNewScanline() { }
  logNewFrame() { }
  logExecute() { }
  logInterrupt() { }
  logRead() { }
  logWrite() { }
  logIORead() { }
  logIOWrite() { }
  logVRAMRead() { }
  logVRAMWrite() { }
  logIllegal() { }
  logWait() { }
  logDMARead() { }
  logDMAWrite() { }
  logData() { }
  addLogBuffer(src: Uint32Array) { }
}

/// CONVENIENCE

export interface BasicMachineControlsState {
  inputs: Uint8Array;
}

export interface BasicMachineState extends BasicMachineControlsState {
  c: any; // TODO
  ram: Uint8Array;
}

export abstract class BasicHeadlessMachine implements HasCPU, Bus, AcceptsROM, Probeable,
  SavesState<BasicMachineState>, SavesInputState<BasicMachineControlsState> {

  abstract cpuFrequency: number;
  abstract defaultROMSize: number;

  abstract cpu: CPU;
  abstract ram: Uint8Array;

  rom: Uint8Array;
  inputs: Uint8Array = new Uint8Array(32);
  handler: (key, code, flags) => void; // keyboard handler

  nullProbe = new NullProbe();
  probe: ProbeAll = this.nullProbe;

  abstract read(a: number): number;
  abstract write(a: number, v: number): void;

  setKeyInput(key: number, code: number, flags: number): void {
    this.handler && this.handler(key, code, flags);
  }
  connectProbe(probe: ProbeAll): void {
    this.probe = probe || this.nullProbe;
  }
  reset() {
    this.cpu.reset();
  }
  loadROM(data: Uint8Array, title?: string): void {
    if (!this.rom) this.rom = new Uint8Array(this.defaultROMSize);
    if (data.length > this.rom.length)
      throw new Error(`ROM too big: ${data.length} > ${this.rom.length}`);
    this.rom.set(data);
  }
  loadState(state) {
    this.cpu.loadState(state.c);
    this.ram.set(state.ram);
    this.inputs.set(state.inputs);
  }
  saveState() {
    return {
      c: this.cpu.saveState(),
      ram: this.ram.slice(0),
      inputs: this.inputs.slice(0),
    };
  }
  loadControlsState(state) {
    this.inputs.set(state.inputs);
  }
  saveControlsState() {
    return {
      inputs: this.inputs.slice(0)
    };
  }
  advanceCPU() {
    var c = this.cpu as any;
    var n = 1;
    if (this.cpu.isStable()) { this.probe.logExecute(this.cpu.getPC(), this.cpu.getSP()); }
    if (c.advanceClock) { c.advanceClock(); }
    else if (c.advanceInsn) { n = c.advanceInsn(1); }
    this.probe.logClocks(n);
    return n;
  }
  probeMemoryBus(membus: Bus & Partial<Bus32>): Bus & Partial<Bus32> {
    return {
      read: (a) => {
        let val = membus.read(a);
        this.probe.logRead(a, val);
        return val;
      },
      write: (a, v) => {
        this.probe.logWrite(a, v);
        membus.write(a, v);
      },
      read32: (a) => {
        let val = membus.read32(a);
        this.probe.logRead(a, val);
        return val;
      },
      write32: (a, v) => {
        this.probe.logWrite(a, v);
        membus.write32(a, v);
      }
    };
  }
  connectCPUMemoryBus(membus: Bus): void {
    this.cpu.connectMemoryBus(this.probeMemoryBus(membus as Bus&Bus32));
  }
  probeIOBus(iobus: Bus): Bus {
    return {
      read: (a) => {
        let val = iobus.read(a);
        this.probe.logIORead(a, val);
        return val;
      },
      write: (a, v) => {
        this.probe.logIOWrite(a, v);
        iobus.write(a, v);
      },
    };
  }
  probeDMABus(iobus: Bus): Bus {
    return {
      read: (a) => {
        let val = iobus.read(a);
        this.probe.logDMARead(a, val);
        return val;
      },
      write: (a, v) => {
        this.probe.logDMAWrite(a, v);
        iobus.write(a, v);
      }
    };
  }
  connectCPUIOBus(iobus: Bus): void {
    this.cpu['connectIOBus'](this.probeIOBus(iobus));
  }
}

export abstract class BasicMachine extends BasicHeadlessMachine implements SampledAudioSource {

  abstract canvasWidth: number;
  abstract numVisibleScanlines: number;
  abstract sampleRate: number;
  overscan: boolean = false;
  rotate: number = 0;
  aspectRatio: number;

  pixels: Uint32Array;
  audio: SampledAudioSink;

  scanline: number;

  getAudioParams(): SampledAudioParams {
    return { sampleRate: this.sampleRate, stereo: false };
  }
  connectAudio(audio: SampledAudioSink): void {
    this.audio = audio;
  }
  getVideoParams(): VideoParams {
    return {
      width: this.canvasWidth,
      height: this.numVisibleScanlines,
      aspect: this.aspectRatio,
      overscan: this.overscan,
      rotate: this.rotate
    };
  }
  connectVideo(pixels: Uint32Array): void {
    this.pixels = pixels;
  }
}

export abstract class BasicScanlineMachine extends BasicMachine implements RasterFrameBased {

  abstract numTotalScanlines: number;
  abstract cpuCyclesPerLine: number;

  abstract startScanline(): void;
  abstract drawScanline(): void;

  frameCycles: number;

  advanceFrame(trap: TrapCondition): number {
    this.preFrame();
    var endLineClock = 0;
    var steps = 0;
    this.probe.logNewFrame();
    this.frameCycles = 0;
    for (var sl = 0; sl < this.numTotalScanlines; sl++) {
      endLineClock += this.cpuCyclesPerLine; // could be fractional
      this.scanline = sl;
      this.startScanline();
      while (this.frameCycles < endLineClock) {
        if (trap && trap()) {
          sl = 999;
          break;
        }
        this.frameCycles += this.advanceCPU();
        steps++;
      }
      this.drawScanline();
      this.probe.logNewScanline();
      this.probe.logClocks(Math.floor(this.frameCycles - endLineClock)); // remainder of prev. line
    }
    this.postFrame();
    return steps; // TODO: return steps, not clock? for recorder
  }
  preFrame() { }
  postFrame() { }
  getRasterY() { return this.scanline; }
  getRasterX() { return this.frameCycles % this.cpuCyclesPerLine; }
}
