@Stateful
public class MOS65C02 extends CPU {
    private static final Logger LOG = Logger.getLogger(MOS65C02.class.getName());

    public boolean readAddressTriggersEvent = true;
    static int RESET_VECTOR = 0x00FFFC;
    static int INT_VECTOR = 0x00FFFE;
    @Stateful
    public int A = 0x0FF;
    @Stateful
    public int X = 0x0FF;
    @Stateful
    public int Y = 0x0FF;
    @Stateful
    public int C = 1;
    @Stateful
    public boolean interruptSignalled = false;
    @Stateful
    public boolean Z = true;
    @Stateful
    public boolean I = true;
    @Stateful
    public boolean D = true;
    @Stateful
    public boolean B = true;
    @Stateful
    public boolean V = true;
    @Stateful
    public boolean N = true;
    @Stateful
    public int STACK = 0xFF;
    @ConfigurableField(name = "BRK on bad opcode", description = "If on, unrecognized opcodes will be treated as BRK.  Otherwise, they will be NOP")
    public boolean breakOnBadOpcode = false;
    @ConfigurableField(name = "Ext. opcode warnings", description = "If on, uses of 65c02 extended opcodes (or undocumented 6502 opcodes -- which will fail) will be logged to stdout for debugging purposes")
    public boolean warnAboutExtendedOpcodes = false;

    private RAM getMemory() {
        return computer.getMemory();
    }

    public MOS65C02(Computer computer) {
        super(computer);
        clearState();
    }

    @Override
    public void reconfigure() {
    }

    @Override
    public void clearState() {
        A = 0x0ff;
        X = 0x0ff;
        Y = 0x0ff;
        C = 1;
        interruptSignalled = false;
        Z = true;
        I = true;
        D = true;
        B = true;
        V = true;
        N = true;
        STACK = 0xff;
        setWaitCycles(0);
    }
    
    public enum OPCODE {
        ADC_IMM(0x0069, COMMAND.ADC, MODE.IMMEDIATE, 2),
        ADC_ZP(0x0065, COMMAND.ADC, MODE.ZEROPAGE, 3),
        ADC_ZP_X(0x0075, COMMAND.ADC, MODE.ZEROPAGE_X, 4),
        ADC_AB(0x006D, COMMAND.ADC, MODE.ABSOLUTE, 4),
        ADC_IND_ZP(0x0072, COMMAND.ADC, MODE.INDIRECT_ZP, 5, true),
        ADC_IND_ZP_X(0x0061, COMMAND.ADC, MODE.INDIRECT_ZP_X, 6),
        ADC_AB_X(0x007D, COMMAND.ADC, MODE.ABSOLUTE_X, 4),
        ADC_AB_Y(0x0079, COMMAND.ADC, MODE.ABSOLUTE_Y, 4),
        ADC_IND_ZP_Y(0x0071, COMMAND.ADC, MODE.INDIRECT_ZP_Y, 5),
        AND_IMM(0x0029, COMMAND.AND, MODE.IMMEDIATE, 2),
        AND_ZP(0x0025, COMMAND.AND, MODE.ZEROPAGE, 3),
        AND_ZP_X(0x0035, COMMAND.AND, MODE.ZEROPAGE_X, 4),
        AND_AB(0x002D, COMMAND.AND, MODE.ABSOLUTE, 4),
        AND_IND_ZP(0x0032, COMMAND.AND, MODE.INDIRECT_ZP, 5, true),
        AND_IND_ZP_X(0x0021, COMMAND.AND, MODE.INDIRECT_ZP_X, 6),
        AND_AB_X(0x003D, COMMAND.AND, MODE.ABSOLUTE_X, 4),
        AND_AB_Y(0x0039, COMMAND.AND, MODE.ABSOLUTE_Y, 4),
        AND_IND_ZP_Y(0x0031, COMMAND.AND, MODE.INDIRECT_ZP_Y, 5),
        ASL(0x000A, COMMAND.ASL_A, MODE.IMPLIED, 2),
        ASL_ZP(0x0006, COMMAND.ASL, MODE.ZEROPAGE, 5),
        ASL_ZP_X(0x0016, COMMAND.ASL, MODE.ZEROPAGE_X, 6),
        ASL_AB(0x000E, COMMAND.ASL, MODE.ABSOLUTE, 6),
        ASL_AB_X(0x001E, COMMAND.ASL, MODE.ABSOLUTE_X, 7),
        BCC_REL(0x0090, COMMAND.BCC, MODE.RELATIVE, 2),
        BCS_REL(0x00B0, COMMAND.BCS, MODE.RELATIVE, 2),
        BBR0(0x00f, COMMAND.BBR0, MODE.ZP_REL, 5, true),
        BBR1(0x01f, COMMAND.BBR1, MODE.ZP_REL, 5, true),
        BBR2(0x02f, COMMAND.BBR2, MODE.ZP_REL, 5, true),
        BBR3(0x03f, COMMAND.BBR3, MODE.ZP_REL, 5, true),
        BBR4(0x04f, COMMAND.BBR4, MODE.ZP_REL, 5, true),
        BBR5(0x05f, COMMAND.BBR5, MODE.ZP_REL, 5, true),
        BBR6(0x06f, COMMAND.BBR6, MODE.ZP_REL, 5, true),
        BBR7(0x07f, COMMAND.BBR7, MODE.ZP_REL, 5, true),
        BBS0(0x08f, COMMAND.BBS0, MODE.ZP_REL, 5, true),
        BBS1(0x09f, COMMAND.BBS1, MODE.ZP_REL, 5, true),
        BBS2(0x0af, COMMAND.BBS2, MODE.ZP_REL, 5, true),
        BBS3(0x0bf, COMMAND.BBS3, MODE.ZP_REL, 5, true),
        BBS4(0x0cf, COMMAND.BBS4, MODE.ZP_REL, 5, true),
        BBS5(0x0df, COMMAND.BBS5, MODE.ZP_REL, 5, true),
        BBS6(0x0ef, COMMAND.BBS6, MODE.ZP_REL, 5, true),
        BBS7(0x0ff, COMMAND.BBS7, MODE.ZP_REL, 5, true),
        BEQ_REL0(0x00F0, COMMAND.BEQ, MODE.RELATIVE, 2),
        BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 3, true),
        BIT_ZP(0x0024, COMMAND.BIT, MODE.ZEROPAGE, 3),
        BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 3, true),
        BIT_AB(0x002C, COMMAND.BIT, MODE.ABSOLUTE, 4),
        BIT_AB_X(0x003C, COMMAND.BIT, MODE.ABSOLUTE_X, 4, true),
        BMI_REL(0x0030, COMMAND.BMI, MODE.RELATIVE, 2),
        BNE_REL(0x00D0, COMMAND.BNE, MODE.RELATIVE, 2),
        BPL_REL(0x0010, COMMAND.BPL, MODE.RELATIVE, 2),
        BRA_REL(0x0080, COMMAND.BRA, MODE.RELATIVE, 2, true),
        //        BRK(0x0000, COMMAND.BRK, MODE.IMPLIED, 7),
        // Do this so that BRK is treated as a two-byte instruction
        BRK(0x0000, COMMAND.BRK, MODE.IMMEDIATE, 7),
        BVC_REL(0x0050, COMMAND.BVC, MODE.RELATIVE, 2),
        BVS_REL(0x0070, COMMAND.BVS, MODE.RELATIVE, 2),
        CLC(0x0018, COMMAND.CLC, MODE.IMPLIED, 2),
        CLD(0x00D8, COMMAND.CLD, MODE.IMPLIED, 2),
        CLI(0x0058, COMMAND.CLI, MODE.IMPLIED, 2),
        CLV(0x00B8, COMMAND.CLV, MODE.IMPLIED, 2),
        CMP_IMM(0x00C9, COMMAND.CMP, MODE.IMMEDIATE, 2),
        CMP_ZP(0x00C5, COMMAND.CMP, MODE.ZEROPAGE, 3),
        CMP_ZP_X(0x00D5, COMMAND.CMP, MODE.ZEROPAGE_X, 4),
        CMP_AB(0x00CD, COMMAND.CMP, MODE.ABSOLUTE, 4),
        CMP_IND_ZP_X(0x00C1, COMMAND.CMP, MODE.INDIRECT_ZP_X, 6),
        CMP_AB_X(0x00DD, COMMAND.CMP, MODE.ABSOLUTE_X, 4),
        CMP_AB_Y(0x00D9, COMMAND.CMP, MODE.ABSOLUTE_Y, 4),
        CMP_IND_ZP_Y(0x00D1, COMMAND.CMP, MODE.INDIRECT_ZP_Y, 5),
        CMP_IND_ZP(0x00D2, COMMAND.CMP, MODE.INDIRECT_ZP, 5, true),
        CPX_IMM(0x00E0, COMMAND.CPX, MODE.IMMEDIATE, 2),
        CPX_ZP(0x00E4, COMMAND.CPX, MODE.ZEROPAGE, 3),
        CPX_AB(0x00EC, COMMAND.CPX, MODE.ABSOLUTE, 4),
        CPY_IMM(0x00C0, COMMAND.CPY, MODE.IMMEDIATE, 2),
        CPY_ZP(0x00C4, COMMAND.CPY, MODE.ZEROPAGE, 3),
        CPY_AB(0x00CC, COMMAND.CPY, MODE.ABSOLUTE, 4),
        DEC(0x003A, COMMAND.DEA, MODE.IMPLIED, 2, true),
        DEC_ZP(0x00C6, COMMAND.DEC, MODE.ZEROPAGE, 5),
        DEC_ZP_X(0x00D6, COMMAND.DEC, MODE.ZEROPAGE_X, 6),
        DEC_AB(0x00CE, COMMAND.DEC, MODE.ABSOLUTE, 6),
        DEC_AB_X(0x00DE, COMMAND.DEC, MODE.ABSOLUTE_X, 7),
        DEX(0x00CA, COMMAND.DEX, MODE.IMPLIED, 2),
        DEY(0x0088, COMMAND.DEY, MODE.IMPLIED, 2),
        EOR_IMM(0x0049, COMMAND.EOR, MODE.IMMEDIATE, 2),
        EOR_ZP(0x0045, COMMAND.EOR, MODE.ZEROPAGE, 3),
        EOR_ZP_X(0x0055, COMMAND.EOR, MODE.ZEROPAGE_X, 4),
        EOR_AB(0x004D, COMMAND.EOR, MODE.ABSOLUTE, 4),
        EOR_IND_ZP(0x0052, COMMAND.EOR, MODE.INDIRECT_ZP, 5, true),
        EOR_IND_ZP_X(0x0041, COMMAND.EOR, MODE.INDIRECT_ZP_X, 6),
        EOR_AB_X(0x005D, COMMAND.EOR, MODE.ABSOLUTE_X, 4),
        EOR_AB_Y(0x0059, COMMAND.EOR, MODE.ABSOLUTE_Y, 4),
        EOR_IND_ZP_Y(0x0051, COMMAND.EOR, MODE.INDIRECT_ZP_Y, 5),
        INC(0x001A, COMMAND.INA, MODE.IMPLIED, 2, true),
        INC_ZP(0x00E6, COMMAND.INC, MODE.ZEROPAGE, 5),
        INC_ZP_X(0x00F6, COMMAND.INC, MODE.ZEROPAGE_X, 6),
        INC_AB(0x00EE, COMMAND.INC, MODE.ABSOLUTE, 6),
        INC_AB_X(0x00FE, COMMAND.INC, MODE.ABSOLUTE_X, 7),
        INX(0x00E8, COMMAND.INX, MODE.IMPLIED, 2),
        INY(0x00C8, COMMAND.INY, MODE.IMPLIED, 2),
        JMP_AB(0x004C, COMMAND.JMP, MODE.ABSOLUTE, 3, false, false),
        JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 5),
        JMP_IND_X(0x007C, COMMAND.JMP, MODE.INDIRECT_X, 6, true),
        JSR_AB(0x0020, COMMAND.JSR, MODE.ABSOLUTE, 6, false, false),
        LDA_IMM(0x00A9, COMMAND.LDA, MODE.IMMEDIATE, 2),
        LDA_ZP(0x00A5, COMMAND.LDA, MODE.ZEROPAGE, 3),
        LDA_ZP_X(0x00B5, COMMAND.LDA, MODE.ZEROPAGE_X, 4),
        LDA_AB(0x00AD, COMMAND.LDA, MODE.ABSOLUTE, 4),
        LDA_IND_ZP_X(0x00A1, COMMAND.LDA, MODE.INDIRECT_ZP_X, 6),
        LDA_AB_X(0x00BD, COMMAND.LDA, MODE.ABSOLUTE_X, 4),
        LDA_AB_Y(0x00B9, COMMAND.LDA, MODE.ABSOLUTE_Y, 4),
        LDA_IND_ZP_Y(0x00B1, COMMAND.LDA, MODE.INDIRECT_ZP_Y, 5),
        LDA_IND_ZP(0x00B2, COMMAND.LDA, MODE.INDIRECT_ZP, 5, true),
        LDX_IMM(0x00A2, COMMAND.LDX, MODE.IMMEDIATE, 2),
        LDX_ZP(0x00A6, COMMAND.LDX, MODE.ZEROPAGE, 3),
        LDX_ZP_Y(0x00B6, COMMAND.LDX, MODE.ZEROPAGE_Y, 4),
        LDX_AB(0x00AE, COMMAND.LDX, MODE.ABSOLUTE, 4),
        LDX_AB_Y(0x00BE, COMMAND.LDX, MODE.ABSOLUTE_Y, 4),
        LDY_IMM(0x00A0, COMMAND.LDY, MODE.IMMEDIATE, 2),
        LDY_ZP(0x00A4, COMMAND.LDY, MODE.ZEROPAGE, 3),
        LDY_ZP_X(0x00B4, COMMAND.LDY, MODE.ZEROPAGE_X, 4),
        LDY_AB(0x00AC, COMMAND.LDY, MODE.ABSOLUTE, 4),
        LDY_AB_X(0x00BC, COMMAND.LDY, MODE.ABSOLUTE_X, 4),
        LSR(0x004A, COMMAND.LSR_A, MODE.IMPLIED, 2),
        LSR_ZP(0x0046, COMMAND.LSR, MODE.ZEROPAGE, 5),
        LSR_ZP_X(0x0056, COMMAND.LSR, MODE.ZEROPAGE_X, 6),
        LSR_AB(0x004E, COMMAND.LSR, MODE.ABSOLUTE, 6),
        LSR_AB_X(0x005E, COMMAND.LSR, MODE.ABSOLUTE_X, 7),
        NOP(0x00EA, COMMAND.NOP, MODE.IMPLIED, 2),
        SPECIAL(0x00FC, COMMAND.NOP_SPECIAL, MODE.ABSOLUTE, 4),
        ORA_IMM(0x0009, COMMAND.ORA, MODE.IMMEDIATE, 2),
        ORA_ZP(0x0005, COMMAND.ORA, MODE.ZEROPAGE, 3),
        ORA_ZP_X(0x0015, COMMAND.ORA, MODE.ZEROPAGE_X, 4),
        ORA_AB(0x000D, COMMAND.ORA, MODE.ABSOLUTE, 4),
        ORA_IND_ZP(0x0012, COMMAND.ORA, MODE.INDIRECT_ZP, 5, true),
        ORA_IND_ZP_X(0x0001, COMMAND.ORA, MODE.INDIRECT_ZP_X, 6),
        ORA_AB_X(0x001D, COMMAND.ORA, MODE.ABSOLUTE_X, 4),
        ORA_AB_Y(0x0019, COMMAND.ORA, MODE.ABSOLUTE_Y, 4),
        ORA_IND_ZP_Y(0x0011, COMMAND.ORA, MODE.INDIRECT_ZP_Y, 5),
        PHA(0x0048, COMMAND.PHA, MODE.IMPLIED, 3),
        PHP(0x0008, COMMAND.PHP, MODE.IMPLIED, 3),
        PHX(0x00DA, COMMAND.PHX, MODE.IMPLIED, 3, true),
        PHY(0x005A, COMMAND.PHY, MODE.IMPLIED, 3, true),
        PLA(0x0068, COMMAND.PLA, MODE.IMPLIED, 4),
        PLP(0x0028, COMMAND.PLP, MODE.IMPLIED, 4),
        PLX(0x00FA, COMMAND.PLX, MODE.IMPLIED, 4, true),
        PLY(0x007A, COMMAND.PLY, MODE.IMPLIED, 4, true),
        RMB0(0x007, COMMAND.RMB0, MODE.ZEROPAGE, 5, true),
        RMB1(0x017, COMMAND.RMB1, MODE.ZEROPAGE, 5, true),
        RMB2(0x027, COMMAND.RMB2, MODE.ZEROPAGE, 5, true),
        RMB3(0x037, COMMAND.RMB3, MODE.ZEROPAGE, 5, true),
        RMB4(0x047, COMMAND.RMB4, MODE.ZEROPAGE, 5, true),
        RMB5(0x057, COMMAND.RMB5, MODE.ZEROPAGE, 5, true),
        RMB6(0x067, COMMAND.RMB6, MODE.ZEROPAGE, 5, true),
        RMB7(0x077, COMMAND.RMB7, MODE.ZEROPAGE, 5, true),
        ROL(0x002A, COMMAND.ROL_A, MODE.IMPLIED, 2),
        ROL_ZP(0x0026, COMMAND.ROL, MODE.ZEROPAGE, 5),
        ROL_ZP_X(0x0036, COMMAND.ROL, MODE.ZEROPAGE_X, 6),
        ROL_AB(0x002E, COMMAND.ROL, MODE.ABSOLUTE, 6),
        ROL_AB_X(0x003E, COMMAND.ROL, MODE.ABSOLUTE_X, 7),
        ROR(0x006A, COMMAND.ROR_A, MODE.IMPLIED, 2),
        ROR_ZP(0x0066, COMMAND.ROR, MODE.ZEROPAGE, 5),
        ROR_ZP_X(0x0076, COMMAND.ROR, MODE.ZEROPAGE_X, 6),
        ROR_AB(0x006E, COMMAND.ROR, MODE.ABSOLUTE, 6),
        ROR_AB_X(0x007E, COMMAND.ROR, MODE.ABSOLUTE_X, 7),
        RTI(0x0040, COMMAND.RTI, MODE.IMPLIED, 6),
        RTS(0x0060, COMMAND.RTS, MODE.IMPLIED, 6),
        SBC_IMM(0x00E9, COMMAND.SBC, MODE.IMMEDIATE, 2),
        SBC_ZP(0x00E5, COMMAND.SBC, MODE.ZEROPAGE, 3),
        SBC_ZP_X(0x00F5, COMMAND.SBC, MODE.ZEROPAGE_X, 4),
        SBC_AB(0x00ED, COMMAND.SBC, MODE.ABSOLUTE, 4),
        SBC_IND_ZP(0x00F2, COMMAND.SBC, MODE.INDIRECT_ZP, 5, true),
        SBC_IND_ZP_X(0x00E1, COMMAND.SBC, MODE.INDIRECT_ZP_X, 6),
        SBC_AB_X(0x00FD, COMMAND.SBC, MODE.ABSOLUTE_X, 4),
        SBC_AB_Y(0x00F9, COMMAND.SBC, MODE.ABSOLUTE_Y, 4),
        SBC_IND_ZP_Y(0x00F1, COMMAND.SBC, MODE.INDIRECT_ZP_Y, 5),
        SEC(0x0038, COMMAND.SEC, MODE.IMPLIED, 2),
        SED(0x00F8, COMMAND.SED, MODE.IMPLIED, 2),
        SEI(0x0078, COMMAND.SEI, MODE.IMPLIED, 2),
        SMB0(0x087, COMMAND.SMB0, MODE.ZEROPAGE, 5, true),
        SMB1(0x097, COMMAND.SMB1, MODE.ZEROPAGE, 5, true),
        SMB2(0x0a7, COMMAND.SMB2, MODE.ZEROPAGE, 5, true),
        SMB3(0x0b7, COMMAND.SMB3, MODE.ZEROPAGE, 5, true),
        SMB4(0x0c7, COMMAND.SMB4, MODE.ZEROPAGE, 5, true),
        SMB5(0x0d7, COMMAND.SMB5, MODE.ZEROPAGE, 5, true),
        SMB6(0x0e7, COMMAND.SMB6, MODE.ZEROPAGE, 5, true),
        SMB7(0x0f7, COMMAND.SMB7, MODE.ZEROPAGE, 5, true),
        STA_ZP(0x0085, COMMAND.STA, MODE.ZEROPAGE, 3),
        STA_ZP_X(0x0095, COMMAND.STA, MODE.ZEROPAGE_X, 4),
        STA_AB(0x008D, COMMAND.STA, MODE.ABSOLUTE, 4),
        STA_AB_X(0x009D, COMMAND.STA, MODE.ABSOLUTE_X, 5),
        STA_AB_Y(0x0099, COMMAND.STA, MODE.ABSOLUTE_Y, 5),
        STA_IND_ZP(0x0092, COMMAND.STA, MODE.INDIRECT_ZP, 5, true),
        STA_IND_ZP_X(0x0081, COMMAND.STA, MODE.INDIRECT_ZP_X, 6),
        STA_IND_ZP_Y(0x0091, COMMAND.STA, MODE.INDIRECT_ZP_Y, 6),
        STP(0x00DB, COMMAND.STP, MODE.IMPLIED, 3, true),
        STX_ZP(0x0086, COMMAND.STX, MODE.ZEROPAGE, 3),
        STX_ZP_Y(0x0096, COMMAND.STX, MODE.ZEROPAGE_Y, 4),
        STX_AB(0x008E, COMMAND.STX, MODE.ABSOLUTE, 4),
        STY_ZP(0x0084, COMMAND.STY, MODE.ZEROPAGE, 3),
        STY_ZP_X(0x0094, COMMAND.STY, MODE.ZEROPAGE_X, 4),
        STY_AB(0x008C, COMMAND.STY, MODE.ABSOLUTE, 4),
        STZ_ZP(0x0064, COMMAND.STZ, MODE.ZEROPAGE, 3, true),
        STZ_ZP_X(0x0074, COMMAND.STZ, MODE.ZEROPAGE_X, 4, true),
        STZ_AB(0x009C, COMMAND.STZ, MODE.ABSOLUTE, 4, true),
        STZ_AB_X(0x009E, COMMAND.STZ, MODE.ABSOLUTE_X, 5, true),
        TAX(0x00AA, COMMAND.TAX, MODE.IMPLIED, 2),
        TAY(0x00A8, COMMAND.TAY, MODE.IMPLIED, 2),
        TRB_ZP(0x0014, COMMAND.TRB, MODE.ZEROPAGE, 5, true),
        TRB_AB(0x001C, COMMAND.TRB, MODE.ABSOLUTE, 6, true),
        TSB_ZP(0x0004, COMMAND.TSB, MODE.ZEROPAGE, 5, true),
        TSB_AB(0x000C, COMMAND.TSB, MODE.ABSOLUTE, 6, true),
        TSX(0x00BA, COMMAND.TSX, MODE.IMPLIED, 2),
        TXA(0x008A, COMMAND.TXA, MODE.IMPLIED, 2),
        TXS(0x009A, COMMAND.TXS, MODE.IMPLIED, 2),
        TYA(0x0098, COMMAND.TYA, MODE.IMPLIED, 2),
        WAI(0x00CB, COMMAND.WAI, MODE.IMPLIED, 3, true);
        private int code;
        private boolean isExtendedOpcode;

        public int getCode() {
            return code;
        }
        private int waitCycles;

        public int getWaitCycles() {
            return waitCycles;
        }
        private COMMAND command;

        public COMMAND getCommand() {
            return command;
        }
        private MODE addressingMode;

        public MODE getMode() {
            return addressingMode;
        }
        int address = 0;
        int value = 0;
        boolean shouldFetch;

        private void fetch(MOS65C02 cpu) {
            address = getMode().calculator.calculateAddress(cpu);
            value = shouldFetch ? getMode().calculator.getValue(!command.isStoreOnly(), cpu) : 0;
        }

        public void execute(MOS65C02 cpu) {
            command.getProcessor().processCommand(address, value, addressingMode, cpu);
        }

        private OPCODE(int val, COMMAND c, MODE m, int wait) {
            this(val, c, m, wait, m.fetchValue, false);
        }

        private OPCODE(int val, COMMAND c, MODE m, int wait, boolean extended) {
            this(val, c, m, wait, m.fetchValue, extended);
        }

        private OPCODE(int val, COMMAND c, MODE m, int wait, boolean fetch, boolean extended) {
            code = val;
            waitCycles = wait - 1;
            command = c;
            addressingMode = m;
            isExtendedOpcode = extended;
            shouldFetch = fetch;
        }
    }

    public static interface AddressCalculator {

        abstract int calculateAddress(MOS65C02 cpu);

        default int getValue(boolean generateEvent, MOS65C02 cpu) {
            int address = calculateAddress(cpu);
            return (address > -1) ? (0x0ff & cpu.getMemory().read(address, TYPE.READ_DATA, generateEvent, false)) : 0;
        }
    }

    public enum MODE {

        IMPLIED(1, "", (cpu) -> -1, false),
        //        RELATIVE(2, "#$~1 ($R)"),
        RELATIVE(2, "$R", (cpu) -> {
            int pc = cpu.getProgramCounter();
            int address = pc + 2 + cpu.getMemory().read(pc + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            // The wait cycles are not added unless the branch actually happens!
            cpu.setPageBoundaryPenalty((address & 0x00ff00) != ((pc+2) & 0x00ff00));
            return address;
        }, false),
        IMMEDIATE(2, "#$~1", (cpu) -> cpu.getProgramCounter() + 1),
        ZEROPAGE(2, "$~1", (cpu) -> cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) & 0x00FF),
        ZEROPAGE_X(2, "$~1,X", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X)),
        ZEROPAGE_Y(2, "$~1,Y", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.Y)),
        INDIRECT(3, "$(~2~1)", (cpu) -> {
            int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            return cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false);
        }),
        INDIRECT_X(3, "$(~2~1,X)", (cpu) -> {
            int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X;
            return cpu.getMemory().readWord(address & 0x0FFFF, TYPE.READ_DATA, true, false);
        }),
        INDIRECT_ZP(2, "$(~1)", (cpu) -> {
            int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false);
        }),
        INDIRECT_ZP_X(2, "$(~1,X)", (cpu) -> {
            int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X;
            return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false);
        }),
        INDIRECT_ZP_Y(2, "$(~1),Y", (cpu) -> {
            int address = 0x00FF & cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            address = cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false);
            int address2 = address + cpu.Y;
            if ((address & 0x00ff00) != (address2 & 0x00ff00)) {
                cpu.addWaitCycles(1);
            }
            return address2;
        }),
        ABSOLUTE(3, "$~2~1", (cpu) -> cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false)),
        ABSOLUTE_X(3, "$~2~1,X", (cpu) -> {
            int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            int address = 0x0FFFF & (address2 + cpu.X);
            if ((address & 0x00FF00) != (address2 & 0x00FF00)) {
                cpu.addWaitCycles(1);
            }
            return address;
        }),
        ABSOLUTE_Y(3, "$~2~1,Y", (cpu) -> {
            int address2 = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
            int address = 0x0FFFF & (address2 + cpu.Y);
            if ((address & 0x00FF00) != (address2 & 0x00FF00)) {
                cpu.addWaitCycles(1);
            }
            return address;
        }),
        ZP_REL(3, "$~1,$R", new AddressCalculator() {
            @Override
            public int calculateAddress(MOS65C02 cpu) {
                // Note: This is two's compliment addition and the cpu.getMemory().read() returns a signed 8-bit value
                int pc = cpu.getProgramCounter();
                int address = pc + 3 + cpu.getMemory().read(pc + 2, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
                // The wait cycles are not added unless the branch actually happens!
                cpu.setPageBoundaryPenalty((address & 0x00ff00) != ((pc+3) & 0x00ff00));
                return address;
            }

            @Override
            public int getValue(boolean isRead, MOS65C02 cpu) {
                int pc = cpu.getProgramCounter();
                int address = 0x0ff & cpu.getMemory().read(pc + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
                return cpu.getMemory().read(address, TYPE.READ_DATA, true, false);
            }
        });
        private int size;

        public int getSize() {
            return this.size;
        }
//        private String format;
//
//        public String getFormat() {
//            return this.format;
//        }
        private AddressCalculator calculator;

        public int calcAddress(MOS65C02 cpu) {
            return calculator.calculateAddress(cpu);
        }
        private boolean indirect;

        public boolean isIndirect() {
            return indirect;
        }
        String f1;
        String f2;
        boolean twoByte = false;
        boolean relative = false;
        boolean implied = true;
        boolean fetchValue = true;

        private MODE(int size, String fmt, AddressCalculator calc) {
            this(size, fmt, calc, true);
        }
        private MODE(int size, String fmt, AddressCalculator calc, boolean fetch) {
            this.fetchValue = fetch;
            this.size = size;
            if (fmt.contains("~")) {
                this.f1 = fmt.substring(0, fmt.indexOf('~'));
                this.f2 = fmt.substring(fmt.indexOf("~1") + 2);
                if (fmt.contains("~2")) {
                    twoByte = true;
                }
                implied = false;
            } else if (fmt.contains("R")) {
                this.f1 = fmt.substring(0, fmt.indexOf('R'));
                f2 = "";
                relative = true;
                implied = false;
            }
//            this.format = fmt;

            this.calculator = calc;
            this.indirect = toString().startsWith("INDIRECT");
        }

        public MOS65C02.AddressCalculator getCalculator() {
            return calculator;
        }

        public String formatMode(int pc, MOS65C02 cpu) {
            if (implied) {
                return "";
            } else {
                int b1 = 0x00ff & cpu.getMemory().readRaw((pc + 1) & 0x0FFFF);
                if (relative) {
                    String R = wordString(pc + 2 + (byte) b1);
                    return f1 + R;
                } else if (twoByte) {
                    int b2 = 0x00ff & cpu.getMemory().readRaw((pc + 2) & 0x0FFFF);
                    return f1 + byte2(b2) + byte2(b1) + f2;
                } else {
                    return f1 + byte2(b1) + f2;
                }
            }
        }
    }

    public static interface CommandProcessor {

        public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu);
    }

    private static class BBRCommand implements CommandProcessor {

        int bit;

        public BBRCommand(int bit) {
            this.bit = bit;
        }

        @Override
        public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
            if ((value & (1 << bit)) == 0) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }
    }

    private static class BBSCommand implements CommandProcessor {

        int bit;

        public BBSCommand(int bit) {
            this.bit = bit;
        }

        @Override
        public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
            if ((value & (1 << bit)) != 0) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }
    }

    private static class RMBCommand implements CommandProcessor {

        int bit;

        public RMBCommand(int bit) {
            this.bit = bit;
        }

        @Override
        public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
            int mask = 0x0ff ^ (1 << bit);
            cpu.getMemory().write(address, (byte) (value & mask), true, false);
        }
    }

    private static class SMBCommand implements CommandProcessor {

        int bit;

        public SMBCommand(int bit) {
            this.bit = bit;
        }

        @Override
        public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
            int mask = 1 << bit;
            cpu.getMemory().write(address, (byte) (value | mask), true, false);
        }
    }

    public enum COMMAND {
        ADC((address, value, addressMode, cpu) -> {
            int w;
            cpu.V = ((cpu.A ^ value) & 0x080) == 0;
            if (cpu.D) {
                // Decimal Mode
                w = (cpu.A & 0x0f) + (value & 0x0f) + cpu.C;
                if (w >= 10) {
                    w = 0x010 | ((w + 6) & 0x0f);
                }
                w += (cpu.A & 0x0f0) + (value & 0x00f0);
                if (w >= 0x0A0) {
                    cpu.C = 1;
                    if (cpu.V && w >= 0x0180) {
                        cpu.V = false;
                    }
                    w += 0x060;
                } else {
                    cpu.C = 0;
                    if (cpu.V && w < 0x080) {
                        cpu.V = false;
                    }
                }
            } else {
                // Binary Mode
                w = cpu.A + value + cpu.C;
                if (w >= 0x0100) {
                    cpu.C = 1;
                    if (cpu.V && w >= 0x0180) {
                        cpu.V = false;
                    }
                } else {
                    cpu.C = 0;
                    if (cpu.V && w < 0x080) {
                        cpu.V = false;
                    }
                }
            }
            cpu.A = w & 0x0ff;
            cpu.setNZ(cpu.A);
        }),
        AND((address, value, addressMode, cpu) -> {
            cpu.A &= value;
            cpu.setNZ(cpu.A);
        }),
        ASL((address, value, addressMode, cpu) -> {
            cpu.C = ((value & 0x080) != 0) ? 1 : 0;
            value = 0x0FE & (value << 1);
            cpu.setNZ(value);
            // Emulate correct behavior of fetch-store-modify
            // http://forum.6502.org/viewtopic.php?f=4&t=1617&view=previous
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
        }),
        ASL_A((address, value, addressMode, cpu) -> {
            cpu.C = cpu.A >> 7;
            cpu.A = 0x0FE & (cpu.A << 1);
            cpu.setNZ(cpu.A);
        }),
        BBR0(new BBRCommand(0)),
        BBR1(new BBRCommand(1)),
        BBR2(new BBRCommand(2)),
        BBR3(new BBRCommand(3)),
        BBR4(new BBRCommand(4)),
        BBR5(new BBRCommand(5)),
        BBR6(new BBRCommand(6)),
        BBR7(new BBRCommand(7)),
        BBS0(new BBSCommand(0)),
        BBS1(new BBSCommand(1)),
        BBS2(new BBSCommand(2)),
        BBS3(new BBSCommand(3)),
        BBS4(new BBSCommand(4)),
        BBS5(new BBSCommand(5)),
        BBS6(new BBSCommand(6)),
        BBS7(new BBSCommand(7)),
        BCC((address, value, addressMode, cpu) -> {
            if (cpu.C == 0) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BCS((address, value, addressMode, cpu) -> {
            if (cpu.C != 0) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BEQ((address, value, addressMode, cpu) -> {
            if (cpu.Z) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BIT((address, value, addressMode, cpu) -> {
            int result = (cpu.A & value);
            cpu.Z = result == 0;
            cpu.N = (value & 0x080) != 0;
            // As per http://www.6502.org/tutorials/vflag.html
            if (addressMode != MODE.IMMEDIATE) {
                cpu.V = (value & 0x040) != 0;
            }
        }),
        BMI((address, value, addressMode, cpu) -> {
            if (cpu.N) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BNE((address, value, addressMode, cpu) -> {
            if (!cpu.Z) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BPL((address, value, addressMode, cpu) -> {
            if (!cpu.N) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BRA((address, value, addressMode, cpu) -> {
            cpu.setProgramCounter(address);
            cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 1 : 0);
        }),
        BRK((address, value, addressMode, cpu) -> {
            cpu.BRK();
        }),
        BVC((address, value, addressMode, cpu) -> {
            if (!cpu.V) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        BVS((address, value, addressMode, cpu) -> {
            if (cpu.V) {
                cpu.setProgramCounter(address);
                cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
            }
        }),
        CLC((address, value, addressMode, cpu) -> {
            cpu.C = 0;
        }),
        CLD((address, value, addressMode, cpu) -> {
            cpu.D = false;
        }),
        CLI((address, value, addressMode, cpu) -> {
            cpu.I = false;
            cpu.interruptSignalled = false;
        }),
        CLV((address, value, addressMode, cpu) -> {
            cpu.V = false;
        }),
        CMP((address, value, addressMode, cpu) -> {
            int val = cpu.A - value;
            cpu.C = (val >= 0) ? 1 : 0;
            cpu.setNZ(val);
        }),
        CPX((address, value, addressMode, cpu) -> {
            int val = cpu.X - value;
            cpu.C = (val >= 0) ? 1 : 0;
            cpu.setNZ(val);
        }),
        CPY((address, value, addressMode, cpu) -> {
            int val = cpu.Y - value;
            cpu.C = (val >= 0) ? 1 : 0;
            cpu.setNZ(val);
        }),
        DEC((address, value, addressMode, cpu) -> {
            value = 0x0FF & (value - 1);
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.setNZ(value);
        }),
        DEA((address, value, addressMode, cpu) -> {
            cpu.A = 0x0FF & (cpu.A - 1);
            cpu.setNZ(cpu.A);
        }),
        DEX((address, value, addressMode, cpu) -> {
            cpu.X = 0x0FF & (cpu.X - 1);
            cpu.setNZ(cpu.X);
        }),
        DEY((address, value, addressMode, cpu) -> {
            cpu.Y = 0x0FF & (cpu.Y - 1);
            cpu.setNZ(cpu.Y);
        }),
        EOR((address, value, addressMode, cpu) -> {
            cpu.A = 0x0FF & (cpu.A ^ value);
            cpu.setNZ(cpu.A);
        }),
        INC((address, value, addressMode, cpu) -> {
            value = 0x0ff & (value + 1);
            // emulator correct fetch-modify-store behavior
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.setNZ(value);
        }),
        INA((address, value, addressMode, cpu) -> {
            cpu.A = 0x0FF & (cpu.A + 1);
            cpu.setNZ(cpu.A);
        }),
        INX((address, value, addressMode, cpu) -> {
            cpu.X = 0x0FF & (cpu.X + 1);
            cpu.setNZ(cpu.X);
        }),
        INY((address, value, addressMode, cpu) -> {
            cpu.Y = 0x0FF & (cpu.Y + 1);
            cpu.setNZ(cpu.Y);
        }),
        JMP((address, value, addressMode, cpu) -> {
            cpu.setProgramCounter(address);
        }),
        JSR((address, value, addressMode, cpu) -> {
            cpu.JSR(address);
        }),
        LDA((address, value, addressMode, cpu) -> {
            cpu.A = value;
            cpu.setNZ(cpu.A);
        }),
        LDX((address, value, addressMode, cpu) -> {
            cpu.X = value;
            cpu.setNZ(cpu.X);
        }),
        LDY((address, value, addressMode, cpu) -> {
            cpu.Y = value;
            cpu.setNZ(cpu.Y);
        }),
        LSR((address, value, addressMode, cpu) -> {
            cpu.C = (value & 1);
            value = (value >> 1) & 0x07F;
            cpu.setNZ(value);
            // emulator correct fetch-modify-store behavior
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
        }),
        LSR_A((address, value, addressMode, cpu) -> {
            cpu.C = cpu.A & 1;
            cpu.A = (cpu.A >> 1) & 0x07F;
            cpu.setNZ(cpu.A);
        }),
        NOP((address, value, addressMode, cpu) -> {
        }),
        NOP_SPECIAL((address, value, addressMode, cpu) -> {
            byte param1 = (byte) (address & 0x0ff);
            byte param2 = (byte) (address >> 8);
            cpu.performExtendedCommand(param1, param2);
        }),
        ORA((address, value, addressMode, cpu) -> {
            cpu.A |= value;
            cpu.setNZ(cpu.A);
        }),
        PHA((address, value, addressMode, cpu) -> {
            cpu.push((byte) cpu.A);
        }),
        PHP((address, value, addressMode, cpu) -> {
            cpu.push((cpu.getStatus()));
        }),
        PHX((address, value, addressMode, cpu) -> {
            cpu.push((byte) cpu.X);
        }),
        PHY((address, value, addressMode, cpu) -> {
            cpu.push((byte) cpu.Y);
        }),
        PLA((address, value, addressMode, cpu) -> {
            cpu.A = 0x0FF & cpu.pop();
            cpu.setNZ(cpu.A);
        }),
        PLP((address, value, addressMode, cpu) -> {
            cpu.setStatus(cpu.pop());
        }),
        PLX((address, value, addressMode, cpu) -> {
            cpu.X = 0x0FF & cpu.pop();
            cpu.setNZ(cpu.X);
        }),
        PLY((address, value, addressMode, cpu) -> {
            cpu.Y = 0x0FF & cpu.pop();
            cpu.setNZ(cpu.Y);
        }),
        RMB0(new RMBCommand(0)),
        RMB1(new RMBCommand(1)),
        RMB2(new RMBCommand(2)),
        RMB3(new RMBCommand(3)),
        RMB4(new RMBCommand(4)),
        RMB5(new RMBCommand(5)),
        RMB6(new RMBCommand(6)),
        RMB7(new RMBCommand(7)),
        ROL((address, value, addressMode, cpu) -> {
            int oldC = cpu.C;
            cpu.C = value >> 7;
            value = 0x0ff & ((value << 1) | oldC);
            cpu.setNZ(value);
            // emulator correct fetch-modify-store behavior
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
        }),
        ROL_A((address, value, addressMode, cpu) -> {
            int oldC = cpu.C;
            cpu.C = cpu.A >> 7;
            cpu.A = 0x0ff & ((cpu.A << 1) | oldC);
            cpu.setNZ(cpu.A);
        }),
        ROR((address, value, addressMode, cpu) -> {
            int oldC = cpu.C << 7;
            cpu.C = value & 1;
            value = 0x0ff & ((value >> 1) | oldC);
            cpu.setNZ(value);
            // emulator correct fetch-modify-store behavior
            cpu.getMemory().write(address, (byte) value, true, false);
            cpu.getMemory().write(address, (byte) value, true, false);
        }),
        ROR_A((address, value, addressMode, cpu) -> {
            int oldC = cpu.C << 7;
            cpu.C = cpu.A & 1;
            cpu.A = 0x0ff & ((cpu.A >> 1) | oldC);
            cpu.setNZ(cpu.A);
        }),
        RTI((address, value, addressMode, cpu) -> {
            cpu.returnFromInterrupt();
        }),
        RTS((address, value, addressMode, cpu) -> {
            cpu.setProgramCounter(cpu.popWord() + 1);
        }),
        SBC((address, value, addressMode, cpu) -> {
            cpu.V = ((cpu.A ^ value) & 0x080) != 0;
            int w;
            if (cpu.D) {
                int temp = 0x0f + (cpu.A & 0x0f) - (value & 0x0f) + cpu.C;
                if (temp < 0x10) {
                    w = 0;
                    temp -= 6;
                } else {
                    w = 0x10;
                    temp -= 0x10;
                }
                w += 0x00f0 + (cpu.A & 0x00f0) - (value & 0x00f0);
                if (w < 0x100) {
                    cpu.C = 0;
                    if (cpu.V && w < 0x080) {
                        cpu.V = false;
                    }
                    w -= 0x60;
                } else {
                    cpu.C = 1;
                    if (cpu.V && w >= 0x180) {
                        cpu.V = false;
                    }
                }
                w += temp;
            } else {
                w = 0x0ff + cpu.A - value + cpu.C;
                if (w < 0x100) {
                    cpu.C = 0;
                    if (cpu.V && (w < 0x080)) {
                        cpu.V = false;
                    }
                } else {
                    cpu.C = 1;
                    if (cpu.V && (w >= 0x180)) {
                        cpu.V = false;
                    }
                }
            }
            cpu.A = w & 0x0ff;
            cpu.setNZ(cpu.A);
        }),
        SEC((address, value, addressMode, cpu) -> {
            cpu.C = 1;
        }),
        SED((address, value, addressMode, cpu) -> {
            cpu.D = true;
        }),
        SEI((address, value, addressMode, cpu) -> {
            cpu.I = true;
        }),
        SMB0(new SMBCommand(0)),
        SMB1(new SMBCommand(1)),
        SMB2(new SMBCommand(2)),
        SMB3(new SMBCommand(3)),
        SMB4(new SMBCommand(4)),
        SMB5(new SMBCommand(5)),
        SMB6(new SMBCommand(6)),
        SMB7(new SMBCommand(7)),
        STA(true, (address, value, addressMode, cpu) -> {
            cpu.getMemory().write(address, (byte) cpu.A, true, false);
        }),
        STP((address, value, addressMode, cpu) -> {
            cpu.suspend();
        }),
        STX(true, (address, value, addressMode, cpu) -> {
            cpu.getMemory().write(address, (byte) cpu.X, true, false);
        }),
        STY(true, (address, value, addressMode, cpu) -> {
            cpu.getMemory().write(address, (byte) cpu.Y, true, false);
        }),
        STZ(true, (address, value, addressMode, cpu) -> {
            cpu.getMemory().write(address, (byte) 0, true, false);
        }),
        TAX((address, value, addressMode, cpu) -> {
            cpu.X = cpu.A;
            cpu.setNZ(cpu.X);
        }),
        TAY((address, value, addressMode, cpu) -> {
            cpu.Y = cpu.A;
            cpu.setNZ(cpu.Y);
        }),
        TRB((address, value, addressMode, cpu) -> {
            cpu.C = (value & cpu.A) != 0 ? 1 : 0;
            cpu.getMemory().write(address, (byte) (value & ~cpu.A), true, false);
        }),
        TSB((address, value, addressMode, cpu) -> {
            cpu.C = (value & cpu.A) != 0 ? 1 : 0;
            cpu.getMemory().write(address, (byte) (value | cpu.A), true, false);
        }),
        TSX((address, value, addressMode, cpu) -> {
            cpu.X = cpu.STACK;
            cpu.setNZ(cpu.STACK);
        }),
        TXA((address, value, addressMode, cpu) -> {
            cpu.A = cpu.X;
            cpu.setNZ(cpu.X);
        }),
        TXS((address, value, addressMode, cpu) -> {
            cpu.STACK = cpu.X;
        }),
        TYA((address, value, addressMode, cpu) -> {
            cpu.A = cpu.Y;
            cpu.setNZ(cpu.Y);
        }),
        WAI((address, value, addressMode, cpu) -> {
            cpu.waitForInterrupt();
        });
        private CommandProcessor processor;

        public CommandProcessor getProcessor() {
            return processor;
        }
        private boolean storeOnly;

        public boolean isStoreOnly() {
            return storeOnly;
        }

        private COMMAND(CommandProcessor processor) {
            this(false, processor);
        }

        private COMMAND(boolean storeOnly, CommandProcessor processor) {
            this.storeOnly = storeOnly;
            this.processor = processor;
        }
    }
    static private OPCODE[] opcodes;

    static {
        opcodes = new OPCODE[256];
        for (OPCODE o : OPCODE.values()) {
            opcodes[o.getCode()] = o;
        }
    }

    @Override
    protected void executeOpcode() {
        if (interruptSignalled) {
            processInterrupt();
        }
        int pc = getProgramCounter();

        String traceEntry = null;
        if (isSingleTraceEnabled() || isTraceEnabled() || isLogEnabled() || warnAboutExtendedOpcodes) {
            traceEntry = getState().toUpperCase() + "  " + Integer.toString(pc, 16) + " : " + disassemble();
            captureSingleTrace(traceEntry);
            if (isTraceEnabled()) {
                LOG.log(Level.INFO, traceEntry);
            }
            if (isLogEnabled()) {
                log(traceEntry);
            }
        }
        // This makes it possible to trap the memory read of an opcode, when PC == Address, you know it is executing that opcode.
        int op = 0x00ff & getMemory().read(pc, TYPE.EXECUTE, true, false);
        OPCODE opcode = opcodes[op];
        if (traceEntry != null && warnAboutExtendedOpcodes && opcode != null && opcode.isExtendedOpcode) {
            LOG.log(Level.WARNING, ">>EXTENDED OPCODE DETECTED {0}<<", Integer.toHexString(opcode.code));
            LOG.log(Level.WARNING, traceEntry);
            if (isLogEnabled()) {
                log(">>EXTENDED OPCODE DETECTED " + Integer.toHexString(opcode.code) + "<<");
                log(traceEntry);
            }
        }        
        if (opcode == null) {
            // handle bad opcode as a NOP
            int wait = 0;
            int bytes = 2;
            int n = op & 0x0f;
            switch (n) {
                case 2:
                    wait = 2;
                    break;
                case 3:
                case 7:
                case 0x0b:
                case 0x0f:
                    wait = 1;
                    bytes = 1;
                    break;
                case 4:
                    bytes = 2;
                    if ((op & 0x0f0) == 0x040) {
                        wait = 3;
                    } else {
                        wait = 4;
                    }   break;
                case 0x0c:
                    bytes = 3;
                    if ((op & 0x0f0) == 0x050) {
                        wait = 8;
                    } else {
                        wait = 4;
                    }   break;
                default:
            }
            incrementProgramCounter(bytes);
            addWaitCycles(wait);

            if (isLogEnabled() || breakOnBadOpcode) {
                LOG.log(Level.WARNING, "Unrecognized opcode {0} at {1}", new Object[]{Integer.toHexString(op), Integer.toHexString(pc)});
            }
            if (breakOnBadOpcode) {
                OPCODE.BRK.execute(this);
            }
        } else {
            opcode.fetch(this);
            incrementProgramCounter(opcode.getMode().getSize());
            opcode.execute(this);
            addWaitCycles(opcode.getWaitCycles());
        }
    }

    private void setNZ(int value) {
        N = (value & 0x080) != 0;
        Z = (value & 0x0ff) == 0;
    }

    public void pushWord(int val) {
        push((byte) (val >> 8));
        push((byte) (val & 0x00ff));
    }

    public int popWord() {
        return (0x0FF & pop()) | (0x0ff00 & (pop() << 8));
    }

    public void push(byte val) {
        getMemory().write(0x0100 + STACK, val, true, false);
        STACK = (STACK - 1) & 0x0FF;
    }

    public byte pop() {
        STACK = (STACK + 1) & 0x0FF;
        byte val = getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false);
        return val;
    }

    private byte getStatus() {
        return (byte) ((N ? 0x080 : 0)
                | (V ? 0x040 : 0)
                | 0x020
                | (B ? 0x010 : 0)
                | (D ? 0x08 : 0)
                | (I ? 0x04 : 0)
                | (Z ? 0x02 : 0)
                | ((C > 0) ? 0x01 : 0));
    }

    private void setStatus(byte b) {
        N = (b & 0x080) != 0;
        V = (b & 0x040) != 0;
        // B flag is unaffected in this way.
        D = (b & 0x08) != 0;
        I = (b & 0x04) != 0;
        Z = (b & 0x02) != 0;
        C = (char) (b & 0x01);
    }

    private void returnFromInterrupt() {
        setStatus(pop());
        setProgramCounter(popWord());
    }

    private void waitForInterrupt() {
        I = true;
        suspend();
    }

    @Override
    public void JSR(int address) {
            pushPC();
            setProgramCounter(address);
    }

    public void BRK() {
        if (isLogEnabled()) {
            LOG.log(Level.WARNING, "BRK at ${0}", Integer.toString(getProgramCounter(), 16));
            dumpTrace();
        }
        B = true;
        // 65c02 clears D flag on BRK
        D = false;
        interruptSignalled = true;
    }

    // Hardware IRQ generated
    @Override
    public void generateInterrupt() {
        B = false;
        interruptSignalled = true;
        resume();
    }

    private void processInterrupt() {
        if (!interruptSignalled) {
            return;
        }
        interruptSignalled = false;
        if (!I || B) {
            I = false;
            pushWord(getProgramCounter());
            push(getStatus());
            I = true;
            int newPC = getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false);
//            System.out.println("Interrupt generated, setting PC to (" + Integer.toString(INT_VECTOR, 16) + ") = " + Integer.toString(newPC, 16));
            setProgramCounter(newPC);
        }
    }

    public int getSTACK() {
        return STACK;
    }

    // Cold/Warm boot procedure
    @Override
    public void reset() {
        pushWord(getProgramCounter());
        push(getStatus());
        //        STACK = 0x0ff;
//        B = false;
        B = true;
//        C = 1;
        D = false;
//        I = true;
//        N = true;
//        V = true;
//        Z = true;
        int newPC = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false);
        LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)});
        setProgramCounter(newPC);
    }

    @Override
    protected String getDeviceName() {
        return "65C02 Processor";
    }

    private static String byte2(int b) {
        String out = Integer.toString(b & 0x0FF, 16);
        if (out.length() == 1) {
            return "0" + out;
        }
        return out;
    }

    private static String wordString(int w) {
        String out = Integer.toHexString(w);
        if (out.length() == 1) {
            return "000" + out;
        }
        if (out.length() == 2) {
            return "00" + out;
        }
        if (out.length() == 3) {
            return "0" + out;
        }
        return out;
    }

    public String getState() {
        StringBuilder out = new StringBuilder();
        out.append(byte2(A)).append(" ");
        out.append(byte2(X)).append(" ");
        out.append(byte2(Y)).append(" ");
        //        out += "PC:"+wordString(getProgramCounter())+" ";
        out.append("01").append(byte2(STACK)).append(" ");
        out.append(getFlags());
        return out.toString();
    }

    public String getFlags() {
        StringBuilder out = new StringBuilder();
        out.append(N ? "N" : ".");
        out.append(V ? "V" : ".");
        out.append("R");
        out.append(B ? "B" : ".");
        out.append(D ? "D" : ".");
        out.append(I ? "I" : ".");
        out.append(Z ? "Z" : ".");
        out.append((C != 0) ? "C" : ".");
        return out.toString();
    }

    public String disassemble() {
        int pc = getProgramCounter();
//        RAM ram = cpu.getMemory();
        int op = getMemory().readRaw(pc);
        OPCODE o = opcodes[op & 0x0ff];
        if (o == null) {
            return "???";
        }
        String format = o.getMode().formatMode(pc, this);
//        format = format.replaceAll("~1", byte2(b1));
//        format = format.replaceAll("~2", byte2(b2));
//        format = format.replaceAll("R", R);
        /*
         String mem = wordString(pc) + ":" + byte2(op) + " " +
         ((o.getMode().getSize() > 1) ?
         byte2(b1) : "  " ) + " " +
         ((o.getMode().getSize() > 2) ?
         byte2(b2) : "  " ) + "  ";
         */
        StringBuilder out = new StringBuilder(o.getCommand().toString());
        out.append(" ").append(format);
        if (o.getMode().isIndirect()) {
            out.append(" >> $").append(Integer.toHexString(o.getMode().getCalculator().calculateAddress(this)));
        }
        return out.toString();
    }
    private boolean pageBoundaryPenalty = false;

    private void setPageBoundaryPenalty(boolean b) {
        pageBoundaryPenalty = b;
    }

    @Override
    public void pushPC() {
        pushWord(getProgramCounter() - 1);
    }

    /**
     * Special commands -- these are usually treated as NOP but can be reused for emulator controls
     * !byte $fc, $65, $00 ; Turn off tracing
     * !byte $fc, $65, $01 ; Turn on tracing
     * !byte $fc, $50, NN ; print number NN to stdout
     * !byte $fc, $5b, NN ; print number NN to stdout with newline
     * !byte $fc, $5c, NN ; print character NN to stdout
     * @param param1
     * @param param2 
     */
    public void performExtendedCommand(byte param1, byte param2) {
//        LOG.log(Level.INFO, "Extended command {0},{1}", new Object[]{Integer.toHexString(param1), Integer.toHexString(param2)});
        switch (param1 & 0x0ff) {
            case 0x50:
                // System out
                System.out.print(param2 & 0x0ff);
                break;
            case 0x5b:
                // System out (with line break)
                System.out.println(param2 & 0x0ff);
                break;
            case 0x5c:
                // System out (with line break)
                System.out.println((char) (param2 & 0x0ff));           
                break;
            case 0x65:
                // CPU functions
                switch (param2 & 0x0ff) {
                    case 0x00:
                        // Turn off tracing
                        trace = false;
                        break;
                    case 0x01:
                        // Turn on tracing
                        trace = true;
                        break;
                }
                break;
            case 0x64:
                // Memory functions
                getMemory().performExtendedCommand(param2 & 0x0ff);
            default:
        }
    }
}
 @Stateful
abstract public class RAM128k extends RAM {
    Logger LOG = Logger.getLogger(RAM128k.class.getName());

    Map<String, PagedMemory> banks;

    private Map<String, PagedMemory> getBanks() {
        if (banks == null) {
            banks = new HashMap<>();

            banks.put("main", mainMemory);
            banks.put("lc", languageCard);
            banks.put("lc2", languageCard2);
            banks.put("//e rom (80-col)", cPageRom);
            banks.put("//e rom", rom);
            banks.put("blank", blank);
            banks.put("aux", getAuxMemory());
            banks.put("aux lc", getAuxLanguageCard());
            banks.put("aux lc2", getAuxLanguageCard2());
            cards[1].ifPresent(c -> banks.put("card1a", c.getCxRom()));
            cards[1].ifPresent(c -> banks.put("card1b", c.getC8Rom()));
            cards[2].ifPresent(c -> banks.put("card2a", c.getCxRom()));
            cards[2].ifPresent(c -> banks.put("card2b", c.getC8Rom()));
            cards[3].ifPresent(c -> banks.put("card3a", c.getCxRom()));
            cards[3].ifPresent(c -> banks.put("card3b", c.getC8Rom()));
            cards[4].ifPresent(c -> banks.put("card4a", c.getCxRom()));
            cards[4].ifPresent(c -> banks.put("card4b", c.getC8Rom()));
            cards[5].ifPresent(c -> banks.put("card5a", c.getCxRom()));
            cards[5].ifPresent(c -> banks.put("card5b", c.getC8Rom()));
            cards[6].ifPresent(c -> banks.put("card6a", c.getCxRom()));
            cards[6].ifPresent(c -> banks.put("card6b", c.getC8Rom()));
            cards[7].ifPresent(c -> banks.put("card7a", c.getCxRom()));
            cards[7].ifPresent(c -> banks.put("card7b", c.getC8Rom()));
        }

        return banks;
    }

    @Override
    public void performExtendedCommand(int param) {
        switch (param) {
            case 0xda:
                // 64 da : Dump all memory mappings
                System.out.println("Active banks");
                for (int i = 0; i < 256; i++) {
                    byte[] read = activeRead.get(i);
                    byte[] write = activeWrite.get(i);
                    String readBank = getBanks().keySet().stream().filter(bank->{
                        PagedMemory mem = getBanks().get(bank);
                        for (byte[] page : mem.getMemory()) {
                            if (page == read) {
                                return true;
                            }
                        }
                        return false;
                    }).findFirst().orElse("unknown");
                    String writeBank = getBanks().keySet().stream().filter(bank->{
                        PagedMemory mem = getBanks().get(bank);
                        for (byte[] page : mem.getMemory()) {
                            if (page == write) {
                                return true;
                            }
                        }
                        return false;
                    }).findFirst().orElse("unknown");
                    LOG.log(Level.INFO,"Bank {0}\t{1}\t{2}", new Object[]{Integer.toHexString(i), readBank, writeBank});
                }
            default:
        }
    }

    @Stateful
    public PagedMemory mainMemory;
    @Stateful
    public PagedMemory languageCard;
    @Stateful
    public PagedMemory languageCard2;
    public PagedMemory cPageRom;
    public PagedMemory rom;
    public PagedMemory blank;

    public RAM128k(Computer computer) {
        super(computer);
        mainMemory = new PagedMemory(0xc000, PagedMemory.Type.RAM, computer);
        rom = new PagedMemory(0x3000, PagedMemory.Type.FIRMWARE_MAIN, computer);
        cPageRom = new PagedMemory(0x1000, PagedMemory.Type.SLOW_ROM, computer);
        languageCard = new PagedMemory(0x3000, PagedMemory.Type.LANGUAGE_CARD, computer);
        languageCard2 = new PagedMemory(0x1000, PagedMemory.Type.LANGUAGE_CARD, computer);
        activeRead = new PagedMemory(0x10000, PagedMemory.Type.RAM, computer);
        activeWrite = new PagedMemory(0x10000, PagedMemory.Type.RAM, computer);
        blank = new PagedMemory(0x100, PagedMemory.Type.RAM, computer);

        // Format memory with FF FF 00 00 pattern
        for (int i = 0; i < 0x0100; i++) {
            blank.get(0)[i] = (byte) 0x0FF;
        }
        initMemoryPattern(mainMemory);
    }

    public final void initMemoryPattern(PagedMemory mem) {
        // Format memory with FF FF 00 00 pattern
        for (int i = 0; i < 0x0100; i++) {
            for (int j = 0; j < 0x0c0; j++) {
                byte use = (byte) ((i % 4) > 1 ? 0x0FF : 0x00);
                mem.get(j)[i] = use;
            }
        }
    }

    private final Semaphore configurationSemaphone = new Semaphore(1, true);

    /**
     *
     */
    @Override
    public void configureActiveMemory() {
        try {
            log("MMU Switches");
            configurationSemaphone.acquire();
            // First off, set up read/write for main memory (might get changed later on)
            activeRead.fillBanks(SoftSwitches.RAMRD.getState() ? getAuxMemory() : mainMemory);
            activeWrite.fillBanks(SoftSwitches.RAMWRT.getState() ? getAuxMemory() : mainMemory);

            // Handle language card softswitches
            activeRead.fillBanks(rom);
            //activeRead.fillBanks(cPageRom);
            for (int i = 0x0c0; i < 0x0d0; i++) {
                activeWrite.set(i, null);
            }
            if (SoftSwitches.LCRAM.isOn()) {
                if (SoftSwitches.AUXZP.isOff()) {
                    activeRead.fillBanks(languageCard);
                    if (SoftSwitches.LCBANK1.isOff()) {
                        activeRead.fillBanks(languageCard2);
                    }
                } else {
                    activeRead.fillBanks(getAuxLanguageCard());
                    if (SoftSwitches.LCBANK1.isOff()) {
                        activeRead.fillBanks(getAuxLanguageCard2());
                    }
                }
            }

            if (SoftSwitches.LCWRITE.isOn()) {
                if (SoftSwitches.AUXZP.isOff()) {
                    activeWrite.fillBanks(languageCard);
                    if (SoftSwitches.LCBANK1.isOff()) {
                        activeWrite.fillBanks(languageCard2);
                    }
                } else {
                    activeWrite.fillBanks(getAuxLanguageCard());
                    if (SoftSwitches.LCBANK1.isOff()) {
                        activeWrite.fillBanks(getAuxLanguageCard2());
                    }
                }
            } else {
                // Make 0xd000 - 0xffff non-writable!
                for (int i = 0x0d0; i < 0x0100; i++) {
                    activeWrite.set(i, null);
                }
            }

            // Handle 80STORE logic for bankswitching video ram
            if (SoftSwitches._80STORE.isOn()) {
                activeRead.setBanks(0x04, 0x04, 0x04,
                        SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
                activeWrite.setBanks(0x04, 0x04, 0x04,
                        SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
                if (SoftSwitches.HIRES.isOn()) {
                    activeRead.setBanks(0x020, 0x020, 0x020,
                            SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
                    activeWrite.setBanks(0x020, 0x020, 0x020,
                            SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory);
                }
            }

            // Handle zero-page bankswitching
            if (SoftSwitches.AUXZP.getState()) {
                // Aux pages 0 and 1
                activeRead.setBanks(0, 2, 0, getAuxMemory());
                activeWrite.setBanks(0, 2, 0, getAuxMemory());
            } else {
                // Main pages 0 and 1
                activeRead.setBanks(0, 2, 0, mainMemory);
                activeWrite.setBanks(0, 2, 0, mainMemory);
            }

            /*
            INTCXROM   SLOTC3ROM  C1,C2,C4-CF   C3
            0         0          slot         rom
            0         1          slot         slot
            1         -          rom          rom
             */
            if (SoftSwitches.CXROM.getState()) {
                // Enable C1-CF to point to rom
                activeRead.setBanks(0, 0x0F, 0x0C1, cPageRom);
            } else {
                // Enable C1-CF to point to slots
                for (int slot = 1; slot <= 7; slot++) {
                    PagedMemory page = getCard(slot).map(Card::getCxRom).orElse(blank);
                    activeRead.setBanks(0, 1, 0x0c0 + slot, page);
                }
                if (getActiveSlot() == 0) {
                    for (int i = 0x0C8; i < 0x0D0; i++) {
                        activeRead.set(i, blank.get(0));
                    }
                } else {
                    getCard(getActiveSlot()).ifPresent(c -> activeRead.setBanks(0, 8, 0x0c8, c.getC8Rom()));
                }
                if (SoftSwitches.SLOTC3ROM.isOff()) {
                    // Enable C3 to point to internal ROM
                    activeRead.setBanks(2, 1, 0x0C3, cPageRom);
                }
                if (SoftSwitches.INTC8ROM.isOn()) {
                    // Enable C8-CF to point to internal ROM
                    activeRead.setBanks(7, 8, 0x0C8, cPageRom);
                }
            }
            // All ROM reads not intecepted will return 0xFF! (TODO: floating bus)
            activeRead.set(0x0c0, blank.get(0));
            configurationSemaphone.release();
        } catch (InterruptedException ex) {
            Logger.getLogger(RAM128k.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public void log(String message) {
        CPU cpu = computer.getCpu();
        if (cpu != null && cpu.isLogEnabled()) {
            String stack = "";
            for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
                stack += e.getClassName() + "." + e.getMethodName() + "(" + e.getLineNumber() + ");";
            }
            cpu.log(stack);
            cpu.log(message + ";" + SoftSwitches.RAMRD + ";" + SoftSwitches.RAMWRT + ";" + SoftSwitches.AUXZP + ";" + SoftSwitches._80STORE + ";" + SoftSwitches.HIRES + ";" + SoftSwitches.PAGE2 + ";" + SoftSwitches.LCBANK1 + ";" + SoftSwitches.LCRAM + ";" + SoftSwitches.LCWRITE);
        }
    }

    /**
     *
     * @param path
     * @throws java.io.IOException
     */
    @Override
    protected void loadRom(String path) throws IOException {
        // Remap writable ram to reflect rom file structure
        byte[] ignore = new byte[256];
        activeWrite.set(0, ignore);  // Ignore first bank of data
        for (int i = 1; i < 17; i++) {
            activeWrite.set(i, ignore);
        }
        activeWrite.setBanks(0, cPageRom.getMemory().length, 0x011, cPageRom);
        activeWrite.setBanks(0, rom.getMemory().length, 0x020, rom);
        //----------------------
        InputStream inputRom = getClass().getClassLoader().getResourceAsStream(path);
        int read = 0;
        int addr = 0;
        byte[] in = new byte[1024];
        while (addr < 0x00FFFF && (read = inputRom.read(in)) > 0) {
            for (int i = 0; i < read; i++) {
                write(addr++, in[i], false, false);
            }
        }
//            System.out.println("Finished reading rom with " + inputRom.available() + " bytes left unread!");
        //dump();
        configureActiveMemory();
    }

    /**
     * @return the mainMemory
     */
    public PagedMemory getMainMemory() {
        return mainMemory;
    }

    abstract public PagedMemory getAuxVideoMemory();

    abstract public PagedMemory getAuxMemory();

    abstract public PagedMemory getAuxLanguageCard();

    abstract public PagedMemory getAuxLanguageCard2();

    /**
     * @return the languageCard
     */
    public PagedMemory getLanguageCard() {
        return languageCard;
    }

    /**
     * @return the languageCard2
     */
    public PagedMemory getLanguageCard2() {
        return languageCard2;
    }

    /**
     * @return the cPageRom
     */
    public PagedMemory getcPageRom() {
        return cPageRom;
    }

    /**
     * @return the rom
     */
    public PagedMemory getRom() {
        return rom;
    }

    void copyFrom(RAM128k currentMemory) {
        // This is really quick and dirty but should be sufficient to avoid most crashes...
        blank = currentMemory.blank;
        cPageRom = currentMemory.cPageRom;
        rom = currentMemory.rom;
        listeners = currentMemory.listeners;
        mainMemory = currentMemory.mainMemory;
        languageCard = currentMemory.languageCard;
        languageCard2 = currentMemory.languageCard2;
        cards = currentMemory.cards;
        activeSlot = currentMemory.activeSlot;
    }
}
 public enum SoftSwitches {

    _80STORE(new MemorySoftSwitch("80Store", 0x0c000, 0x0c001, 0x0c018, RAMEvent.TYPE.WRITE, false)),
    RAMRD(new MemorySoftSwitch("AuxRead (RAMRD)", 0x0c002, 0x0c003, 0x0c013, RAMEvent.TYPE.WRITE, false)),
    RAMWRT(new MemorySoftSwitch("AuxWrite (RAMWRT)", 0x0c004, 0x0c005, 0x0c014, RAMEvent.TYPE.WRITE, false)),
    CXROM(new MemorySoftSwitch("IntCXROM", 0x0c006, 0x0c007, 0x0c015, RAMEvent.TYPE.WRITE, false)),
    AUXZP(new MemorySoftSwitch("AuxZeroPage", 0x0c008, 0x0c009, 0x0c016, RAMEvent.TYPE.WRITE, false)),
    SLOTC3ROM(new MemorySoftSwitch("C3ROM", 0x0c00a, 0x0c00b, 0x0c017, RAMEvent.TYPE.WRITE, false)),
    INTC8ROM(new IntC8SoftSwitch()),
    LCBANK1(new MemorySoftSwitch("LangCardBank1",
    new int[]{0x0c088, 0x0c089, 0x0c08a, 0x0c08b, 0x0c08c, 0x0c08d, 0x0c08e, 0x0c08f},
    new int[]{0x0c080, 0x0c081, 0x0c082, 0x0c083, 0x0c084, 0x0c085, 0x0c086, 0x0c087},
    new int[]{0x0c011}, RAMEvent.TYPE.ANY, false)),
    LCRAM(new MemorySoftSwitch("LangCardRam/HRAMRD'",
    new int[]{0x0c081, 0x0c082, 0x0c085, 0x0c086, 0x0c089, 0x0c08a, 0x0c08d, 0x0c08e},
    new int[]{0x0c080, 0x0c083, 0x0c084, 0x0c087, 0x0c088, 0x0c08b, 0x0c08c, 0x0c08f},
    new int[]{0x0c012}, RAMEvent.TYPE.ANY, false)),
    LCWRITE(new Memory2SoftSwitch("LangCardWrite",
    new int[]{0x0c080, 0x0c082, 0x0c084, 0x0c086, 0x0c088, 0x0c08a, 0x0c08c, 0x0c08e},
    new int[]{0x0c081, 0x0c083, 0x0c085, 0x0c087, 0x0c089, 0x0c08b, 0x0c08d, 0x0c08f},
    null, RAMEvent.TYPE.ANY, true)),
    //Renamed as per Sather 5-7
    _80COL(new VideoSoftSwitch("80ColumnVideo (80COL/80VID)", 0x0c00c, 0x0c00d, 0x0c01f, RAMEvent.TYPE.WRITE, false)),
    ALTCH(new VideoSoftSwitch("Mousetext", 0x0c00e, 0x0c00f, 0x0c01e, RAMEvent.TYPE.WRITE, false){
        @Override
        public void stateChanged() {
            super.stateChanged();
            computer.getVideo().forceRefresh();
        }
    }),
    TEXT(new VideoSoftSwitch("Text", 0x0c050, 0x0c051, 0x0c01a, RAMEvent.TYPE.ANY, true)),
    MIXED(new VideoSoftSwitch("Mixed", 0x0c052, 0x0c053, 0x0c01b, RAMEvent.TYPE.ANY, false)),
    PAGE2(new VideoSoftSwitch("Page2", 0x0c054, 0x0c055, 0x0c01c, RAMEvent.TYPE.ANY, false) {
        @Override
        public void stateChanged() {
//            if (computer == null) {
//                return;
//            }
//            if (computer == null && computer.getMemory() == null) {
//                return;
//            }
//            if (computer == null && computer.getVideo() == null) {
//                return;
//            }

            // PAGE2 is a hybrid switch; 80STORE ? memory : video
            if (_80STORE.isOn()) {
                computer.getMemory().configureActiveMemory();
            } else {
                computer.getVideo().configureVideoMode();
            }
        }
    }),
    HIRES(new VideoSoftSwitch("Hires", 0x0c056, 0x0c057, 0x0c01d, RAMEvent.TYPE.ANY, false)),
    DHIRES(new VideoSoftSwitch("Double-hires", 0x0c05f, 0x0c05e, 0x0c07f, RAMEvent.TYPE.ANY, false)),
    PB0(new MemorySoftSwitch("Pushbutton0", -1, -1, 0x0c061, RAMEvent.TYPE.ANY, null)),
    PB1(new MemorySoftSwitch("Pushbutton1", -1, -1, 0x0c062, RAMEvent.TYPE.ANY, null)),
    PB2(new MemorySoftSwitch("Pushbutton2", -1, -1, 0x0c063, RAMEvent.TYPE.ANY, null)),
    PDLTRIG(new SoftSwitch("PaddleTrigger",
    null,
    new int[]{0x0c070, 0x0c071, 0x0c072, 0x0c073, 0x0c074, 0x0c075, 0x0c076, 0x0c077,
        0x0c078, 0x0c079, 0x0c07a, 0x0c07b, 0x0c07c, 0x0c07d, 0x0c07e, 0x0c07f},
    null, RAMEvent.TYPE.ANY, false) {
        @Override
        protected byte readSwitch() {
            setState(true);
            return computer.getVideo().getFloatingBus();
        }

        @Override
        public void stateChanged() {
        }
    }),
    PDL0(new MemorySoftSwitch("Paddle0", -1, -1, 0x0c064, RAMEvent.TYPE.ANY, false)),
    PDL1(new MemorySoftSwitch("Paddle1", -1, -1, 0x0c065, RAMEvent.TYPE.ANY, false)),
    PDL2(new MemorySoftSwitch("Paddle2", -1, -1, 0x0c066, RAMEvent.TYPE.ANY, false)),
    PDL3(new MemorySoftSwitch("Paddle3", -1, -1, 0x0c067, RAMEvent.TYPE.ANY, false)),
    AN0(new MemorySoftSwitch("Annunciator0", 0x0c058, 0x0c059, -1, RAMEvent.TYPE.ANY, false)),
    AN1(new MemorySoftSwitch("Annunciator1", 0x0c05a, 0x0c05b, -1, RAMEvent.TYPE.ANY, false)),
    AN2(new MemorySoftSwitch("Annunciator2", 0x0c05c, 0x0c05d, -1, RAMEvent.TYPE.ANY, false)),
    AN3(new MemorySoftSwitch("Annunciator3", 0x0c05e, 0x0c05f, -1, RAMEvent.TYPE.ANY, false)),
    KEYBOARD(new KeyboardSoftSwitch(
    "Keyboard",
    new int[]{0x0c010, 0x0c11, 0x0c012, 0x0c013, 0x0c014, 0x0c015, 0x0c016, 0x0c017,
        0x0c018, 0x0c019, 0x0c01a, 0x0c01b, 0x0c01c, 0x0c01d, 0x0c01e, 0x0c01f},
    null,
    new int[]{0x0c000, 0x0c001, 0x0c002, 0x0c003, 0x0c004, 0x0c005, 0x0c006, 0x0c007,
        0x0c008, 0x0c009, 0x0c00a, 0x0c00b, 0x0c00c, 0x0c00d, 0x0c00e, 0x0c00f, 0x0c010},
    RAMEvent.TYPE.WRITE, false)),
    //C010 should clear keyboard strobe when read as well
    KEYBOARD_STROBE_READ(new SoftSwitch("KeyStrobe_Read", 0x0c010, -1, -1, RAMEvent.TYPE.READ, false) {
        @Override
        protected byte readSwitch() {
            return computer.getVideo().getFloatingBus();
        }

        @Override
        public void stateChanged() {
            KEYBOARD.getSwitch().setState(false);
        }
    }),
    TAPEOUT(new MemorySoftSwitch("TapeOut", 0x0c020, 0x0c020, 0x0c060, RAMEvent.TYPE.ANY, false)),
    VBL(new VideoSoftSwitch("VBL", -1, -1, 0x0c019, RAMEvent.TYPE.ANY, false)),
    FLOATING_BUS(new SoftSwitch("FloatingBus", null, null, new int[]{0x0C050, 0x0C051, 0x0C052, 0x0C053, 0x0C054}, RAMEvent.TYPE.READ, null) {
        @Override
        protected byte readSwitch() {
            if (computer.getVideo() == null) {
                return 0;
            }
            return computer.getVideo().getFloatingBus();
        }

        @Override
        public void stateChanged() {
        }
    });

    /*
     2C:VBL (new MemorySoftSwitch(0x0c070, 0x0*, 0x0c041, RAMEvent.TYPE.ANY, false)),
     2C:VBLENABLE (new MemorySoftSwitch(0x0c05a, 0x0c05b, 0x0-, RAMEvent.TYPE.ANY, false)),
     2C:XINT (new MemorySoftSwitch(0x0c015 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)),
     2C:YINT (new MemorySoftSwitch(0x0c017 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)),
     2C:MBUTTON (new MemorySoftSwitch(0x0*, 0x0*, 0x0c063, RAMEvent.TYPE.ANY, false)),
     2C:80/40 switch (new MemorySoftSwitch(0x0*, 0x0*, 0x0c060, RAMEvent.TYPE.ANY, false)),
     2C:XDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c066, RAMEvent.TYPE.ANY, false)),
     2C:YDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c067, RAMEvent.TYPE.ANY, false)),    
     */
    private final SoftSwitch softswitch;

    /**
     * Creates a new instance of SoftSwitches
     */
    private SoftSwitches(SoftSwitch softswitch) {
        this.softswitch = softswitch;
    }

    public SoftSwitch getSwitch() {
        return softswitch;
    }

    public boolean getState() {
        return softswitch.getState();
    }

    public final boolean isOn() {
        return softswitch.getState();
    }

    public final boolean isOff() {
        return !softswitch.getState();
    }

    @Override
    public String toString() {
        return softswitch.toString();
    }
} public class ConfigurationUIController {
    public static final String DELIMITER = "~!~";

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private VBox settingsVbox;

    @FXML
    private SplitPane splitPane;

    @FXML
    private ScrollPane settingsScroll;

    @FXML
    private TreeView<ConfigNode> deviceTree;

    @FXML
    private ScrollPane treeScroll;

    @FXML
    void reloadConfig(MouseEvent event) {
        Configuration.loadSettings();
        resetDeviceTree();
    }

    @FXML
    void saveConfig(MouseEvent event) {
        applyConfig(event);
        Configuration.saveSettings();
    }

    @FXML
    void applyConfig(MouseEvent event) {
        Configuration.applySettings(Configuration.BASE);
        resetDeviceTree();
    }

    @FXML
    void cancelConfig(MouseEvent event) {
        Configuration.buildTree();
        resetDeviceTree();
    }

    @FXML
    public void initialize() {
        assert settingsVbox != null : "fx:id=\"settingsVbox\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert splitPane != null : "fx:id=\"splitPane\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert settingsScroll != null : "fx:id=\"settingsScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert deviceTree != null : "fx:id=\"deviceTree\" was not injected: check your FXML file 'Configuration.fxml'.";
        assert treeScroll != null : "fx:id=\"treeScroll\" was not injected: check your FXML file 'Configuration.fxml'.";
        resetDeviceTree();
        deviceTree.getSelectionModel().selectedItemProperty().addListener(this::selectionChanged);
        deviceTree.maxWidthProperty().bind(treeScroll.widthProperty());
    }
    
    private void resetDeviceTree() {
        Set<String> expanded = new HashSet<>();
        String current = getCurrentNodePath();
        getExpandedNodes("", deviceTree.getRoot(), expanded);
        deviceTree.setRoot(Configuration.BASE);
        setExpandedNodes("", deviceTree.getRoot(), expanded);
        setCurrentNodePath(current);
    }

    private void getExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
        if (root == null) return;
        root.getChildren().stream().filter((item) -> (item.isExpanded())).forEach((item) -> {
            String name = prefix+item.toString();
            expanded.add(name);
            getExpandedNodes(name+DELIMITER, item, expanded);
        });
    }

    private void setExpandedNodes(String prefix, TreeItem<ConfigNode> root, Set<String> expanded) {
        if (root == null) return;
        root.getChildren().stream().forEach((item) -> {
            String name = prefix+item.toString();
            if (expanded.contains(name)) {
                item.setExpanded(true);
            }
            setExpandedNodes(name+DELIMITER, item, expanded);
        });
    }

    private String getCurrentNodePath() {
        TreeItem<ConfigNode> current = deviceTree.getSelectionModel().getSelectedItem();
        if (current == null) return null;
        String out = current.toString();
        while (current.getParent() != null) {
            out = current.getParent().toString()+DELIMITER+current;
            current = current.getParent();
        }
        return out;
    }
    
    private void setCurrentNodePath(String value) {
        if (value == null) return;
        String[] parts = value.split(Pattern.quote(DELIMITER));
        TreeItem<ConfigNode> current = deviceTree.getRoot();
        for (String part : parts) {
            for (TreeItem child : current.getChildren()) {
                if (child.toString().equals(part)) {
                    current = child;
                }
            }
        }
        deviceTree.getSelectionModel().select(current);
    }
    
    private void selectionChanged(
            ObservableValue<? extends TreeItem<ConfigNode>> observable,
            TreeItem<ConfigNode> oldValue,
            TreeItem<ConfigNode> newValue) {
        clearForm();
        buildForm((ConfigNode) newValue);
    }

    private void clearForm() {
        settingsVbox.getChildren().clear();
    }

    private void buildForm(ConfigNode node) {
        if (node == null) {
            return;
        }
        node.hotkeys.forEach((name, values) -> {
            settingsVbox.getChildren().add(buildKeyShortcutRow(node, name, values));
        });
        node.settings.forEach((name, value) -> {
            settingsVbox.getChildren().add(buildSettingRow(node, name, value));
        });
    }

    private Node buildSettingRow(ConfigNode node, String settingName, Serializable value) {
        ConfigurableField fieldInfo = Configuration.getConfigurableFieldInfo(node.subject, settingName);
        if (fieldInfo == null) {
            return null;
        }
        HBox row = new HBox();
        row.getStyleClass().add("setting-row");
        Label label = new Label(fieldInfo.name());
        label.getStyleClass().add("setting-label");
        label.setMinWidth(150.0);
        Node widget = buildEditField(node, settingName, value);
        label.setLabelFor(widget);
        row.getChildren().add(label);
        row.getChildren().add(widget);
        return row;
    }

    private Node buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
        InvokableAction actionInfo = Configuration.getInvokableActionInfo(node.subject, actionName);
        if (actionInfo == null) {
            return null;
        }
        HBox row = new HBox();
        row.getStyleClass().add("setting-row");
        Label label = new Label(actionInfo.name());
        label.getStyleClass().add("setting-keyboard-shortcut");
        label.setMinWidth(150.0);
        String value = Arrays.stream(values).collect(Collectors.joining(" or "));
        Text widget = new Text(value);
        widget.setWrappingWidth(180.0);
        widget.getStyleClass().add("setting-keyboard-value");
        widget.setOnMouseClicked((event) -> {
            editKeyboardShortcut(node, actionName, widget);
        });
        label.setLabelFor(widget);
        row.getChildren().add(label);
        row.getChildren().add(widget);
        return row;
    }

    private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
        throw new UnsupportedOperationException("Not supported yet.");
    }    
    
    private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
        Field field;
        try {
            field = node.subject.getClass().getField(settingName);
        } catch (NoSuchFieldException | SecurityException ex) {
            return null;
        }
        Class type = field.getType();
        if (type == java.lang.String.class) {
            return buildTextField(node, settingName, value, null);
        } else if (type.isPrimitive()) {
            if (type == Integer.TYPE || type == Short.TYPE || type == Byte.TYPE) {
                return buildTextField(node, settingName, value, "-?[0-9]+");
            } else if (type == Float.TYPE || type == Double.TYPE) {
                return buildTextField(node, settingName, value, "-?[0-9]*(\\.[0-9]+)?");
            } else if (type == Boolean.TYPE) {
                return buildBooleanField(node, settingName, value);
            } else {
                return buildTextField(node, settingName, value, null);
            }
        } else if (type.equals(File.class)) {
            // TODO: Add file support!
        } else if (Class.class.isEnum()) {
            // TODO: Add enumeration support!
        } else if (ISelection.class.isAssignableFrom(type)) {
            return buildDynamicSelectComponent(node, settingName, value);
        }
        return null;
    }

    private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) {
        TextField widget = new TextField(String.valueOf(value));
        widget.textProperty().addListener((e) -> {
            node.setFieldValue(settingName, widget.getText());
        });
        return widget;
    }

    private Node buildBooleanField(ConfigNode node, String settingName, Serializable value) {
        CheckBox widget = new CheckBox();
        widget.setSelected(value.equals(Boolean.TRUE));
        widget.selectedProperty().addListener((e) -> {
            node.setFieldValue(settingName, widget.isSelected());
        });
        return widget;
    }

    private Node buildDynamicSelectComponent(ConfigNode node, String settingName, Serializable value) {
        try {
            DynamicSelection sel = (DynamicSelection) node.subject.getClass().getField(settingName).get(node.subject);
            ChoiceBox widget = new ChoiceBox(FXCollections.observableList(new ArrayList(sel.getSelections().keySet())));
            widget.setMinWidth(175.0);
            widget.setConverter(new StringConverter() {
                @Override
                public String toString(Object object) {
                    return (String) sel.getSelections().get(object);
                }
                
                @Override
                public Object fromString(String string) {
                    return sel.findValueByMatch(string);
                }
            });
            Object selected = value == null ? null : widget.getConverter().fromString(String.valueOf(value));
            if (selected == null) {
                widget.getSelectionModel().selectFirst();
            } else {
                widget.setValue(selected);
            }
            widget.valueProperty().addListener((Observable e) -> {
                node.setFieldValue(settingName, widget.getConverter().toString(widget.getValue()));
            });
            return widget;
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
            Logger.getLogger(ConfigurationUIController.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }
} public interface Reconfigurable {
    public String getName();
    public String getShortName();
    public void reconfigure();
} public abstract class CPU extends Device {
    private static final Logger LOG = Logger.getLogger(CPU.class.getName());

    public CPU(Computer computer) {
        super(computer);
    }
    
    @Override
    public String getShortName() {
        return "cpu";
    }
    private Debugger debugger = null;
    @ConfigurableField(name = "Enable trace to STDOUT", shortName = "trace")
    public boolean trace = false;

    public boolean isTraceEnabled() {
        return trace;
    }

    public void setTraceEnabled(boolean t) {
        trace = t;
    }
    @ConfigurableField(name = "Trace length", shortName = "traceSize", description = "Number of most recent trace lines to keep for debugging errors.  Zero == disabled")
    public int traceLength = 0;
    private ArrayList<String> traceLog = new ArrayList<>();

    public boolean isLogEnabled() {
        return (traceLength > 0);
    }

    public void log(String line) {
        if (!isLogEnabled()) {
            return;
        }
        while (traceLog.size() >= traceLength) {
            traceLog.remove(0);
        }
        traceLog.add(line);
    }
    
    public void dumpTrace() {
        computer.pause();
        ArrayList<String> newLog = new ArrayList<>();
        ArrayList<String> oldLog = traceLog;
        traceLog = newLog;
        computer.resume();
        LOG.log(Level.INFO, "Most recent {0} instructions:", traceLength);
        oldLog.stream().forEach(LOG::info);
        oldLog.clear();
    }

    public void setDebug(Debugger d) {
        debugger = d;
        suspend();
    }

    public void clearDebug() {
        debugger = null;
        resume();
    }
    //@ConfigurableField(name="Program Counter")
    public int programCounter = 0;

    public int getProgramCounter() {
        return programCounter;
    }

    public void setProgramCounter(int programCounter) {
        this.programCounter = 0x00FFFF & programCounter;
    }

    public void incrementProgramCounter(int amount) {
        this.programCounter += amount;
        this.programCounter = 0x00FFFF & this.programCounter;
    }

    /**
     * Process a single tick of the main processor clock. Either we're waiting
     * to execute the next instruction, or the next instruction is ready to go
     */
    @Override
    public void tick() {
        try {
            if (debugger != null) {
                if (!debugger.isActive() && debugger.hasBreakpoints()) {
                    debugger.getBreakpoints().stream().filter((i) -> (i == getProgramCounter())).forEach((_item) -> {
                        debugger.setActive(true);
                    });
                }
                if (debugger.isActive()) {
                    debugger.updateStatus();
                    if (!debugger.takeStep()) {
                        // If the debugger is active and we aren't ready for the next step, sleep and exit
                        // Without the sleep, this would constitute a very rapid-fire loop and would eat
                        // an unnecessary amount of CPU.
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException ex) {
                            Logger.getLogger(CPU.class.getName()).log(Level.SEVERE, null, ex);
                        }
                        return;
                    }
                }
            }
        } catch (Throwable t) {
            // ignore
        }
        executeOpcode();
    }
    /*
     * Execute the current opcode at the current program counter
     *@return number of cycles to wait until next command can be executed
     */

    protected abstract void executeOpcode();

    public abstract void reset();

    public abstract void generateInterrupt();

    abstract public void pushPC();

    @Override
    public void attach() {
    }

    abstract public void JSR(int pointer);

    boolean singleTraceEnabled = false;
    public String lastTrace = "START";
    public void performSingleTrace() {
        singleTraceEnabled = true;
    }
    public boolean isSingleTraceEnabled() {
        return singleTraceEnabled;
    }
    public String getLastTrace() {
        return lastTrace;
    }
    public void captureSingleTrace(String trace) {
        lastTrace = trace;
        singleTraceEnabled = false;
    }

    abstract public void clearState();
} public abstract class Computer implements Reconfigurable {

    public RAM memory;
    public CPU cpu;
    public Video video;
    public Keyboard keyboard;
    public StateManager stateManager;
    public Motherboard motherboard;
    public boolean romLoaded;
    @ConfigurableField(category = "advanced", name = "State management", shortName = "rewind", description = "This enables rewind support, but consumes a lot of memory when active.")
    public boolean enableStateManager;
    public final SoundMixer mixer;
    final private BooleanProperty runningProperty = new SimpleBooleanProperty(false);

    /**
     * Creates a new instance of Computer
     */
    public Computer() {
        keyboard = new Keyboard(this);
        mixer = new SoundMixer(this);
        romLoaded = false;
    }

    public RAM getMemory() {
        return memory;
    }

    public Motherboard getMotherboard() {
        return motherboard;
    }

    ChangeListener<Boolean> runningPropertyListener = (prop, oldVal, newVal) -> runningProperty.set(newVal);
    public void setMotherboard(Motherboard m) {
        if (motherboard != null && motherboard.isRunning()) {
            motherboard.suspend();
        }
        motherboard = m;
    }

    public BooleanProperty getRunningProperty() {
        return runningProperty;
    }
    
    public boolean isRunning() {
        return getRunningProperty().get();
    }
    
    public void notifyVBLStateChanged(boolean state) {
        for (Optional<Card> c : getMemory().cards) {
            c.ifPresent(card -> card.notifyVBLStateChanged(state));
        }
        if (state && stateManager != null) {
            stateManager.notifyVBLActive();
        }
    }

    public void setMemory(RAM memory) {
        if (this.memory != memory) {
            if (this.memory != null) {
                this.memory.detach();
            }
            memory.attach();
        }
        this.memory = memory;
    }

    public void waitForNextCycle() {
        //@TODO IMPLEMENT TIMER SLEEP CODE!
    }

    public Video getVideo() {
        return video;
    }

    public void setVideo(Video video) {
        this.video = video;
    }

    public CPU getCpu() {
        return cpu;
    }

    public void setCpu(CPU cpu) {
        this.cpu = cpu;
    }

    public void loadRom(String path) throws IOException {
        memory.loadRom(path);
        romLoaded = true;
    }

    public void deactivate() {
        if (cpu != null) {
            cpu.suspend();
        }
        if (motherboard != null) {
            motherboard.suspend();
        }
        if (video != null) {
            video.suspend();            
        }
        if (mixer != null) {
            mixer.detach();
        }
    }

    @InvokableAction(
            name = "Cold boot",
            description = "Process startup sequence from power-up",
            category = "general",
            alternatives = "Full reset;reset emulator",
            consumeKeyEvent = true,
            defaultKeyMapping = {"Ctrl+Shift+Backspace", "Ctrl+Shift+Delete"})
    public void invokeColdStart() {
        if (!romLoaded) {
            Thread delayedStart = new Thread(() -> {
                while (!romLoaded) {
                    Thread.yield();
                }
                coldStart();
            });
            delayedStart.start();
        } else {
            coldStart();
        }
    }

    public abstract void coldStart();

    @InvokableAction(
            name = "Warm boot",
            description = "Process user-initatiated reboot (ctrl+apple+reset)",
            category = "general",
            alternatives = "reboot;reset;three-finger-salute;restart",
            defaultKeyMapping = {"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"})
    public void invokeWarmStart() {
        warmStart();
    }

    public abstract void warmStart();

    public Keyboard getKeyboard() {
        return this.keyboard;
    }

    protected abstract void doPause();

    protected abstract void doResume();

    @InvokableAction(name = "Pause", description = "Stops the computer, allowing reconfiguration of core elements", alternatives = "freeze;halt", defaultKeyMapping = {"meta+pause", "alt+pause"})
    public boolean pause() {
        boolean result = getRunningProperty().get();
        doPause();
        getRunningProperty().set(false);
        return result;
    }

    @InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume;play", defaultKeyMapping = {"meta+shift+pause", "alt+shift+pause"})
    public void resume() {
        doResume();
        getRunningProperty().set(true);
    }

    @Override
    public void reconfigure() {
        mixer.reconfigure();
        if (enableStateManager) {
            stateManager = StateManager.getInstance(this);
        } else {
            stateManager = null;
            StateManager.getInstance(this).invalidate();
        }
    }
} public abstract class Debugger {

    public abstract void updateStatus();
    private boolean active = false;
    public boolean step = false;

    public void setActive(boolean state) {
        active = state;
    }

    public boolean isActive() {
        return active;
    }
    private final List<Integer> breakpoints = new ArrayList<>();

    public List<Integer> getBreakpoints() {
        return breakpoints;
    }
    private boolean hasBreakpoints = false;

    boolean hasBreakpoints() {
        return hasBreakpoints;
    }

    public void updateBreakpoints() {
        hasBreakpoints = false;
        for (Integer i : breakpoints) {
            if (i != null) {
                hasBreakpoints = true;
            }
        }
    }

    boolean takeStep() {
        if (step) {
            step = false;
            return true;
        }
        return false;
    }
}
 public abstract class RAM implements Reconfigurable {

    public PagedMemory activeRead;
    public PagedMemory activeWrite;
    public List<RAMListener> listeners;
    public List<RAMListener>[] listenerMap;
    public List<RAMListener>[] ioListenerMap;
    public Optional<Card>[] cards;
    // card 0 = 80 column card firmware / system rom
    public int activeSlot = 0;
    protected final Computer computer;

    /**
     * Creates a new instance of RAM
     *
     * @param computer
     */
    public RAM(Computer computer) {
        this.computer = computer;
        listeners = new ArrayList<>();
        cards = new Optional[8];
        for (int i = 0; i < 8; i++) {
            cards[i] = Optional.empty();
        }
        refreshListenerMap();
    }

    public void setActiveCard(int slot) {
        if (activeSlot != slot) {
            activeSlot = slot;
            configureActiveMemory();
        } else if (!SoftSwitches.CXROM.getState()) {
            configureActiveMemory();
        }
    }

    public int getActiveSlot() {
        return activeSlot;
    }

    public Optional<Card>[] getAllCards() {
        return cards;
    }

    public Optional<Card> getCard(int slot) {
        if (slot >= 1 && slot <= 7) {
            return cards[slot];
        }
        return Optional.empty();
    }

    public void addCard(Card c, int slot) {
        removeCard(slot);
        cards[slot] = Optional.of(c);
        c.setSlot(slot);
        c.attach();
    }

    public void removeCard(Card c) {
        c.suspend();
        c.detach();
        removeCard(c.getSlot());
    }

    public void removeCard(int slot) {
        cards[slot].ifPresent(Card::suspend);
        cards[slot].ifPresent(Card::detach);
        cards[slot] = Optional.empty();
    }

    abstract public void configureActiveMemory();

    public void write(int address, byte b, boolean generateEvent, boolean requireSynchronization) {
        byte[] page = activeWrite.getMemoryPage(address);
        if (page == null) {
            if (generateEvent) {
                callListener(RAMEvent.TYPE.WRITE, address, 0, b, requireSynchronization);
            }
        } else {
            int offset = address & 0x0FF;
            byte old = page[offset];
            if (generateEvent) {
                page[offset] = callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization);
            } else {
                page[offset] = b;
            }
        }
    }

    public void writeWord(int address, int w, boolean generateEvent, boolean requireSynchronization) {
        write(address, (byte) (w & 0x0ff), generateEvent, requireSynchronization);
        write(address + 1, (byte) (w >> 8), generateEvent, requireSynchronization);
    }
    
    public byte readRaw(int address) {
        //    if (address >= 65536) return 0;
        return activeRead.getMemoryPage(address)[address & 0x0FF];
    }

    public byte read(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSyncronization) {
        //    if (address >= 65536) return 0;
        byte value = activeRead.getMemoryPage(address)[address & 0x0FF];
//        if (triggerEvent || ((address & 0x0FF00) == 0x0C000)) {
        if (triggerEvent || (address & 0x0FFF0) == 0x0c030) {
            value = callListener(eventType, address, value, value, requireSyncronization);
        }
        return value;
    }

    public int readWordRaw(int address) {
        int lsb = 0x00ff & readRaw(address);
        int msb = (0x00ff & readRaw(address + 1)) << 8;
        return msb + lsb;
    }

    public int readWord(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSynchronization) {
        int lsb = 0x00ff & read(address, eventType, triggerEvent, requireSynchronization);
        int msb = (0x00ff & read(address + 1, eventType, triggerEvent, requireSynchronization)) << 8;
        int value = msb + lsb;
        return value;
    }

    private void mapListener(RAMListener l, int address) {
        if ((address & 0x0FF00) == 0x0C000) {
            int index = address & 0x0FF;
            List<RAMListener> ioListeners = ioListenerMap[index];
            if (ioListeners == null) {
                ioListeners = new ArrayList<>();
                ioListenerMap[index] = ioListeners;
            }
            if (!ioListeners.contains(l)) {
                ioListeners.add(l);
            }
        } else {
            int index = address >> 8;
            List<RAMListener> otherListeners = listenerMap[index];
            if (otherListeners == null) {
                otherListeners = new ArrayList<>();
                listenerMap[index] = otherListeners;
            }
            if (!otherListeners.contains(l)) {
                otherListeners.add(l);
            }
        }
    }

    private void addListenerRange(RAMListener l) {
        if (l.getScope() == RAMEvent.SCOPE.ADDRESS) {
            mapListener(l, l.getScopeStart());
        } else {
            int start = 0;
            int end = 0x0ffff;
            if (l.getScope() == RAMEvent.SCOPE.RANGE) {
                start = l.getScopeStart();
                end = l.getScopeEnd();
            }
            for (int i = start; i <= end; i++) {
                mapListener(l, i);
            }
        }
    }

    private void refreshListenerMap() {
        listenerMap = new ArrayList[256];
        ioListenerMap = new ArrayList[256];
        listeners.stream().forEach((l) -> {
            addListenerRange(l);
        });
    }

    public RAMListener observe(RAMEvent.TYPE type, int address, RAMEvent.RAMEventHandler handler) {
        return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
            @Override
            protected void doConfig() {
                setScopeStart(address);
            }

            @Override
            protected void doEvent(RAMEvent e) {
                handler.handleEvent(e);
            }
        });
    }

    public RAMListener observe(RAMEvent.TYPE type, int address, boolean auxFlag, RAMEvent.RAMEventHandler handler) {
        return addListener(new RAMListener(type, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
            @Override
            protected void doConfig() {
                setScopeStart(address);
            }

            @Override
            protected void doEvent(RAMEvent e) {
                if (isAuxFlagCorrect(e, auxFlag)) {
                    handler.handleEvent(e);
                }
            }
        });
    }

    public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, RAMEvent.RAMEventHandler handler) {
        return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
            @Override
            protected void doConfig() {
                setScopeStart(addressStart);
                setScopeEnd(addressEnd);
            }

            @Override
            protected void doEvent(RAMEvent e) {
                handler.handleEvent(e);
            }
        });
    }

    public RAMListener observe(RAMEvent.TYPE type, int addressStart, int addressEnd, boolean auxFlag, RAMEvent.RAMEventHandler handler) {
        return addListener(new RAMListener(type, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) {
            @Override
            protected void doConfig() {
                setScopeStart(addressStart);
                setScopeEnd(addressEnd);
            }

            @Override
            protected void doEvent(RAMEvent e) {
                if (isAuxFlagCorrect(e, auxFlag)) {
                    handler.handleEvent(e);
                }
            }
        });
    }

    private boolean isAuxFlagCorrect(RAMEvent e, boolean auxFlag) {
        if (e.getAddress() < 0x0100) {
            if (SoftSwitches.AUXZP.getState() != auxFlag) {
                return false;
            }
        } else if (SoftSwitches.RAMRD.getState() != auxFlag) {
            return false;
        }
        return true;
    }

    public RAMListener addListener(final RAMListener l) {
        boolean restart = computer.pause();
        if (listeners.contains(l)) {
            return l;
        }
        listeners.add(l);
        addListenerRange(l);
        if (restart) {
            computer.resume();
        }
        return l;
    }

    public void removeListener(final RAMListener l) {
        boolean restart = computer.pause();
        listeners.remove(l);
        refreshListenerMap();
        if (restart) {
            computer.resume();
        }
    }

    public byte callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue, boolean requireSyncronization) {
        List<RAMListener> activeListeners;
        if (requireSyncronization) {
            computer.getCpu().suspend();
        }
        if ((address & 0x0FF00) == 0x0C000) {
            activeListeners = ioListenerMap[address & 0x0FF];
            if (activeListeners == null && t.isRead()) {
                if (requireSyncronization) {
                    computer.getCpu().resume();
                }
                return computer.getVideo().getFloatingBus();
            }
        } else {
            activeListeners = listenerMap[(address >> 8) & 0x0ff];
        }
        if (activeListeners != null) {
            RAMEvent e = new RAMEvent(t, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, address, oldValue, newValue);
            activeListeners.stream().forEach((l) -> {
                l.handleEvent(e);
            });
            if (requireSyncronization) {
                computer.getCpu().resume();
            }
            return (byte) e.getNewValue();
        }
        if (requireSyncronization) {
            computer.getCpu().resume();
        }
        return (byte) newValue;
    }

    abstract protected void loadRom(String path) throws IOException;

    abstract public void attach();

    abstract public void detach();

    abstract public void performExtendedCommand(int i);
}
 public abstract class RAMListener implements RAMEvent.RAMEventHandler {
    
    private RAMEvent.TYPE type;
    private RAMEvent.SCOPE scope;
    private RAMEvent.VALUE value;
    private int scopeStart;
    private int scopeEnd;
    private int valueStart;
    private int valueEnd;
    private int valueAmount;

    /**
     * Creates a new instance of RAMListener
     * @param t
     * @param s
     * @param v
     */
    public RAMListener(RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) {
        setType(t);
        setScope(s);
        setValue(v);
        doConfig();
    }

    public RAMEvent.TYPE getType() {
        return type;
    }

    public final void setType(RAMEvent.TYPE type) {
        this.type = type;
    }

    public RAMEvent.SCOPE getScope() {
        return scope;
    }

    public final void setScope(RAMEvent.SCOPE scope) {
        this.scope = scope;
    }

    public RAMEvent.VALUE getValue() {
        return value;
    }

    public final void setValue(RAMEvent.VALUE value) {
        this.value = value;
    }

    public int getScopeStart() {
        return scopeStart;
    }

    public void setScopeStart(int scopeStart) {
        this.scopeStart = scopeStart;
    }

    public int getScopeEnd() {
        return scopeEnd;
    }

    public void setScopeEnd(int scopeEnd) {
        this.scopeEnd = scopeEnd;
    }

    public int getValueStart() {
        return valueStart;
    }

    public void setValueStart(int valueStart) {
        this.valueStart = valueStart;
    }

    public int getValueEnd() {
        return valueEnd;
    }

    public void setValueEnd(int valueEnd) {
        this.valueEnd = valueEnd;
    }

    public int getValueAmount() {
        return valueAmount;
    }

    public void setValueAmount(int valueAmount) {
        this.valueAmount = valueAmount;
    }

    public boolean isRelevant(RAMEvent e) {
        // Skip event if it's not the right type
        if (type != TYPE.ANY && e.getType() != TYPE.ANY) {
            if ((type != e.getType())) {
                if (type == TYPE.READ) {
                    if (!e.getType().isRead()) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
        }
        // Skip event if it's not in the scope we care about
        if (scope != RAMEvent.SCOPE.ANY) {
            if (scope == RAMEvent.SCOPE.ADDRESS && e.getAddress() != scopeStart) {
                return false;
            } else if (scope == RAMEvent.SCOPE.RANGE && (e.getAddress() < scopeStart || e.getAddress() > scopeEnd)) {
                return false;
            }
        }

        // Skip event if the value modification is uninteresting
        if (value != RAMEvent.VALUE.ANY) {
            if (value == RAMEvent.VALUE.CHANGE_BY && e.getNewValue() - e.getOldValue() != valueAmount) {
                return false;
            } else if (value == RAMEvent.VALUE.EQUALS && e.getNewValue() != valueAmount) {
                return false;
            } else if (value == RAMEvent.VALUE.NOT_EQUALS && e.getNewValue() == valueAmount) {
                return false;
            } else if (value == RAMEvent.VALUE.RANGE && (e.getNewValue() < valueStart || e.getNewValue() > valueEnd)) {
                return false;
            }
        }

        // Ok, so we've filtered out the uninteresting stuff
        // If we've made it this far then the event is valid.
        return true;
    }
    
    @Override
    public void handleEvent(RAMEvent e) {
        if (isRelevant(e)) {
            doEvent(e);
        }
    }

    abstract protected void doConfig();

    abstract protected void doEvent(RAMEvent e);
}
 public class IdeController {

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;

    @FXML // fx:id="saveMenuItem"
    private MenuItem saveMenuItem; // Value injected by FXMLLoader

    @FXML // fx:id="saveAllMenuItem"
    private MenuItem saveAllMenuItem; // Value injected by FXMLLoader

    @FXML // fx:id="saveAsMenuItem"
    private MenuItem saveAsMenuItem; // Value injected by FXMLLoader

    @FXML // fx:id="closeMenuItem"
    private MenuItem closeMenuItem; // Value injected by FXMLLoader

    @FXML // fx:id="closeAllMenuItem"
    private MenuItem closeAllMenuItem; // Value injected by FXMLLoader

    @FXML // fx:id="editMenu"
    private Menu editMenu; // Value injected by FXMLLoader

    @FXML // fx:id="runMenu"
    private Menu runMenu; // Value injected by FXMLLoader

    @FXML // fx:id="autocompile"
    private ToggleGroup autocompile; // Value injected by FXMLLoader

    @FXML // fx:id="tabPane"
    private TabPane tabPane; // Value injected by FXMLLoader

    @FXML // fx:id="statusBar"
    private ToolBar statusBar; // Value injected by FXMLLoader

    @FXML
    void onCloseAllClicked(ActionEvent event) {
        openDocuments.forEach((Tab t, Program proxy) -> {
            if (!event.isConsumed()) {
                closeTab(t, event);
            }
        });
    }

    @FXML
    void onCloseClicked(ActionEvent event) {
        getCurrentTab().ifPresent((t) -> closeTab(t, event));
    }

    public Optional<Tab> getCurrentTab() {
        return Optional.ofNullable(tabPane.getSelectionModel().getSelectedItem());
    }

    public Optional<Program> getCurrentProgram() {
        return getCurrentTab().map(t -> openDocuments.get(t));
    }

    @FXML
    void newApplesoftBasicClicked(ActionEvent event) {
        Program tab = createTab(DocumentType.applesoft, null, true);
    }

    @FXML
    void newApplesoftBasicFromMemoryClicked(ActionEvent event) {
        Alert warningAlert = new Alert(Alert.AlertType.CONFIRMATION);
        warningAlert.setTitle("Is Applesoft running?");
        warningAlert.setContentText("If you proceed and applesoft is not running or there is no active program then the emulator might freeze.  Press Cancel if you are unsure.");
        Optional<ButtonType> result = warningAlert.showAndWait();
        if (result.get() == ButtonType.OK) {
            Program tab = createTab(DocumentType.applesoft, null, false);
        }
    }

    @FXML
    void newAssemblyListingClicked(ActionEvent event) {
        createTab(DocumentType.assembly, null, false);
    }

    @FXML
    void newHexdataClicked(ActionEvent event) {
        createTab(DocumentType.hex, null, false);
    }

    @FXML
    void newPlainTextClicked(ActionEvent event) {
        createTab(DocumentType.plain, null, false);
    }

    Map<Tab, Program> openDocuments = new HashMap<>();
    Map<Option, Object> globalOptions = new EnumMap<>(Option.class);

    private Program createTab(DocumentType type, File document, boolean isBlank) {
        WebView editor = new WebView();
        Program proxy = new Program(type, globalOptions);
        proxy.initEditor(editor, document, isBlank);
        Tab t = new Tab(proxy.getName(), editor);
        tabPane.getTabs().add(t);
        openDocuments.put(t, proxy);
        t.setOnCloseRequest(this::handleCloseTabRequest);
        return proxy;
    }

    private void handleCloseTabRequest(Event e) {
        Tab t = (Tab) e.getTarget();
        closeTab(t, e);
    }

    private void closeTab(Tab t, Event e) {
        tabPane.getTabs().remove(t);
        openDocuments.remove(t);
    }

    @FXML
    void onOpenClicked(ActionEvent event) {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Open document");
        for (DocumentType type : Program.DocumentType.values()) {
            chooser.getExtensionFilters().add(
                    new FileChooser.ExtensionFilter(type.name(), type.extensions)
            );
        }
        chooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("All Files", "*.*")
        );
        File file = chooser.showOpenDialog(JaceApplication.getApplication().primaryStage);
        if (file != null && file.isFile() && file.exists()) {
            DocumentType type = DocumentType.fromFile(file);
            createTab(type, file, true);
        }
    }

    @FXML
    void onSaveAllClicked(ActionEvent event) {
        openDocuments.forEach((Tab t, Program p) -> {
            if (p.isChanged()) {
                if (p.getFile().isPresent()) {
                    p.save(p.getFile().get());
                    t.setText(p.getName());
                } else {
                    tabPane.getSelectionModel().select(t);
                    onSaveAsClicked(event);
                }
            }
        });
    }

    @FXML
    void onSaveAsClicked(ActionEvent event) {
        getCurrentProgram().ifPresent(program -> {
            program.save(chooseFileToSave(program.getType()));
            getCurrentTab().get().setText(program.getName());
        });
    }

    @FXML
    void onSaveClicked(ActionEvent event) {
        getCurrentProgram().ifPresent(program -> {
            program.save(
                    program.getFile()
                    .orElseGet(() -> chooseFileToSave(program.getType())));
            getCurrentTab().get().setText(program.getName());
        });
    }

    private File chooseFileToSave(DocumentType type) {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Save " + type.name() + " document");
        chooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter(type.name(), type.extensions),
                new FileChooser.ExtensionFilter("All Files", "*.*")
        );
        return chooser.showSaveDialog(JaceApplication.getApplication().primaryStage);
    }

    @FXML
    void executeClicked(ActionEvent event) {
        getCurrentProgram().ifPresent(program -> {
            program.execute();
            updateStatusMessages(program.lastResult);
        });
    }

    @FXML
    void testCompileClicked(ActionEvent event) {
        getCurrentProgram().ifPresent(program -> {
            program.test();
            updateStatusMessages(program.lastResult);
        });
    }

    @FXML
    void viewCompilerOutputClicked(ActionEvent event) {

    }

    @FXML
    void viewSymbolTableClicked(ActionEvent event) {

    }

    @FXML // This method is called by the FXMLLoader when initialization is complete
    public void initialize() {
        assert saveMenuItem != null : "fx:id=\"saveMenuItem\" was not injected: check your FXML file 'editor.fxml'.";
        assert saveAllMenuItem != null : "fx:id=\"saveAllMenuItem\" was not injected: check your FXML file 'editor.fxml'.";
        assert saveAsMenuItem != null : "fx:id=\"saveAsMenuItem\" was not injected: check your FXML file 'editor.fxml'.";
        assert closeMenuItem != null : "fx:id=\"closeMenuItem\" was not injected: check your FXML file 'editor.fxml'.";
        assert closeAllMenuItem != null : "fx:id=\"closeAllMenuItem\" was not injected: check your FXML file 'editor.fxml'.";
        assert editMenu != null : "fx:id=\"editMenu\" was not injected: check your FXML file 'editor.fxml'.";
        assert runMenu != null : "fx:id=\"runMenu\" was not injected: check your FXML file 'editor.fxml'.";
        assert autocompile != null : "fx:id=\"autocompile\" was not injected: check your FXML file 'editor.fxml'.";
        assert tabPane != null : "fx:id=\"tabPane\" was not injected: check your FXML file 'editor.fxml'.";
        assert statusBar != null : "fx:id=\"statusBar\" was not injected: check your FXML file 'editor.fxml'.";

        tabPane.getTabs().addListener((ListChangeListener.Change<? extends Tab> c) -> {
            boolean hasNoItems = c.getList().isEmpty();
            saveMenuItem.setDisable(hasNoItems);
            saveAsMenuItem.setDisable(hasNoItems);
            saveAllMenuItem.setDisable(hasNoItems);
            closeMenuItem.setDisable(hasNoItems);
            closeAllMenuItem.setDisable(hasNoItems);
            editMenu.setDisable(hasNoItems);
            runMenu.setDisable(hasNoItems);
        });
    }

    private void updateStatusMessages(CompileResult lastResult) {
        String message = "Compiler was " + (lastResult.isSuccessful() ? " successful" : " NOT SUCCESSFUL");
        message += " -- ";
        message += lastResult.getErrors().size() + " error(s) and "+lastResult.getWarnings().size()+" warning(s) reported.";
        statusBar.getItems().clear();
        statusBar.getItems().add(new Label(message));
    }
} public class Utility {

    static Reflections reflections = new Reflections("jace");

    public static Set<Class> findAllSubclasses(Class clazz) {
        return reflections.getSubTypesOf(clazz);
    }

    //------------------------------ String comparators
    /**
     * Rank two strings similarity in terms of distance The lower the number,
     * the more similar these strings are to each other See:
     * http://en.wikipedia.org/wiki/Levenshtein_distance#Computing_Levenshtein_distance
     *
     * @param s
     * @param t
     * @return Distance (lower means a closer match, zero is identical)
     */
    public static int levenshteinDistance(String s, String t) {
        if (s == null || t == null || s.length() == 0 || t.length() == 0) {
            return Integer.MAX_VALUE;
        }

        s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "");
        t = t.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", "");
        int m = s.length();
        int n = t.length();
        int[][] dist = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            dist[i][0] = i;
        }
        for (int i = 1; i <= n; i++) {
            dist[0][i] = i;
        }
        for (int j = 1; j <= n; j++) {
            for (int i = 1; i <= m; i++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dist[i][j] = dist[i - 1][j - 1];
                } else {
                    int del = dist[i - 1][j] + 1;
                    int insert = dist[i][j - 1] + 1;
                    int sub = dist[i - 1][j - 1] + 1;
                    dist[i][j] = Math.min(Math.min(del, insert), sub);
                }
            }
        }
        return dist[m][n];
    }

    /**
     * Normalize distance based on longest string
     * @param s
     * @param t
     * @return Similarity ranking, higher is better
     */
    public static int adjustedLevenshteinDistance(String s, String t) {
        return Math.max(s.length(), t.length()) - levenshteinDistance(s, t);
    }


    /**
     * Compare strings based on a tally of similar patterns found, using a fixed
     * search window The resulting score is heavily penalized if the strings
     * differ greatly in length This is not as efficient as levenshtein, so it's
     * only used as a tie-breaker.
     *
     * @param c1
     * @param c2
     * @param width Search window size
     * @return Overall similarity score (higher is better)
     */
    public static double rankMatch(String c1, String c2, int width) {
        double score = 0;
        String s1 = c1.toLowerCase();
        String s2 = c2.toLowerCase();
        for (int i = 0; i < s1.length() + 1 - width; i++) {
            String m = s1.substring(i, i + width);
            int j = 0;
            while ((j = s2.indexOf(m, j)) > -1) {
                score += width;
                j++;
            }
        }
        double l1 = s1.length();
        double l2 = s2.length();
        // If the two strings are equivilent in length, the score is higher
        // If the two strings are different in length, the score is adjusted lower depending on how large the difference is
        // This is offset just a hair for tuning purposes
        double adjustment = (Math.min(l1, l2) / Math.max(l1, l2)) + 0.1;
        return score * adjustment * adjustment;
    }

    public static String join(Collection<String> c, String d) {
        return c.stream().collect(Collectors.joining(d));
    }

    private static boolean isHeadless = false;

    public static void setHeadlessMode(boolean headless) {
        isHeadless = headless;
        if (Emulator.instance == null && headless) {
            Emulator.instance = new Emulator(Collections.emptyList());
        }
    }

    public static boolean isHeadlessMode() {
        return isHeadless;
    }

    public static Optional<Image> loadIcon(String filename) {
        if (isHeadless) {
            return Optional.empty();
        }
        InputStream stream = Utility.class.getClassLoader().getResourceAsStream("jace/data/" + filename);
        return Optional.of(new Image(stream));
    }

    public static Optional<Label> loadIconLabel(String filename) {
        if (isHeadless) {
            return Optional.empty();
        }
        Image img = loadIcon(filename).get();
        Label label = new Label() {
            @Override
            public boolean equals(Object obj) {
                if (obj instanceof Label) {
                    Label l2 = (Label) obj;
                    return super.equals(l2) || l2.getText().equals(getText());
                } else {
                    return super.equals(obj);
                }
            }

            @Override
            public int hashCode() {
                return getText().hashCode();
            }
        };
        label.setGraphic(new ImageView(img));
        label.setAlignment(Pos.CENTER);
        label.setContentDisplay(ContentDisplay.TOP);
        label.setTextFill(Color.WHITE);
        DropShadow shadow = new DropShadow(5.0, Color.BLACK);
        label.setEffect(shadow);
        return Optional.of(label);
    }

    public static void confirm(String title, String message, Runnable accept) {
        Platform.runLater(() -> {
            Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
            confirm.setContentText(message);
            confirm.setTitle(title);
            Optional<ButtonType> response = confirm.showAndWait();
            response.ifPresent(b -> {
                if (b.getButtonData().isDefaultButton()) {
                    (new Thread(accept)).start();
                }
            });
        });
    }

//    public static void runModalProcess(String title, final Runnable runnable) {
////        final JDialog frame = new JDialog(Emulator.getFrame());
//        final JProgressBar progressBar = new JProgressBar();
//        progressBar.setIndeterminate(true);
//        final JPanel contentPane = new JPanel();
//        contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
//        contentPane.setLayout(new BorderLayout());
//        contentPane.add(new JLabel(title), BorderLayout.NORTH);
//        contentPane.add(progressBar, BorderLayout.CENTER);
//        frame.setContentPane(contentPane);
//        frame.pack();
//        frame.setLocationRelativeTo(null);
//        frame.setVisible(true);
//
//        new Thread(() -> {
//            runnable.run();
//            frame.setVisible(false);
//            frame.dispose();
//        }).start();
//    }
    public static class RankingComparator implements Comparator<String> {

        String match;

        public RankingComparator(String match) {
            // Adding a space helps respect word boundaries as part of the match
            // In the case of very close matches this is another tie-breaker
            // Especially for very small search terms
            this.match = match + " ";
        }

        @Override
        public int compare(String o1, String o2) {
            double s1 = adjustedLevenshteinDistance(match, o1);
            double s2 = adjustedLevenshteinDistance(match, o2);
            if (s2 == s1) {
                s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2);
                s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2);
                if (s2 == s1) {
                    return (o1.compareTo(o2));
                } else {
                    // Normalize result to -1, 0 or 1 so there is no rounding issues!
                    return (int) Math.signum(s2 - s1);
                }
            } else {
                return (int) (s2 - s1);
            }
        }
    }

    /**
     * Given a desired search string and a search space of recognized
     * selections, identify the best match in the list
     *
     * @param match String to search for
     * @param search Space of all valid results
     * @return Best match found, or null if there was nothing close to a match
     * found.
     */
    public static String findBestMatch(String match, Collection<String> search) {
        if (search == null || search.isEmpty()) {
            return null;
        }
        RankingComparator r = new RankingComparator(match);
        List<String> candidates = new ArrayList<>(search);
        Collections.sort(candidates, r);
//	for (String c : candidates) {
//	    double m2 = rankMatch(c, match, 2);
//	    double m3 = rankMatch(c, match, 3);
//	    double m4 = rankMatch(c, match, 4);
//	    double l = levenshteinDistance(match, c);
//	    System.out.println(match + "->" + c + ":" + l + " -- "+ m2 + "," + m3 + "," + "(" + (m2 + m3) + ")");
//	}
//	double score = rankMatch(match, candidates.get(0), 2);
        double score = adjustedLevenshteinDistance(match, candidates.get(0));
        if (score > 1) {
            return candidates.get(0);
        }
        return null;
    }

    public static void printStackTrace() {
        System.out.println("CURRENT STACK TRACE:");
        for (StackTraceElement s : Thread.currentThread().getStackTrace()) {
            System.out.println(s.getClassName() + "." + s.getMethodName() + " (line " + s.getLineNumber() + ") " + (s.isNativeMethod() ? "NATIVE" : ""));
        }
        System.out.println("END OF STACK TRACE");
    }

    public static int parseHexInt(Object s) {
        if (s == null) {
            return -1;
        }
        if (s instanceof Integer) {
            return (Integer) s;
        }
        String val = String.valueOf(s).trim();
        int base = 10;
        if (val.startsWith("$")) {
            base = 16;
            val = val.contains(" ") ? val.substring(1, val.indexOf(' ')) : val.substring(1);
        } else if (val.startsWith("0x")) {
            base = 16;
            val = val.contains(" ") ? val.substring(2, val.indexOf(' ')) : val.substring(2);
        }
        try {
            return Integer.parseInt(val, base);
        } catch (NumberFormatException ex) {
            gripe("This isn't a valid number: " + val + ".  If you put a $ in front of that then I'll know you meant it to be a hex number.");
            throw ex;
        }
    }

    public static void gripe(final String message) {
        Platform.runLater(() -> {
            Alert errorAlert = new Alert(Alert.AlertType.ERROR);
            errorAlert.setContentText(message);
            errorAlert.setTitle("Error");
            errorAlert.show();
        });
    }

    public static Object findChild(Object object, String fieldName) {
        if (object instanceof Map) {
            Map map = (Map) object;
            for (Object key : map.keySet()) {
                if (key.toString().equalsIgnoreCase(fieldName)) {
                    return map.get(key);
                }
            }
            return null;
        }
        try {
            Field f = object.getClass().getField(fieldName);
            return f.get(object);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
            for (Method m : object.getClass().getMethods()) {
                if (m.getName().equalsIgnoreCase("get" + fieldName) && m.getParameterTypes().length == 0) {
                    try {
                        return m.invoke(object, new Object[0]);
                    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) {
                    }
                }
            }
        }
        return null;
    }

    public static Object setChild(Object object, String fieldName, String value, boolean hex) {
        if (object instanceof Map) {
            Map map = (Map) object;
            for (Object key : map.entrySet()) {
                if (key.toString().equalsIgnoreCase(fieldName)) {
                    map.put(key, value);
                    return null;
                }
            }
            return null;
        }
        Field f;
        try {
            f = object.getClass().getField(fieldName);
        } catch (NoSuchFieldException ex) {
            System.out.println("Object type " + object.getClass().getName() + " has no field named " + fieldName);
            Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        } catch (SecurityException ex) {
            Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        Object useValue = deserializeString(value, f.getType(), hex);
        try {
            f.set(object, useValue);
            return useValue;
        } catch (IllegalArgumentException | IllegalAccessException ex) {
            for (Method m : object.getClass().getMethods()) {
                if (m.getName().equalsIgnoreCase("set" + fieldName) && m.getParameterTypes().length == 0) {
                    try {
                        m.invoke(object, useValue);
                    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) {
                    }
                }
            }
        }
        return useValue;
    }
    static Map<Class, Map<String, Object>> enumCache = new HashMap<>();

    public static Object findClosestEnumConstant(String value, Class type) {
        Map<String, Object> enumConstants = enumCache.get(type);
        if (enumConstants == null) {
            Object[] constants = type.getEnumConstants();
            enumConstants = new HashMap<>();
            for (Object o : constants) {
                enumConstants.put(o.toString(), o);
            }
            enumCache.put(type, enumConstants);
        }

        String key = findBestMatch(value, enumConstants.keySet());
        if (key == null) {
            return null;
        }
        return enumConstants.get(key);
    }

    public static Object deserializeString(String value, Class type, boolean hex) {
        int radix = hex ? 16 : 10;
        if (type.equals(Integer.TYPE) || type == Integer.class) {
            value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", "");
            try {
                return Integer.parseInt(value, radix);
            } catch (NumberFormatException ex) {
                return null;
            }
        } else if (type.equals(Short.TYPE) || type == Short.class) {
            value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", "");
            try {
                return Short.parseShort(value, radix);
            } catch (NumberFormatException ex) {
                return null;
            }
        } else if (type.equals(Long.TYPE) || type == Long.class) {
            value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", "");
            try {
                return Long.parseLong(value, radix);
            } catch (NumberFormatException ex) {
                return null;
            }
        } else if (type.equals(Byte.TYPE) || type == Byte.class) {
            try {
                value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", "");
                return Byte.parseByte(value, radix);
            } catch (NumberFormatException ex) {
                return null;
            }
        } else if (type.equals(Boolean.TYPE) || type == Boolean.class) {
            return Boolean.valueOf(value);
        } else if (type == File.class) {
            return new File(String.valueOf(value));
        } else if (type.isEnum()) {
            value = value.replaceAll("[\\.\\s\\-]", "");
            return findClosestEnumConstant(value, type);
        }
        return null;
    }

    public static Object getProperty(Object object, String path) {
        String[] paths = path.split("\\.");
        for (String path1 : paths) {
            object = findChild(object, path1);
            if (object == null) {
                return null;
            }
        }
        return object;
    }

    public static Object setProperty(Object object, String path, String value, boolean hex) {
        String[] paths = path.split("\\.");
        for (int i = 0; i < paths.length - 1; i++) {
            object = findChild(object, paths[i]);
            if (object == null) {
                return null;
            }
        }
        return setChild(object, paths[paths.length - 1], value, hex);
    }

    static Map<InvokableAction, Runnable> allActions = null;

    public static Map<InvokableAction, Runnable> getAllInvokableActions() {
        if (allActions == null) {
            allActions = new HashMap<>();
            Configuration.BASE.getTreeAsStream().forEach((Configuration.ConfigNode node) -> {
                for (Method m : node.subject.getClass().getMethods()) {
                    if (m.isAnnotationPresent(InvokableAction.class)) {
                        allActions.put(m.getAnnotation(InvokableAction.class), () -> {
                            try {
                                m.invoke(node.subject);
                            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                                Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        });
                    }
                }
            });
        }
        return allActions;
    }

    public static Runnable getNamedInvokableAction(String action) {
        Map<InvokableAction, Runnable> actions = getAllInvokableActions();
        List<InvokableAction> actionsList = new ArrayList(actions.keySet());
        actionsList.sort((a,b) -> Integer.compare(getActionNameMatch(action, a), getActionNameMatch(action, b)));
//        for (InvokableAction a : actionsList) {
//            String actionName = a.alternatives() == null ? a.name() : (a.name() + ";" + a.alternatives());
//            System.out.println("Score for " + action + " evaluating " + a.name() + ": " + getActionNameMatch(action, a));
//        }
        return actions.get(actionsList.get(0));
    }

    private static int getActionNameMatch(String str, InvokableAction action) {
        int nameMatch = levenshteinDistance(str, action.name());
        if (action.alternatives() != null) {
            for (String alt : action.alternatives().split(";")) {
                nameMatch = Math.min(nameMatch, levenshteinDistance(str, alt));
            }
        }
        return nameMatch;
    }
}
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
import jace.apple2e.SoftSwitches;
import jace.config.ConfigurableField;
import jace.config.ConfigurationUIController;
import jace.config.InvokableAction;
import jace.config.Reconfigurable;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.Debugger;
import jace.core.RAM;
import jace.core.RAMListener;
import jace.ide.IdeController;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import static jace.core.Utility.*;

            ram.write(address + i, data[i], false, true);
        }
        CPU cpu = Emulator.computer.getCpu();
        Emulator.computer.getCpu().setProgramCounter(address);
        Emulator.computer.resume();
    }

    @InvokableAction(
            name = "Toggle Debug",
            category = "debug",
            description = "Show/hide the debug panel",
            alternatives = "Show Debug;Hide Debug;Inspect",
            defaultKeyMapping = "ctrl+shift+d")
    public static void toggleDebugPanel() {
//        AbstractEmulatorFrame frame = Emulator.getFrame();
//        if (frame == null) {
//            return;
//        }
//        frame.setShowDebug(!frame.isShowDebug());
//        frame.reconfigure();
//        Emulator.resizeVideo();
    }

    @InvokableAction(
            name = "Toggle fullscreen",
            category = "general",
            description = "Activate/deactivate fullscreen mode",
            alternatives = "fullscreen;maximize",
            defaultKeyMapping = "ctrl+shift+f")
    public static void toggleFullscreen() {
        Platform.runLater(() -> {
            Stage stage = JaceApplication.getApplication().primaryStage;
            stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
            stage.setFullScreen(!stage.isFullScreen());
            JaceApplication.getApplication().controller.setAspectRatioEnabled(stage.isFullScreen());
        });
    }

    @InvokableAction(
            name = "Save Raw Screenshot",
            category = "general",
            description = "Save raw (RAM) format of visible screen",
            alternatives = "screendump;raw screenshot",
            defaultKeyMapping = "ctrl+shift+z")
    public static void saveScreenshotRaw() throws FileNotFoundException, IOException {
        SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss");
        String timestamp = df.format(new Date());
        String type;
        int start = Emulator.computer.getVideo().getCurrentWriter().actualWriter().getYOffset(0);
        int len;
        if (start < 0x02000) {
            // Lo-res or double-lores
            len = 0x0400;
            type = "gr";
        } else {
            // Hi-res or double-hires
            len = 0x02000;
            type = "hgr";
        }
        boolean dres = SoftSwitches._80COL.getState() && (SoftSwitches.DHIRES.getState() || start < 0x02000);
        if (dres) {
            type = "d" + type;
        }
        File outFile = new File("screen_" + type + "_a" + Integer.toHexString(start) + "_" + timestamp);
        try (FileOutputStream out = new FileOutputStream(outFile)) {
            RAM128k ram = (RAM128k) Emulator.computer.memory;
            Emulator.computer.pause();
            if (dres) {
                for (int i = 0; i < len; i++) {
                    out.write(ram.getAuxVideoMemory().readByte(start + i));
                }
            }
            for (int i = 0; i < len; i++) {
                out.write(ram.getMainMemory().readByte(start + i));
            }
        }
        System.out.println("Wrote screenshot to " + outFile.getAbsolutePath());
    }

    @InvokableAction(
            name = "Save Screenshot",
            category = "general",
            description = "Save image of visible screen",
            alternatives = "Save image;save framebuffer;screenshot",
            defaultKeyMapping = "ctrl+shift+s")
    public static void saveScreenshot() throws IOException {
        FileChooser select = new FileChooser();
        Emulator.computer.pause();
        Image i = Emulator.computer.getVideo().getFrameBuffer();
//        BufferedImage bufImageARGB = SwingFXUtils.fromFXImage(i, null);
        File targetFile = select.showSaveDialog(JaceApplication.getApplication().primaryStage);
        if (targetFile == null) {
            return;
        }
        String filename = targetFile.getName();
        System.out.println("Writing screenshot to " + filename);
        String extension = filename.substring(filename.lastIndexOf(".") + 1);
//        BufferedImage bufImageRGB = new BufferedImage(bufImageARGB.getWidth(), bufImageARGB.getHeight(), BufferedImage.OPAQUE);
//
//        Graphics2D graphics = bufImageRGB.createGraphics();
//        graphics.drawImage(bufImageARGB, 0, 0, null);
//
//        ImageIO.write(bufImageRGB, extension, targetFile);
//        graphics.dispose();
    }

    public static final String CONFIGURATION_DIALOG_NAME = "Configuration";

    @InvokableAction(
            name = "Configuration",
            category = "general",
            description = "Edit emulator configuraion",
            alternatives = "Reconfigure;Preferences;Settings;Config",
            defaultKeyMapping = {"f4", "ctrl+shift+c"})
    public static void showConfig() {
        FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/Configuration.fxml"));
        fxmlLoader.setResources(null);
        try {
            Stage configWindow = new Stage();
            AnchorPane node = (AnchorPane) fxmlLoader.load();

            ConfigurationUIController controller = fxmlLoader.getController();
