
import { Platform, BasePlatform } from "../common/baseplatform";
import { PLATFORMS, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, getMousePos, KeyFlags } from "../common/emu";
import { SampleAudio } from "../common/audio";
import { WaveformView, WaveformProvider, WaveformMeta } from "../ide/waveform";
import { HDLModuleRunner, HDLModuleTrace, HDLUnit, isLogicType } from "../common/hdl/hdltypes";
import { HDLModuleJS } from "../common/hdl/hdlruntime";
import { HDLModuleWASM } from "../common/hdl/hdlwasm";
import Split = require("split.js");
import { FileData } from "../common/workertypes";

interface WaveformSignal extends WaveformMeta {
  name: string;
}

var VERILOG_PRESETS = [
  {id:'clock_divider.v', name:'Clock Divider'},
  {id:'binary_counter.v', name:'Binary Counter'},
  {id:'hvsync_generator.v', name:'Video Sync Generator'},
  {id:'test_hvsync.v', name:'Test Pattern'},
  {id:'7segment.v', name:'7-Segment Decoder'},
  {id:'digits10.v', name:'Bitmapped Digits'},
  {id:'scoreboard.v', name:'Scoreboard'},
  {id:'ball_absolute.v', name:'Ball Motion (absolute position)'},
  {id:'ball_slip_counter.v', name:'Ball Motion (slipping counter)'},
  {id:'ball_paddle.v', name:'Brick Smash Game'},
  {id:'chardisplay.v', name:'RAM Text Display'},
  {id:'switches.v', name:'Switch Inputs'},
  {id:'paddles.v', name:'Paddle Inputs'},
  {id:'sprite_bitmap.v', name:'Sprite Bitmaps'},
  {id:'sprite_renderer.v', name:'Sprite Rendering'},
  {id:'racing_game.v', name:'Racing Game'},
  {id:'sprite_rotation.v', name:'Sprite Rotation'},
  {id:'tank.v', name:'Tank Game'},
  {id:'sound_generator.v', name:'Sound Generator'},
  {id:'lfsr.v', name:'Linear Feedback Shift Register'},
  {id:'starfield.v', name:'Scrolling Starfield'},

  {id:'alu.v', name:'ALU', category:'CPU'},
  {id:'cpu8.v', name:'Simple 8-Bit CPU'},
  {id:'racing_game_cpu.v', name:'Racing Game with CPU'},
  {id:'framebuffer.v', name:'Frame Buffer'},
  {id:'tile_renderer.v', name:'Tile Renderer'},
  {id:'sprite_scanline_renderer.v', name:'Sprite Scanline Renderer'},
  {id:'cpu16.v', name:'16-Bit CPU'},
  {id:'cpu_platform.v', name:'CPU Platform'},
  {id:'test2.asm', name:'16-bit ASM Game'},

  {id:'cpu6502.v', name:'6502 CPU', category:'Extra'},
  {id:'femtorv32.v', name:'RISC-V CPU'},
  {id:'framebuffer_riscv.v', name:'RISC-V Frame Buffer'},

  {id:'test_pattern.ice', name:'Test Pattern', category:'Silice'},
  {id:'copperbars.ice', name:'Animated Bars'},
  {id:'rototexture.ice', name:'Rotating Texture'},
  //{id:'life.ice', name:'Conway\'s Life (Silice)'},
];

var VERILOG_KEYCODE_MAP = makeKeycodeMap([
  [Keys.LEFT,  0, 0x1],
  [Keys.RIGHT, 0, 0x2],
  [Keys.UP,    0, 0x4],
  [Keys.DOWN,  0, 0x8],
  [Keys.A,     0, 0x10],
  [Keys.B,     0, 0x20],
  [Keys.P2_LEFT,  1, 0x1],
  [Keys.P2_RIGHT, 1, 0x2],
  [Keys.P2_UP,    1, 0x4],
  [Keys.P2_DOWN,  1, 0x8],
  [Keys.P2_A,     1, 0x10],
  [Keys.P2_B,     1, 0x20],
  [Keys.START,     2, 0x1],
  [Keys.P2_START,  2, 0x2],
  [Keys.SELECT,    2, 0x4],
  [Keys.P2_SELECT, 2, 0x8],
  [Keys.VK_7, 2, 0x10],
]);

const TRACE_BUFFER_DWORDS = 0x40000;
const CYCLES_PER_FILL = 20;
const SHOW_INTERNAL_SIGNALS = false; // TODO: make this a config value

// PLATFORM

var VerilogPlatform = function(mainElement, options) {
  this.__proto__ = new (BasePlatform as any)();
  
  var video : RasterVideo;
  var audio;
  var poller;
  var useAudio = false;
  var usePaddles = false;
  var videoWidth  = 292;
  var videoHeight = 256;
  var maxVideoLines = 262+40; // vertical hold
  var idata : Uint32Array;
  var timer : AnimationTimer;
  var timerCallback;
  var top : HDLModuleRunner;
  var cyclesPerFrame = (256+23+7+23)*262; // 4857480/60 Hz

  // control inputs
  var switches = [0,0,0];
  var keycode = 0;

  // inspect feature  
  var inspect_obj, inspect_sym;
  var inspect_data = new Uint32Array(videoWidth * videoHeight);

  // for scope
  var module_name;
  //var trace_ports;
  var trace_signals;
  var trace_buffer;
  var trace_index;

  // for virtual CRT
  var framex=0;
  var framey=0;
  var frameidx=0;
  var framehsync=false;
  var framevsync=false;
  var scanlineCycles = 0;

  var RGBLOOKUP = [
    0xff222222,
    0xff2222ff,
    0xff22ff22,
    0xff22ffff,
    0xffff2222,
    0xffff22ff,
    0xffffff22,
    0xffffffff,
    0xff999999,
    0xff9999ff,
    0xff99ff99,
    0xff99ffff,
    0xffff9999,
    0xffff99ff,
    0xffffff99,
    0xff666666,
  ];

  var debugCond;
  var frameRate = 0;

  function vidtick() {
    top.tick2(1);
    if (useAudio) {
      audio.feedSample(top.state.spkr, 1);
    }
    resetKbdStrobe();
    if (debugCond && debugCond()) {
      debugCond = null;
    }
  }

  function resetKbdStrobe() {
    if (keycode && keycode >= 128 && top.state.keystrobe) { // keystrobe = clear hi bit of key buffer
      keycode = keycode & 0x7f;
      top.state.keycode = keycode;
    }
  }

  // inner Platform class
    
 class _VerilogPlatform extends BasePlatform implements WaveformProvider {
 
  waveview : WaveformView;
  wavediv : JQuery;
  topdiv : JQuery;
  split;
  hasvideo : boolean;

  sourceFileFetch : (path:string) => FileData;

  getPresets() { return VERILOG_PRESETS; }

  setVideoParams(width:number, height:number, clock:number) {
    videoWidth = width;
    videoHeight = height;
    cyclesPerFrame = clock;
    maxVideoLines = height+40;
  }

  async start() {
    //await loadScript('./lib/binaryen.js'); // TODO: remove
    video = new RasterVideo(mainElement,videoWidth,videoHeight,{overscan:true});
    video.create();
    poller = setKeyboardFromMap(video, switches, VERILOG_KEYCODE_MAP, (o,key,code,flags) => {
      if (flags & KeyFlags.KeyDown) {
        keycode = code | 0x80;
      }
    }, true); // true = always send function
    var vcanvas = $(video.canvas);
    idata = video.getFrameData();
    timerCallback = () => {
      if (!this.isRunning())
        return;
      if (top) top.state.switches = switches[0];
      this.updateFrame();
    };
    this.setFrameRate(60);
    // setup scope
    trace_buffer = new Uint32Array(TRACE_BUFFER_DWORDS);
    var overlay = $("#emuoverlay").show();
    this.topdiv = $('<div class="emuspacer">').appendTo(overlay);
    vcanvas.appendTo(this.topdiv);
    this.wavediv = $('<div class="emuscope">').appendTo(overlay);
    this.split = Split( [this.topdiv[0], this.wavediv[0]], {
      minSize: [0,0],
      sizes: [99,1],
      direction: 'vertical',
      gutterSize: 16,
      onDrag: () => {
        this.resize();
        //if (this.waveview) this.waveview.recreate();
        //vcanvas.css('position','relative');
        //vcanvas.css('top', -this.wavediv.height()+'px');
      },
    });
    // setup mouse events
    video.setupMouseEvents();
  }
  
  // TODO: pollControls() { poller.poll(); }
  
  resize() {
    if (this.waveview) this.waveview.recreate();
  }
  
  setGenInputs() {
    useAudio = audio != null && top.state.spkr != null;
    usePaddles = top.state.hpaddle != null || top.state.vpaddle != null;
    //TODO debugCond = this.getDebugCallback();
    top.state.switches_p1 = switches[0];
    top.state.switches_p2 = switches[1];
    top.state.switches_gen = switches[2];
    top.state.keycode = keycode;
  }
  
  updateVideoFrame() {
    //this.topdiv.show(); //show crt
    this.setGenInputs();
    var fps = this.getFrameRate();
    // darken the previous frame?
    var sync = fps > 45;
    if (!sync) {
      var mask = fps > 5 ? 0xe7ffffff : 0x7fdddddd;
      for (var i=0; i<idata.length; i++)
        idata[i] &= mask;
    }
    // paint into frame, synched with vsync if full speed
    var trace = this.isScopeVisible();
    this.updateVideoFrameCycles(Math.ceil(cyclesPerFrame * fps/60), sync, trace);
    if (fps < 0.25) {
      idata[frameidx] = -1;
    }
    //this.restartDebugState();
    this.refreshVideoFrame();
    // set scope offset
    if (trace && this.waveview) {
      this.waveview.setCurrentTime(Math.floor(trace_index/trace_signals.length));
    }
  }
  
  isScopeVisible() {
    return this.split.getSizes()[1] > 2; // TODO?
  }

  // TODO: merge with prev func  
  advance(novideo : boolean) : number {
    this.setGenInputs();
    this.updateVideoFrameCycles(cyclesPerFrame, true, false);
    if (!novideo) {
      this.refreshVideoFrame();
    }
    if (this.isBlocked()) {
      this.pause();
    }
    return cyclesPerFrame; //TODO?
  }
  
  refreshVideoFrame() {
    this.updateInspectionFrame();
    video.updateFrame();
    this.updateInspectionPostFrame();
  }
  
  refreshScopeOverlay() {
    // TODO
  }
  
  updateScopeFrame() {
    this.split.setSizes([0,100]); // ensure scope visible
    //this.topdiv.hide();// hide crt
    var done = this.fillTraceBuffer(CYCLES_PER_FILL * trace_signals.length);
    if (done)
      this.pause(); // TODO?
    // TODO
  }
  
  updateScope() {
    // create scope, if visible
    if (this.isScopeVisible()) {
      if (!this.waveview) {
        this.waveview = new WaveformView(this.wavediv[0] as HTMLElement, this);
      } else {
        this.waveview.refresh();
      }
    }
  }

  updateFrame() {
    if (!top) return;
    if (this.hasvideo)
      this.updateVideoFrame();
    else
      this.updateScopeFrame();
    this.updateScope();
  }

  updateInspectionFrame() {
    useAudio = false;
    if (inspect_obj && inspect_sym) {
      var COLOR_BIT_OFF = 0xffff6666;
      var COLOR_BIT_ON  = 0xffff9999;
      var i = videoWidth;
      for (var y=0; y<videoHeight-2; y++) {
        for (var x=0; x<videoWidth; x++) {
          var val = inspect_data[i];
          idata[i++] = (val & 1) ? COLOR_BIT_ON : COLOR_BIT_OFF;
        }
      }
    }
  }

  updateInspectionPostFrame() {
    if (inspect_obj && inspect_sym) {
      var ctx = video.getContext();
      var val = inspect_data[inspect_data.length-1];
      ctx.fillStyle = "black";
      ctx.fillRect(18, videoHeight-8, 100, 8);
      ctx.fillStyle = "white";
      ctx.fillText(val.toString(10) + " $" + val.toString(16), 20, videoHeight-1);
    }
  }

  updateVideoFrameCycles(ncycles:number, sync:boolean, trace:boolean) : void {
    ncycles |= 0;
    var inspect = inspect_obj != null && inspect_sym != null;
    // use fast trace buffer-based update?
    if (sync && !trace && !inspect && (top as HDLModuleTrace).trace != null && scanlineCycles > 0) {
      this.updateVideoFrameFast((top as any) as HDLModuleTrace);
      this.updateRecorder();
      return;
    }
    // use slow cycle-by-cycle version (needed on 1st frame to set scanlineCycles anyway)
    if (!sync) scanlineCycles = 0;
    var trace0 = trace_index;
    while (ncycles--) {
      if (trace) {
        this.snapshotTrace();
        if (trace_index == trace0) trace = false; // kill trace when wraps around
      }
      vidtick();
      if (framex++ < videoWidth) {
        if (framey < videoHeight) {
          if (inspect) {
            inspect_data[frameidx] = inspect_obj[inspect_sym];
          }
          let rgb = top.state.rgb;
          idata[frameidx] = rgb & 0x80000000 ? rgb : RGBLOOKUP[rgb & 15];
          frameidx++;
        }
      } else if (!framehsync && top.state.hsync) {
        framehsync = true;
      } else if ((framehsync && !top.state.hsync) || framex > videoWidth*2) {
        if (sync && framehsync) scanlineCycles = framex; // set cycles/scanline for fast update function
        framehsync = false;
        framex = 0;
        framey++;
        top.state.hpaddle = framey > video.paddle_x ? 1 : 0;
        top.state.vpaddle = framey > video.paddle_y ? 1 : 0;
      }
      if (framey > maxVideoLines || top.state.vsync) {
        framevsync = true;
        framey = 0;
        framex = 0;
        frameidx = 0;
        top.state.hpaddle = 0;
        top.state.vpaddle = 0;
      } else {
        var wasvsync = framevsync;
        framevsync = false;
        if (sync && wasvsync) {
          this.updateRecorder();
          return; // exit when vsync ends
        }
      }
    }
  }

  tick2(cycles: number) {
    // if a key is pressed, check for strobe after every cycle
    if (keycode >= 128) {
      while (cycles-- > 0) {
        top.tick2(1);
        resetKbdStrobe();
      } 
    } else {
      top.tick2(cycles);
    }
  }

  // use trace buffer to update video
  updateVideoFrameFast(tmod: HDLModuleTrace) {
    if (scanlineCycles <= 0) throw new Error(`scanlineCycles must be > 0`);
    var maxLineCycles = 1009; // prime number so we eventually sync up
    var nextlineCycles = scanlineCycles || maxLineCycles;
    frameidx = 0;
    // audio feed
    function spkr() { if (useAudio) audio.feedSample(tmod.trace.spkr, 1); }
    // iterate through a frame of scanlines + room for vsync
    for (framey=0; framey<videoHeight*2; framey++) {
      if (usePaddles && framey < videoHeight) {
        top.state.hpaddle = framey > video.paddle_x ? 1 : 0;
        top.state.vpaddle = framey > video.paddle_y ? 1 : 0;
      }
      // generate frames in trace buffer
      if (nextlineCycles > 0) {
        this.tick2(nextlineCycles);
      }
      // convert trace buffer to video/audio
      var n = 0;
      // draw scanline visible pixels
      if (framey < videoHeight) {
        for (framex=0; framex<videoWidth; framex++) {
          var rgb = tmod.trace.rgb;
          //if (tmod.trace.hsync) rgb ^= Math.random() * 15;
          idata[frameidx++] = rgb & 0x80000000 ? rgb : RGBLOOKUP[rgb & 15];
          spkr();
          tmod.nextTrace();
        }
        n += videoWidth;
      }
      // find hsync
      var hsyncStart=0, hsyncEnd=0;
      while (n < nextlineCycles) {
        if (tmod.trace.hsync) {
          if (!hsyncStart) hsyncStart = n;
          hsyncEnd = n;
        } else if (hsyncEnd) {
          break;
        }
        spkr();
        tmod.nextTrace();
        n++;
      }
      // see if our scanline cycle count is stable (can't read tmod.trace after end of line)
      if (hsyncStart < hsyncEnd && hsyncEnd == nextlineCycles-1) {
        // scanline cycle count locked in, reset buffer to improve cache locality
        nextlineCycles = scanlineCycles;
      } else if (hsyncEnd > 0) {
        // our cycle count is not in sync with scanline
        // say our scanline lasts 100 cycles
        // we just read 300 cycles, and hsync ended at 80
        // we'll toss the extra cycles in the buffer
        // next scanline should end @ (80 + 100*N) cycles
        // could be 180, 280, 380 ...
        // so we should read 100*N - 300 cycles where N > 2
        // TODO: determine scanlineCycles here instead of letting slow loop do it
        let newCycles = scanlineCycles * 2 - ((nextlineCycles - n) % scanlineCycles);
        //console.log('scanline', framey, scanlineCycles, nextlineCycles, n, hsyncStart, hsyncEnd, newCycles);
        nextlineCycles = newCycles;
      } else {
        nextlineCycles = maxLineCycles;
      }
      tmod.resetTrace();
      // exit when vsync starts and then stops
      if (tmod.trace.vsync) {
        framevsync = true;
        top.state.hpaddle = 0;
        top.state.vpaddle = 0;
        framex = framey = frameidx = 0;
      } else if (framevsync) {
        framevsync = false;
        break;
      }
    }
  }

  snapshotTrace() {
    var arr = trace_signals;
    for (var i=0; i<arr.length; i++) {
      var v = arr[i];
      var z = top.state[v.name];
      trace_buffer[trace_index] = z+0;
      trace_index++;
    }
    if (trace_index >= trace_buffer.length - arr.length)
      trace_index = 0;
  }

  fillTraceBuffer(count:number) : boolean {
    var max_index = Math.min(trace_buffer.length - trace_signals.length, trace_index + count);
    while (trace_index < max_index) {
      this.snapshotTrace();
      if (!top.isStopped() && !top.isFinished()) {
        top.tick();
      }
      if (trace_index == 0)
        break;
    }
    top.state.reset = 0; // need to de-assert reset when using no-video mode
    return (trace_index == 0);
  }
  
  getSignalMetadata() : WaveformMeta[] {
    return trace_signals;
  }
  
  getSignalData(index:number, start:number, len:number) : number[] {
    // TODO: not efficient
    var skip = this.getSignalMetadata().length;
    var last = trace_buffer.length - trace_signals.length; // TODO: refactor, and not correct
    var wrap = this.hasvideo; // TODO?
    var a = [];
    index += skip * start;
    while (index < last && a.length < len) {
      a.push(trace_buffer[index]);
      index += skip;
      if (wrap && index >= last) // TODO: what if starts with index==last
        index = 0;
    }
    return a;
  }

  setSignalValue(index:number, value:number) {
    var meta = this.getSignalMetadata()[index];
    top.state[meta.label] = value;
    this.reset();
  }

  printErrorCodeContext(e, code) {
    if (e.lineNumber && e.message) {
      var lines = code.split('\n');
      var s = e.message + '\n';
      for (var i=0; i<lines.length; i++) {
        if (i > e.lineNumber-5 && i < e.lineNumber+5) {
          s += lines[i] + '\n';
        }
      }
      console.log(s);
    }
  }

  dispose() {
    if (top) {
        top.dispose();
        top = null;
    }
  }

  async loadROM(title:string, output:any) {
    var unit = output as HDLUnit;
    var topmod = unit.modules['TOP'];
    if (unit.modules && topmod) {
      {
        // initialize top module and constant pool
        var useWASM = true;
        var topcons = useWASM ? HDLModuleWASM : HDLModuleJS;
        var _top = new topcons(topmod, unit.modules['@CONST-POOL@']);
        _top.getFileData = this.sourceFileFetch;
        await _top.init();
        this.dispose();
        top = _top;
        // create signal array
        var signals : WaveformSignal[] = [];
        for (var key in topmod.vardefs) {
          var vardef = topmod.vardefs[key];
          if (isLogicType(vardef.dtype)) {
            signals.push({
              name: key,
              label: vardef.origName,
              input: vardef.isInput,
              output: vardef.isOutput,
              len: vardef.dtype.left+1
            });
          }
        }
        trace_signals = signals;
        if (!SHOW_INTERNAL_SIGNALS) {
          trace_signals = trace_signals.filter((v) => { return !v.label.startsWith("__V"); }); // remove __Vclklast etc
        }
        trace_index = 0;
        // reset
        if (top instanceof HDLModuleWASM) {
          top.randomizeOnReset = true;
        }
        // query output signals -- video or not?
        this.hasvideo = top.state.vsync != null && top.state.hsync != null && top.state.rgb != null;
        if (this.hasvideo) {
          const IGNORE_SIGNALS = ['clk','reset'];
          trace_signals = trace_signals.filter((v) => { return IGNORE_SIGNALS.indexOf(v.name)<0; }); // remove clk, reset
          this.showVideoControls();
        } else {
          this.hideVideoControls();
        }
      }
    }
    // randomize values
    top.powercycle();
    // replace program ROM, if using the assembler
    // TODO: fix this, it ain't good
    if (output.program_rom && output.program_rom_variable) {
      if (top.state[output.program_rom_variable]) {
        if (top.state[output.program_rom_variable].length != output.program_rom.length)
          alert("ROM size mismatch -- expected " + top.state[output.program_rom_variable].length + " got " + output.program_rom.length);
        else
          top.state[output.program_rom_variable].set(output.program_rom);
      } else {
        alert("No program_rom variable found (" + output.program_rom_variable + ")");
      }
    }
    // restart audio
    this.restartAudio();
    if (this.waveview) {
      this.waveview.recreate();
    }
    // assert reset pin, wait 100 cycles if using video
    this.reset();
  }
 
  showVideoControls() {
    $("#speed_bar").show();
    $("#run_bar").show();
    $("#dbg_record").show();
  }

  hideVideoControls() {
    $("#speed_bar").hide();
    $("#run_bar").hide();
    $("#dbg_record").hide();
  }
  
  restartAudio() {
    // stop/start audio
    var hasAudio = top && top.state.spkr != null && frameRate > 1;
    if (audio && !hasAudio) {
      audio.stop();
      audio = null;
    } else if (!audio && hasAudio) {
      audio = new SampleAudio(cyclesPerFrame * this.getFrameRate());
      if (this.isRunning())
        audio.start();
    }
  }

  isRunning() {
    return timer && timer.isRunning();
  }
  pause() {
    timer.stop();
    if (audio) audio.stop();
  }
  resume() {
    timer.start();
    if (audio) audio.start();
  }

  isBlocked() {
    return top && top.isFinished();
  }
  isStopped() {
    return top && top.isStopped();
  }

  setFrameRate(rateHz) {
    frameRate = rateHz;
    var fps = Math.min(60, rateHz*cyclesPerFrame);
    if (!timer || timer.frameRate != fps) {
      var running = this.isRunning();
      if (timer) timer.stop();
      timer = new AnimationTimer(fps, timerCallback);
      if (running) timer.start();
    }
    if (audio) {
      audio.stop();
      audio = null;
    }
    this.restartAudio();
  }  
  getFrameRate() { return frameRate; }

  reset() {
    if (!top) return;
    // TODO: how do we avoid clobbering user-modified signals?
    trace_index = 0;
    if (trace_buffer) trace_buffer.fill(0);
    if (video) video.setRotate(top.state.rotate ? -90 : 0);
    $("#verilog_bar").hide();
    if (this.hasvideo) {
      top.state.reset = 1;
      top.tick2(100);
      top.state.reset = 0;
    } else {
      top.state.reset = 1; // reset will be de-asserted later
      this.resume(); // TODO?
    }
  }
  tick() {
    if (!top) return;
    top.tick2(1);
  }
  getToolForFilename(fn) {
    if (fn.endsWith(".asm")) return "jsasm";
    else if (fn.endsWith(".ice")) return "silice";
    else return "verilator";
  }
  getDefaultExtensions() { return [".v", ".asm", ".ice"]; }

  inspect(name:string) : string {
    if (!top) return;
    // check for valid identifier
    if (!name || !name.match(/^\w+$/)) { 
      inspect_obj = inspect_sym = null;
      return;
    }
    // search for partial name
    var val;
    for (let key in top.state) {
      if (key == name || key.endsWith("$"+name)) {
        name = key;
        val = top.state[name];
      }
    }
    // did we find a number?
    if (typeof(val) === 'number') {
      inspect_obj = top.state;
      inspect_sym = name;
    } else {
      inspect_obj = inspect_sym = null;
    }
  }

  // DEBUGGING

  getDebugTree() {
    return {
      runtime: top,
      state: top && top.getGlobals()
    }
  }

  saveState() {
    return {o: top && top.saveState()};
  }

  loadState(state) {
    if (state.o) top.loadState(state.o);
  }

  saveControlsState() {
    return {
      p1x: video.paddle_x,
      p1y: video.paddle_y,
      sw0: switches[0],
      sw1: switches[1],
      sw2: switches[2],
      keycode: keycode
    };
  }
  loadControlsState(state) {
    video.paddle_x = state.p1x;
    video.paddle_y = state.p1y;
    switches[0] = state.sw0;
    switches[1] = state.sw1;
    switches[2] = state.sw2;
    keycode = state.keycode;
  }
  getDownloadFile() {
    if (top instanceof HDLModuleJS) {
      return {
        extension:".js", 
        blob: new Blob([top.getJSCode()], {type:"text/plain"})
      };
    } else if (top instanceof HDLModuleWASM) {
      return {
        extension:".wat",
        blob: new Blob([top.bmod.emitText()], {type:"text/plain"})
      };
    }
  }
  getHDLModuleRunner() {
    return top;
  }
  showHelp() {
    return "https://8bitworkshop.com/docs/platforms/verilog/";
  }

 } // end of inner class
 return new _VerilogPlatform();  
};

////////////////

var VERILOG_VGA_PRESETS = [
  {id:'hvsync_generator.v', name:'Video Sync Generator'},
  {id:'test_hvsync.v', name:'Test Pattern'},
  {id:'chardisplay.v', name:'RAM Text Display'},
  {id:'starfield.v', name:'Scrolling Starfield'},
  {id:'ball_paddle.v', name:'Brick Smash Game'},
];


var VerilogVGAPlatform = function(mainElement, options) {
  this.__proto__ = new (VerilogPlatform as any)(mainElement, options);

  this.getPresets = function() { return VERILOG_VGA_PRESETS; }

  this.setVideoParams(800-64, 520, 25000000);
}

////////////////

PLATFORMS['verilog'] = VerilogPlatform;
PLATFORMS['verilog-vga'] = VerilogVGAPlatform;
PLATFORMS['verilog-test'] = VerilogPlatform;
