
import { Z80, Z80State } from "../common/cpu/ZilogZ80";
import { BasicScanlineMachine, AcceptsPaddleInput, Bus } from "../common/devices";
import { KeyFlags, newAddressDecoder, padBytes, Keys, makeKeycodeMap, newKeyboardHandler } from "../common/emu";
import { TssChannelAdapter, MasterAudio, AY38910_Audio } from "../common/audio";
import { hex, rgb2bgr, lzgmini, stringToByteArray } from "../common/util";

// http://metopal.com/projects/ballybook/doku.php

// TODO: fix keys, more controllers, vibrato/noise, border color
// http://atariage.com/forums/topic/251416-programming-the-bally-arcadeastrocade/
// https://ballyalley.com/faqs/BPA%20Video%20Hardware%20FAQ/Bally_Professional_Arcade_Video_Hardware%20(2001)(Tony%20Miller).pdf
// https://atariage.com/forums/topic/290858-notes-from-a-2019-interview-with-jamie-fenton/ (http://www.fentonia.com/bio/)
// https://thehistoryofhowweplay.wordpress.com/2018/04/03/interview-tom-mchugh/
// https://ballyalley.com/faqs/Programmers%20of%20the%20Astrocade%20Built-In%20Programs.txt

const ASTROCADE_KEYCODE_MAP = makeKeycodeMap([
  // player 1
  [Keys.UP, 0x10, 0x1],
  [Keys.DOWN, 0x10, 0x2],
  [Keys.LEFT, 0x10, 0x4],
  [Keys.RIGHT, 0x10, 0x8],
  [Keys.A, 0x10, 0x10],
  // player 2
  [Keys.P2_UP, 0x11, 0x1],
  [Keys.P2_DOWN, 0x11, 0x2],
  [Keys.P2_LEFT, 0x11, 0x4],
  [Keys.P2_RIGHT, 0x11, 0x8],
  [Keys.P2_A, 0x11, 0x10],
  // keypad $14
  [Keys.VK_P, 0x14, 0x1],
  [Keys.VK_SLASH, 0x14, 0x2],
  [Keys.VK_X, 0x14, 0x4],
  [Keys.VK_MINUS, 0x14, 0x8],
  [Keys.VK_COMMA, 0x14, 0x10],
  [Keys.VK_EQUALS, 0x14, 0x20],
  // keypad $15
  [Keys.VK_O, 0x15, 0x1],
  [Keys.VK_L, 0x15, 0x2],
  [Keys.VK_9, 0x15, 0x4],
  [Keys.VK_6, 0x15, 0x8],
  [Keys.VK_3, 0x15, 0x10],
  [Keys.VK_PERIOD, 0x15, 0x20],
  // keypad $16
  [Keys.VK_I, 0x16, 0x1],
  [Keys.VK_K, 0x16, 0x2],
  [Keys.VK_8, 0x16, 0x4],
  [Keys.VK_5, 0x16, 0x8],
  [Keys.VK_2, 0x16, 0x10],
  [Keys.VK_0, 0x16, 0x20],
  // keypad $17
  [Keys.VK_U, 0x17, 0x1],
  [Keys.VK_J, 0x17, 0x2],
  [Keys.VK_7, 0x17, 0x4],
  [Keys.VK_4, 0x17, 0x8],
  [Keys.VK_1, 0x17, 0x10],
  [Keys.VK_BACK_SLASH, 0x17, 0x20],
]);

const audioOversample = 2;

const _BallyAstrocade = function(arcade:boolean) {

  var cpu : Z80;
  var ram : Uint8Array;
  var membus, iobus : Bus;
  var rom, bios;
  var pixels;
  var psg;
  //var watchdog_counter;
  const swidth = arcade ? 320 : 160;
  const sheight = arcade ? 204 : 102;
  const swbytes = swidth >> 2;
  const samask = arcade ? 0x3fff : 0xfff;
  const INITIAL_WATCHDOG = 256;
  const PIXEL_ON = 0xffeeeeee;
  const PIXEL_OFF = 0xff000000;

  // state variables
  var inputs = new Uint8Array(0x20);
  var magicop = 0;
  var xpand = 0;
  var xplower = false;
  var shift2 = 0;
  var horcb = 0;
  var inmod = 0;
  var inlin = 0;
  var infbk = 0;
  var verbl = sheight;
  var palette = new Uint32Array(8);
  var palinds = new Uint8Array(8);
  var refreshlines = 0;
  var dirtylines = new Uint8Array(arcade ? 262 : 131);
  var rotdata = new Uint8Array(4);
  var rotcount = 0;
  var intst = 0;
  var waitstates = 0;
  var patboard = new Uint8Array(0x08);
  var patdest = 0;

  function ramwrite(a: number, v: number) {
    // set RAM
    ram[a] = v;
    waitstates++;
    // mark scanline as dirty
    dirtylines[((a & samask) / swbytes) | 0] = 1;
    // this was old behavior where we updated instantly
    // but it had problems if we had mid-screen palette changes
    //ramupdate(a, v);
  }

  function ramupdate(a: number, v: number) {
    var ofs = a * 4 + 3; // 4 pixels per byte
    for (var i = 0; i < 4; i++) {
      var lr = ((a % swbytes) >= (horcb & 0x3f)) ? 0 : 4;
      pixels[ofs--] = palette[lr + (v & 3)];
      v >>= 2;
    }
  }

  function refreshline(y: number) {
    var ofs = y * swidth / 4;
    for (var i = 0; i < swidth / 4; i++)
      ramupdate(ofs + i, ram[ofs + i]);
  }

  function magicwrite(a: number, v: number) {
    // expand
    if (magicop & 0x8) {
      var v2 = 0;
      if (!xplower)
        v >>= 4;
      for (var i = 0; i < 4; i++) {
        var pix = (v & 1) ? ((xpand >> 2) & 3) : (xpand & 3);
        v2 |= pix << (i * 2);
        v >>= 1;
      }
      v = v2;
      xplower = !xplower;
    }
    // rotate
    if (magicop & 0x4) {
      if (rotcount & 4) {
        // drain buffer
        var sh = 2 * (~rotcount & 3);
        v = (((rotdata[3] >> sh) & 3) << 6) |
          (((rotdata[2] >> sh) & 3) << 4) |
          (((rotdata[1] >> sh) & 3) << 2) |
          (((rotdata[0] >> sh) & 3) << 0);
      } else {
        // fill buffer
        rotdata[rotcount & 3] = v;
      }
      rotcount++;
    } else {
      // shift
      var sh = (magicop & 3) << 1;
      var v2 = (v >> sh) | shift2;
      shift2 = (v << (8 - sh)) & 0xff;
      v = v2;
    }
    // flop
    if (magicop & 0x40) {
      v =
        ((v & 0x03) << 6) |
        ((v & 0x0c) << 2) |
        ((v & 0x30) >> 2) |
        ((v & 0xc0) >> 6);
    }
    // or/xor
    if (magicop & 0x30) {
      var oldv = ram[a];
      // collision detect
      var icpt = 0;
      if ((oldv & 0xc0) && (v & 0xc0)) icpt |= 0x1;
      if ((oldv & 0x30) && (v & 0x30)) icpt |= 0x2;
      if ((oldv & 0x0c) && (v & 0x0c)) icpt |= 0x4;
      if ((oldv & 0x03) && (v & 0x03)) icpt |= 0x8;
      // apply op
      if (magicop & 0x10)
        v |= oldv;
      if (magicop & 0x20)
        v ^= oldv; // TODO: what if both?
      // upper 4 bits persist, lower are just since last write
      intst = (intst & 0xf0) | icpt | (icpt << 4);
    }
    // commit write to ram/screen
    ramwrite(a, v);
  }

  function setpalette(a: number, v: number) {
    palinds[a & 7] = v & 0xff;
    palette[a & 7] = ASTROCADE_PALETTE[v & 0xff];
    refreshall();
  }

  function setbordercolor() {
    var col = horcb >> 6;
    // TODO
  }

  function refreshall() {
    refreshlines = sheight;
  }

  // bally astrocade pattern board
  // https://github.com/mamedev/mame/blob/7ff10685c56a6e123336c684e5e96fdb9e8b3674/src/mame/midway/astrocde_v.cpp#L726
  function xfer_patboard() {
    let m_pattern_source = patboard[0] | (patboard[1] << 8);
    let m_pattern_mode = patboard[2] & 0x3f;
    let m_pattern_skip = patboard[3];
    let m_pattern_dest = (patdest & 0xff) | (patboard[4] << 8);
    let m_pattern_width = patboard[5];
    let m_pattern_height = patboard[6] + 1;

    let curwidth: number;
    let u13ff: number = 0;
    let cycles: number = 0;

    u13ff = 0;

    if ((m_pattern_mode & 0x02) === 0) {
      u13ff = 1;
    }

    function incrementSource(): void {
      if (u13ff && (m_pattern_mode & 0x04) !== 0 && (curwidth !== 0 || (m_pattern_mode & 0x08) === 0)) {
        m_pattern_source++;
      }

      if ((m_pattern_mode & 0x02) !== 0) {
        u13ff ^= 1;
      }
    }

    function incrementDest(): void {
      if (curwidth !== 0) {
        if ((m_pattern_mode & 0x20) !== 0) {
          m_pattern_dest++;
        } else {
          m_pattern_dest--;
        }
      }
    }

    // Loop over height
    while (m_pattern_height >= 0) {
      let carry: number;

      curwidth = m_pattern_width;

      // Loop over width
      while (curwidth >= 0) {
        let busaddr: number;
        let busdata: number;

        // Read Phase
        busaddr = (m_pattern_mode & 0x01) === 0 ? m_pattern_source : m_pattern_dest;

        if (curwidth === 0 && (m_pattern_mode & 0x08) !== 0) {
          busdata = 0;
        } else {
          busdata = membus.read(m_pattern_source);
        }

        if ((m_pattern_mode & 0x01) === 0) {
          incrementSource();
        } else {
          incrementDest();
        }

        // Write Phase
        busaddr = (m_pattern_mode & 0x01) !== 0 ? m_pattern_source : m_pattern_dest;
        ramwrite(busaddr, busdata);

        if ((m_pattern_mode & 0x01) === 0) {
          incrementDest();
        } else {
          incrementSource();
        }

        cycles += 4;
        curwidth--;
      }

      // At the end of each row, adjust m_pattern_dest
      carry = ((m_pattern_dest & 0xff) + m_pattern_skip) & 0x100;
      m_pattern_dest = (m_pattern_dest & 0xff00) | ((m_pattern_dest + m_pattern_skip) & 0xff);

      if ((m_pattern_mode & 0x10) === 0) {
        m_pattern_dest += carry;
      } else {
        m_pattern_dest -= carry ^ 0x100;
      }

      m_pattern_height--;
    }

    // Adjust m_maincpu.icount
    // m_maincpu.adjust_icount(-cycles);
    // Replace the above line with the actual adjustment of icount.
  }
    
  this.drawScanline = (sl:number) => {
    // interrupt
    if (sl == inlin && (inmod & 0x8)) {
      cpu.retryInterrupts = !(inmod & 0x4);
      this.probe && this.probe.logInterrupt(infbk);
      cpu.interrupt(infbk);
    }
    // refresh this line in frame buffer?
    if (sl < sheight && refreshlines > 0) {
      dirtylines[sl] = 0;
      refreshline(sl);
      refreshlines--;
    }
    else if (dirtylines[sl]) {
      dirtylines[sl] = 0;
      refreshline(sl);
    }
  }
  
  this.init = (machine:BallyAstrocade, c:Z80, r:Uint8Array, inp:Uint8Array<ArrayBuffer>, psgg) => {
    cpu = c;
    ram = r;
    inputs = inp;
    psg = psgg;
    if (!arcade) {
      //bios = padBytes(ASTROCADE_MINIMAL_BIOS, 0x2000);
      bios = padBytes(new lzgmini().decode(stringToByteArray(atob(ASTROLIBRE_BIOS_LZG))), 0x2000);
      // game console
      membus = {
        read: newAddressDecoder([
          [0x0000, 0x1fff, 0x1fff, function(a) { return bios[a]; }],
          [0x2000, 0x3fff, 0x1fff, function(a) { return rom ? rom[a] : 0; }],
          [0x4000, 0x4fff, 0xfff, function(a) { waitstates++; return ram[a]; }],
        ]),
        write: newAddressDecoder([
          [0x4000, 0x4fff, 0xfff, ramwrite],
          [0x0000, 0x3fff, 0xfff, magicwrite],
        ]),
      }
    } else {
      // arcade game (TODO: wait states 1/4 of the time)
      membus = {
        read: newAddressDecoder([
          [0x4000, 0x7fff, 0x3fff, function(a) { return ram[a]; }],	// screen RAM
          [0xd000, 0xdfff, 0x0fff, function(a) { return ram[a + 0x4000]; }], // static RAM
          [0x0000, 0x3fff, 0x3fff, function(a) { return rom ? rom[a] : 0; }], // ROM
          [0x8000, 0xbfff, 0x3fff, function(a) { return rom ? rom[a + 0x4000] : 0; }], // ROM
        ]),
        write: newAddressDecoder([
          [0x4000, 0x7fff, 0x3fff, ramwrite],
          [0x0000, 0x3fff, 0x3fff, magicwrite],
          [0xd000, 0xdfff, 0x0fff, function(a, v) { ramwrite(a + 0x4000, v); }], // static RAM
        ]),
      }
    }
    iobus = {
      read: function(addr) {
        addr &= 0x1f;
        var rtn;
        switch (addr) {
          case 8:
            rtn = intst;
            intst = 0;
            break;
          default:
            rtn = inputs[addr];
            break;
          // $10 = watchdog
        }
        return rtn;
      },
      write: function(addr, val) {
        if (addr == 0xa55b) {
          // TODO: protected_ram_enable_w
          return;
        }
        addr &= 0xff;
        // pattern board
        if (addr > 0x78 && addr < 0x80) {
          patboard[addr & 7] = val;
          if (addr == 0x72) { patdest = 0; }
          if (addr == 0x74) { patdest = (patdest + patboard[3]) & 0xff; }
          if (addr == 0x7e) { xfer_patboard(); }
          return;
        }
        addr &= 0x1f;
        val &= 0xff;
        switch (addr) {
          case 0:
          case 1:
          case 2:
          case 3:
          case 4:
          case 5:
          case 6:
          case 7:
            setpalette(addr, val);
            break;
          case 9:   // HORCB (horizontal boundary byte)
            horcb = val;
            setbordercolor();
            refreshall();
            break;
          case 0xa: // VERBL (vertical blank)
            verbl = arcade ? val : val >> 1;
            refreshall();
            break;
          case 0xb: // OTIR (set palette)
            var c = cpu.saveState();
            //console.log(c.BC>>8, c.HL);
            setpalette((c.BC >> 8) - 1, membus.read(c.HL));
            break;
          case 0xc: // magic register
            magicop = val;
            shift2 = 0;
            rotcount = 0;
            xplower = false;
            break;
          case 0xd: // INFBK (interrupt feedback)
            infbk = val;
            break;
          case 0xe: // INMOD (interrupt enable)
            inmod = val;
            break;
          case 0xf: // INLIN (interrupt line)
            inlin = arcade ? val : val >> 1;
            break;
          case 0x10:
          case 0x11:
          case 0x12:
          case 0x13:
          case 0x14:
          case 0x15:
          case 0x16:
          case 0x17:
            psg.setACRegister(addr, val);
            break;
          case 0x18:
            var c = cpu.saveState();
            psg.setACRegister((c.BC >> 8) - 1, membus.read(c.HL));
            break;
          case 0x19: // XPAND
            xpand = val;	
            break;
          case 0x1a:
          case 0x1b:
          case 0x1c:
          case 0x1d:
          case 0x1e:
            //psg2.setACRegister(addr - 0x1a, val);
            break;
          default:
            //console.log('IO write', hex(addr, 4), hex(val, 2));
            break;
        }
      }
    }
    machine.connectCPUMemoryBus(membus);
    machine.connectCPUIOBus(iobus);
    this.membus = membus;
    this.iobus = iobus;
    // default palette
    for (var i = 0; i < 8; i++) {
      setpalette(i,i);
    }
  };

  this.resetWaitStates = function(sl) {
    var n = sl < verbl ? waitstates : 0; // only wait if video active
    waitstates = 0;
    return n;
  }

  this.loadState = (state) => {
    cpu.loadState(state.c);
    ram.set(state.ram);
    palette.set(state.palette);
    palinds.set(state.palinds);
    magicop = state.magicop;
    xpand = state.xpand;
    xplower = state.xplower;
    shift2 = state.shift2;
    horcb = state.horcb;
    inmod = state.inmod;
    inlin = state.inlin;
    infbk = state.infbk;
    verbl = state.verbl;
    rotcount = state.rotcount;
    rotdata.set(state.rotdata);
    intst = state.intst;
    inputs.set(state.inputs);
    patboard.set(state.patboard);
    patdest = state.patdest;
    refreshall();
  }
    
  this.saveState = () => {
    return {
      c: cpu.saveState(),
      ram: ram.slice(0),
      inputs: inputs.slice(0),
      palette: palette.slice(0),
      palinds: palinds.slice(0),
      magicop: magicop,
      xpand: xpand,
      xplower: xplower,
      shift2: shift2,
      horcb: horcb,
      inmod: inmod,
      inlin: inlin,
      infbk: infbk,
      verbl: verbl,
      rotcount: rotcount,
      rotdata: rotdata.slice(0),
      intst: intst,
      patboard: patboard.slice(0),
      patdest: patdest,
    };
  }
  this.reset = () => {
    // TODO?
    magicop = xpand = inmod = inlin = infbk = shift2 = horcb = 0;
    verbl = sheight;
    xplower = false;
    //watchdog_counter = INITIAL_WATCHDOG;
  }
  
  this.toLongString = (st) =>  {
    var s = "";
    //s += " Scan Y: " + st.sl;
    s += "\n  INLIN: " + st.inlin;
    s += "\n  VERBL: " + st.verbl;
    s += "\nMAGICOP: $" + hex(st.magicop);
    s += "\n  XPAND: $" + hex(st.xpand);
    s += "\nXPLOWER: " + st.xplower;
    s += "\n SHIFT2: $" + hex(st.shift2);
    s += "\n  HORCB: $" + hex(st.horcb);
    s += "\n  INMOD: $" + hex(st.inmod);
    s += "\n  INFBK: $" + hex(st.infbk);
    s += "\n  INTST: $" + hex(st.intst); // intercept status
    s += "\nPalette: ";
    for (var i = 0; i < 8; i++)
      s += hex(palinds[i]);
    s += "\n";
    return s;
  }
  
  this.connectVideo = function(pix) {
    pixels = pix;
  }
  this.loadROM = function(data) {
    rom = data;
  }
  this.loadBIOS = function(data) {
    bios = data;
  }
  this.getVisiblePixelWords = function() {
    return verbl * swidth;
  }
}

export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddleInput {
  
  cpuFrequency = 1789000;
  numTotalScanlines = 262;
  numVisibleScanlines : number;
  cpuCyclesPerLine : number;
  defaultROMSize : number;
  canvasWidth : number;
  sampleRate = 60 * 262 * audioOversample;
  ram : Uint8Array;
  cpu : Z80;
  m; // _BallyAstrocade
  arcade : boolean;

  psg: AstrocadeAudio;
  audioadapter;
  backbuffer : Uint32Array;
  frontbuffer : Uint32Array;

  constructor(arcade:boolean) {
    super();
    this.arcade = arcade;
    this.cpu = new Z80();
    this.psg = new AstrocadeAudio(new MasterAudio());
    this.audioadapter = new TssChannelAdapter(this.psg.psg, audioOversample, this.sampleRate);
    this.handler = newKeyboardHandler(this.inputs, ASTROCADE_KEYCODE_MAP);
    this.defaultROMSize = arcade ? 0xb000 : 0x2000;
    this.ram = new Uint8Array(arcade ? 0x5000 : 0x1000);
    this.numVisibleScanlines = arcade ? 204 : 102;
    this.canvasWidth = arcade ? 320 : 160;
    this.cpuCyclesPerLine = (this.cpuFrequency / (60 * this.numVisibleScanlines * 1.286)) | 0;
    //this.cpuCyclesPerHBlank = Math.floor(this.cpuCyclesPerLine * 0.33);
    //this.cpuCyclesPerVisible = this.cpuCyclesPerLine - this.cpuCyclesPerHBlank;
    this.m = new _BallyAstrocade(arcade);
    this.m.init(this, this.cpu, this.ram, this.inputs, this.psg);
    if (arcade) {
      this.inputs[0x10] = 0xff; // switches (active low)
      this.inputs[0x11] = 0xff; // switches (active low)
      this.inputs[0x12] = 0x00;
      this.inputs[0x13] = 0x08; // dip switches
    }
  }
  
  read(a:number) : number {
    return this.m.membus.read(a);
  }
  write(a:number, v:number) : void {
    this.m.membus.write(a,v);
  }
  
  connectVideo(pixels:Uint32Array) : void {
    super.connectVideo(pixels);
    this.frontbuffer = pixels;
    this.backbuffer = new Uint32Array(pixels.length);
    this.m.connectVideo(this.backbuffer);
  }
  preFrame() {
    this.m.resetWaitStates(0);
  }
  postFrame() {
    // copy back buffer to front buffer, omitting bottom non-visible pixels
    var nbytes = this.m.getVisiblePixelWords();
    this.frontbuffer.set(this.backbuffer.slice(0, nbytes), 0);
    this.frontbuffer.fill(0, nbytes);
  }

  setPaddleInput(controller:number, value:number) : void {
    switch (controller) {
      case 0: this.inputs[0x1c] = value & 0xff; break;
      case 1: this.inputs[0x1d] = value & 0xff; break;
    }
  }

  startScanline() {
    this.audio && this.audioadapter.generate(this.audio);
  }
  
  drawScanline() {
    var sl = this.scanline;
    this.m.drawScanline(sl);
    /*
    if (watchdog_counter-- <= 0) {
      console.log("WATCHDOG FIRED"); // TODO: alert on video
      this.reset();
    }
    */
  }

  advanceCPU() {
    var clk = super.advanceCPU();
    // TODO: disable 33% of the time (hblank)
    var xtra = this.m.resetWaitStates(this.scanline);
    if (xtra) {
      clk += xtra;
      this.probe.logClocks(xtra);
    }
    return clk;
  }

  loadROM(data) {
    super.loadROM(data);
    this.m.loadROM(this.rom);
    this.reset();
  }
  loadBIOS(data) {
    this.m.loadBIOS(padBytes(data, 0x2000));
    this.reset();
  }
  reset() {
    super.reset();
    this.m.reset();
  }
  loadState(state) {
    this.m.loadState(state);
  }
  saveState() {
    return this.m.saveState();
  }
  getDebugCategories() {
    return ['CPU','Stack','Astro'];
  }
  getDebugInfo(category, state) {
    switch (category) {
      case 'Astro': return this.m.toLongString(state);
    }
  }
  getRasterCanvasPosition() {
    return { x: this.getRasterX(), y: this.getRasterY() };
  }
  getVideoParams() {
    if (this.arcade) {
      return { width: 320, height: 204, rotate: 180 };
    } else {
      return { width: 160, height: 102 };
    }
  }
}

/////

// TODO: https://github.com/mamedev/mame/blob/master/src/devices/sound/astrocde.cpp
class AstrocadeAudio extends AY38910_Audio {
  setACRegister(addr: number, val: number) {
    addr &= 0x7;
    val &= 0xff;
    switch (addr) {
      case 0:
        this.psg.setClock(1789000 * 16 / (val + 1));
        this.psg.writeRegisterAY(7, 0x7 ^ 0xff); // disable noise
        break;
      case 1:
      case 2:
      case 3:
        var i = (addr - 1) * 2;
        var j = val * 2 + 1;
        this.psg.writeRegisterAY(i, j & 0xff); // freq lo
        this.psg.writeRegisterAY(i + 1, (j >> 8) & 0xff); // freq hi
        break;
      case 5:
        this.psg.writeRegisterAY(10, val & 0xf); // tone c vol
        break;
      case 6:
        this.psg.writeRegisterAY(8, val & 0xf);	// tone a vol
        this.psg.writeRegisterAY(9, (val >> 4) & 0xf); // tone b vol
        break;
    }
  }
}

/////

//http://glankonian.com/~lance/astrocade_palette.html
var ASTROCADE_PALETTE = [0x000000, 0x242424, 0x484848, 0x6D6D6D, 0x919191, 0xB6B6B6, 0xDADADA, 0xFFFFFF, 0x2500BB, 0x4900E0, 0x6E11FF, 0x9235FF, 0xB75AFF, 0xDB7EFF, 0xFFA3FF, 0xFFC7FF, 0x4900B0, 0x6D00D5, 0x9201F9, 0xB625FF, 0xDA4AFF, 0xFF6EFF, 0xFF92FF, 0xFFB7FF, 0x6A009F, 0x8E00C3, 0xB300E7, 0xD718FF, 0xFB3CFF, 0xFF61FF, 0xFF85FF, 0xFFA9FF, 0x870087, 0xAB00AB, 0xD000D0, 0xF40EF4, 0xFF32FF, 0xFF56FF, 0xFF7BFF, 0xFF9FFF, 0x9F006A, 0xC3008E, 0xE700B3, 0xFF07D7, 0xFF2CFB, 0xFF50FF, 0xFF74FF, 0xFF99FF, 0xB00049, 0xD5006D, 0xF90092, 0xFF05B6, 0xFF29DA, 0xFF4DFF, 0xFF72FF, 0xFF96FF, 0xBB0025, 0xE00049, 0xFF006E, 0xFF0692, 0xFF2AB7, 0xFF4FDB, 0xFF73FF, 0xFF98FF, 0xBF0000, 0xE30024, 0xFF0048, 0xFF0B6D, 0xFF3091, 0xFF54B6, 0xFF79DA, 0xFF9DFF, 0xBB0000, 0xE00000, 0xFF0023, 0xFF1447, 0xFF396C, 0xFF5D90, 0xFF82B5, 0xFFA6D9, 0xB00000, 0xD50000, 0xF90000, 0xFF2124, 0xFF4548, 0xFF6A6C, 0xFF8E91, 0xFFB3B5, 0x9F0000, 0xC30000, 0xE70C00, 0xFF3003, 0xFF5527, 0xFF794B, 0xFF9E70, 0xFFC294, 0x870000, 0xAB0000, 0xD01E00, 0xF44200, 0xFF670A, 0xFF8B2E, 0xFFAF53, 0xFFD477, 0x6A0000, 0x8E0D00, 0xB33100, 0xD75600, 0xFB7A00, 0xFF9E17, 0xFFC33B, 0xFFE75F, 0x490000, 0x6D2100, 0x924500, 0xB66A00, 0xDA8E00, 0xFFB305, 0xFFD729, 0xFFFC4E, 0x251100, 0x493500, 0x6E5A00, 0x927E00, 0xB7A300, 0xDBC700, 0xFFEB1E, 0xFFFF43, 0x002500, 0x244900, 0x486D00, 0x6D9200, 0x91B600, 0xB6DB00, 0xDAFF1B, 0xFFFF3F, 0x003700, 0x005B00, 0x238000, 0x47A400, 0x6CC900, 0x90ED00, 0xB5FF1E, 0xD9FF43, 0x004700, 0x006C00, 0x009000, 0x24B400, 0x48D900, 0x6CFD05, 0x91FF29, 0xB5FF4E, 0x005500, 0x007900, 0x009D00, 0x03C200, 0x27E600, 0x4BFF17, 0x70FF3B, 0x94FF5F, 0x005F00, 0x008300, 0x00A800, 0x00CC00, 0x0AF00A, 0x2EFF2E, 0x53FF53, 0x77FF77, 0x006500, 0x008A00, 0x00AE00, 0x00D203, 0x00F727, 0x17FF4B, 0x3BFF70, 0x5FFF94, 0x006800, 0x008C00, 0x00B100, 0x00D524, 0x00F948, 0x05FF6C, 0x29FF91, 0x4EFFB5, 0x006600, 0x008B00, 0x00AF23, 0x00D447, 0x00F86C, 0x00FF90, 0x1EFFB5, 0x43FFD9, 0x006100, 0x008524, 0x00AA48, 0x00CE6D, 0x00F391, 0x00FFB6, 0x1BFFDA, 0x3FFFFE, 0x005825, 0x007C49, 0x00A16E, 0x00C592, 0x00EAB7, 0x00FFDB, 0x1EFFFF, 0x43FFFF, 0x004B49, 0x00706D, 0x009492, 0x00B9B6, 0x00DDDA, 0x05FFFF, 0x29FFFF, 0x4EFFFF, 0x003C6A, 0x00608E, 0x0085B3, 0x00A9D7, 0x00CEFB, 0x17F2FF, 0x3BFFFF, 0x5FFFFF, 0x002A87, 0x004FAB, 0x0073D0, 0x0097F4, 0x0ABCFF, 0x2EE0FF, 0x53FFFF, 0x77FFFF, 0x00179F, 0x003BC3, 0x0060E7, 0x0384FF, 0x27A8FF, 0x4BCDFF, 0x70F1FF, 0x94FFFF, 0x0002B0, 0x0027D5, 0x004BF9, 0x2470FF, 0x4894FF, 0x6CB9FF, 0x91DDFF, 0xB5FFFF, 0x0000BB, 0x0013E0, 0x2337FF, 0x475BFF, 0x6C80FF, 0x90A4FF, 0xB5C9FF, 0xD9EDFF];
// swap palette RGB to BGR
for (var i = 0; i < 256; i++) {
  var x = ASTROCADE_PALETTE[i];
  ASTROCADE_PALETTE[i] = rgb2bgr(x) | 0xff000000;
}

/*
0000 F3            [ 4]  196 	DI	; disable interrupts
0001 21 00 20      [10]  197 	LD	HL,#0x2000
0004 7E            [ 7]  198 	LD	A,(HL) ; A <- mem[0x2000]
0005 FE 55         [ 7]  199 	CP	#0x55 ; found sentinel byte? ($55)
0007 CA 0D 00      [10]  200 	JP	Z,FoundSentinel ; yes, load program
000A C3 00 20      [10]  201 	JP	_main ; jump to test program
000D                     202 	FoundSentinel:
000D 31 CE 4F      [10]  203 	LD	SP,#0x4fce ; position stack below BIOS vars
0010 CD 84 02      [17]  204 	CALL	_bios_init ; misc. bios init routines
0013 21 05 20      [10]  205 	LD	HL,#0x2005 ; cartridge start vector
0016 7E            [ 7]  206 	LD	A,(HL)
0017 23            [ 6]  207 	INC	HL
0018 66            [ 7]  208 	LD	H,(HL)
0019 6F            [ 4]  209 	LD	L,A
001A E9            [ 4]  210 	JP	(HL) ; jump to cart start vector
*/
var ASTROCADE_MINIMAL_BIOS = [
  0xf3, 0x21, 0x00, 0x20, 0x7e, 0xfe, 0x55, 0xca, 0x0d, 0x00, 0xc3, 0x00, 0x20,
  0x31, 0xce, 0x4f,
  0x21, 0x05, 0x20, 0x7e, 0x23, 0x66, 0x6f,
  0xe9,
];

var ASTROLIBRE_BIOS_LZG = ` `;

