/*
 *  6502 assembler and simulator in Javascript
 *  (C)2006-2010 Stian Soreng - www.6502asm.com
 *
 *  Adapted by Nick Morgan
 *  https://github.com/skilldrick/6502js
 *
 *  Released under the GNU General Public License
 *  see http://gnu.org/licenses/gpl.html
 */

'use strict';

// where program starts
var origin = 0;
var codeSize = 0;

function SimulatorWidget(node) {
  var $node = $(node);
  var ui = UI();
  var display = Display();
  var memory = Memory();
  var labels = Labels();
  var simulator = Simulator();
  var assembler = Assembler();

  function initialize() {
    stripText();
    ui.initialize();
    display.initialize();
    simulator.reset();

    $node.find('.assembleButton').click(function () {
      assembler.assembleCode();
    });
    $node.find('.runButton').click(function() {
      // load hex dump into KIM-1 memory
      for (let i = origin; i < origin + codeSize; i++) {
        if (origin < 1700) RAM[i] = memory.get(i);
        else RIOT[i - 0x1700] = memory.get(i);
      }
      
      // reset CPU
      cpu.reset();
      
      // fix stack
      for (let i = 0x01f8; i <= 0x01fb; i++)
        RAM[i] = 0;
    });
    
    //$node.find('.runButton').click(simulator.stopDebugger);
    $node.find('.resetButton').click(simulator.reset);
    $node.find('.hexdumpButton').click(assembler.hexdump);
    $node.find('.disassembleButton').click(assembler.disassemble);
    $node.find('.debug').change(function () {
      var debug = $(this).is(':checked');
      if (debug) {
        ui.debugOn();
        simulator.enableDebugger();
      } else {
        ui.debugOff();
        simulator.stopDebugger();
      }
    });
    $node.find('.monitoring').change(function () {
      var state = this.checked;
      ui.toggleMonitor(state);
      simulator.toggleMonitor(state);
    });
    $node.find('.start, .length').blur(simulator.handleMonitorRangeChange);
    $node.find('.stepButton').click(simulator.debugExec);
    $node.find('.gotoButton').click(simulator.gotoAddr);
    $node.find('.notesButton').click(function() {
      alert('dump');
    });

    var editor = $node.find('.code');

    editor.on('keypress input', simulator.stop);
    editor.on('keypress input', ui.initialize);
    editor.keydown(ui.captureTabInEditor);

    $(document).keypress(memory.storeKeypress);

    simulator.handleMonitorRangeChange();
  }

  function stripText() {
    //Remove leading and trailing space in textarea
    var text = $node.find('.code').val();
    text = text.replace(/^\n+/, '').replace(/\s+$/, '');
    $node.find('.code').val(text);
  }

  function UI() {
    var currentState;

    var start = {
      assemble: true,
      run: [false, 'Upload'],
      reset: false,
      hexdump: true,
      disassemble: true,
      debug: [false, false]
    };
    var assembled = {
      assemble: true,
      run: [true, 'Upload'],
      reset: true,
      hexdump: true,
      disassemble: true,
      debug: [true, false]
    };
    var running = {
      assemble: false,
      run: [true, 'Stop'],
      reset: true,
      hexdump: true,
      disassemble: true,
      debug: [true, false]
    };
    var debugging = {
      assemble: false,
      reset: true,
      hexdump: true,
      disassemble: true,
      debug: [true, true]
    };
    var postDebugging = {
      assemble: false,
      reset: true,
      hexdump: true,
      disassemble: true,
      debug: [true, false]
    };


    function setState(state) {
      $node.find('.assembleButton').attr('disabled', !state.assemble);
      if (state.run) {
        $node.find('.runButton').attr('disabled', !state.run[0]);
        //$node.find('.runButton').val(state.run[1]);
      }
      $node.find('.resetButton').attr('disabled', !state.reset);
      $node.find('.hexdumpButton').attr('disabled', !state.hexdump);
      $node.find('.disassembleButton').attr('disabled', !state.disassemble);
      $node.find('.debug').attr('disabled', !state.debug[0]);
      $node.find('.debug').attr('checked', state.debug[1]);
      $node.find('.stepButton').attr('disabled', !state.debug[1]);
      $node.find('.gotoButton').attr('disabled', !state.debug[1]);
      currentState = state;
    }

    function initialize() {
      setState(start);
    }

    function play() {
      setState(running);
    }

    function stop() {
      setState(assembled);
    }

    function debugOn() {
      setState(debugging);
    }

    function debugOff() {
      setState(postDebugging);
    }

    function assembleSuccess() {
      setState(assembled);
    }

    function toggleMonitor (state) {
      $node.find('.monitor').toggle(state);
    }

    function showNotes() {
      $node.find('.messages code').html($node.find('.notes').html());
    }

    function captureTabInEditor(e) {
      // Tab Key
      if(e.keyCode === 9) {

        // Prevent focus loss
        e.preventDefault();

        // Insert tab at caret position (instead of losing focus)
        var caretStart = this.selectionStart,
            caretEnd   = this.selectionEnd,
            currentValue = this.value;

        this.value = currentValue.substring(0, caretStart) + "\t" + currentValue.substring(caretEnd);

        // Move cursor forwards one (after tab)
        this.selectionStart = this.selectionEnd = caretStart + 1;
      }
    }

    return {
      initialize: initialize,
      play: play,
      stop: stop,
      assembleSuccess: assembleSuccess,
      debugOn: debugOn,
      debugOff: debugOff,
      toggleMonitor: toggleMonitor,
      showNotes: showNotes,
      captureTabInEditor: captureTabInEditor
    };
  }


  function Display() {
    var displayArray = [];
    var palette = [
      "#000000", "#ffffff", "#880000", "#aaffee",
      "#cc44cc", "#00cc55", "#0000aa", "#eeee77",
      "#dd8855", "#664400", "#ff7777", "#333333",
      "#777777", "#aaff66", "#0088ff", "#bbbbbb"
    ];
    var ctx;
    var width;
    var height;
    var pixelSize;
    var numX = 32;
    var numY = 32;

    function initialize() {
      var canvas = $node.find('.screen')[0];
      width = canvas.width;
      height = canvas.height;
      pixelSize = width / numX;
      ctx = canvas.getContext('2d');
      reset();
    }

    function reset() {
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, width, height);
    }

    function updatePixel(addr) {
      ctx.fillStyle = palette[memory.get(addr) & 0x0f];
      var y = Math.floor((addr - 0x200) / 32);
      var x = (addr - 0x200) % 32;
      ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
    }

    return {
      initialize: initialize,
      reset: reset,
      updatePixel: updatePixel
    };
  }

  function Memory() {
    var memArray = new Array(1024);

    function set(addr, val) {
      return memArray[addr] = val;
    }

    function get(addr) {
      return memArray[addr];
    }

    function getWord(addr) {
      return get(addr) + (get(addr + 1) << 8);
    }

    // Poke a byte, don't touch any registers
    function storeByte(addr, value) {
      set(addr, value & 0xff);
      if ((addr >= 0x200) && (addr <= 0x5ff)) {
        display.updatePixel(addr);
      }
    }

    // Store keycode in ZP $ff
    function storeKeypress(e) {
      var value = e.which;
      memory.storeByte(0xff, value);
    }

    function format(start, length) {
      var html = '';
      var n;
      var cols = parseInt(prompt('How many columns to display?', '24'));

      for (var x = 0; x < length; x++) {
        if ((x % cols) === 0) {
          if (x > 0) { html += "\n"; }
          n = (start + x);
          html += num2hex(((n >> 8) & 0xff));
          html += num2hex((n & 0xff));
          html += ": ";
        }

        html += num2hex(RAM[start + x]);
        html += " ";
      }
      return html;
    }

    return {
      set: set,
      get: get,
      getWord: getWord,
      storeByte: storeByte,
      storeKeypress: storeKeypress,
      format: format
    };
  }

  function Simulator() {
    var regA = 0;
    var regX = 0;
    var regY = 0;
    var regP = 0;
    var regPC = origin;
    var regSP = 0xff;
    var codeRunning = false;
    var debug = false;
    var monitoring = false;
    var executeId;

    // Set zero and negative processor flags based on result
    function setNVflags(value) {
      if (value) {
        regP &= 0xfd;
      } else {
        regP |= 0x02;
      }
      if (value & 0x80) {
        regP |= 0x80;
      } else {
        regP &= 0x7f;
      }
    }

    function setCarryFlagFromBit0(value) {
      regP = (regP & 0xfe) | (value & 1);
    }

    function setCarryFlagFromBit7(value) {
      regP = (regP & 0xfe) | ((value >> 7) & 1);
    }

    function setNVflagsForRegA() {
      setNVflags(regA);
    }

    function setNVflagsForRegX() {
      setNVflags(regX);
    }

    function setNVflagsForRegY() {
      setNVflags(regY);
    }

    var ORA = setNVflagsForRegA;
    var AND = setNVflagsForRegA;
    var EOR = setNVflagsForRegA;
    var ASL = setNVflags;
    var LSR = setNVflags;
    var ROL = setNVflags;
    var ROR = setNVflags;
    var LDA = setNVflagsForRegA;
    var LDX = setNVflagsForRegX;
    var LDY = setNVflagsForRegY;

    function BIT(value) {
      if (value & 0x80) {
        regP |= 0x80;
      } else {
        regP &= 0x7f;
      }
      if (value & 0x40) {
        regP |= 0x40;
      } else {
        regP &= ~0x40;
      }
      if (regA & value) {
        regP &= 0xfd;
      } else {
        regP |= 0x02;
      }
    }

    function CLC() {
      regP &= 0xfe;
    }

    function SEC() {
      regP |= 1;
    }


    function CLV() {
      regP &= 0xbf;
    }

    function setOverflow() {
      regP |= 0x40;
    }

    function DEC(addr) {
      var value = memory.get(addr);
      value--;
      value &= 0xff;
      memory.storeByte(addr, value);
      setNVflags(value);
    }

    function INC(addr) {
      var value = memory.get(addr);
      value++;
      value &= 0xff;
      memory.storeByte(addr, value);
      setNVflags(value);
    }

    function jumpBranch(offset) {
      if (offset > 0x7f) {
        regPC = (regPC - (0x100 - offset));
      } else {
        regPC = (regPC + offset);
      }
    }

    function overflowSet() {
      return regP & 0x40;
    }

    function decimalMode() {
      return regP & 8;
    }

    function carrySet() {
      return regP & 1;
    }

    function negativeSet() {
      return regP & 0x80;
    }

    function zeroSet() {
      return regP & 0x02;
    }

    function doCompare(reg, val) {
      if (reg >= val) {
        SEC();
      } else {
        CLC();
      }
      val = (reg - val);
      setNVflags(val);
    }

    function testSBC(value) {
      var tmp, w;
      if ((regA ^ value) & 0x80) {
        setOverflow();
      } else {
        CLV();
      }

      if (decimalMode()) {
        tmp = 0xf + (regA & 0xf) - (value & 0xf) + carrySet();
        if (tmp < 0x10) {
          w = 0;
          tmp -= 6;
        } else {
          w = 0x10;
          tmp -= 0x10;
        }
        w += 0xf0 + (regA & 0xf0) - (value & 0xf0);
        if (w < 0x100) {
          CLC();
          if (overflowSet() && w < 0x80) { CLV(); }
          w -= 0x60;
        } else {
          SEC();
          if (overflowSet() && w >= 0x180) { CLV(); }
        }
        w += tmp;
      } else {
        w = 0xff + regA - value + carrySet();
        if (w < 0x100) {
          CLC();
          if (overflowSet() && w < 0x80) { CLV(); }
        } else {
          SEC();
          if (overflowSet() && w >= 0x180) { CLV(); }
        }
      }
      regA = w & 0xff;
      setNVflagsForRegA();
    }

    function testADC(value) {
      var tmp;
      if ((regA ^ value) & 0x80) {
        CLV();
      } else {
        setOverflow();
      }

      if (decimalMode()) {
        tmp = (regA & 0xf) + (value & 0xf) + carrySet();
        if (tmp >= 10) {
          tmp = 0x10 | ((tmp + 6) & 0xf);
        }
        tmp += (regA & 0xf0) + (value & 0xf0);
        if (tmp >= 160) {
          SEC();
          if (overflowSet() && tmp >= 0x180) { CLV(); }
          tmp += 0x60;
        } else {
          CLC();
          if (overflowSet() && tmp < 0x80) { CLV(); }
        }
      } else {
        tmp = regA + value + carrySet();
        if (tmp >= 0x100) {
          SEC();
          if (overflowSet() && tmp >= 0x180) { CLV(); }
        } else {
          CLC();
          if (overflowSet() && tmp < 0x80) { CLV(); }
        }
      }
      regA = tmp & 0xff;
      setNVflagsForRegA();
    }

    var instructions = {
      i00: function () {
        codeRunning = false;
        //BRK
      },

      i01: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        var value = memory.get(addr);
        regA |= value;
        ORA();
      },

      i05: function () {
        var zp = popByte();
        regA |= memory.get(zp);
        ORA();
      },

      i06: function () {
        var zp = popByte();
        var value = memory.get(zp);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        memory.storeByte(zp, value);
        ASL(value);
      },

      i08: function () {
        stackPush(regP | 0x30);
        //PHP
      },

      i09: function () {
        regA |= popByte();
        ORA();
      },

      i0a: function () {
        setCarryFlagFromBit7(regA);
        regA = (regA << 1) & 0xff;
        ASL(regA);
      },

      i0d: function () {
        regA |= memory.get(popWord());
        ORA();
      },

      i0e: function () {
        var addr = popWord();
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        memory.storeByte(addr, value);
        ASL(value);
      },

      i10: function () {
        var offset = popByte();
        if (!negativeSet()) { jumpBranch(offset); }
        //BPL
      },

      i11: function () {
        var zp = popByte();
        var value = memory.getWord(zp) + regY;
        regA |= memory.get(value);
        ORA();
      },

      i15: function () {
        var addr = (popByte() + regX) & 0xff;
        regA |= memory.get(addr);
        ORA();
      },

      i16: function () {
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        memory.storeByte(addr, value);
        ASL(value);
      },

      i18: function () {
        CLC();
      },

      i19: function () {
        var addr = popWord() + regY;
        regA |= memory.get(addr);
        ORA();
      },

      i1d: function () {
        var addr = popWord() + regX;
        regA |= memory.get(addr);
        ORA();
      },

      i1e: function () {
        var addr = popWord() + regX;
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        memory.storeByte(addr, value);
        ASL(value);
      },

      i20: function () {
        var addr = popWord();
        var currAddr = regPC - 1;
        stackPush(((currAddr >> 8) & 0xff));
        stackPush((currAddr & 0xff));
        regPC = addr;
        //JSR
      },

      i21: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        var value = memory.get(addr);
        regA &= value;
        AND();
      },

      i24: function () {
        var zp = popByte();
        var value = memory.get(zp);
        BIT(value);
      },

      i25: function () {
        var zp = popByte();
        regA &= memory.get(zp);
        AND();
      },

      i26: function () {
        var sf = carrySet();
        var addr = popByte();
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        value |= sf;
        memory.storeByte(addr, value);
        ROL(value);
      },

      i28: function () {
        regP = stackPop() | 0x30; // There is no B bit!
        //PLP
      },

      i29: function () {
        regA &= popByte();
        AND();
      },

      i2a: function () {
        var sf = carrySet();
        setCarryFlagFromBit7(regA);
        regA = (regA << 1) & 0xff;
        regA |= sf;
        ROL(regA);
      },

      i2c: function () {
        var value = memory.get(popWord());
        BIT(value);
      },

      i2d: function () {
        var value = memory.get(popWord());
        regA &= value;
        AND();
      },

      i2e: function () {
        var sf = carrySet();
        var addr = popWord();
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        value |= sf;
        memory.storeByte(addr, value);
        ROL(value);
      },

      i30: function () {
        var offset = popByte();
        if (negativeSet()) { jumpBranch(offset); }
        //BMI
      },

      i31: function () {
        var zp = popByte();
        var value = memory.getWord(zp) + regY;
        regA &= memory.get(value);
        AND();
      },

      i35: function () {
        var addr = (popByte() + regX) & 0xff;
        regA &= memory.get(addr);
        AND();
      },

      i36: function () {
        var sf = carrySet();
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        value |= sf;
        memory.storeByte(addr, value);
        ROL(value);
      },

      i38: function () {
        SEC();
      },

      i39: function () {
        var addr = popWord() + regY;
        var value = memory.get(addr);
        regA &= value;
        AND();
      },

      i3d: function () {
        var addr = popWord() + regX;
        var value = memory.get(addr);
        regA &= value;
        AND();
      },

      i3e: function () {
        var sf = carrySet();
        var addr = popWord() + regX;
        var value = memory.get(addr);
        setCarryFlagFromBit7(value);
        value = (value << 1) & 0xff;
        value |= sf;
        memory.storeByte(addr, value);
        ROL(value);
      },

      i40: function () {
        regP = stackPop() | 0x30; // There is no B bit!
        regPC = stackPop() | (stackPop() << 8);
        //RTI
      },

      i41: function () {
        var zp = (popByte() + regX) & 0xff;
        var value = memory.getWord(zp);
        regA ^= memory.get(value);
        EOR();
      },

      i45: function () {
        var addr = popByte() & 0xff;
        var value = memory.get(addr);
        regA ^= value;
        EOR();
      },

      i46: function () {
        var addr = popByte() & 0xff;
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        memory.storeByte(addr, value);
        LSR(value);
      },

      i48: function () {
        stackPush(regA);
        //PHA
      },

      i49: function () {
        regA ^= popByte();
        EOR();
      },

      i4a: function () {
        setCarryFlagFromBit0(regA);
        regA = regA >> 1;
        LSR(regA);
      },

      i4c: function () {
        regPC = popWord();
        //JMP
      },

      i4d: function () {
        var addr = popWord();
        var value = memory.get(addr);
        regA ^= value;
        EOR();
      },

      i4e: function () {
        var addr = popWord();
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        memory.storeByte(addr, value);
        LSR(value);
      },

      i50: function () {
        var offset = popByte();
        if (!overflowSet()) { jumpBranch(offset); }
        //BVC
      },

      i51: function () {
        var zp = popByte();
        var value = memory.getWord(zp) + regY;
        regA ^= memory.get(value);
        EOR();
      },

      i55: function () {
        var addr = (popByte() + regX) & 0xff;
        regA ^= memory.get(addr);
        EOR();
      },

      i56: function () {
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        memory.storeByte(addr, value);
        LSR(value);
      },

      i58: function () {
        regP &= ~0x04;
        throw new Error("Interrupts not implemented");
        //CLI
      },

      i59: function () {
        var addr = popWord() + regY;
        var value = memory.get(addr);
        regA ^= value;
        EOR();
      },

      i5d: function () {
        var addr = popWord() + regX;
        var value = memory.get(addr);
        regA ^= value;
        EOR();
      },

      i5e: function () {
        var addr = popWord() + regX;
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        memory.storeByte(addr, value);
        LSR(value);
      },

      i60: function () {
        regPC = (stackPop() | (stackPop() << 8)) + 1;
        //RTS
      },

      i61: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        var value = memory.get(addr);
        testADC(value);
        //ADC
      },

      i65: function () {
        var addr = popByte();
        var value = memory.get(addr);
        testADC(value);
        //ADC
      },

      i66: function () {
        var sf = carrySet();
        var addr = popByte();
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        if (sf) { value |= 0x80; }
        memory.storeByte(addr, value);
        ROR(value);
      },

      i68: function () {
        regA = stackPop();
        setNVflagsForRegA();
        //PLA
      },

      i69: function () {
        var value = popByte();
        testADC(value);
        //ADC
      },

      i6a: function () {
        var sf = carrySet();
        setCarryFlagFromBit0(regA);
        regA = regA >> 1;
        if (sf) { regA |= 0x80; }
        ROR(regA);
      },

      i6c: function () {
        regPC = memory.getWord(popWord());
        //JMP
      },

      i6d: function () {
        var addr = popWord();
        var value = memory.get(addr);
        testADC(value);
        //ADC
      },

      i6e: function () {
        var sf = carrySet();
        var addr = popWord();
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        if (sf) { value |= 0x80; }
        memory.storeByte(addr, value);
        ROR(value);
      },

      i70: function () {
        var offset = popByte();
        if (overflowSet()) { jumpBranch(offset); }
        //BVS
      },

      i71: function () {
        var zp = popByte();
        var addr = memory.getWord(zp);
        var value = memory.get(addr + regY);
        testADC(value);
        //ADC
      },

      i75: function () {
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        testADC(value);
        //ADC
      },

      i76: function () {
        var sf = carrySet();
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        if (sf) { value |= 0x80; }
        memory.storeByte(addr, value);
        ROR(value);
      },

      i78: function () {
        regP |= 0x04;
        throw new Error("Interrupts not implemented");
        //SEI
      },

      i79: function () {
        var addr = popWord();
        var value = memory.get(addr + regY);
        testADC(value);
        //ADC
      },

      i7d: function () {
        var addr = popWord();
        var value = memory.get(addr + regX);
        testADC(value);
        //ADC
      },

      i7e: function () {
        var sf = carrySet();
        var addr = popWord() + regX;
        var value = memory.get(addr);
        setCarryFlagFromBit0(value);
        value = value >> 1;
        if (sf) { value |= 0x80; }
        memory.storeByte(addr, value);
        ROR(value);
      },

      i81: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        memory.storeByte(addr, regA);
        //STA
      },

      i84: function () {
        memory.storeByte(popByte(), regY);
        //STY
      },

      i85: function () {
        memory.storeByte(popByte(), regA);
        //STA
      },

      i86: function () {
        memory.storeByte(popByte(), regX);
        //STX
      },

      i88: function () {
        regY = (regY - 1) & 0xff;
        setNVflagsForRegY();
        //DEY
      },

      i8a: function () {
        regA = regX & 0xff;
        setNVflagsForRegA();
        //TXA
      },

      i8c: function () {
        memory.storeByte(popWord(), regY);
        //STY
      },

      i8d: function () {
        memory.storeByte(popWord(), regA);
        //STA
      },

      i8e: function () {
        memory.storeByte(popWord(), regX);
        //STX
      },

      i90: function () {
        var offset = popByte();
        if (!carrySet()) { jumpBranch(offset); }
        //BCC
      },

      i91: function () {
        var zp = popByte();
        var addr = memory.getWord(zp) + regY;
        memory.storeByte(addr, regA);
        //STA
      },

      i94: function () {
        memory.storeByte((popByte() + regX) & 0xff, regY);
        //STY
      },

      i95: function () {
        memory.storeByte((popByte() + regX) & 0xff, regA);
        //STA
      },

      i96: function () {
        memory.storeByte((popByte() + regY) & 0xff, regX);
        //STX
      },

      i98: function () {
        regA = regY & 0xff;
        setNVflagsForRegA();
        //TYA
      },

      i99: function () {
        memory.storeByte(popWord() + regY, regA);
        //STA
      },

      i9a: function () {
        regSP = regX & 0xff;
        //TXS
      },

      i9d: function () {
        var addr = popWord();
        memory.storeByte(addr + regX, regA);
        //STA
      },

      ia0: function () {
        regY = popByte();
        LDY();
      },

      ia1: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        regA = memory.get(addr);
        LDA();
      },

      ia2: function () {
        regX = popByte();
        LDX();
      },

      ia4: function () {
        regY = memory.get(popByte());
        LDY();
      },

      ia5: function () {
        regA = memory.get(popByte());
        LDA();
      },

      ia6: function () {
        regX = memory.get(popByte());
        LDX();
      },

      ia8: function () {
        regY = regA & 0xff;
        setNVflagsForRegY();
        //TAY
      },

      ia9: function () {
        regA = popByte();
        LDA();
      },

      iaa: function () {
        regX = regA & 0xff;
        setNVflagsForRegX();
        //TAX
      },

      iac: function () {
        regY = memory.get(popWord());
        LDY();
      },

      iad: function () {
        regA = memory.get(popWord());
        LDA();
      },

      iae: function () {
        regX = memory.get(popWord());
        LDX();
      },

      ib0: function () {
        var offset = popByte();
        if (carrySet()) { jumpBranch(offset); }
        //BCS
      },

      ib1: function () {
        var zp = popByte();
        var addr = memory.getWord(zp) + regY;
        regA = memory.get(addr);
        LDA();
      },

      ib4: function () {
        regY = memory.get((popByte() + regX) & 0xff);
        LDY();
      },

      ib5: function () {
        regA = memory.get((popByte() + regX) & 0xff);
        LDA();
      },

      ib6: function () {
        regX = memory.get((popByte() + regY) & 0xff);
        LDX();
      },

      ib8: function () {
        CLV();
      },

      ib9: function () {
        var addr = popWord() + regY;
        regA = memory.get(addr);
        LDA();
      },

      iba: function () {
        regX = regSP & 0xff;
        LDX();
        //TSX
      },

      ibc: function () {
        var addr = popWord() + regX;
        regY = memory.get(addr);
        LDY();
      },

      ibd: function () {
        var addr = popWord() + regX;
        regA = memory.get(addr);
        LDA();
      },

      ibe: function () {
        var addr = popWord() + regY;
        regX = memory.get(addr);
        LDX();
      },

      ic0: function () {
        var value = popByte();
        doCompare(regY, value);
        //CPY
      },

      ic1: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        var value = memory.get(addr);
        doCompare(regA, value);
        //CPA
      },

      ic4: function () {
        var value = memory.get(popByte());
        doCompare(regY, value);
        //CPY
      },

      ic5: function () {
        var value = memory.get(popByte());
        doCompare(regA, value);
        //CPA
      },

      ic6: function () {
        var zp = popByte();
        DEC(zp);
      },

      ic8: function () {
        regY = (regY + 1) & 0xff;
        setNVflagsForRegY();
        //INY
      },

      ic9: function () {
        var value = popByte();
        doCompare(regA, value);
        //CMP
      },

      ica: function () {
        regX = (regX - 1) & 0xff;
        setNVflagsForRegX();
        //DEX
      },

      icc: function () {
        var value = memory.get(popWord());
        doCompare(regY, value);
        //CPY
      },

      icd: function () {
        var value = memory.get(popWord());
        doCompare(regA, value);
        //CPA
      },

      ice: function () {
        var addr = popWord();
        DEC(addr);
      },

      id0: function () {
        var offset = popByte();
        if (!zeroSet()) { jumpBranch(offset); }
        //BNE
      },

      id1: function () {
        var zp = popByte();
        var addr = memory.getWord(zp) + regY;
        var value = memory.get(addr);
        doCompare(regA, value);
        //CMP
      },

      id5: function () {
        var value = memory.get((popByte() + regX) & 0xff);
        doCompare(regA, value);
        //CMP
      },

      id6: function () {
        var addr = (popByte() + regX) & 0xff;
        DEC(addr);
      },

      id8: function () {
        regP &= 0xf7;
        //CLD
      },

      id9: function () {
        var addr = popWord() + regY;
        var value = memory.get(addr);
        doCompare(regA, value);
        //CMP
      },

      idd: function () {
        var addr = popWord() + regX;
        var value = memory.get(addr);
        doCompare(regA, value);
        //CMP
      },

      ide: function () {
        var addr = popWord() + regX;
        DEC(addr);
      },

      ie0: function () {
        var value = popByte();
        doCompare(regX, value);
        //CPX
      },

      ie1: function () {
        var zp = (popByte() + regX) & 0xff;
        var addr = memory.getWord(zp);
        var value = memory.get(addr);
        testSBC(value);
        //SBC
      },

      ie4: function () {
        var value = memory.get(popByte());
        doCompare(regX, value);
        //CPX
      },

      ie5: function () {
        var addr = popByte();
        var value = memory.get(addr);
        testSBC(value);
        //SBC
      },

      ie6: function () {
        var zp = popByte();
        INC(zp);
      },

      ie8: function () {
        regX = (regX + 1) & 0xff;
        setNVflagsForRegX();
        //INX
      },

      ie9: function () {
        var value = popByte();
        testSBC(value);
        //SBC
      },

      iea: function () {
        //NOP
      },

      i42: function () {
        //WDM  -- pseudo op for emulator: arg 0 to output A to message box
        var value = popByte();
        if (value == 0)
          message(String.fromCharCode(regA));
      },

      iec: function () {
        var value = memory.get(popWord());
        doCompare(regX, value);
        //CPX
      },

      ied: function () {
        var addr = popWord();
        var value = memory.get(addr);
        testSBC(value);
        //SBC
      },

      iee: function () {
        var addr = popWord();
        INC(addr);
      },

      if0: function () {
        var offset = popByte();
        if (zeroSet()) { jumpBranch(offset); }
        //BEQ
      },

      if1: function () {
        var zp = popByte();
        var addr = memory.getWord(zp);
        var value = memory.get(addr + regY);
        testSBC(value);
        //SBC
      },

      if5: function () {
        var addr = (popByte() + regX) & 0xff;
        var value = memory.get(addr);
        testSBC(value);
        //SBC
      },

      if6: function () {
        var addr = (popByte() + regX) & 0xff;
        INC(addr);
      },

      if8: function () {
        regP |= 8;
        //SED
      },

      if9: function () {
        var addr = popWord();
        var value = memory.get(addr + regY);
        testSBC(value);
        //SBC
      },

      ifd: function () {
        var addr = popWord();
        var value = memory.get(addr + regX);
        testSBC(value);
        //SBC
      },

      ife: function () {
        var addr = popWord() + regX;
        INC(addr);
      },

      ierr: function () {
        message("Address $" + addr2hex(regPC) + " - unknown opcode");
        codeRunning = false;
      }
    };

    function stackPush(value) {
      memory.set((regSP & 0xff) + 0x100, value & 0xff);
      regSP--;
      if (regSP < 0) {
        regSP &= 0xff;
        message("6502 Stack filled! Wrapping...");
      }
    }

    function stackPop() {
      var value;
      regSP++;
      if (regSP >= 0x100) {
        regSP &= 0xff;
        message("6502 Stack emptied! Wrapping...");
      }
      value = memory.get(regSP + 0x100);
      return value;
    }

    // Pops a byte
    function popByte() {
      return(memory.get(regPC++) & 0xff);
    }

    // Pops a little-endian word
    function popWord() {
      return popByte() + (popByte() << 8);
    }

    // Executes the assembled code
    function runBinary() {
      if (codeRunning) {
        // Switch OFF everything
        stop();
        ui.stop();
      } else {
        ui.play();
        codeRunning = true;
        executeId = setInterval(multiExecute, 15);
      }
    }

    function multiExecute() {
      if (!debug) {
        // use a prime number of iterations to avoid aliasing effects

        for (var w = 0; w < 97; w++) {
          execute();
        }
      }
      updateDebugInfo();
    }


    function executeNextInstruction() {
      var instructionName = popByte().toString(16).toLowerCase();
      if (instructionName.length === 1) {
        instructionName = '0' + instructionName;
      }
      var instruction = instructions['i' + instructionName];

      if (instruction) {
        instruction();
      } else {
        instructions.ierr();
      }
    }

    // Executes one instruction. This is the main part of the CPU simulator.
    function execute(debugging) {
      if (!codeRunning && !debugging) { return; }

      setRandomByte();
      executeNextInstruction();

      if ((regPC === 0) || (!codeRunning && !debugging)) {
        stop();
        message("Program end at PC=$" + addr2hex(regPC - 1));
        ui.stop();
      }
    }

    function setRandomByte() {
      memory.set(0xfe, Math.floor(Math.random() * 256));
    }

    function updateMonitor() {
      if (monitoring) {
        var start = parseInt($node.find('.start').val(), 16);
        var length = parseInt($node.find('.length').val(), 16);

        var end = start + length - 1;

        var monitorNode = $node.find('.monitor code');

        if (!isNaN(start) && !isNaN(length) && start >= 0 && length > 0 && end <= 0xffff) {
          monitorNode.html(memory.format(start, length));
        } else {
          monitorNode.html('Cannot monitor this range. Valid ranges are between $0000 and $ffff, inclusive.');
        }
      }
    }

    function handleMonitorRangeChange() {

      var $start  = $node.find('.start'),
          $length = $node.find('.length'),
          start   = parseInt($start.val(), 16),
          length  = parseInt($length.val(), 16),
          end     = start + length - 1;

      $start.removeClass('monitor-invalid');
      $length.removeClass('monitor-invalid');

      if(isNaN(start) || start < 0 || start > 0xffff) {

        $start.addClass('monitor-invalid');

      } else if(isNaN(length) || end > 0xffff) {

        $length.addClass('monitor-invalid');
      }
    }

    // Execute one instruction and print values
    function debugExec() {
      //if (codeRunning) {
        execute(true);
      //}
      updateDebugInfo();
    }

    function updateDebugInfo() {
      var html = "A=$" + num2hex(regA) + " X=$" + num2hex(regX) + " Y=$" + num2hex(regY) + "<br />";
      html += "SP=$" + num2hex(regSP) + " PC=$" + addr2hex(regPC);
      html += "<br />";
      html += "NV-BDIZC<br />";
      for (var i = 7; i >=0; i--) {
        html += regP >> i & 1;
      }
      $node.find('.minidebugger').html(html);
      updateMonitor();
    }

    // gotoAddr() - Set PC to address (or address of label)
    function gotoAddr() {
      var inp = prompt("Enter address or label", "");
      var addr = 0;
      if (labels.find(inp)) {
        addr = labels.getPC(inp);
      } else {
        if (inp.match(/^0x[0-9a-f]{1,4}$/i)) {
          inp = inp.replace(/^0x/, "");
          addr = parseInt(inp, 16);
        } else if (inp.match(/^\$[0-9a-f]{1,4}$/i)) {
          inp = inp.replace(/^\$/, "");
          addr = parseInt(inp, 16);
        }
      }
      if (addr === 0) {
        message("Unable to find/parse given address/label");
      } else {
        regPC = addr;
      }
      updateDebugInfo();
    }


    function stopDebugger() {
      debug = false;
    }

    function enableDebugger() {
      debug = true;
      if (codeRunning) {
        updateDebugInfo();
      }
    }

    // reset() - Reset CPU and memory.
    function reset() {
      display.reset();
      for (var i = 0; i < 1024; i++) { // clear ZP, stack and screen
        memory.set(i, 0x00);
      }
      regA = regX = regY = 0;
      regPC = origin;
      regSP = 0xff;
      regP = 0x30;
      updateDebugInfo();
    }

    function stop() {
      codeRunning = false;
      clearInterval(executeId);
      //message("\nStopped\n");
    }

    function toggleMonitor (state) {
      monitoring = state;
    }

    return {
      runBinary: runBinary,
      enableDebugger: enableDebugger,
      stopDebugger: stopDebugger,
      debugExec: debugExec,
      gotoAddr: gotoAddr,
      reset: reset,
      stop: stop,
      toggleMonitor: toggleMonitor,
      handleMonitorRangeChange: handleMonitorRangeChange
    };
  }


  function Labels() {
    var labelIndex = [];

    function indexLines(lines, symbols) {
      for (var i = 0; i < lines.length; i++) {
        if (!indexLine(lines[i], symbols)) {
          message("**Label already defined at line " + (i + 1) + ":** " + lines[i]);
          return false;
        }
      }
      return true;
    }

    // Extract label if line contains one and calculate position in memory.
    // Return false if label already exists.
    function indexLine(input, symbols) {
          
      // Figure out how many bytes this instruction takes
      var currentPC = assembler.getCurrentPC();
      assembler.assembleLine(input, 0, symbols); //TODO: find a better way for Labels to have access to assembler

      // Find command or label
      if (input.match(/^\w+:/)) {
        var label = input.replace(/(^\w+):.*$/, "$1");
        
        if (symbols.lookup(label)) {
          message("**Label " + label + "is already used as a symbol; please rename one of them**");
          return false;
        }
        
        return push(label + "|" + currentPC);
      }
      return true;
    }

    // Push label to array. Return false if label already exists.
    function push(name) {
      if (find(name)) {
        return false;
      }
      labelIndex.push(name + "|");
      return true;
    }

    // Returns true if label exists.
    function find(name) {
      var nameAndAddr;
      for (var i = 0; i < labelIndex.length; i++) {
        nameAndAddr = labelIndex[i].split("|");
        if (name === nameAndAddr[0]) {
          return true;
        }
      }
      return false;
    }

    // Associates label with address
    function setPC(name, addr) {
      var nameAndAddr;
      for (var i = 0; i < labelIndex.length; i++) {
        nameAndAddr = labelIndex[i].split("|");
        if (name === nameAndAddr[0]) {
          labelIndex[i] = name + "|" + addr;
          return true;
        }
      }
      return false;
    }

    // Get address associated with label
    function getPC(name) {
      var nameAndAddr;
      for (var i = 0; i < labelIndex.length; i++) {
        nameAndAddr = labelIndex[i].split("|");
        if (name === nameAndAddr[0]) {
          return (nameAndAddr[1]);
        }
      }
      return -1;
    }

    function displayMessage() {
      var str = "Found " + labelIndex.length + " label";
      if (labelIndex.length !== 1) {
        str += "s";
      }
      message(str + ".");
    }

    function reset() {
      labelIndex = [];
    }

    return {
      indexLines: indexLines,
      find: find,
      getPC: getPC,
      displayMessage: displayMessage,
      reset: reset
    };
  }


  function Assembler() {
    var defaultCodePC;
    var codeLen;
    var codeAssembledOK = false;
    var wasOutOfRangeBranch = false;

    var Opcodes = [
      /* Name, Imm,  ZP,   ZPX,  ZPY,  ABS, ABSX, ABSY,  IND, INDX, INDY, SNGL, BRA */
      ["ADC", 0x69, 0x65, 0x75, null, 0x6d, 0x7d, 0x79, null, 0x61, 0x71, null, null],
      ["AND", 0x29, 0x25, 0x35, null, 0x2d, 0x3d, 0x39, null, 0x21, 0x31, null, null],
      ["ASL", null, 0x06, 0x16, null, 0x0e, 0x1e, null, null, null, null, 0x0a, null],
      ["BIT", null, 0x24, null, null, 0x2c, null, null, null, null, null, null, null],
      ["BPL", null, null, null, null, null, null, null, null, null, null, null, 0x10],
      ["BMI", null, null, null, null, null, null, null, null, null, null, null, 0x30],
      ["BVC", null, null, null, null, null, null, null, null, null, null, null, 0x50],
      ["BVS", null, null, null, null, null, null, null, null, null, null, null, 0x70],
      ["BCC", null, null, null, null, null, null, null, null, null, null, null, 0x90],
      ["BCS", null, null, null, null, null, null, null, null, null, null, null, 0xb0],
      ["BNE", null, null, null, null, null, null, null, null, null, null, null, 0xd0],
      ["BEQ", null, null, null, null, null, null, null, null, null, null, null, 0xf0],
      ["BRK", null, null, null, null, null, null, null, null, null, null, 0x00, null],
      ["CMP", 0xc9, 0xc5, 0xd5, null, 0xcd, 0xdd, 0xd9, null, 0xc1, 0xd1, null, null],
      ["CPX", 0xe0, 0xe4, null, null, 0xec, null, null, null, null, null, null, null],
      ["CPY", 0xc0, 0xc4, null, null, 0xcc, null, null, null, null, null, null, null],
      ["DEC", null, 0xc6, 0xd6, null, 0xce, 0xde, null, null, null, null, null, null],
      ["EOR", 0x49, 0x45, 0x55, null, 0x4d, 0x5d, 0x59, null, 0x41, 0x51, null, null],
      ["CLC", null, null, null, null, null, null, null, null, null, null, 0x18, null],
      ["SEC", null, null, null, null, null, null, null, null, null, null, 0x38, null],
      ["CLI", null, null, null, null, null, null, null, null, null, null, 0x58, null],
      ["SEI", null, null, null, null, null, null, null, null, null, null, 0x78, null],
      ["CLV", null, null, null, null, null, null, null, null, null, null, 0xb8, null],
      ["CLD", null, null, null, null, null, null, null, null, null, null, 0xd8, null],
      ["SED", null, null, null, null, null, null, null, null, null, null, 0xf8, null],
      ["INC", null, 0xe6, 0xf6, null, 0xee, 0xfe, null, null, null, null, null, null],
      ["JMP", null, null, null, null, 0x4c, null, null, 0x6c, null, null, null, null],
      ["JSR", null, null, null, null, 0x20, null, null, null, null, null, null, null],
      ["LDA", 0xa9, 0xa5, 0xb5, null, 0xad, 0xbd, 0xb9, null, 0xa1, 0xb1, null, null],
      ["LDX", 0xa2, 0xa6, null, 0xb6, 0xae, null, 0xbe, null, null, null, null, null],
      ["LDY", 0xa0, 0xa4, 0xb4, null, 0xac, 0xbc, null, null, null, null, null, null],
      ["LSR", null, 0x46, 0x56, null, 0x4e, 0x5e, null, null, null, null, 0x4a, null],
      ["NOP", null, null, null, null, null, null, null, null, null, null, 0xea, null],
      ["ORA", 0x09, 0x05, 0x15, null, 0x0d, 0x1d, 0x19, null, 0x01, 0x11, null, null],
      ["TAX", null, null, null, null, null, null, null, null, null, null, 0xaa, null],
      ["TXA", null, null, null, null, null, null, null, null, null, null, 0x8a, null],
      ["DEX", null, null, null, null, null, null, null, null, null, null, 0xca, null],
      ["INX", null, null, null, null, null, null, null, null, null, null, 0xe8, null],
      ["TAY", null, null, null, null, null, null, null, null, null, null, 0xa8, null],
      ["TYA", null, null, null, null, null, null, null, null, null, null, 0x98, null],
      ["DEY", null, null, null, null, null, null, null, null, null, null, 0x88, null],
      ["INY", null, null, null, null, null, null, null, null, null, null, 0xc8, null],
      ["ROR", null, 0x66, 0x76, null, 0x6e, 0x7e, null, null, null, null, 0x6a, null],
      ["ROL", null, 0x26, 0x36, null, 0x2e, 0x3e, null, null, null, null, 0x2a, null],
      ["RTI", null, null, null, null, null, null, null, null, null, null, 0x40, null],
      ["RTS", null, null, null, null, null, null, null, null, null, null, 0x60, null],
      ["SBC", 0xe9, 0xe5, 0xf5, null, 0xed, 0xfd, 0xf9, null, 0xe1, 0xf1, null, null],
      ["STA", null, 0x85, 0x95, null, 0x8d, 0x9d, 0x99, null, 0x81, 0x91, null, null],
      ["TXS", null, null, null, null, null, null, null, null, null, null, 0x9a, null],
      ["TSX", null, null, null, null, null, null, null, null, null, null, 0xba, null],
      ["PHA", null, null, null, null, null, null, null, null, null, null, 0x48, null],
      ["PLA", null, null, null, null, null, null, null, null, null, null, 0x68, null],
      ["PHP", null, null, null, null, null, null, null, null, null, null, 0x08, null],
      ["PLP", null, null, null, null, null, null, null, null, null, null, 0x28, null],
      ["STX", null, 0x86, null, 0x96, 0x8e, null, null, null, null, null, null, null],
      ["STY", null, 0x84, 0x94, null, 0x8c, null, null, null, null, null, null, null],
      ["WDM", 0x42, 0x42, null, null, null, null, null, null, null, null, null, null],
      ["---", null, null, null, null, null, null, null, null, null, null, null, null]
    ];
    
    function assembleCode() {
    	origin = parseInt($('#assemblystart').val());
    	doAssemble(origin);
    }
    
    // Assembles the code into memory
    function doAssemble(origin) {
      var BOOTSTRAP_ADDRESS = origin;

      wasOutOfRangeBranch = false;
  
      simulator.reset();
      labels.reset();
      defaultCodePC = BOOTSTRAP_ADDRESS;
      $node.find('.messages code').empty();

      var code = $node.find('.code').val();
      code += "\n\n";
      var lines = code.split("\n");
      codeAssembledOK = true;

      message("Preprocessing ...");
      var symbols = preprocess(lines);

      message("Indexing labels ...");
      defaultCodePC = BOOTSTRAP_ADDRESS;
      if (!labels.indexLines(lines, symbols)) {
        return false;
      }
      labels.displayMessage();

      defaultCodePC = BOOTSTRAP_ADDRESS;
      message("Assembling code ...");
      
      codeLen = 0;
      for (var i = 0; i < lines.length; i++) {
        if (!assembleLine(lines[i], i, symbols)) {
          codeAssembledOK = false;
          break;
        }
      }

      if (codeLen === 0) {
        codeAssembledOK = false;
        message("No code to run.");
      }

      if (codeAssembledOK) {
        ui.assembleSuccess();
        memory.set(defaultCodePC, 0x00); //set a null byte at the end of the code
      } else {

        var str = lines[i].replace("<", "&lt;").replace(">", "&gt;");

        if(!wasOutOfRangeBranch) {
          message("**Syntax error line " + (i + 1) + ": " + str + "**");
        } else {
          message('**Out of range branch on line ' + (i + 1) + ' (branches are limited to -128 to +127): ' + str + '**');
        }

        ui.initialize();
        return false;
      }

      message("Code assembled successfully, " + codeLen + " bytes.");
      codeSize = codeLen;
      return true;
    }

    // Sanitize input: remove comments and trim leading/trailing whitespace
    function sanitize(line) {
      // remove comments
      var no_comments = line.replace(/^(.*?);.*/, "$1");
  
      // trim line
      return no_comments.replace(/^\s+/, "").replace(/\s+$/, "");
    }

    function preprocess(lines) {
      var table = [];
      var PREFIX = "__"; // Using a prefix avoids clobbering any predefined properties
      
      function lookup(key) {
        if (table.hasOwnProperty(PREFIX + key)) return table[PREFIX + key];
        else return undefined;
      }
      
      function add(key, value) {
        var valueAlreadyExists = table.hasOwnProperty(PREFIX + key)
        if (!valueAlreadyExists) {
          table[PREFIX + key] = value;
        }
      }
        
      // Build the substitution table
      for (var i = 0; i < lines.length; i++) {
        lines[i] = sanitize(lines[i]);
        var match_data = lines[i].match(/^define\s+(\w+)\s+(\S+)/);
        if (match_data) {
          add(match_data[1], sanitize(match_data[2]));
          lines[i] = ""; // We're done with this preprocessor directive, so delete it
        }
      }
      
      // Callers will only need the lookup function
      return {
        lookup: lookup
      }
    }
  
    // Assembles one line of code.
    // Returns true if it assembled successfully, false otherwise.
    function assembleLine(input, lineno, symbols) {
      var label, command, param, addr;

      // Find command or label
      if (input.match(/^\w+:/)) {
        label = input.replace(/(^\w+):.*$/, "$1");
        if (input.match(/^\w+:[\s]*\w+.*$/)) {
          input = input.replace(/^\w+:[\s]*(.*)$/, "$1");
          command = input.replace(/^(\w+).*$/, "$1");
        } else {
          command = "";
        }
      } else {
        command = input.replace(/^(\w+).*$/, "$1");
      }

      // Nothing to do for blank lines
      if (command === "") {
        return true;
      }

      command = command.toUpperCase();

      if (input.match(/^\*\s*=\s*\$?[0-9a-f]*$/)) {
        // equ spotted
        param = input.replace(/^\s*\*\s*=\s*/, "");
        if (param[0] === "$") {
          param = param.replace(/^\$/, "");
          addr = parseInt(param, 16);
        } else {
          addr = parseInt(param, 10);
        }
        if ((addr < 0) || (addr > 0xffff)) {
          message("Unable to relocate code outside 64k memory");
          return false;
        }
        defaultCodePC = addr;
        return true;
      }

      if (input.match(/^\w+\s+.*?$/)) {
        param = input.replace(/^\w+\s+(.*?)/, "$1");
      } else if (input.match(/^\w+$/)) {
        param = "";
      } else {
        return false;
      }

      param = param.replace(/[ ]/g, "");

      if (command === "DCB") {
        return DCB(param);
      }
      for (var o = 0; o < Opcodes.length; o++) {
        if (Opcodes[o][0] === command) {
          if (checkSingle(param, Opcodes[o][11])) { return true; }
          if (checkImmediate(param, Opcodes[o][1], symbols)) { return true; }
          if (checkZeroPage(param, Opcodes[o][2], symbols)) { return true; }
          if (checkZeroPageX(param, Opcodes[o][3], symbols)) { return true; }
          if (checkZeroPageY(param, Opcodes[o][4], symbols)) { return true; }
          if (checkAbsoluteX(param, Opcodes[o][6], symbols)) { return true; }
          if (checkAbsoluteY(param, Opcodes[o][7], symbols)) { return true; }
          if (checkIndirect(param, Opcodes[o][8], symbols)) { return true; }
          if (checkIndirectX(param, Opcodes[o][9], symbols)) { return true; }
          if (checkIndirectY(param, Opcodes[o][10], symbols)) { return true; }
          if (checkAbsolute(param, Opcodes[o][5], symbols)) { return true; }
          if (checkBranch(param, Opcodes[o][12])) { return true; }
        }
      }
      
      return false; // Unknown syntax
    }

    function DCB(param) {
      var values, number, str, ch;
      values = param.split(",");
      if (values.length === 0) { return false; }
      for (var v = 0; v < values.length; v++) {
        str = values[v];
        if (str) {
          ch = str.substring(0, 1);
          if (ch === "$") {
            number = parseInt(str.replace(/^\$/, ""), 16);
            pushByte(number);
          } else if (ch ==="%") {
            number = parseInt(str.replace(/^%/, ""), 2)
            pushByte(number)
          } else if (ch >= "0" && ch <= "9") {
            number = parseInt(str, 10);
            pushByte(number);
          } else {
            return false;
          }
        }
      }
      return true;
    }
    
    // Try to parse the given parameter as a byte operand.
    // Returns the (positive) value if successful, otherwise -1
    function tryParseByteOperand(param, symbols) {
      if (param.match(/^\w+$/)) {
        var lookupVal = symbols.lookup(param); // Substitute symbol by actual value, then proceed
        if (lookupVal) {
          param = lookupVal;
        }
      }
      
      var value;
      var match_data;

      // Is it a decimal operand?
      match_data = param.match(/^([0-9]{1,3})$/);
      if (match_data) {
        value = parseInt(match_data[1], 10);
      }

      // Is it a hexadecimal operand?
      match_data = param.match(/^\$([0-9a-f]{1,2})$/i);
      if (match_data) {
        value = parseInt(match_data[1], 16);
      }

      // Is it a binary operand?
      match_data = param.match(/^%([0-1]{1,8})$/);
      if (match_data) {
        value = parseInt(match_data[1], 2);
      }
      
      // Validate range
      if (value >= 0 && value <= 0xff) {
        return value;
      } else {
        return -1;  
      }
    }
    
    // Try to parse the given parameter as a word operand.
    // Returns the (positive) value if successful, otherwise -1
    function tryParseWordOperand(param, symbols) {
      if (param.match(/^\w+$/)) {
        var lookupVal = symbols.lookup(param); // Substitute symbol by actual value, then proceed
        if (lookupVal) {
          param = lookupVal;
        }
      }
      
      var value;
    
      // Is it a hexadecimal operand?
      var match_data = param.match(/^\$([0-9a-f]{3,4})$/i);
      if (match_data) {
        value = parseInt(match_data[1], 16);
      } else {
        // Is it a decimal operand?
        match_data = param.match(/^([0-9]{1,5})$/i);
        if (match_data) {
          value = parseInt(match_data[1], 10);
        }
      }
      
      // Validate range
      if (value >= 0 && value <= 0xffff) {
        return value;
      } else {
        return -1;
      }
    }

    // Common branch function for all branches (BCC, BCS, BEQ, BNE..)
    function checkBranch(param, opcode) {
      var addr;
      if (opcode === null) { return false; }

      addr = -1;
      if (param.match(/\w+/)) {
        addr = labels.getPC(param);
      }
      if (addr === -1) { pushWord(0x00); return false; }
      pushByte(opcode);

      var distance = addr - defaultCodePC - 1;

      if(distance < -128 || distance > 127) {
          wasOutOfRangeBranch = true;
          return false;
      }

      pushByte(distance);
      return true;
    }

    // Check if param is immediate and push value
    function checkImmediate(param, opcode, symbols) {
      var value, label, hilo, addr;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^#([\w\$%]+)$/i);
      if (match_data) {
        var operand = tryParseByteOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushByte(operand);
          return true;
        }
      }
      
      // Label lo/hi
      if (param.match(/^#[<>]\w+$/)) {
        label = param.replace(/^#[<>](\w+)$/, "$1");
        hilo = param.replace(/^#([<>]).*$/, "$1");
        pushByte(opcode);
        if (labels.find(label)) {
          addr = labels.getPC(label);
          switch(hilo) {
          case ">":
            pushByte((addr >> 8) & 0xff);
            return true;
          case "<":
            pushByte(addr & 0xff);
            return true;
          default:
            return false;
          }
        } else {
          pushByte(0x00);
          return true;
        }
      }
      
      return false;
    }

    // Check if param is indirect and push value
    function checkIndirect(param, opcode, symbols) {
      var value;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^\(([\w\$]+)\)$/i);
      if (match_data) {
        var operand = tryParseWordOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushWord(operand);
          return true;
        }
      }
      return false;
    }

    // Check if param is indirect X and push value
    function checkIndirectX(param, opcode, symbols) {
      var value;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^\(([\w\$]+),X\)$/i);
      if (match_data) {
        var operand = tryParseByteOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushByte(operand);
          return true;
        }
      }
      return false;
    }

    // Check if param is indirect Y and push value
    function checkIndirectY(param, opcode, symbols) {
      var value;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^\(([\w\$]+)\),Y$/i);
      if (match_data) {
        var operand = tryParseByteOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushByte(operand);
          return true;
        }
      }
      return false;
    }

    // Check single-byte opcodes
    function checkSingle(param, opcode) {
      if (opcode === null) { return false; }
      // Accumulator instructions are counted as single-byte opcodes
      if (param !== "" && param !== "A") { return false; }
      pushByte(opcode);
      return true;
    }

    // Check if param is ZP and push value
    function checkZeroPage(param, opcode, symbols) {
      var value;
      if (opcode === null) { return false; }

      var operand = tryParseByteOperand(param, symbols);
      if (operand >= 0) {
        pushByte(opcode);
        pushByte(operand);
        return true;
      }
      
      return false;
    }

    // Check if param is ABSX and push value
    function checkAbsoluteX(param, opcode, symbols) {
      var number, value, addr;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^([\w\$]+),X$/i);
      if (match_data) {
        var operand = tryParseWordOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushWord(operand);
          return true;
        }
      }

      // it could be a label too..
      if (param.match(/^\w+,X$/i)) {
        param = param.replace(/,X$/i, "");
        pushByte(opcode);
        if (labels.find(param)) {
          addr = labels.getPC(param);
          if (addr < 0 || addr > 0xffff) { return false; }
          pushWord(addr);
          return true;
        } else {
          pushWord(0xffff); // filler, only used while indexing labels
          return true;
        }
      }

      return false;
    }

    // Check if param is ABSY and push value
    function checkAbsoluteY(param, opcode, symbols) {
      var number, value, addr;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^([\w\$]+),Y$/i);
      if (match_data) {
        var operand = tryParseWordOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushWord(operand);
          return true;
        }
      }

      // it could be a label too..
      if (param.match(/^\w+,Y$/i)) {
        param = param.replace(/,Y$/i, "");
        pushByte(opcode);
        if (labels.find(param)) {
          addr = labels.getPC(param);
          if (addr < 0 || addr > 0xffff) { return false; }
          pushWord(addr);
          return true;
        } else {
          pushWord(0xffff); // filler, only used while indexing labels
          return true;
        }
      }
      return false;
    }

    // Check if param is ZPX and push value
    function checkZeroPageX(param, opcode, symbols) {
      var number, value;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^([\w\$]+),X$/i);
      if (match_data) {
        var operand = tryParseByteOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushByte(operand);
          return true;
        }
      }
      
      return false;
    }

    // Check if param is ZPY and push value
    function checkZeroPageY(param, opcode, symbols) {
      var number, value;
      if (opcode === null) { return false; }
      
      var match_data = param.match(/^([\w\$]+),Y$/i);
      if (match_data) {
        var operand = tryParseByteOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushByte(operand);
          return true;
        }
      }
      
      return false;
    }

    // Check if param is ABS and push value
    function checkAbsolute(param, opcode, symbols) {
      var value, number, addr;
      if (opcode === null) { return false; }

      var match_data = param.match(/^([\w\$]+)$/i);
      if (match_data) {
        var operand = tryParseWordOperand(match_data[1], symbols);
        if (operand >= 0) {
          pushByte(opcode);
          pushWord(operand);
          return true;
        }
      }

      // it could be a label too..
      if (param.match(/^\w+$/)) {
        pushByte(opcode);
        if (labels.find(param)) {
          addr = (labels.getPC(param));
          if (addr < 0 || addr > 0xffff) { return false; }
          pushWord(addr);
          return true;
        } else {
          pushWord(0xffff); // filler, only used while indexing labels
          return true;
        }
      }
      return false;
    }

    // Push a byte to memory
    function pushByte(value) {
      memory.set(defaultCodePC, value & 0xff);
      defaultCodePC++;
      codeLen++;
    }

    // Push a word to memory in little-endian order
    function pushWord(value) {
      pushByte(value & 0xff);
      pushByte((value >> 8) & 0xff);
    }

    function openPopup(content, title) {
      var w = window.open('', title, 'width=650,height=300,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no');

      var html = "<html><head>";
      html += "<link href='style.css' rel='stylesheet' type='text/css' />";
      html += "<title>" + title + "</title></head><body>";
      html += "<pre><code>";

      html += content;

      html += "</code></pre></body></html>";
      w.document.write(html);
      w.document.close();
    }

    // Dump binary as hex to new window
    function hexdump() {
      var org = parseInt(prompt('Enter the starting address where the program is loaded, e.g. 0x0200:', '0x0200'));
      var len = parseInt(prompt('Enter the end address where the program ends, e.g. 0x020A:', '0x0300')) - org;            
      openPopup(memory.format(org, len), 'Hexdump');
    }

    // TODO: Create separate disassembler object?
    var addressingModes = [
      null,
      'Imm',
      'ZP',
      'ZPX',
      'ZPY',
      'ABS',
      'ABSX',
      'ABSY',
      'IND',
      'INDX',
      'INDY',
      'SNGL',
      'BRA'
    ];

    var instructionLength = {
      Imm: 2,
      ZP: 2,
      ZPX: 2,
      ZPY: 2,
      ABS: 3,
      ABSX: 3,
      ABSY: 3,
      IND: 3,
      INDX: 2,
      INDY: 2,
      SNGL: 1,
      BRA: 2
    };

    function getModeAndCode(byte) {
      var index;
      var line = Opcodes.filter(function (line) {
        var possibleIndex = line.indexOf(byte);
        if (possibleIndex > -1) {
          index = possibleIndex;
          return true;
        }
      })[0];

      if (!line) { //instruction not found
        return {
          opCode: '???',
          mode: 'SNGL'
        };
      } else {
        return {
          opCode: line[0],
          mode: addressingModes[index]
        };
      }
    }

    function createInstruction(address) {
      var bytes = [];
      var opCode;
      var args = [];
      var mode;

      function isAccumulatorInstruction() {
        var accumulatorBytes = [0x0a, 0x4a, 0x2a, 0x6a];
        if (accumulatorBytes.indexOf(bytes[0]) > -1) {
          return true;
        }
      }

      function isBranchInstruction() {
        return opCode.match(/^B/) && !(opCode == 'BIT' || opCode == 'BRK');
      }

      //This is gnarly, but unavoidably so?
      function formatArguments() {
        var argsString = args.map(num2hex).reverse().join('');

        if (isBranchInstruction()) {
          var destination = address + 2;
          if (args[0] > 0x7f) {
            destination -= 0x100 - args[0];
          } else {
            destination += args[0];
          }
          argsString = addr2hex(destination);
        }

        if (argsString) {
          argsString = '$' + argsString;
        }
        if (mode == 'Imm') {
          argsString = '#' + argsString;
        }
        if (mode.match(/X$/)) {
          argsString += ',X';
        }
        if (mode.match(/^IND/)) {
          argsString = '(' + argsString + ')';
        }
        if (mode.match(/Y$/)) {
          argsString += ',Y';
        }

        if (isAccumulatorInstruction()) {
          argsString = 'A';
        }

        return argsString;
      }

      return {
        addByte: function (byte) {
          bytes.push(byte);
        },
        setModeAndCode: function (modeAndCode) {
          opCode = modeAndCode.opCode;
          mode = modeAndCode.mode;
        },
        addArg: function (arg) {
          args.push(arg);
        },
        toString: function () {
          var bytesString = bytes.map(num2hex).join(' ');
          var padding = Array(11 - bytesString.length).join(' ');
          return '$' + addr2hex(address) + '    ' + bytesString + padding + opCode +
            ' ' + formatArguments(args);
        }
      };
    }

    function disassemble() {
      var startAddress = parseInt(prompt('Enter the starting address where the program is loaded, e.g. 0x0200:', '0x0200'));
      var endAddress = parseInt(prompt('Enter the end address where the program ends, e.g. 0x020A:', '0x0400'));
      var currentAddress = startAddress;      
      var instructions = [];
      var length;
      var inst;
      var byte;
      var modeAndCode;

      while (currentAddress < endAddress) {
        inst = createInstruction(currentAddress);
        byte = RAM[currentAddress];
        inst.addByte(byte);

        modeAndCode = getModeAndCode(byte);
        length = instructionLength[modeAndCode.mode];
        inst.setModeAndCode(modeAndCode);

        for (var i = 1; i < length; i++) {
          currentAddress++;
          byte = RAM[currentAddress];
          inst.addByte(byte);
          inst.addArg(byte);
        }
        instructions.push(inst);
        currentAddress++;
      }

      var html = 'Address  Hexdump   Dissassembly\n';
      html +=    '-------------------------------\n';
      html += instructions.join('\n');
      openPopup(html, 'Disassembly');
    }

    return {
      assembleLine: assembleLine,
      assembleCode: assembleCode,
      getCurrentPC: function () {
        return defaultCodePC;
      },
      hexdump: hexdump,
      disassemble: disassemble
    };
  }


  function addr2hex(addr) {
    return num2hex((addr >> 8) & 0xff) + num2hex(addr & 0xff);
  }

  function num2hex(nr) {
    var str = "0123456789abcdef";
    var hi = ((nr & 0xf0) >> 4);
    var lo = (nr & 15);
    return (str.substring(hi, hi + 1) + str.substring(lo, lo + 1)).toUpperCase();
  }

  // Prints text in the message window
  function message(text) {
    if (text.length>1)
      text += '\n'; // allow putc operations from the simulator (WDM opcode)
    $node.find('.messages code').append(text).scrollTop(10000);
  }

  initialize();
}

$(document).ready(function () {
  $('.widget').each(function () {
    SimulatorWidget(this);
  });
});
