/* -*- mode: Javascript; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * Copyright 2010-2013 Will Scullin <scullin@scullinsteel.com>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

/*globals toHex: false, debug: false*/
/*exported CPU6502 */

function CPU6502(options)
{
    options = options || {};

    var is65C02 = options['65C02'] ? true : false;

    /* Registers */
    var pc = 0, // Program Counter
        sr = 0x20, // Process Status Register
        ar = 0, // Accumulator
        xr = 0, // X Register
        yr = 0, // Y Register
        sp = 0xff; // Stack Pointer
    
    /* Addressing Mode */
    var modes = {
        accumulator: 0,           // A (Accumulator)
        implied:     1,           // Implied
        immediate:   2,           // # Immediate
        absolute:    3,           // a Absolute
        zeroPage:    4,           // zp Zero Page
        relative:    5,           // r Relative

        absoluteX:   6,           // a,X Absolute, X
        absoluteY:   7,           // a,Y Absolute, Y
        zeroPageX:   8,           // zp,X Zero Page, X
        zeroPageY:   9,           // zp,Y Zero Page, Y

        absoluteIndirect:   10,  // (a) Indirect
        zeroPageXIndirect:  11, // (zp,X) Zero Page Indexed Indirect 
        zeroPageIndirectY:  12, // (zp),Y Zero Page Indexed with Y
        
        /* 65c02 */
        zeroPageIndirect:   13,  // (zp),
        absoluteXIndirect:  14   // (a, X)
    };

    var sizes = {
        0 /* modes.accumulator */: 1,
        1 /* modes.implied */: 1,
        2 /* modes.immediate */: 2,
        3 /* modes.absolute */: 3,
        4 /* modes.zeroPage */: 2,
        5 /* modes.relative */: 2,
        6 /* modes.absoluteX */: 3,
        7 /* modes.absoluteY */: 3,
        8 /* modes.zeroPageX */: 2,
        9 /* modes.zeroPageY */: 2,
        10 /* modes.indirect */: 3,
        11 /* modes.zeroPageXIndirect */: 2,
        12 /* modes.zeroPageYIndirect */: 2,

        13 /* mode.zeroPageIndirect */: 2,
        14 /* mode.absoluteXIndirect */: 3
    };

    /* Flags */
    var flags = {
        N: 0x80, // Negative
        V: 0x40, // oVerflow
        B: 0x10, // Break
        D: 0x08, // Decimal
        I: 0x04, // Interrupt
        Z: 0x02, // Zero
        C: 0x01  // Carry
    };
        
    /* Memory Locations */
    var loc = {
        STACK: 0x100,
        NMI: 0xFFFA,
        RESET: 0xFFFC,
        BRK: 0xFFFE
    };

    var idx;
    
    var readPages = [];
    var writePages = [];
    var resetHandlers = [];
    var inCallback = false;
    var cycles = 0;

    var blankPage = {
        read: function() { return 0; },
        write: function() {}
    };

    for (idx = 0; idx < 0x100; idx++) {
        readPages[idx] = blankPage;
        writePages[idx] = blankPage;
    }

    function setFlag(f, on) {
        sr = on ? (sr | f) : (sr & ~f);
    }

    function testNZ(val) {
        sr = val === 0 ? (sr | flags.Z) : (sr & ~flags.Z);
        sr = val & 0x80 ? (sr | flags.N) : (sr & ~flags.N);

        return val;
    }

    function testZ(val) {
        sr = val === 0 ? (sr | flags.Z) : (sr & ~flags.Z);

        return val;
    }

    function add(a, b, sub) {
        if (sub) 
            b ^= 0xff;
  
        // KEGS
        var c, v;
        if (sr & flags.D) {
            c = (a & 0x0f) + (b & 0x0f) + (sr & flags.C);
            if (sub) {
                if (c < 0x10)
                    c = (c - 0x06) & 0x0f;
                c += (a & 0xf0) + (b & 0xf0);
                v = (c >> 1) ^ c;
                if (c < 0x100) 
                    c = (c + 0xa0) & 0xff;
            } else {
                if (c > 0x09)
                    c = (c - 0x0a) | 0x10; // carry to MSN
                c += (a & 0xf0) + (b & 0xf0);
                v = (c >> 1) ^ c;
                if (c > 0x99)
                    c += 0x60;
            }
        } else {
            c = a + b + (sr & flags.C);
            v = (c ^ a) & 0x80;
        }

        if ((a ^ b) & 0x80)
            v = 0;

        setFlag(flags.C, c > 0xff);
        setFlag(flags.V, v);

        return testNZ(c & 0xff);
    }

    function increment(a) {
        return testNZ((a + 0x01) & 0xff);
    }

    function decrement(a) {
        return testNZ((a + 0xff) & 0xff);
    }

    function readBytePC(dbg) {
        var addr = (pc++) & 0xffff,
            page = addr >> 8,
            off = addr & 0xff;

        return readPages[page].read(page, off, dbg);
    }

    function readByte(addr, dbg) {
        var page = addr >> 8,
            off = addr & 0xff;

        return readPages[page].read(page, off, dbg);
    }

    function writeByte(addr, val) {
        var page = addr >> 8,
            off = addr & 0xff;
        
        writePages[page].write(page, off, val);
    }

    function readWord(addr, dbg) {
        return readByte(addr, dbg) | (readByte(addr + 1, dbg) << 8);
    }

    function readWordPC(dbg) {
        return readBytePC(dbg) | (readBytePC(dbg) << 8);
    }

    function readZPWord(addr, dbg) {
        var lsb, msb;

        lsb = readByte(addr & 0xff, dbg);
        msb = readByte((addr + 1) & 0xff, dbg);
        
        return (msb << 8) | lsb;
    }

    function pushByte(val) {
        writeByte(loc.STACK | sp, val);
        sp = (sp + 0xff) & 0xff;
    }

    function pushWord(val) {
        pushByte(val >> 8);
        pushByte(val & 0xff);
    }

    function pullByte() {
        sp = (sp + 0x01) & 0xff;
        var result = readByte(loc.STACK | sp);
        return result;
    }

    function pullWord() {
        var lsb = pullByte(),
            msb = pullByte();

        return (msb << 8) | lsb;
    }

    function indirectBug(addr) {
        var page = addr & 0xff00;
        var off = addr & 0xff;
        var lsb = readByte(page | (off & 0xff));
        var msb = readByte(page | ((off + 0x01) & 0xff));

        return (msb << 8) | lsb;
    }

    /*
     * Read functions
     */

    // #$00
    function readImmediate() {
        return readBytePC();
    }

    // $0000
    function readAbsolute() {
        return readByte(readWordPC());
    }
    
    // $00
    function readZeroPage() {
        return readByte(readBytePC());
    }
          
    // $0000,X
    function readAbsoluteX() {
        var addr = readWordPC(), oldPage = addr >> 8, page;
        addr = (addr + xr) & 0xffff;
        page = addr >> 8;
        if (page != oldPage) {
            cycles++;
        }
        return readByte(addr);
    }
    
    // $0000,Y
    function readAbsoluteY() {
        var addr = readWordPC(), oldPage = addr >> 8, page;
        addr = (addr + yr) & 0xffff;
        page = addr >> 8;
        if (page != oldPage) {
            cycles++;
        }
        return readByte(addr);
    }
    
    // $00,X
    function readZeroPageX() {
        return readByte((readBytePC() + xr) & 0xff);
    }
    
    // $00,Y
    function readZeroPageY() {
        return readByte((readBytePC() + yr) & 0xff);
    }
    
    // ($00,X)
    function readZeroPageXIndirect() {
        return readByte(readZPWord((readBytePC() + xr) & 0xff));
    }
    
    // ($00),Y
    function readZeroPageIndirectY() {
        var addr = readZPWord(readBytePC()), oldPage = addr >> 8, page;
        addr = (addr + yr) & 0xffff;
        page = addr >> 8;
        if (page != oldPage) {
            cycles++;
        }
        return readByte(addr);
    }
    
    // ($00) (65C02)
    function readZeroPageIndirect() {
        return readByte(readZPWord(readBytePC()));
    }

    /*
     * Write Functions
     */

    // $0000
    function writeAbsolute(val) {
        writeByte(readWordPC(), val);
    }
            
    // $00
    function writeZeroPage(val) {
        writeByte(readBytePC(), val);
    }

    // $0000,X
    function writeAbsoluteX(val) {
        writeByte((readWordPC() + xr) & 0xffff, val);
    }
            
    // $0000,Y
    function writeAbsoluteY(val) {
        writeByte((readWordPC() + yr) & 0xffff, val);
    }

    // $00,X
    function writeZeroPageX(val) {
        writeByte((readBytePC() + xr) & 0xff, val);
    }

    // $00,Y
    function writeZeroPageY(val) {
        writeByte((readBytePC() + yr) & 0xff, val);
    }

    // ($00,X)
    function writeZeroPageXIndirect(val) {
        writeByte(readZPWord((readBytePC() + xr) & 0xff), val);
    }

    // ($00),Y
    function writeZeroPageIndirectY(val) {
        writeByte((readZPWord(readBytePC()) + yr) & 0xffff, val);
    }

    // ($00) (65C02)
    function writeZeroPageIndirect(val) {
        writeByte(readZPWord(readBytePC()), val);
    }

    // $00
    function readAddrZeroPage() {
        return readBytePC();
    }

    // $00,X
    function readAddrZeroPageX() {
        return (readBytePC() + xr) & 0xff;
    }

    // $0000 (65C02)
    function readAddrAbsolute() {
        return readWordPC();
    }

    // $0000 
    function readAddrAbsoluteIndirectBug() {
        return indirectBug(readWordPC());
    }

    // ($0000)
    function readAddrAbsoluteIndirect() {
        return readWord(readWordPC());
    }

    // $0000,X
    function readAddrAbsoluteX() {
        return (readWordPC() + xr) & 0xffff;
    }

    // $(0000,X)
    function readAddrAbsoluteXIndirect() {
        return readWord((readWordPC() + xr) & 0xffff);
    }

    /* Break */
    function brk(readFn) {
        readFn();
        pushWord(pc);
        pushByte(sr | flags.B);
        if (is65C02) {
            setFlag(flags.D, false);
        }
        setFlag(flags.I, true);
        pc = readWord(loc.BRK);
    }
    
    /* Load Accumulator */
    function lda(readFn) {
        ar = testNZ(readFn());
    }
    
    /* Load X Register */
    function ldx(readFn) {
        xr = testNZ(readFn());
    }

    /* Load Y Register */
    function ldy(readFn) {
        yr = testNZ(readFn());
    }

    /* Store Accumulator */
    function sta(writeFn) {
        writeFn(ar);
    }

    /* Store X Register */
    function stx(writeFn) {
        writeFn(xr);
    }

    /* Store Y Register */
    function sty(writeFn) {
        writeFn(yr);
    }

    /* Store Zero */
    function stz(writeFn) {
        writeFn(0);
    }

    /* Add with Carry */
    function adc(readFn) {
        ar = add(ar, readFn(), false);
    }
    
    /* Subtract with Carry */
    function sbc(readFn) {
        ar = add(ar, readFn(), true);
    }

    /* Increment Memory */
    function incA() {
        ar = increment(ar);
    }
    
    function inc(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, increment(readByte(addr)));
    }

    /* Increment X */
    function inx() {
        xr = increment(xr);
    }

    /* Increment Y */
    function iny() {
        yr = increment(yr);
    }

    /* Decrement Memory */
    function decA() {
        ar = decrement(ar);
    }

    function dec(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, decrement(readByte(addr)));
    }

    /* Decrement X */
    function dex() {
        xr = decrement(xr); 
    }

    /* Decrement Y */
    function dey() {
        yr = decrement(yr); 
    }

    function shiftLeft(val) {
        setFlag(flags.C, val & 0x80);
        return testNZ((val << 1) & 0xff);
    }

    /* Arithmatic Shift Left */
    function aslA() {
        ar = shiftLeft(ar);
    }
    
    function asl(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, shiftLeft(readByte(addr)));
    }

    function shiftRight(val) {
        setFlag(flags.C, val & 0x01);
        return testNZ(val >> 1);
    }

    /* Logical Shift Right */
    function lsrA() {
        ar = shiftRight(ar);
    }
    
    function lsr(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, shiftRight(readByte(addr)));
    }

    function rotateLeft(val) {
        var c = (sr & flags.C);
        setFlag(flags.C, val & 0x80);
        return testNZ(((val << 1) | (c ? 0x01 : 0x00)) & 0xff);
    }

    /* Rotate Left */
    function rolA() {
        ar = rotateLeft(ar);
    }

    function rol(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, rotateLeft(readByte(addr)));
    }

    function rotateRight(a) {
        var c = (sr & flags.C);
        setFlag(flags.C, a & 0x01);
        return testNZ((a >> 1) | (c ? 0x80 : 0x00));
    }

    /* Rotate Right */
    function rorA() {
        ar = rotateRight(ar);
    }

    function ror(readAddrFn) {
        var addr = readAddrFn();
        writeByte(addr, rotateRight(readByte(addr)));
    }

    /* Logical And Accumulator */
    function and(readFn) {
        ar = testNZ(ar & readFn());
    }

    /* Logical Or Accumulator */
    function ora(readFn) {
        ar = testNZ(ar | readFn());
    }

    /* Logical Exclusive Or Accumulator */
    function eor(readFn) {
        ar = testNZ(ar ^ readFn());
    }

    /* Test and Reset Bits */
    function trb(readAddrFn) {
        var addr = readAddrFn(),
            val = readByte(addr);
        testZ(val & ar);
        writeByte(addr, val & ~ar);
    }

    /* Test and Set Bits */
    function tsb(readAddrFn) {
        var addr = readAddrFn(),
            val = readByte(addr);
        testZ(val & ar);
        writeByte(addr, val | ar);
    }

    /* Bit */
    function bit(readFn) {
        var val = readFn();
        setFlag(flags.Z, !(val & ar));
        setFlag(flags.N, val & 0x80);
        setFlag(flags.V, val & 0x40);
    }

    /* Bit Immediate*/
    function bitI(readFn) {
        var val = readFn();
        setFlag(flags.Z, !(val & ar));
    }

    function compare(a, b) 
    {
        b = (b ^ 0xff);
        var c = a + b + 1;
        setFlag(flags.C, c > 0xff);
        testNZ(c & 0xff);
    }

    function cmp(readFn) {
        compare(ar, readFn());
    }
    
    function cpx(readFn) {
        compare(xr, readFn());
    }

    function cpy(readFn) {
        compare(yr, readFn());
    }

    /* Branches */
    function brs(f) {
        var off = readBytePC(); // changes pc
        if (f & sr) {
            var oldPC = pc;
            pc += off > 127 ? off - 256 : off;
            cycles++;
            if ((pc >> 8) != (oldPC >> 8)) cycles++;
        }
    }

    function brc(f) {
        var off = readBytePC(); // changes pc
        if (!(f & sr)) {
            var oldPC = pc;
            pc += off > 127 ? off - 256 : off;
            cycles++;
            if ((pc >> 8) != (oldPC >> 8)) cycles++;
        }
    }
    
    /* Transfers and stack */
    function tax() { testNZ(xr = ar); }

    function txa() { testNZ(ar = xr); }

    function tay() { testNZ(yr = ar); }

    function tya() { testNZ(ar = yr); }

    function tsx() { testNZ(xr = sp); }

    function txs() { sp = xr; }

    function pha() { pushByte(ar); }

    function pla() { testNZ(ar = pullByte()); }

    function phx() { pushByte(xr); }

    function plx() { testNZ(xr = pullByte()); }

    function phy() { pushByte(yr); }

    function ply() { testNZ(yr = pullByte()); }

    function php() { pushByte(sr | flags.B); }

    function plp() { sr = (pullByte() & ~flags.B) | 0x20; }

    /* Jump */
    function jmp(readAddrFn) {
        pc = readAddrFn();
    }

    /* Jump Subroutine */
    function jsr(readAddrFn) {
        var dest = readAddrFn();
        pushWord(pc - 1);
        pc = dest;
    }

    /* Return from Subroutine */
    function rts() {
        pc = (pullWord() + 1) & 0xffff;
    }

    /* Return from Subroutine */
    function rti() {
        sr = pullByte() & 0xef;
        pc = pullWord();
    }

    /* Set and Clear */
    function set(flag) {
        sr |= flag;
    }

    function clr(flag) {
        sr &= ~flag;
    }

    /* No-Op */
    function nop() {
    }

    var ops = {
        // LDA
        0xa9: ["LDA", lda, readImmediate, modes.immediate, 2],
        0xa5: ["LDA", lda, readZeroPage, modes.zeroPage, 3],
        0xb5: ["LDA", lda, readZeroPageX, modes.zeroPageX, 4],
        0xad: ["LDA", lda, readAbsolute, modes.absolute, 4],
        0xbd: ["LDA", lda, readAbsoluteX, modes.absoluteX, 4],
        0xb9: ["LDA", lda, readAbsoluteY, modes.absoluteY, 4],
        0xa1: ["LDA", lda, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0xb1: ["LDA", lda, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // LDX
        0xa2: ["LDX", ldx, readImmediate, modes.immediate, 2],
        0xa6: ["LDX", ldx, readZeroPage, modes.zeroPage, 3],
        0xb6: ["LDX", ldx, readZeroPageY, modes.zeroPageY, 4],
        0xae: ["LDX", ldx, readAbsolute, modes.absolute, 4],
        0xbe: ["LDX", ldx, readAbsoluteY, modes.absoluteY, 4],

        // LDY
        0xa0: ["LDY", ldy, readImmediate, modes.immediate, 2],
        0xa4: ["LDY", ldy, readZeroPage, modes.zeroPage, 3],
        0xb4: ["LDY", ldy, readZeroPageX, modes.zeroPageX, 4],
        0xac: ["LDY", ldy, readAbsolute, modes.absolute, 4],
        0xbc: ["LDY", ldy, readAbsoluteX, modes.absoluteX, 4],

        // STA
        0x85: ["STA", sta, writeZeroPage, modes.zeroPage, 3],
        0x95: ["STA", sta, writeZeroPageX, modes.zeroPageX, 4],
        0x8d: ["STA", sta, writeAbsolute, modes.absolute, 4],
        0x9d: ["STA", sta, writeAbsoluteX, modes.absoluteX, 5],
        0x99: ["STA", sta, writeAbsoluteY, modes.absoluteY, 5],
        0x81: ["STA", sta, writeZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0x91: ["STA", sta, writeZeroPageIndirectY, modes.zeroPageIndirectY, 6],

        // STX
        0x86: ["STX", stx, writeZeroPage, modes.zeroPage, 3],
        0x96: ["STX", stx, writeZeroPageY, modes.zeroPageY, 4],
        0x8e: ["STX", stx, writeAbsolute, modes.absolute, 4],

        // STY
        0x84: ["STY", sty, writeZeroPage, modes.zeroPage, 3],
        0x94: ["STY", sty, writeZeroPageX, modes.zeroPageX, 4],
        0x8c: ["STY", sty, writeAbsolute, modes.absolute, 4],

        // ADC
        0x69: ["ADC", adc, readImmediate, modes.immediate, 2],
        0x65: ["ADC", adc, readZeroPage, modes.zeroPage, 3],
        0x75: ["ADC", adc, readZeroPageX, modes.zeroPageX, 4],
        0x6D: ["ADC", adc, readAbsolute, modes.absolute, 4],
        0x7D: ["ADC", adc, readAbsoluteX, modes.absoluteX, 4],
        0x79: ["ADC", adc, readAbsoluteY, modes.absoluteY, 4],
        0x61: ["ADC", adc, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0x71: ["ADC", adc, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // SBC
        0xe9: ["SBC", sbc, readImmediate, modes.immediate, 2],
        0xe5: ["SBC", sbc, readZeroPage, modes.zeroPage, 3],
        0xf5: ["SBC", sbc, readZeroPageX, modes.zeroPageX, 4],
        0xeD: ["SBC", sbc, readAbsolute, modes.absolute, 4],
        0xfD: ["SBC", sbc, readAbsoluteX, modes.absoluteX, 4],
        0xf9: ["SBC", sbc, readAbsoluteY, modes.absoluteY, 4],
        0xe1: ["SBC", sbc, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0xf1: ["SBC", sbc, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // INC
        0xe6: ["INC", inc, readAddrZeroPage, modes.zeroPage, 5],
        0xf6: ["INC", inc, readAddrZeroPageX, modes.zeroPageX, 6],
        0xee: ["INC", inc, readAddrAbsolute, modes.absolute, 6],
        0xfe: ["INC", inc, readAddrAbsoluteX, modes.absoluteX, 7],

        // INX
        0xe8: ["INX", inx, null, modes.implied, 2],

        // INY
        0xc8: ["INY", iny, null, modes.implied, 2],

        // DEC
        0xc6: ["DEC", dec, readAddrZeroPage, modes.zeroPage, 5],
        0xd6: ["DEC", dec, readAddrZeroPageX, modes.zeroPageX, 6],
        0xce: ["DEC", dec, readAddrAbsolute, modes.absolute, 6],
        0xde: ["DEC", dec, readAddrAbsoluteX, modes.absoluteX, 7],

        // DEX
        0xca: ["DEX", dex, null, modes.implied, 2],

        // DEY
        0x88: ["DEY", dey, null, modes.implied, 2],

        // ASL
        0x0A: ["ASL", aslA, null, modes.accumulator, 2],
        0x06: ["ASL", asl, readAddrZeroPage, modes.zeroPage, 5],
        0x16: ["ASL", asl, readAddrZeroPageX, modes.zeroPageX, 6],
        0x0E: ["ASL", asl, readAddrAbsolute, modes.absolute, 6],
        0x1E: ["ASL", asl, readAddrAbsoluteX, modes.absoluteX, 7],

        // LSR
        0x4A: ["LSR", lsrA, null, modes.accumulator, 2],
        0x46: ["LSR", lsr, readAddrZeroPage, modes.zeroPage, 5],
        0x56: ["LSR", lsr, readAddrZeroPageX, modes.zeroPageX, 6],
        0x4E: ["LSR", lsr, readAddrAbsolute, modes.absolute, 6],
        0x5E: ["LSR", lsr, readAddrAbsoluteX, modes.absoluteX, 7],

        // ROL
        0x2A: ["ROL", rolA, null, modes.accumulator, 2],
        0x26: ["ROL", rol, readAddrZeroPage, modes.zeroPage, 5],
        0x36: ["ROL", rol, readAddrZeroPageX, modes.zeroPageX, 6],
        0x2E: ["ROL", rol, readAddrAbsolute, modes.absolute, 6],
        0x3E: ["ROL", rol, readAddrAbsoluteX, modes.absoluteX, 7],

        // ROR
        0x6A: ["ROR", rorA, null, modes.accumulator, 2],
        0x66: ["ROR", ror, readAddrZeroPage, modes.zeroPage, 5],
        0x76: ["ROR", ror, readAddrZeroPageX, modes.zeroPageX, 6],
        0x6E: ["ROR", ror, readAddrAbsolute, modes.absolute, 6],
        0x7E: ["ROR", ror, readAddrAbsoluteX, modes.absoluteX, 7],

        // AND
        0x29: ["AND", and, readImmediate, modes.immediate, 2],
        0x25: ["AND", and, readZeroPage, modes.zeroPage, 2],
        0x35: ["AND", and, readZeroPageX, modes.zeroPageX, 3],
        0x2D: ["AND", and, readAbsolute, modes.absolute, 4],
        0x3D: ["AND", and, readAbsoluteX, modes.absoluteX, 4],
        0x39: ["AND", and, readAbsoluteY, modes.absoluteY, 4],
        0x21: ["AND", and, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0x31: ["AND", and, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // ORA
        0x09: ["ORA", ora, readImmediate, modes.immediate, 2],
        0x05: ["ORA", ora, readZeroPage, modes.zeroPage, 2],
        0x15: ["ORA", ora, readZeroPageX, modes.zeroPageX, 3],
        0x0D: ["ORA", ora, readAbsolute, modes.absolute, 4],
        0x1D: ["ORA", ora, readAbsoluteX, modes.absoluteX, 4],
        0x19: ["ORA", ora, readAbsoluteY, modes.absoluteY, 4],
        0x01: ["ORA", ora, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0x11: ["ORA", ora, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // EOR
        0x49: ["EOR", eor, readImmediate, modes.immediate, 2],
        0x45: ["EOR", eor, readZeroPage, modes.zeroPage, 3],
        0x55: ["EOR", eor, readZeroPageX, modes.zeroPageX, 4],
        0x4D: ["EOR", eor, readAbsolute, modes.absolute, 4],
        0x5D: ["EOR", eor, readAbsoluteX, modes.absoluteX, 4],
        0x59: ["EOR", eor, readAbsoluteY, modes.absoluteY, 4],
        0x41: ["EOR", eor, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0x51: ["EOR", eor, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // CMP
        0xc9: ["CMP", cmp, readImmediate, modes.immediate, 2],
        0xc5: ["CMP", cmp, readZeroPage, modes.zeroPage, 3],
        0xd5: ["CMP", cmp, readZeroPageX, modes.zeroPageX, 4],
        0xcD: ["CMP", cmp, readAbsolute, modes.absolute, 4],
        0xdD: ["CMP", cmp, readAbsoluteX, modes.absoluteX, 4],
        0xd9: ["CMP", cmp, readAbsoluteY, modes.absoluteY, 4],
        0xc1: ["CMP", cmp, readZeroPageXIndirect, modes.zeroPageXIndirect, 6],
        0xd1: ["CMP", cmp, readZeroPageIndirectY, modes.zeroPageIndirectY, 5],

        // CPX
        0xE0: ["CPX", cpx, readImmediate, modes.immediate, 2],
        0xE4: ["CPX", cpx, readZeroPage, modes.zeroPage, 3],
        0xEC: ["CPX", cpx, readAbsolute, modes.absolute, 4],

        // CPY
        0xC0: ["CPY", cpy, readImmediate, modes.immediate, 2],
        0xC4: ["CPY", cpy, readZeroPage, modes.zeroPage, 3],
        0xCC: ["CPY", cpy, readAbsolute, modes.absolute, 4],

        // BIT
        0x24: ["BIT", bit, readZeroPage, modes.zeroPage, 3],
        0x2C: ["BIT", bit, readAbsolute, modes.absolute, 4],

        // BCC
        0x90: ["BCC", brc, flags.C, modes.relative, 2],

        // BCS
        0xB0: ["BCS", brs, flags.C, modes.relative, 2],

        // BEQ
        0xF0: ["BEQ", brs, flags.Z, modes.relative, 2],

        // BMI
        0x30: ["BMI", brs, flags.N, modes.relative, 2],

        // BNE
        0xD0: ["BNE", brc, flags.Z, modes.relative, 2],

        // BPL
        0x10: ["BPL", brc, flags.N, modes.relative, 2],

        // BVC
        0x50: ["BVC", brc, flags.V, modes.relative, 2],

        // BVS
        0x70: ["BVS", brs, flags.V, modes.relative, 2],

        // TAX
        0xAA: ["TAX", tax, null, modes.implied, 2],

        // TXA
        0x8A: ["TXA", txa, null, modes.implied, 2],

        // TAY
        0xA8: ["TAY", tay, null, modes.implied, 2],

        // TYA
        0x98: ["TYA", tya, null, modes.implied, 2],

        // TSX
        0xBA: ["TSX", tsx, null, modes.implied, 2],

        // TXS
        0x9A: ["TXS", txs, null, modes.implied, 2],

        // PHA
        0x48: ["PHA", pha, null, modes.implied, 3],

        // PLA
        0x68: ["PLA", pla, null, modes.implied, 4],

        // PHP
        0x08: ["PHP", php, null, modes.implied, 3],

        // PLP
        0x28: ["PLP", plp, null, modes.implied, 4],

        // JMP
        0x4C: ["JMP", jmp, 
               readAddrAbsolute, modes.absolute, 3],
        0x6C: ["JMP", jmp, 
               readAddrAbsoluteIndirectBug, modes.absoluteIndirect, 5],

        // JSR
        0x20: ["JSR", jsr, readAddrAbsolute, modes.absolute, 6],

        // RTS
        0x60: ["RTS", rts, null, modes.implied, 6],
        
        // RTI
        0x40: ["RTI", rti, null, modes.implied, 6],

        // SEC
        0x38: ["SEC", set, flags.C, modes.implied, 2],

       // SED
        0xF8: ["SED", set, flags.D, modes.implied, 2],

        // SEI
        0x78: ["SEI", set, flags.I, modes.implied, 2],

        // CLC
        0x18: ["CLC", clr, flags.C, modes.implied, 2],

        // CLD
        0xD8: ["CLD", clr, flags.D, modes.implied, 2],

        // CLI
        0x58: ["CLI", clr, flags.I, modes.implied, 2],

        // CLV
        0xB8: ["CLV", clr, flags.V, modes.implied, 2],

        // NOP
        0xea: ["NOP", nop, null, modes.implied, 2],

        // BRK
        0x00: ["BRK", brk, readImmediate, modes.immediate, 7]
    };

    /* 65C02 Instructions */
    
    var cops = {
        // INC / DEC A
        0x1A: ["INC", incA, null, modes.accumulator, 2],
        0x3A: ["DEC", decA, null, modes.accumulator, 2],

        // Indirect Zero Page for the masses
        0x12: ["ORA", ora, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0x32: ["AND", and, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0x52: ["EOR", eor, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0x72: ["ADC", adc, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0x92: ["STA", sta, writeZeroPageIndirect, modes.zeroPageIndirect, 5],
        0xB2: ["LDA", lda, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0xD2: ["CMP", cmp, readZeroPageIndirect, modes.zeroPageIndirect, 5],
        0xF2: ["SBC", sbc, readZeroPageIndirect, modes.zeroPageIndirect, 5],

        // Better BIT
        0x34: ["BIT", bit, readZeroPageX, modes.zeroPageX, 4],
        0x3C: ["BIT", bit, readAbsoluteX, modes.absoluteX, 4],
        0x89: ["BIT", bitI, readImmediate, modes.immediate, 2],

        // JMP absolute indirect indexed
        0x6C: ["JMP", jmp, readAddrAbsoluteIndirect, 
               modes.absoluteIndirect, 6],
        0x7C: ["JMP", jmp, readAddrAbsoluteXIndirect, 
               modes.absoluteXIndirect, 6],

        // BRA
        0x80: ["BRA", brc, 0, modes.relative, 3],

        // PHX
        0xDA: ["PHX", phx, null, modes.implied, 3],

        // PHY
        0x5A: ["PHY", phy, null, modes.implied, 3],

        // PLX
        0xFA: ["PLX", plx, null, modes.implied, 3],

        // PLY
        0x7A: ["PLY", ply, null, modes.implied, 3],

        // STZ
        0x64: ["STZ", stz, writeZeroPage, modes.zeroPage, 3],
        0x74: ["STZ", stz, writeZeroPageX, modes.zeroPageX, 4],
        0x9C: ["STZ", stz, writeAbsolute, modes.absolute, 4],
        0x9E: ["STZ", stz, writeAbsoluteX, modes.absoluteX, 5],
        
        // TRB
        0x14: ["TRB", trb, readAddrZeroPage, modes.zeroPage, 5],
        0x1C: ["TRB", trb, readAddrAbsolute, modes.absolute, 6],

        // TSB
        0x04: ["TSB", tsb, readAddrZeroPage, modes.zeroPage, 5],
        0x0C: ["TSB", tsb, readAddrAbsolute, modes.absolute, 6]
    };

    if (is65C02) {
        for (var key in cops) {
            ops[key] = cops[key];
        }
    }

    function unknown(b) {
        var unk;

        if (is65C02) {
            unk = ["NOP (" + toHex(b) + ")", function() {
                debug("Unknown OpCode: " + toHex(b) + " at " + toHex(pc - 1, 4));
            }, null, modes.implied, 2];
        } else {
            unk = ["???", 
                   function() { /* debug("Unknown OpCode: " + toHex(b) +
                                      " at " + toHex(pc - 1, 4)); */ }, 
                   null, modes.implied,
                   1];
        }
        ops[b] = unk;
        return unk;
    }

    /* Certain browsers benefit from using arrays over maps */
    var opary = [];

    for (idx = 0; idx < 0x100; idx++) {
        opary[idx] = ops[idx] || unknown(idx);
    }

    function dumpArgs(addr, m, symbols) {
        function toHexOrSymbol(v, n) {
            if (symbols && symbols[v]) {
                return symbols[v];
            } else {
                return "$" + toHex(v, n);
            }
        }
        var result = "";
        switch (m) {
        case modes.implied:
            break;
        case modes.immediate:
            result = "#" + toHexOrSymbol(readByte(addr, true));
            break;
        case modes.absolute:
            result = "" + toHexOrSymbol(readWord(addr, true), 4);
            break;
        case modes.zeroPage:
            result = "" + toHexOrSymbol(readByte(addr, true));
            break;
        case modes.relative:
            {
                var off = readByte(addr, true);
                if (off > 127) {
                    off -= 256;
                }
                addr += off + 1;
                result = "" + toHexOrSymbol(addr, 4) + " (" + off + ")";
            }
            break;
        case modes.absoluteX:
            result = "" + toHexOrSymbol(readWord(addr, true), 4) + ",X";
            break;
        case modes.absoluteY:
            result = "" + toHexOrSymbol(readWord(addr, true), 4) + ",Y";
            break;
        case modes.zeroPageX:
            result = "" + toHexOrSymbol(readByte(addr, true)) + ",X";
            break;
        case modes.zeroPageY:
            result = "" + toHexOrSymbol(readByte(addr, true)) + ",Y";
            break;
        case modes.absoluteIndirect:
            result = "(" + toHexOrSymbol(readWord(addr, true), 4) + ")";
            break;
        case modes.zeroPageXIndirect:
            result = "(" + toHexOrSymbol(readByte(addr, true)) + ",X)";
            break;
        case modes.zeroPageIndirectY:
            result = "(" + toHexOrSymbol(readByte(addr, true)) + "),Y";
            break;
        case modes.accumulator:
            result = "A";
            break;
        case modes.zeroPageIndirect:
            result = "(" + toHexOrSymbol(readByte(addr, true)) + ")";
            break;
        case modes.absoluteXIndirect:
            result = "(" + toHexOrSymbol(readWord(addr, true), 4) + ",X)";
            break;
        default:
            break;
        }
        return result;
    }

    return {
        step: function cpu_step(cb) {
            var op = opary[readBytePC()];
            
            op[1](op[2]);
            cycles += op[4];

            if (cb) {
                inCallback = true;
                cb(this);
                inCallback = false;
            }
        },

        stepN: function(n) {
            var op, idx;

            for (idx = 0; idx < n; idx++) {
                op = opary[readBytePC()];
                op[1](op[2]);
                cycles += op[4];
            }
        },

        stepCycles: function(c) {
            var op, end = cycles + c;

            while (cycles < end) {
                op = opary[readBytePC()];
                op[1](op[2]);
                cycles += op[4];
            }
        },

        stepCyclesDebug: function(c, cb)
        {
            var op, end = cycles + c;

            if (inCallback)
                return;

            while (cycles < end) {
                op = opary[readBytePC()];
                op[1](op[2]);
                cycles += op[4];
        
                if (cb) {
                    inCallback = true;
                    cb(this);
                    inCallback = false;
                }
            }
        },

        addPageHandler: function(pho) {
            for (var idx = pho.start(); idx <= pho.end(); idx++) {
                writePages[idx] = pho;
                if (pho.read)
                    readPages[idx] = pho;
                if (pho.write)
                    writePages[idx] = pho;
            }
            if (pho.reset)
                resetHandlers.push(pho);
        },

        reset: function cpu_reset()
        {
            // cycles = 0;
            sr = 0x20;
            sp = 0xff;
            ar = 0;
            yr = 0;
            xr = 0;
            pc = readWord(loc.RESET);

            for (var idx = 0; idx < resetHandlers.length; idx++) {
                resetHandlers[idx].reset();
            }
        },

        setPC: function(_pc) {
            pc = _pc;
        },

        dumpPC: function(_pc, symbols) {
            if (_pc === undefined) {
                _pc = pc;
            }
            var b = readByte(_pc, true),
                op = ops[b],
                size = sizes[op[3]],
                result = toHex(_pc, 4) + "- ";

            if (symbols) {
                if (symbols[_pc]) {
                    result += symbols[_pc] + 
                        "          ".substring(symbols[_pc].length);
                } else {
                    result += "          ";
                }
            }

            for (var idx = 0; idx < 4; idx++) {
                if (idx < size) {
                    result += toHex(readByte(_pc + idx, true)) + " ";
                } else {
                    result += "   ";
                }
            }

            if (op === undefined)
                result += "??? (" + toHex(b) + ")";
            else
                result += op[0] + " " + dumpArgs(_pc + 1, op[3], symbols);
            
            return result;
        },

        dumpPage: function(start, end) {
            var result = "";
            if (start === undefined) {
                start = pc >> 8;
            }
            if (end === undefined) {
                end = start;
            }
            for (var page = start; page <= end; page++) {
                var b, idx, jdx;
                for (idx = 0; idx < 16; idx++) {
                    result += toHex(page) + toHex(idx << 4) + ": ";
                    for (jdx = 0; jdx < 16; jdx++) {
                        b = readByte(page * 256 + idx * 16 + jdx, true);
                        result += toHex(b) + " ";
                    }
                    result += "        ";
                    for (jdx = 0; jdx < 16; jdx++) {
                        b = readByte(page * 256 + idx * 16 + jdx, true) & 0x7f;
                        if (b >= 0x20 && b < 0x7f) {
                            result += String.fromCharCode(b);
                        } else {
                            result += ".";
                        }
                    }
                    result += "\n";
                }
            }
            return result;
        },

        list: function(_pc, symbols) {
            if (_pc === undefined) {
                _pc = pc;
            }
            var results = [];
            for (var jdx = 0; jdx < 20; jdx++) {
                var b = readByte(_pc), op = ops[b];
                results.push(this.dumpPC(_pc, symbols));
                _pc += sizes[op[3]];
            }
            return results;
        },
    
        cycles: function() { 
            return cycles;
        },

        registers: function() {
            return [pc,ar,xr,yr,sr,sp];
        },

        getState: function() {
            return {
                a: ar,
                x: xr,
                y: yr,
                s: sr,
                pc: pc,
                sp: sp,
                cycles: cycles
            };
        },
        
        setState: function(state) {
            ar = state.a;
            xr = state.x;
            yr = state.y;
            sr = state.s;
            pc = state.pc;
            sp = state.sp;
            cycles = state.cycles;
        },
        
        dumpRegisters: function() {
            return toHex(pc, 4) +
                "-   A=" + toHex(ar) +
                " X=" + toHex(xr) +
                " Y=" + toHex(yr) +
                " P=" + toHex(sr) +
                " S=" + toHex(sp) +
                " " +
                (sr & flags.N ? "N" : "-") + 
                (sr & flags.V ? "V" : "-") + 
                "-" + 
                (sr & flags.B ? "B" : "-") + 
                (sr & flags.D ? "D" : "-") + 
                (sr & flags.I ? "I" : "-") + 
                (sr & flags.Z ? "Z" : "-") + 
                (sr & flags.C ? "C" : "-");
        },

        write: function(page, off, val) {
            writePages[page].write(page, off, val);
        }
    };
}