public class EmulatorUILogic implements Reconfigurable {

    static Debugger debugger;

    static {
        debugger = new Debugger() {
            @Override
            public void updateStatus() {
                enableDebug(true);
                MOS65C02 cpu = (MOS65C02) Emulator.computer.getCpu();
                updateCPURegisters(cpu);
            }
        };
    }

    @ConfigurableField(
            category = "General",
            name = "Speed Setting"
    )
    public int speedSetting = 3;

    @ConfigurableField(
            category = "General",
            name = "Show Drives"
    )
    public boolean showDrives = true;

    public static void updateCPURegisters(MOS65C02 cpu) {
//        DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();
//        debuggerPanel.valueA.setText(Integer.toHexString(cpu.A));
//        debuggerPanel.valueX.setText(Integer.toHexString(cpu.X));
//        debuggerPanel.valueY.setText(Integer.toHexString(cpu.Y));
//        debuggerPanel.valuePC.setText(Integer.toHexString(cpu.getProgramCounter()));
//        debuggerPanel.valueSP.setText(Integer.toHexString(cpu.getSTACK()));
//        debuggerPanel.valuePC2.setText(cpu.getFlags());
//        debuggerPanel.valueINST.setText(cpu.disassemble());
    }

    public static void enableDebug(boolean b) {
//        DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel();
//        debugger.setActive(b);
//        debuggerPanel.enableDebug.setSelected(b);
//        debuggerPanel.setBackground(
//                b ? Color.RED : new Color(0, 0, 0x040));
    }

    public static void enableTrace(boolean b) {
        Emulator.computer.getCpu().setTraceEnabled(b);
    }

    public static void stepForward() {
        debugger.step = true;
    }

    static void registerDebugger() {
        Emulator.computer.getCpu().setDebug(debugger);
    }

    public static Integer getValidAddress(String s) {
        try {
            int addr = Integer.parseInt(s.toUpperCase(), 16);
            if (addr >= 0 && addr < 0x10000) {
                return addr;
            }
            return null;
        } catch (NumberFormatException ex) {
            return null;
        }
    }
    public static List<RAMListener> watches = new ArrayList<>();

//    public static void updateWatchList(final DebuggerPanel panel) {
//        java.awt.EventQueue.invokeLater(() -> {
//            watches.stream().forEach((oldWatch) -> {
//                Emulator.computer.getMemory().removeListener(oldWatch);
//            });
//            if (panel == null) {
//                return;
//            }
//            addWatch(panel.textW1, panel.valueW1);
//            addWatch(panel.textW2, panel.valueW2);
//            addWatch(panel.textW3, panel.valueW3);
//            addWatch(panel.textW4, panel.valueW4);
//        });
//    }
//
//    private static void addWatch(JTextField watch, final JLabel watchValue) {
//        final Integer address = getValidAddress(watch.getText());
//        if (address != null) {
//            //System.out.println("Adding watch for "+Integer.toString(address, 16));
//            RAMListener newListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) {
//                @Override
//                protected void doConfig() {
//                    setScopeStart(address);
//                }
//
//                @Override
//                protected void doEvent(RAMEvent e) {
//                    watchValue.setText(Integer.toHexString(e.getNewValue() & 0x0FF));
//                }
//            };
//            Emulator.computer.getMemory().addListener(newListener);
//            watches.add(newListener);
//            // Print out the current value right away
//            byte b = Emulator.computer.getMemory().readRaw(address);
//            watchValue.setText(Integer.toString(b & 0x0ff, 16));
//        } else {
//            watchValue.setText("00");
//        }
//    }
//    public static void updateBreakpointList(final DebuggerPanel panel) {
//        java.awt.EventQueue.invokeLater(() -> {
//            Integer address;
//            debugger.getBreakpoints().clear();
//            if (panel == null) {
//                return;
//            }
//            address = getValidAddress(panel.textBP1.getText());
//            if (address != null) {
//                debugger.getBreakpoints().add(address);
//            }
//            address = getValidAddress(panel.textBP2.getText());
//            if (address != null) {
//                debugger.getBreakpoints().add(address);
//            }
//            address = getValidAddress(panel.textBP3.getText());
//            if (address != null) {
//                debugger.getBreakpoints().add(address);
//            }
//            address = getValidAddress(panel.textBP4.getText());
//            if (address != null) {
//                debugger.getBreakpoints().add(address);
//            }
//            debugger.updateBreakpoints();
//        });
//    }
//
    @InvokableAction(
            name = "BRUN file",
            category = "file",
            description = "Loads a binary file in memory and executes it. File should end with #06xxxx, where xxxx is the start address in hex",
            alternatives = "Execute program;Load binary;Load program;Load rom;Play single-load game",
            defaultKeyMapping = "ctrl+shift+b")
    public static void runFile() {
        Emulator.computer.pause();
        FileChooser select = new FileChooser();
        File binary = select.showOpenDialog(JaceApplication.getApplication().primaryStage);
        if (binary == null) {
            Emulator.computer.resume();
            return;
        }
        runFileNamed(binary);
    }

    public static void runFileNamed(File binary) {
        String fileName = binary.getName().toLowerCase();
        try {
            if (fileName.contains("#06")) {
                String addressStr = fileName.substring(fileName.length() - 4);
                int address = Integer.parseInt(addressStr, 16);
                brun(binary, address);
            } else if (fileName.contains("#fc")) {
                gripe("BASIC not supported yet");
            }
        } catch (NumberFormatException | IOException ex) {
        }
        Emulator.computer.getCpu().resume();
    }

    public static void brun(File binary, int address) throws FileNotFoundException, IOException {
        // If it was halted already, then it was initiated outside of an opcode execution
        // If it was not yet halted, then it is the case that the CPU is processing another opcode
        // So if that is the case, the program counter will need to be decremented here to compensate
        // TODO: Find a better mousetrap for this one -- it's an ugly hack
        Emulator.computer.pause();
        FileInputStream in = new FileInputStream(binary);
        byte[] data = new byte[in.available()];
        in.read(data);
        RAM ram = Emulator.computer.getMemory();
        for (int i = 0; i < data.length; i++) {
            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();
            controller.initialize();
            Scene s = new Scene(node);
            configWindow.setScene(s);
            configWindow.show();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    @InvokableAction(
            name = "Open IDE",
            category = "development",
            description = "Open new IDE window for Basic/Assembly/Plasma coding",
            alternatives = "IDE;dev;development;acme;assembler;editor",
            defaultKeyMapping = {"ctrl+shift+i"})
    public static void showIDE() {
        FXMLLoader fxmlLoader = new FXMLLoader(EmulatorUILogic.class.getResource("/fxml/editor.fxml"));
        fxmlLoader.setResources(null);
        try {
            Stage editorWindow = new Stage();
            AnchorPane node = (AnchorPane) fxmlLoader.load();
            IdeController controller = fxmlLoader.getController();
            controller.initialize();
            Scene s = new Scene(node);
            editorWindow.setScene(s);
            editorWindow.show();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    static int size = -1;

    @InvokableAction(
            name = "Resize window",
            category = "general",
            description = "Resize the screen to 1x/1.5x/2x/3x video size",
            alternatives = "Aspect;Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;",
            defaultKeyMapping = {"ctrl+shift+a"})
    public static void scaleIntegerRatio() {
        Platform.runLater(() -> {
            if (JaceApplication.getApplication() == null
                    || JaceApplication.getApplication().primaryStage == null) {
                return;
            }
            Stage stage = JaceApplication.getApplication().primaryStage;
            size++;
            if (size > 3) {
                size = 0;
            }
            if (stage.isFullScreen()) {
                JaceApplication.getApplication().controller.toggleAspectRatio();
            } else {
                int width = 0, height = 0;
                switch (size) {
                    case 0: // 1x
                        width = 560;
                        height = 384;
                        break;
                    case 1: // 1.5x
                        width = 840;
                        height = 576;
                        break;
                    case 2: // 2x
                        width = 560 * 2;
                        height = 384 * 2;
                        break;
                    case 3: // 3x (retina) 2880x1800
                        width = 560 * 3;
                        height = 384 * 3;
                        break;
                    default: // 2x
                        width = 560 * 2;
                        height = 384 * 2;
                }
                double vgap = stage.getScene().getY();
                double hgap = stage.getScene().getX();
                stage.setWidth(hgap * 2 + width);
                stage.setHeight(vgap + height);
            }
        });
    }

    @InvokableAction(
            name = "About",
            category = "general",
            description = "Display about window",
            alternatives = "info;credits",
            defaultKeyMapping = {"ctrl+shift+."})
    public static void showAboutWindow() {
        //TODO: Implement
    }

    public static boolean confirm(String message) {
//        return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message);
        return false;
    }

    static final Map<Object, Set<Label>> INDICATORS = new HashMap<>();

    static public void addIndicator(Object owner, Label icon) {
        addIndicator(owner, icon, 250);
    }

    static public void addIndicator(Object owner, Label icon, long TTL) {
        if (JaceApplication.getApplication() == null) {
            return;
        }
        synchronized (INDICATORS) {
            Set<Label> ind = INDICATORS.get(owner);
            if (ind == null) {
                ind = new HashSet<>();
                INDICATORS.put(owner, ind);
            }
            ind.add(icon);
            JaceApplication.getApplication().controller.addIndicator(icon);
        }
    }

    static public void removeIndicator(Object owner, Label icon) {
        if (JaceApplication.singleton == null) {
            return;
        }
        synchronized (INDICATORS) {
            Set<Label> ind = INDICATORS.get(owner);
            if (ind != null) {
                ind.remove(icon);
            }
            JaceApplication.singleton.controller.removeIndicator(icon);
        }
    }

    static public void removeIndicators(Object owner) {
        if (JaceApplication.singleton == null) {
            return;
        }
        synchronized (INDICATORS) {
            Set<Label> ind = INDICATORS.get(owner);
            if (ind == null) {
                return;
            }
            ind.stream().forEach((icon) -> {
                JaceApplication.singleton.controller.removeIndicator(icon);
            });
            INDICATORS.remove(owner);
        }
    }

    static public void addMouseListener(EventHandler<MouseEvent> handler) {
        if (JaceApplication.singleton != null) {
            JaceApplication.singleton.controller.addMouseListener(handler);
        }
    }

    static public void removeMouseListener(EventHandler<MouseEvent> handler) {
        if (JaceApplication.singleton != null) {
            JaceApplication.singleton.controller.removeMouseListener(handler);
        }
    }

    public static void simulateCtrlAppleReset() {
        Computer computer = JaceApplication.singleton.controller.computer;
        computer.keyboard.openApple(true);
        computer.warmStart();
        Platform.runLater(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException ex) {
                Logger.getLogger(EmulatorUILogic.class.getName()).log(Level.SEVERE, null, ex);
            }
            computer.keyboard.openApple(false);
        });
    }

    public static void notify(String message) {
        if (JaceApplication.singleton != null) {
            JaceApplication.singleton.controller.displayNotification(message);
        }
    }

    @Override
    public String getName() {
        return "Jace User Interface";
    }

    @Override
    public String getShortName() {
        return "UI";
    }

    @Override
    public void reconfigure() {
        if (JaceApplication.getApplication() != null) {
            JaceApplication.getApplication().controller.setSpeed(speedSetting);
        }
    }
}
 @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:
        }
    }
}
 public abstract class Card extends Device {

    private final PagedMemory cxRom;
    private final PagedMemory c8Rom;
    private int slot;
    private RAMListener ioListener;
    private RAMListener firmwareListener;
    private RAMListener c8firmwareListener;

    /**
     * Creates a new instance of Card
     *
     * @param computer
     */
    public Card(Computer computer) {
        super(computer);
        cxRom = new PagedMemory(0x0100, PagedMemory.Type.CARD_FIRMWARE, computer);
        c8Rom = new PagedMemory(0x0800, PagedMemory.Type.CARD_FIRMWARE, computer);
    }

    @Override
    public String getShortName() {
        return "s" + getSlot();
    }

    @Override
    public String getName() {
        return getDeviceName() + " (slot " + slot + ")";
    }

    abstract public void reset();

    @Override
    public void attach() {
        registerListeners();
    }

    @Override
    public void detach() {
        suspend();
        unregisterListeners();
        super.detach();
    }

    abstract protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);

    abstract protected void handleFirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);

    abstract protected void handleC8FirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e);

    public int getSlot() {
        return slot;
    }

    public void setSlot(int slot) {
        this.slot = slot;
    }

    public PagedMemory getCxRom() {
        return cxRom;
    }

    public PagedMemory getC8Rom() {
        return c8Rom;
    }

    @Override
    public void reconfigure() {
        boolean restart = suspend();
        unregisterListeners();
        if (restart) {
            resume();
        }
        registerListeners();
    }

    public void notifyVBLStateChanged(boolean state) {
        // Do nothing unless overridden
    }

    public boolean suspendWithCPU() {
        return false;
    }

    protected void registerListeners() {
        RAM memory = computer.getMemory();
        int baseIO = 0x0c080 + slot * 16;
        int baseRom = 0x0c000 + slot * 256;
        ioListener = memory.observe(RAMEvent.TYPE.ANY, baseIO, baseIO + 15, (e) -> {
            int address = e.getAddress() & 0x0f;
            handleIOAccess(address, e.getType(), e.getNewValue(), e);
        });

        firmwareListener = memory.observe(RAMEvent.TYPE.ANY, baseRom, baseRom + 255, (e) -> {
            computer.getMemory().setActiveCard(slot);
            // Sather 6-4: Writes will still go through even when CXROM inhibits slot ROM
            if (SoftSwitches.CXROM.isOff() || !e.getType().isRead()) {
                handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e);
            }
        });

        c8firmwareListener = memory.observe(RAMEvent.TYPE.ANY, 0xc800, 0xcfff, (e) -> {
            if (SoftSwitches.CXROM.isOff() && SoftSwitches.INTC8ROM.isOff()
                    && computer.getMemory().getActiveSlot() == slot) {
                handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e);
            }
        });
    }

    protected void unregisterListeners() {
        computer.getMemory().removeListener(ioListener);
        computer.getMemory().removeListener(firmwareListener);
        computer.getMemory().removeListener(c8firmwareListener);
    }
}
 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 class RAMEvent {

    static public interface RAMEventHandler {
        public void handleEvent(RAMEvent e);
    }

    public enum TYPE {

        READ(true),
        READ_DATA(true),
        EXECUTE(true),
        READ_OPERAND(true),
        WRITE(false),
        ANY(false);
        boolean read = false;

        TYPE(boolean r) {
            this.read = r;
        }

        public boolean isRead() {
            return read;
        }
    };

    public enum SCOPE {

        ADDRESS,
        RANGE,
        ANY
    };

    public enum VALUE {

        ANY,
        RANGE,
        EQUALS,
        NOT_EQUALS,
        CHANGE_BY
    };
    private TYPE type;
    private SCOPE scope;
    private VALUE value;
    private int address, oldValue, newValue;

    /**
     * Creates a new instance of RAMEvent
     * @param t
     * @param s
     * @param v
     * @param address
     * @param oldValue
     * @param newValue
     */
    public RAMEvent(TYPE t, SCOPE s, VALUE v, int address, int oldValue, int newValue) {
        setType(t);
        setScope(s);
        setValue(v);
        this.setAddress(address);
        this.setOldValue(oldValue);
        this.setNewValue(newValue);
    }

    public TYPE getType() {
        return type;
    }

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

    public SCOPE getScope() {
        return scope;
    }

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

    public VALUE getValue() {
        return value;
    }

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

    public int getAddress() {
        return address;
    }

    public final void setAddress(int address) {
        this.address = address;
    }

    public int getOldValue() {
        return oldValue;
    }

    public final void setOldValue(int oldValue) {
        this.oldValue = oldValue;
    }

    public int getNewValue() {
        return newValue;
    }

    public final void setNewValue(int newValue) {
        this.newValue = newValue;
    }
}
 public enum TYPE {

    READ(true),
    READ_DATA(true),
    EXECUTE(true),
    READ_OPERAND(true),
    WRITE(false),
    ANY(false);
    boolean read = false;

    TYPE(boolean r) {
        this.read = r;
    }

    public boolean isRead() {
        return read;
    }
};
 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;
    }
} public abstract class ProdosDriver {
    Computer computer;

    public ProdosDriver(Computer computer) {
        this.computer = computer;
    }
    
    public static int MLI_COMMAND = 0x042;
    public static int MLI_UNITNUMBER = 0x043;
    public static int MLI_BUFFER_ADDRESS = 0x044;
    public static int MLI_BLOCK_NUMBER = 0x046;
    
    public static enum MLI_RETURN {
        NO_ERROR(0), IO_ERROR(0x027), NO_DEVICE(0x028), WRITE_PROTECTED(0x02B);
        public int intValue;

        MLI_RETURN(int val) {
            intValue = val;
        }
    }

    public static enum MLI_COMMAND_TYPE {
        STATUS(0x0), READ(0x01), WRITE(0x02), FORMAT(0x03);
        public int intValue;

        MLI_COMMAND_TYPE(int val) {
            intValue = val;
        }

        public static MLI_COMMAND_TYPE fromInt(int value) {
            for (MLI_COMMAND_TYPE c : values()) {
                if (c.intValue == value) {
                    return c;
                }
            }
            return null;
        }
    }

    abstract public boolean changeUnit(int unitNumber);
    abstract public int getSize();
    abstract public boolean isWriteProtected();
    abstract public void mliFormat() throws IOException;
    abstract public void mliRead(int block, int bufferAddress) throws IOException;
    abstract public void mliWrite(int block, int bufferAddress) throws IOException;
    abstract public Card getOwner();
    
    public void handleMLI() {
        int returnCode = prodosMLI().intValue;
        MOS65C02 cpu = (MOS65C02) computer.getCpu();
        cpu.A = returnCode;
        // Clear carry flag if no error, otherwise set carry flag
        cpu.C = (returnCode == 0x00) ? 00 : 01;
    }
    
    private MLI_RETURN prodosMLI() {
        try {
            RAM memory = computer.getMemory();
            int cmd = memory.readRaw(MLI_COMMAND);
            MLI_COMMAND_TYPE command = MLI_COMMAND_TYPE.fromInt(cmd);
            int unit = (memory.readWordRaw(MLI_UNITNUMBER) & 0x080) > 0 ? 1 : 0;
            if (changeUnit(unit) == false) {
                return MLI_RETURN.NO_DEVICE;
            }
            int block = memory.readWordRaw(MLI_BLOCK_NUMBER);
            int bufferAddress = memory.readWordRaw(MLI_BUFFER_ADDRESS);
//            System.out.println(getOwner().getName()+" MLI Call "+command+", unit "+unit+" Block "+block+" --> "+Integer.toHexString(bufferAddress));
            if (command == null) {
                System.out.println(getOwner().getName()+" Mass storage given bogus command (" + Integer.toHexString(cmd) + "), returning I/O error");
                return MLI_RETURN.IO_ERROR;
            }
            switch (command) {
                case STATUS:
                    int blocks = getSize();
                    MOS65C02 cpu = (MOS65C02) computer.getCpu();
                    cpu.X = blocks & 0x0ff;
                    cpu.Y = (blocks >> 8) & 0x0ff;
                    if (isWriteProtected()) {
                        return MLI_RETURN.WRITE_PROTECTED;
                    }
                    break;
                case FORMAT:
                    mliFormat();
                case READ:
                    mliRead(block, bufferAddress);
                    break;
                case WRITE:
                    mliWrite(block, bufferAddress);
                    break;
                default:
                    System.out.println(getOwner().getName()+" MLI given bogus command (" + Integer.toHexString(cmd) + " = " + command.name() + "), returning I/O error");
                    return MLI_RETURN.IO_ERROR;
            }
            return MLI_RETURN.NO_ERROR;
        } catch (UnsupportedOperationException ex) {
            return MLI_RETURN.WRITE_PROTECTED;
        } catch (IOException ex) {
            System.out.println(getOwner().getName()+" Encountered IO Error, returning error: " + ex.getMessage());
            return MLI_RETURN.IO_ERROR;
        }
    }
} public abstract class SmartportDriver {
    Computer computer;

    public SmartportDriver(Computer computer) {
        this.computer = computer;
    }
    
    public static enum ERROR_CODE {
        NO_ERROR(0), INVALID_COMMAND(0x01), BAD_PARAM_COUNT(0x04), INVALID_UNIT(0x011), INVALID_CODE(0x021), BAD_BLOCK_NUMBER(0x02d);
        int intValue;
        ERROR_CODE(int c) {
            intValue = c;
        }
    }
    
    public void handleSmartport() {
        int returnCode = callSmartport().intValue;
        MOS65C02 cpu = (MOS65C02) computer.getCpu();
        cpu.A = returnCode;
        // Clear carry flag if no error, otherwise set carry flag
        cpu.C = (returnCode == 0x00) ? 00 : 01;
    }

    private ERROR_CODE callSmartport() {
        MOS65C02 cpu = (MOS65C02) computer.getCpu();
        RAM ram = computer.getMemory();
        int callAddress = cpu.popWord() + 1;
        int command = ram.readRaw(callAddress);
        boolean extendedCall = command >= 0x040;
//            command &= 0x0f;
        // Modify stack so that RTS goes to the right place after the smartport device call
        //cpu.pushWord(callAddress + (extendedCall ? 5 : 3));
        // Kludge due to the CPU not getting the faked RTS opcode
        cpu.setProgramCounter(callAddress + (extendedCall ? 5 : 3));

        // Calculate parameter address block
        int parmAddr;
        if (!extendedCall) {
            parmAddr = ram.readWordRaw(callAddress + 1);
        } else {
            // Extended calls -- not gonna happen on this platform anyway
            int parmAddrLo = ram.readWordRaw(callAddress + 1);
            int parmAddrHi = ram.readWordRaw(callAddress + 3);
            parmAddr = parmAddrHi << 16 | parmAddrLo;
        }
        // Now process command
        System.out.println("Received command " + command + " with address block " + Integer.toHexString(parmAddr));
        byte numParms = ram.readRaw(parmAddr);
        int[] params = new int[16];
        for (int i = 0; i < 16; i++) {
            int value = 0x0ff & ram.readRaw(parmAddr + i);
            params[i] = value;
            System.out.print(Integer.toHexString(value) + " ");
        }
        System.out.println();
        int unitNumber = params[1];
        if (!changeUnit(unitNumber)) {
            System.out.println("Invalid unit: "+unitNumber);
            return ERROR_CODE.INVALID_UNIT;
        }
        int dataBuffer = params[2] | (params[3] << 8);
        
        try {
            switch (command) {
                case 0: //Status
                    return returnStatus(dataBuffer, params);
                case 1: //Read Block
                    int blockNum = params[4] | (params[5] << 8) | (params[6] << 16);
                    read(blockNum, dataBuffer);
                    return ERROR_CODE.NO_ERROR;
                    //                  System.out.println("reading "+blockNum+" to $"+Integer.toHexString(dataBuffer));
                case 2: //Write Block
                    blockNum = params[4] | (params[5] << 8) | (params[6] << 16);
                    write(blockNum, dataBuffer);
                    return ERROR_CODE.NO_ERROR;
                case 3: //Format
                case 4: //Control
                case 5: //Init
                case 6: //Open
                case 7: //Close
                case 8: //Read
                case 9: //Write
                default:
                    System.out.println("Unimplemented command "+command);
                    return ERROR_CODE.INVALID_COMMAND;
            }
        } catch (IOException ex) {
            Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
            return ERROR_CODE.INVALID_CODE;
        }
    }

    abstract public boolean changeUnit(int unitNumber);
    abstract public void read(int blockNum, int buffer) throws IOException;
    abstract public void write(int blockNum, int buffer) throws IOException;
    abstract public ERROR_CODE returnStatus(int dataBuffer, int[] params);
} public class MediaCache implements Serializable {

    public static int DELAY_BEFORE_PERSISTING_LIBRARY = 2000;
    public static MediaCache LOCAL_LIBRARY;

    public static MediaEntry getMediaFromFile(File draggedFile) {
        MediaEntry entry = new MediaEntry();
        MediaFile file = new MediaFile();
        file.path = draggedFile;
        file.temporary = false;
        file.activeVersion = true;
        entry.files = new ArrayList<>();
        entry.files.add(file);
        entry.isLocal = true;
        entry.type = DiskType.determineType(draggedFile);
        return entry;
    }

    public static MediaEntry getMediaFromUrl(String url) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    public Set<Long> favorites;
    public Map<String, Set<Long>> nameLookup;
    public Map<String, Set<Long>> categoryLookup;
    public Map<String, Set<Long>> keywordLookup;
    public Map<Long, MediaEntry> mediaLookup;
    public long lastDirtyMarker;

    public MediaCache() {
        favorites = new HashSet<>();
        nameLookup = new HashMap<>();
        categoryLookup = new HashMap<>();
        keywordLookup = new HashMap<>();
        mediaLookup = new HashMap<>();
    }

    public static MediaCache getLocalLibrary() {
        if (LOCAL_LIBRARY == null) {
            LOCAL_LIBRARY = new MediaCache();
            LOCAL_LIBRARY.readLibraryFromDisk();
        }
        return LOCAL_LIBRARY;

    }

    private void cleanup() {
        cleanup(nameLookup);
        cleanup(categoryLookup);
        cleanup(keywordLookup);
    }

    private void cleanup(Map<String, Set<Long>> lookup) {
        Set<String> remove = new HashSet<>();
        lookup.entrySet().stream().forEach((entry) -> {
            if (entry.getValue() == null || entry.getValue().isEmpty()) {
                remove.add(entry.getKey());
            } else {
                boolean hasSomething = false;
                for (Iterator<Long> l = entry.getValue().iterator(); l.hasNext();) {
                    if (mediaLookup.containsKey(l.next())) {
                        hasSomething = true;
                    } else {
                        l.remove();
                    }
                }
                if (!hasSomething) {
                    remove.add(entry.getKey());
                }
            }
        });
        lookup.keySet().removeAll(remove);
    }

    public void add(MediaEntry e) {
        e.isLocal = this.equals(LOCAL_LIBRARY);
        // Randomize ID if it is not already set
        // This is a combination of the string hash as well as a 8-bit sequence number
        if (e.id == 0 || e.id == -1) {
            e.id = e.name.hashCode();
            e.id <<= 8;
            while (mediaLookup.containsKey(e.id)) {
                e.id++;
            }
        }
        mediaLookup.put(e.id, e);
        cacheEntry(nameLookup, e.name, e.id);
        cacheEntry(categoryLookup, e.category, e.id);
        if (e.favorite) {
            favorites.add(e.id);
        }
        for (String s : e.keywords) {
            cacheEntry(keywordLookup, s, e.id);
        }
        markDirty();
    }

    public void remove(MediaEntry e) {
        mediaLookup.remove(e.id);
        removeFiles(e);
        cleanup();
        markDirty();
    }

    public void update(MediaEntry e) {
        remove(e);
        add(e);
    }

    private void cacheEntry(Map<String, Set<Long>> cache, String key, long id) {
        Set<Long> ids = cache.get(key);
        if (ids == null) {
            ids = new HashSet<>();
            cache.put(key, ids);
        }
        ids.add(id);
    }

    public static File getMediaLibraryFolder() {
        String userHome = System.getProperty("user.home");
        if (userHome == null || userHome.equals("")) {
            userHome = ".";
        }
        File f = new File(new File(userHome, ".jace"), "mediaLibrary");
        if (!f.exists()) {
            f.mkdirs();
        }
        return f;
    }

    // Remove file(s) associated with media entry
    private void removeFiles(MediaEntry e) {
        if (e.files == null) {
            return;
        }
        e.files.stream().forEach((f) -> {
            f.path.delete();
        });
        Utility.gripe("All disk images for " + e.name + " have been deleted.");
    }

    public MediaEntry.MediaFile getCurrentFile(MediaEntry e, boolean isPermanent) {
        if (e == null) {
            return null;
        }
        if (e.files == null || e.files.isEmpty()) {
            e.files = new ArrayList<>();
            getLocalLibrary().add(e);
            getLocalLibrary().createBlankFile(e, "Initial", !isPermanent);
//            getLocalLibrary().downloadImage(e, e.files.get(0), true);
        }
        for (MediaEntry.MediaFile f : e.files) {
            if (f.activeVersion) {
                return f;
            }
        }
        e.files.get(0).activeVersion = true;
        return e.files.get(0);
    }

    public void saveFile(MediaEntry e, InputStream data) {
//        saveFile(getCurrentFile(e, MediaLibrary.CREATE_LOCAL_ON_SAVE), data);
    }

    public void saveFile(MediaFile f, InputStream data) {
        // TODO: If file is temporary but is supposed to be created local when saved, then move the save to a permanent file!!
//        if (f.temporary && MediaLibrary.CREATE_LOCAL_ON_SAVE) {
//            f = convertTemporaryFileToLocal(f);
//        }
        FileOutputStream fos = null;
        f.lastWritten = System.currentTimeMillis();
        try {
            fos = new FileOutputStream(f.path, false);
            byte[] b = new byte[4096];
            while (data.available() > 0) {
                int read = data.read(b);
                fos.write(b, 0, read);
            }
            fos.close();
        } catch (FileNotFoundException ex) {
            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
            Utility.gripe("Could not write disk for " + f.path + " -- File not found!");
        } catch (IOException ex) {
            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
            Utility.gripe("Could not write disk for " + f.path + " -- I/O Exception: " + ex.getMessage());
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    boolean disableWrites = false;

    public void readLibraryFromDisk() {
        disableWrites = true;
        ObjectInputStream in = null;
        try {
            File mediaCatalogFile = getMediaLibaryCatalog();
            if (!mediaCatalogFile.exists()) {
                return;
            }
            FileInputStream fileStream = new FileInputStream(mediaCatalogFile);
            in = new ObjectInputStream(fileStream);
            while (fileStream.available() > 0) {
                MediaEntry e = (MediaEntry) in.readObject();
                add(e);
            }
        } catch (IOException | ClassNotFoundException ex) {
            Utility.gripe(ex.getMessage());
            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            disableWrites = false;
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    public void writeLibraryToDisk() {
        ObjectOutputStream out = null;
        try {
            File mediaCatalogFile = getMediaLibaryCatalog();
            out = new ObjectOutputStream(new FileOutputStream(mediaCatalogFile));
            for (MediaEntry e : mediaLookup.values()) {
                out.writeObject(e);
            }
            out.close();
        } catch (FileNotFoundException ex) {
            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    Thread writerWorker;

    public void markDirty() {
        // We don't really care if a remote library is changing
        if (disableWrites || !this.equals(getLocalLibrary())) {
            return;
        }
        lastDirtyMarker = System.nanoTime();
        if (writerWorker == null || !writerWorker.isAlive()) {
            writerWorker = new Thread(new Runnable() {
                long timeCheck = 0;

                @Override
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2);
                            // Wait half the delay before capturing the value
                            // this will help avoid unnecessary delays when the
                            // library is updated a bunch in a small interval of time
                            timeCheck = lastDirtyMarker;
                            Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2);
                        } catch (InterruptedException ex) {
                            Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
                            continue;
                        }
                        // If the invalidation time stamp changed again wait longer
                        if (timeCheck != lastDirtyMarker) {
                            continue;
                        }
                        writeLibraryToDisk();
                        // If the invalidation stamp wasn't changed then the worker can exit
                        if (timeCheck == lastDirtyMarker) {
                            break;
                        }
                    }
                }
            });
            writerWorker.start();
        }
    }

    private MediaFile downloadTempCopy(MediaEntry e) {
//        downloadImage(e, getCurrentFile(e, false), isDownloading);
        return getCurrentFile(e, false);
    }
    static boolean isDownloading = false;
//
//    public void downloadImage(final MediaEntry e, final MediaFile target, boolean wait) {
//        isDownloading = true;
//        Utility.runModalProcess("Loading disk image...", () -> {
//            InputStream in = null;
//            try {
//                URI uri = null;
//                try {
//                    uri = new URI(e.source);
//                } catch (URISyntaxException ex) {
//                    File f = new File(e.source);
//                    if (f.exists()) {
//                        uri = f.toURI();
//                    }
//                }
//                if (uri == null) {
//                    Utility.gripe("Unable to resolve path: " + e.source);
//                    return;
//                }
//                in = uri.toURL().openStream();
//                saveFile(target, in);
//            } catch (MalformedURLException ex) {
//                Utility.gripe("Unable to resolve path: " + e.source);
//                Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
//            } catch (IOException ex) {
//                Utility.gripe("Unable to download file: " + ex.getMessage());
//                Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
//            } finally {
//                isDownloading = false;
//                try {
//                    if (in != null) {
//                        in.close();
//                    }
//                } catch (IOException ex) {
//                    Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex);
//                }
//            }
//        });
//        if (wait) {
//            int timeout = 10000;
//            while (timeout > 0 && isDownloading) {
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException ex) {
//                    return;
//                }
//                timeout -= 100;
//            }
//        }
//    }

    private void createBlankFile(MediaEntry e, String label, boolean isTemporary) {
        MediaFile f = new MediaEntry.MediaFile();
        e.files.add(f);
        f.activeVersion = true;
        f.checksum = 0L;
        f.label = label;
        f.lastWritten = System.currentTimeMillis();
        f.temporary = isTemporary;
        // Now generate new file path
        File mediaFolder = isTemporary ? getTempDirectory() : getMediaLibraryFolder();
        String name = e.name.replaceAll("[^0-9A-Za-z]", "");
        String s1 = e.name.length() > 0 ? name.substring(0, 1) : "_";
        String s2 = e.name.length() > 1 ? name.substring(1, 2) : "_";
        File sub1 = new File(mediaFolder, "_" + s1);
        File sub2 = new File(sub1, "_" + s2);
        sub2.mkdirs();
        f.path = new File(sub2, name + "_" + System.nanoTime());
        if (isTemporary) {
            sub1.deleteOnExit();
            sub2.deleteOnExit();
            f.path.deleteOnExit();
        }
    }

    private MediaFile resolveLocalCopy(MediaEntry e) {
        if (!e.isLocal) {
            e = findLocalEntry(e);
        }
        // If this is a local entry, load the current file
        if (e.isLocal) {
            MediaFile f = getCurrentFile(e, true);
            if (f != null && f.path.exists()) {
                return f;
            }
        }
        // If there is no current file, download it
//        if (MediaLibrary.CREATE_LOCAL_ON_LOAD) {
//            getLocalLibrary().add(e);
//            MediaFile f = getCurrentFile(e, true);
//            downloadImage(e, f, true);
//            return f;
//        } else {
            return downloadTempCopy(e);
//        }
        
    }

    public MediaEntry findLocalEntry(MediaEntry e) {
        for (MediaEntry entry : getLocalLibrary().mediaLookup.values()) {
            if (entry.source.equals(e.source))
                return entry;
        }
        return null;
    }

    private File getTempDirectory() {
        String temp = System.getProperty("java.io.tmpdir");
        File tempDir = null;
        if (temp != null) {
            tempDir = new File(temp);
            if (tempDir.exists() && tempDir.isDirectory()) {
                return tempDir;
            }
            tempDir = new File(getMediaLibraryFolder(), "temp");
            tempDir.mkdirs();
            tempDir.deleteOnExit();
        }
        return tempDir;
    }

    private MediaFile convertTemporaryFileToLocal(MediaFile f) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    private File getMediaLibaryCatalog() {
        return new File(getMediaLibraryFolder(), "mediacache.db");
    }
} public interface MediaConsumer {
    public Optional<Label> getIcon();
    public void setIcon(Optional<Label> i);
    public void insertMedia(MediaEntry e, MediaFile f) throws IOException;
    public MediaEntry getMediaEntry();
    public MediaFile getMediaFile();
    public boolean isAccepted(MediaEntry e, MediaFile f);
    public void eject();
} public interface MediaConsumerParent {
    public MediaConsumer[] getConsumers();
} public class MediaEntry implements Serializable {

    @Override
    public String toString() {
        return (name == null || name.length() == 0) ? "No name" : name;
    }
    
    public long id;
    public boolean isLocal;
    public String source;
    public String name;
    public String[] keywords = new String[0];
    public String category;
    public String description;
    public String year;
    public String author;
    public String publisher;
    public String screenshotURL;
    public String boxFrontURL;
    public String boxBackURL;
    public boolean favorite;
    public DiskType type;
    public String auxtype;
    public boolean writeProtected;
    public List<MediaFile> files;

    public static class MediaFile implements Serializable {
        public long checksum;
        public File path;
        public boolean activeVersion;
        public String label;
        public long lastRead;
        public long lastWritten;
        volatile public boolean temporary = false;
    }    
}
import jace.EmulatorUILogic;
import jace.apple2e.MOS65C02;
import jace.config.ConfigurableField;
import jace.config.Name;
import jace.core.Card;
import jace.core.Computer;
import jace.core.RAMEvent;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.hardware.ProdosDriver;
import jace.hardware.SmartportDriver;
import jace.library.MediaCache;
import jace.library.MediaConsumer;
import jace.library.MediaConsumerParent;
import jace.library.MediaEntry;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/*
 * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package jace.hardware.massStorage;


/**
 * Hard disk and 800k floppy (smartport) controller card. HDV and 2MG images are
 * both supported.
 *
 * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
 */
@Name("Mass Storage Device")
public class CardMassStorage extends Card implements MediaConsumerParent {

    @ConfigurableField(category = "Disk", defaultValue = "", shortName = "d1", name = "Drive 1 disk image", description = "Path of disk 1")
    public String disk1;
    @ConfigurableField(category = "Disk", defaultValue = "", shortName = "d2", name = "Drive 2 disk image", description = "Path of disk 2")
    public String disk2;
    MassStorageDrive drive1;
    MassStorageDrive drive2;

    public CardMassStorage(Computer computer) {
        super(computer);
        drive1 = new MassStorageDrive();
        drive2 = new MassStorageDrive();
        drive1.setIcon(Utility.loadIconLabel("drive-harddisk.png"));
        drive2.setIcon(Utility.loadIconLabel("drive-harddisk.png"));
        drive1.onInsert(this::reconfigure);
        currentDrive = drive1;
    }

    @Override
    public void setSlot(int slot) {
        super.setSlot(slot);
        drive1.getIcon().ifPresent(icon -> icon.setText("S" + getSlot() + "D1"));
        drive2.getIcon().ifPresent(icon -> icon.setText("S" + getSlot() + "D2"));
    }

    @Override
    public String getDeviceName() {
        return "Mass Storage Device";
    }
    // boot0 stores cards*16 of boot device here
    static int SLT16 = 0x02B;
    // "rom" offset where device driver is called by MLI
//    static int DEVICE_DRIVER_OFFSET = 0x042;
    static int DEVICE_DRIVER_OFFSET = 0x0A;
    byte[] cardSignature = new byte[]{
        (byte) 0x0a9 /*NOP*/, 0x020, (byte) 0x0a9, 0x00,
        (byte) 0x0a9, 0x03 /*currentDisk cards*/, (byte) 0x0a9, 0x03c /*currentDisk cards*/,
        (byte) 0xd0, 0x07, 0x60, (byte) 0x0b0,
        0x01 /*firmware cards*/, 0x18, (byte) 0x0b0, 0x5a
    };
    Card theCard = this;
    public MassStorageDrive currentDrive;

    public IDisk getCurrentDisk() {
        if (currentDrive != null) {
            return currentDrive.getCurrentDisk();
        }
        return null;
    }
    ProdosDriver driver = new ProdosDriver(computer) {
        @Override
        public boolean changeUnit(int unit) {
            currentDrive = unit == 0 ? drive1 : drive2;
            return getCurrentDisk() != null;
        }

        @Override
        public int getSize() {
            return getCurrentDisk() != null ? getCurrentDisk().getSize() : 0;
        }

        @Override
        public boolean isWriteProtected() {
            return getCurrentDisk() != null ? getCurrentDisk().isWriteProtected() : true;
        }

        @Override
        public void mliFormat() throws IOException {
            getCurrentDisk().mliFormat();
        }

        @Override
        public void mliRead(int block, int bufferAddress) throws IOException {
            getCurrentDisk().mliRead(block, bufferAddress, computer.getMemory());
        }

        @Override
        public void mliWrite(int block, int bufferAddress) throws IOException {
            getCurrentDisk().mliWrite(block, bufferAddress, computer.getMemory());
        }

        @Override
        public Card getOwner() {
            return theCard;
        }
    };

    @Override
    public void reconfigure() {
        unregisterListeners();
        if (disk1 != null && !disk1.isEmpty()) {
            try {
                MediaEntry entry = MediaCache.getMediaFromFile(new File(disk1));
                disk1 = null;
                drive1.insertMedia(entry, entry.files.get(0));
            } catch (IOException ex) {
                Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        if (disk2 != null && !disk2.isEmpty()) {
            try {
                MediaEntry entry = MediaCache.getMediaFromFile(new File(disk2));
                disk2 = null;
                drive2.insertMedia(entry, entry.files.get(0));
            } catch (IOException ex) {
                Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        if (computer.getCpu() != null) {
            int pc = computer.getCpu().getProgramCounter();
            if (drive1.getCurrentDisk() != null && getSlot() == 7 && (pc >= 0x0c65e && pc <= 0x0c66F)) {
                // If the computer is in a loop trying to boot from cards 6, fast-boot from here instead
                // This is a convenience to boot a hard-drive if the emulator has started waiting for a currentDisk
                currentDrive = drive1;
                EmulatorUILogic.simulateCtrlAppleReset();
            }
        }
        registerListeners();
    }

    @Override
    public void reset() {
    }

    @Override
    protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) {
        // There is no c8 rom for this card
    }

    @Override
    protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) {
        MOS65C02 cpu = (MOS65C02) computer.getCpu();