public interface AddressSpace {

    boolean accepts(int address);

    void setByte(int address, int value);

    int getByte(int address);

} public class Gameboy implements Runnable {

    public static final int TICKS_PER_SEC = 4_194_304;

    private final InterruptManager interruptManager;

    private final Gpu gpu;

    private final Mmu mmu;

    private final Cpu cpu;

    private final Timer timer;

    private final Dma dma;

    private final Hdma hdma;

    private final Display display;

    private final Sound sound;

    private final SerialPort serialPort;

    private final boolean gbc;

    private final SpeedMode speedMode;

    private final Optional<Console> console;

    private volatile boolean doStop;

    private final List<Runnable> tickListeners = new ArrayList<>();

    public Gameboy(GameboyOptions options, Cartridge rom, Display display, Controller controller, SoundOutput soundOutput, SerialEndpoint serialEndpoint) {
        this(options, rom, display, controller, soundOutput, serialEndpoint, Optional.empty());
    }

    public Gameboy(GameboyOptions options, Cartridge rom, Display display, Controller controller, SoundOutput soundOutput, SerialEndpoint serialEndpoint, Optional<Console> console) {
        this.display = display;
        gbc = rom.isGbc();
        speedMode = new SpeedMode();
        interruptManager = new InterruptManager(gbc);
        timer = new Timer(interruptManager, speedMode);
        mmu = new Mmu();

        Ram oamRam = new Ram(0xfe00, 0x00a0);
        dma = new Dma(mmu, oamRam, speedMode);
        gpu = new Gpu(display, interruptManager, dma, oamRam, gbc);
        hdma = new Hdma(mmu);
        sound = new Sound(soundOutput, gbc);
        serialPort = new SerialPort(interruptManager, serialEndpoint, speedMode);
        mmu.addAddressSpace(rom);
        mmu.addAddressSpace(gpu);
        mmu.addAddressSpace(new Joypad(interruptManager, controller));
        mmu.addAddressSpace(interruptManager);
        mmu.addAddressSpace(serialPort);
        mmu.addAddressSpace(timer);
        mmu.addAddressSpace(dma);
        mmu.addAddressSpace(sound);

        mmu.addAddressSpace(new Ram(0xc000, 0x1000));
        if (gbc) {
            mmu.addAddressSpace(speedMode);
            mmu.addAddressSpace(hdma);
            mmu.addAddressSpace(new GbcRam());
            mmu.addAddressSpace(new UndocumentedGbcRegisters());
        } else {
            mmu.addAddressSpace(new Ram(0xd000, 0x1000));
        }
        mmu.addAddressSpace(new Ram(0xff80, 0x7f));
        mmu.addAddressSpace(new ShadowAddressSpace(mmu, 0xe000, 0xc000, 0x1e00));

        cpu = new Cpu(mmu, interruptManager, gpu, display, speedMode);

        interruptManager.disableInterrupts(false);
        if (!options.isUsingBootstrap()) {
            initRegs();
        }

        this.console = console;
    }

    private void initRegs() {
        Registers r = cpu.getRegisters();

        r.setAF(0x01b0);
        if (gbc) {
            r.setA(0x11);
        }
        r.setBC(0x0013);
        r.setDE(0x00d8);
        r.setHL(0x014d);
        r.setSP(0xfffe);
        r.setPC(0x0100);
    }

    public void run() {
        boolean requestedScreenRefresh = false;
        boolean lcdDisabled = false;
        doStop = false;
        while (!doStop) {
            Gpu.Mode newMode = tick();
            if (newMode != null) {
                hdma.onGpuUpdate(newMode);
            }

            if (!lcdDisabled && !gpu.isLcdEnabled()) {
                lcdDisabled = true;
                display.requestRefresh();
                hdma.onLcdSwitch(false);
            } else if (newMode == Gpu.Mode.VBlank) {
                requestedScreenRefresh = true;
                display.requestRefresh();
            }

            if (lcdDisabled && gpu.isLcdEnabled()) {
                lcdDisabled = false;
                display.waitForRefresh();
                hdma.onLcdSwitch(true);
            } else if (requestedScreenRefresh && newMode == Gpu.Mode.OamSearch) {
                requestedScreenRefresh = false;
                display.waitForRefresh();
            }
            console.ifPresent(Console::tick);
            tickListeners.forEach(Runnable::run);
        }
    }

    public void stop() {
        doStop = true;
    }

    public Gpu.Mode tick() {
        timer.tick();
        if (hdma.isTransferInProgress()) {
            hdma.tick();
        } else {
            cpu.tick();
        }
        dma.tick();
        sound.tick();
        serialPort.tick();
        return gpu.tick();
    }

    public AddressSpace getAddressSpace() {
        return mmu;
    }

    public Cpu getCpu() {
        return cpu;
    }

    public SpeedMode getSpeedMode() {
        return speedMode;
    }

    public Gpu getGpu() {
        return gpu;
    }

    public void registerTickListener(Runnable tickListener) {
        tickListeners.add(tickListener);
    }

    public void unregisterTickListener(Runnable tickListener) {
        tickListeners.remove(tickListener);
    }

    public Sound getSound() {
        return sound;
    }
} public interface Command {

    CommandPattern getPattern();

    void run(ParsedCommandLine commandLine);
} public class CommandPattern {

    private final List<String> commandNames;

    private final List<CommandArgument> arguments;

    private final Optional<String> description;

    private CommandPattern(Builder builder) {
        this.commandNames = builder.commandNames;
        this.arguments = builder.arguments;
        this.description = Optional.ofNullable(builder.description);
    }

    public boolean matches(String commandLine) {
        return commandNames
                .stream()
                .filter(commandLine::startsWith)
                .map(String::length)
                .map(commandLine::substring)
                .anyMatch(s -> s.isEmpty() || s.charAt(0) == ' ');
    }

    public List<String> getCommandNames() {
        return commandNames;
    }

    public List<CommandArgument> getArguments() {
        return arguments;
    }

    public Optional<String> getDescription() {
        return description;
    }

    public ParsedCommandLine parse(String commandLine) {
        String commandName = commandNames
                .stream()
                .filter(commandLine::startsWith)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(
                        "Command line [" + commandLine + "] doesn't match command [" + commandNames + "]"));
        List<String> split = split(commandLine.substring(commandName.length()));
        Map<String, String> map = newLinkedHashMap();
        List<String> remaining = Collections.emptyList();
        int i;
        for (i = 0; i < split.size() && i < arguments.size(); i++) {
            String value = split.get(i);
            CommandArgument argDef = arguments.get(i);
            Optional<Set<String>> allowed = argDef.getAllowedValues();
            if (allowed.isPresent()) {
                if (!allowed.get().contains(value)) {
                    throw new IllegalArgumentException("Value " + value + " is not allowed for argument " + argDef.getName() + ". Allowed values: " + allowed.get());
                }
            }
            map.put(argDef.getName(), value);
        }
        if (i < arguments.size()) {
            CommandArgument argDef = arguments.get(i);
            if (argDef.isRequired()) {
                throw new IllegalArgumentException("Argument " + argDef.getName() + " is required");
            }
        }
        if (i < split.size()) {
            remaining = split.subList(i, split.size());
        }
        return new ParsedCommandLine(map, remaining);
    }

    private static List<String> split(String str) {
        List<String> split = new ArrayList<>();
        boolean isEscaped = false;
        StringBuilder currentArg = new StringBuilder();
        for (int i = 0; i <= str.length(); i++) {
            char c;
            if (i < str.length()) {
                c = str.charAt(i);
            } else {
                c = 0;
            }
            if (isEscaped) {
                switch (c) {
                    case '"':
                        isEscaped = false;
                        split.add(currentArg.toString());
                        currentArg.setLength(0);
                        break;

                    case 0:
                        throw new IllegalArgumentException("Missing closing quote");

                    default:
                        currentArg.append(c);
                        break;
                }
            } else {
                switch (c) {
                    case '"':
                        isEscaped = false;
                        break;

                    case ' ':
                    case 0:
                        if (currentArg.length() > 0) {
                            split.add(currentArg.toString());
                            currentArg.setLength(0);
                        }
                        break;

                    default:
                        currentArg.append(c);
                        break;
                }
            }
        }
        return split;
    }

    @Override
    public String toString() {
        return String.format("CommandPattern[%s]", commandNames.toString());
    }

    public static class ParsedCommandLine {

        private Map<String, String> argumentMap;

        private List<String> remainingArguments;

        private ParsedCommandLine(Map<String, String> argumentMap, List<String> remainingArguments) {
            this.argumentMap = argumentMap;
            this.remainingArguments = remainingArguments;
        }

        public String getArgument(String name) {
            return argumentMap.get(name);
        }

        public List<String> getRemainingArguments() {
            return remainingArguments;
        }
    }

    public static class Builder {

        private final List<String> commandNames;

        private final List<CommandArgument> arguments;

        private String description;

        private Builder(String[] commandNames) {
            this.commandNames = ImmutableList.copyOf(commandNames);
            this.arguments = new ArrayList<>();
        }

        public static Builder create(String longName) {
            return new Builder(new String[] {longName});
        }

        public static Builder create(String longName, String shortName) {
            return new Builder(new String[] {longName, shortName});
        }

        public Builder withOptionalArgument(String name) {
            assertNoOptionalLastArgument();
            arguments.add(new CommandArgument(name, false));
            return this;
        }

        public Builder withRequiredArgument(String name) {
            assertNoOptionalLastArgument();
            arguments.add(new CommandArgument(name, true));
            return this;
        }

        public Builder withOptionalValue(String name, String... values) {
            assertNoOptionalLastArgument();
            arguments.add(new CommandArgument(name, false, copyOf(values)));
            return this;
        }

        public Builder withRequiredValue(String name, String... values) {
            assertNoOptionalLastArgument();
            arguments.add(new CommandArgument(name, true, copyOf(values)));
            return this;
        }

        public Builder withDescription(String description) {
            this.description = description;
            return this;
        }

        private void assertNoOptionalLastArgument() {
            if (!arguments.isEmpty() && !arguments.get(arguments.size() - 1).isRequired()) {
                throw new UnsupportedOperationException("Can't add argument after the optional one");
            }
        }

        public CommandPattern build() {
            return new CommandPattern(this);
        }
    }

} public static class ParsedCommandLine {

    private Map<String, String> argumentMap;

    private List<String> remainingArguments;

    private ParsedCommandLine(Map<String, String> argumentMap, List<String> remainingArguments) {
        this.argumentMap = argumentMap;
        this.remainingArguments = remainingArguments;
    }

    public String getArgument(String name) {
        return argumentMap.get(name);
    }

    public List<String> getRemainingArguments() {
        return remainingArguments;
    }
} public class Gpu implements AddressSpace {

    public enum Mode {
        HBlank, VBlank, OamSearch, PixelTransfer
    }

    private final AddressSpace videoRam0;

    private final AddressSpace videoRam1;

    private final AddressSpace oamRam;

    private final Display display;

    private final InterruptManager interruptManager;

    private final Dma dma;

    private final Lcdc lcdc;

    private final boolean gbc;

    private final ColorPalette bgPalette;

    private final ColorPalette oamPalette;

    private final HBlankPhase hBlankPhase;

    private final OamSearch oamSearchPhase;

    private final PixelTransfer pixelTransferPhase;

    private final VBlankPhase vBlankPhase;

    private boolean lcdEnabled = true;

    private int lcdEnabledDelay;

    private MemoryRegisters r;

    private int ticksInLine;

    private Mode mode;

    private GpuPhase phase;

    public Gpu(Display display, InterruptManager interruptManager, Dma dma, Ram oamRam, boolean gbc) {
        this.r = new MemoryRegisters(GpuRegister.values());
        this.lcdc = new Lcdc();
        this.interruptManager = interruptManager;
        this.gbc = gbc;
        this.videoRam0 = new Ram(0x8000, 0x2000);
        if (gbc) {
            this.videoRam1 = new Ram(0x8000, 0x2000);
        } else {
            this.videoRam1 = null;
        }
        this.oamRam = oamRam;
        this.dma = dma;

        this.bgPalette = new ColorPalette(0xff68);
        this.oamPalette = new ColorPalette(0xff6a);
        oamPalette.fillWithFF();

        this.oamSearchPhase = new OamSearch(oamRam, lcdc, r);
        this.pixelTransferPhase = new PixelTransfer(videoRam0, videoRam1, oamRam, display, lcdc, r, gbc, bgPalette, oamPalette);
        this.hBlankPhase = new HBlankPhase();
        this.vBlankPhase = new VBlankPhase();

        this.mode = Mode.OamSearch;
        this.phase = oamSearchPhase.start();

        this.display = display;
    }

    private AddressSpace getAddressSpace(int address) {
        if (videoRam0.accepts(address)/* && mode != Mode.PixelTransfer*/) {
            return getVideoRam();
        } else if (oamRam.accepts(address) && !dma.isOamBlocked()/* && mode != Mode.OamSearch && mode != Mode.PixelTransfer*/) {
            return oamRam;
        } else if (lcdc.accepts(address)) {
            return lcdc;
        } else if (r.accepts(address)) {
            return r;
        } else if (gbc && bgPalette.accepts(address)) {
            return bgPalette;
        } else if (gbc && oamPalette.accepts(address)) {
            return oamPalette;
        } else {
            return null;
        }
    }

    private AddressSpace getVideoRam() {
        if (gbc && (r.get(VBK) & 1) == 1) {
            return videoRam1;
        } else {
            return videoRam0;
        }
    }

    public AddressSpace getVideoRam0() {
        return videoRam0;
    }

    public AddressSpace getVideoRam1() {
        return videoRam1;
    }

    @Override
    public boolean accepts(int address) {
        return getAddressSpace(address) != null;
    }

    @Override
    public void setByte(int address, int value) {
        if (address == STAT.getAddress()) {
            setStat(value);
        } else {
            AddressSpace space = getAddressSpace(address);
            if (space == lcdc) {
                setLcdc(value);
            } else if (space != null) {
                space.setByte(address, value);
            }
        }
    }

    @Override
    public int getByte(int address) {
        if (address == STAT.getAddress()) {
            return getStat();
        } else {
            AddressSpace space = getAddressSpace(address);
            if (space == null) {
                return 0xff;
            } else if (address == VBK.getAddress()) {
                return gbc ? 0xfe : 0xff;
            } else {
                return space.getByte(address);
            }
        }
    }

    public Mode tick() {
        if (!lcdEnabled) {
            if (lcdEnabledDelay != -1) {
                if (--lcdEnabledDelay == 0) {
                    display.enableLcd();
                    lcdEnabled = true;
                }
            }
        }
        if (!lcdEnabled) {
            return null;
        }

        Mode oldMode = mode;
        ticksInLine++;
        if (phase.tick()) {
            // switch line 153 to 0
            if (ticksInLine == 4 && mode == Mode.VBlank && r.get(LY) == 153) {
                r.put(LY, 0);
                requestLycEqualsLyInterrupt();
            }
        } else {
            switch (oldMode) {
                case OamSearch:
                    mode = Mode.PixelTransfer;
                    phase = pixelTransferPhase.start(oamSearchPhase.getSprites());
                    break;

                case PixelTransfer:
                    mode = Mode.HBlank;
                    phase = hBlankPhase.start(ticksInLine);
                    requestLcdcInterrupt(3);
                    break;

                case HBlank:
                    ticksInLine = 0;
                    if (r.preIncrement(LY) == 144) {
                        mode = Mode.VBlank;
                        phase = vBlankPhase.start();
                        interruptManager.requestInterrupt(InterruptType.VBlank);
                        requestLcdcInterrupt(4);
                    } else {
                        mode = Mode.OamSearch;
                        phase = oamSearchPhase.start();
                    }
                    requestLcdcInterrupt(5);
                    requestLycEqualsLyInterrupt();
                    break;

                case VBlank:
                    ticksInLine = 0;
                    if (r.preIncrement(LY) == 1) {
                        mode = Mode.OamSearch;
                        r.put(LY, 0);
                        phase = oamSearchPhase.start();
                        requestLcdcInterrupt(5);
                    } else {
                        phase = vBlankPhase.start();
                    }
                    requestLycEqualsLyInterrupt();
                    break;
            }
        }
        if (oldMode == mode) {
            return null;
        } else {
            return mode;
        }
    }

    public int getTicksInLine() {
        return ticksInLine;
    }

    private void requestLcdcInterrupt(int statBit) {
        if ((r.get(STAT) & (1 << statBit)) != 0) {
            interruptManager.requestInterrupt(InterruptType.LCDC);
        }
    }

    private void requestLycEqualsLyInterrupt() {
        if (r.get(LYC) == r.get(LY)) {
            requestLcdcInterrupt(6);
        }
    }

    private int getStat() {
        return r.get(STAT) | mode.ordinal() | (r.get(LYC) == r.get(LY) ? (1 << 2) : 0) | 0x80;
    }

    private void setStat(int value) {
        r.put(STAT, value & 0b11111000); // last three bits are read-only
    }

    private void setLcdc(int value) {
        lcdc.set(value);
        if ((value & (1 << 7)) == 0) {
            disableLcd();
        } else {
            enableLcd();
        }
    }

    private void disableLcd() {
        r.put(LY, 0);
        this.ticksInLine = 0;
        this.phase = hBlankPhase.start(250);
        this.mode = Mode.HBlank;
        this.lcdEnabled = false;
        this.lcdEnabledDelay = -1;
        display.disableLcd();
    }

    private void enableLcd() {
        lcdEnabledDelay = 244;
    }

    public boolean isLcdEnabled() {
        return lcdEnabled;
    }

    public Lcdc getLcdc() {
        return lcdc;
    }

    public MemoryRegisters getRegisters() {
        return r;
    }

    public boolean isGbc() {
        return gbc;
    }

    public ColorPalette getBgPalette() {
        return bgPalette;
    }

    public Mode getMode() {
        return mode;
    }
} public enum GpuRegister implements MemoryRegisters.Register {

    STAT(0xff41, RW),
    SCY(0xff42, RW),
    SCX(0xff43, RW),
    LY(0xff44, R),
    LYC(0xff45, RW),
    BGP(0xff47, RW),
    OBP0(0xff48, RW),
    OBP1(0xff49, RW),
    WY(0xff4a, RW),
    WX(0xff4b, RW),
    VBK(0xff4f, W);

    private final int address;

    private final MemoryRegisters.RegisterType type;

    GpuRegister(int address, MemoryRegisters.RegisterType type) {
        this.address = address;
        this.type = type;
    }

    @Override
    public int getAddress() {
        return address;
    }

    @Override
    public MemoryRegisters.RegisterType getType() {
        return type;
    }

} public class Lcdc implements AddressSpace {

    private int value = 0x91;

    public boolean isBgAndWindowDisplay() {
        return (value & 0x01) != 0;
    }

    public boolean isObjDisplay() {
        return (value & 0x02) != 0;
    }

    public int getSpriteHeight() {
        return (value & 0x04) == 0 ? 8 : 16;
    }

    public int getBgTileMapDisplay() {
        return (value & 0x08) == 0 ? 0x9800 : 0x9c00;
    }

    public int getBgWindowTileData() {
        return (value & 0x10) == 0 ? 0x9000 : 0x8000;
    }

    public boolean isBgWindowTileDataSigned() {
        return (value & 0x10) == 0;
    }

    public boolean isWindowDisplay() {
        return (value & 0x20) != 0;
    }

    public int getWindowTileMapDisplay() {
        return (value & 0x40) == 0 ? 0x9800 : 0x9c00;
    }

    public boolean isLcdEnabled() {
        return (value & 0x80) != 0;
    }

    @Override
    public boolean accepts(int address) {
        return address == 0xff40;
    }

    @Override
    public void setByte(int address, int value) {
        checkArgument(address == 0xff40);
        this.value = value;
    }

    @Override
    public int getByte(int address) {
        checkArgument(address == 0xff40);
        return value;
    }

    public void set(int value) {
        this.value = value;
    }

    public int get() {
        return value;
    }
} public class TileAttributes {

    public static final TileAttributes EMPTY;

    private static final TileAttributes[] ATTRIBUTES;

    static {
        ATTRIBUTES = new TileAttributes[256];
        for (int i = 0; i < 256; i++) {
            ATTRIBUTES[i] = new TileAttributes(i);
        }
        EMPTY = ATTRIBUTES[0];
    }

    private final int value;

    private TileAttributes(int value) {
        this.value = value;
    }

    public static TileAttributes valueOf(int value) {
        return ATTRIBUTES[value];
    }

    public boolean isPriority() {
        return (value & (1 << 7)) != 0;
    }

    public boolean isYflip() {
        return (value & (1 << 6)) != 0;
    }

    public boolean isXflip() {
        return (value & (1 << 5)) != 0;
    }

    public GpuRegister getDmgPalette() {
        return (value & (1 << 4)) == 0 ? GpuRegister.OBP0 : GpuRegister.OBP1;
    }

    public int getBank() {
        return (value & (1 << 3)) == 0 ? 0 : 1;
    }

    public int getColorPaletteIndex() {
        return value & 0x07;
    }
} public class SwingDisplay extends JPanel implements Display, Runnable {

    public static final int DISPLAY_WIDTH = 160;

    public static final int DISPLAY_HEIGHT = 144;

    private final BufferedImage img;

    public static final int[] COLORS = new int[]{0xe6f8da, 0x99c886, 0x437969, 0x051f2a};

    private final int[] rgb;

    private boolean enabled;

    private int scale;

    private boolean doStop;

    private boolean doRefresh;

    private int i;

    public SwingDisplay(int scale) {
        super();
        GraphicsConfiguration gfxConfig = GraphicsEnvironment.
                getLocalGraphicsEnvironment().getDefaultScreenDevice().
                getDefaultConfiguration();
        img = gfxConfig.createCompatibleImage(DISPLAY_WIDTH, DISPLAY_HEIGHT);
        rgb = new int[DISPLAY_WIDTH * DISPLAY_HEIGHT];
        this.scale = scale;
    }

    @Override
    public void putDmgPixel(int color) {
        rgb[i++] = COLORS[color];
        i = i % rgb.length;
    }

    @Override
    public void putColorPixel(int gbcRgb) {
        rgb[i++] = translateGbcRgb(gbcRgb);
    }

    public static int translateGbcRgb(int gbcRgb) {
        int r = (gbcRgb >> 0) & 0x1f;
        int g = (gbcRgb >> 5) & 0x1f;
        int b = (gbcRgb >> 10) & 0x1f;
        int result = (r * 8) << 16;
        result |= (g * 8) << 8;
        result |= (b * 8) << 0;
        return result;
    }

    @Override
    public synchronized void requestRefresh() {
        doRefresh = true;
        notifyAll();
    }

    @Override
    public synchronized void waitForRefresh() {
        while (doRefresh) {
            try {
                wait(1);
            } catch (InterruptedException e) {
                break;
            }
        }
    }

    @Override
    public void enableLcd() {
        enabled = true;
    }

    @Override
    public void disableLcd() {
        enabled = false;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g.create();
        if (enabled) {
            g2d.drawImage(img, 0, 0, DISPLAY_WIDTH * scale, DISPLAY_HEIGHT * scale, null);
        } else {
            g2d.setColor(new Color(COLORS[0]));
            g2d.drawRect(0, 0, DISPLAY_WIDTH * scale, DISPLAY_HEIGHT * scale);
        }
        g2d.dispose();
    }

    @Override
    public void run() {
        doStop = false;
        doRefresh = false;
        enabled = true;
        while (!doStop) {
            synchronized (this) {
                try {
                    wait(1);
                } catch (InterruptedException e) {
                    break;
                }
            }

            if (doRefresh) {
                img.setRGB(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, rgb, 0, DISPLAY_WIDTH);
                validate();
                repaint();

                synchronized (this) {
                    i = 0;
                    doRefresh = false;
                    notifyAll();
                }
            }
        }
    }

    public void stop() {
        doStop = true;
    }
} public class MemoryRegisters implements AddressSpace {

    public interface Register {
        int getAddress();

        RegisterType getType();
    }

    public enum RegisterType {
        R(true, false), W(false, true), RW(true, true);

        private final boolean allowsRead;

        private final boolean allowsWrite;

        RegisterType(boolean allowsRead, boolean allowsWrite) {
            this.allowsRead = allowsRead;
            this.allowsWrite = allowsWrite;
        }
    }

    private Map<Integer, Register> registers;

    private Map<Integer, Integer> values = new HashMap<>();

    public MemoryRegisters(Register... registers) {
        Map<Integer, Register> map = new HashMap<>();
        for (Register r : registers) {
            if (map.containsKey(r.getAddress())) {
                throw new IllegalArgumentException("Two registers with the same address: " + r.getAddress());
            }
            map.put(r.getAddress(), r);
            values.put(r.getAddress(), 0);
        }
        this.registers = Collections.unmodifiableMap(map);
    }

    private MemoryRegisters(MemoryRegisters original) {
        this.registers = original.registers;
        this.values = Collections.unmodifiableMap(new HashMap<>(original.values));
    }

    public int get(Register reg) {
        if (registers.containsKey(reg.getAddress())) {
            return values.get(reg.getAddress());
        } else {
            throw new IllegalArgumentException("Not valid register: " + reg);
        }
    }

    public void put(Register reg, int value) {
        if (registers.containsKey(reg.getAddress())) {
            values.put(reg.getAddress(), value);
        } else {
            throw new IllegalArgumentException("Not valid register: " + reg);
        }
    }

    public MemoryRegisters freeze() {
        return new MemoryRegisters(this);
    }

    public int preIncrement(Register reg) {
        if (registers.containsKey(reg.getAddress())) {
            int value = values.get(reg.getAddress()) + 1;
            values.put(reg.getAddress(), value);
            return value;
        } else {
            throw new IllegalArgumentException("Not valid register: " + reg);
        }
    }

    @Override
    public boolean accepts(int address) {
        return registers.containsKey(address);
    }

    @Override
    public void setByte(int address, int value) {
        if (registers.get(address).getType().allowsWrite) {
            values.put(address, value);
        }
    }

    @Override
    public int getByte(int address) {
        if (registers.get(address).getType().allowsRead) {
            return values.get(address);
        } else {
            return 0xff;
        }
    }
} public static int toSigned(int byteValue) {
    if ((byteValue & (1 << 7)) == 0) {
        return byteValue;
    } else {
        return byteValue - 0x100;
    }
} public static int[] zip(int data1, int data2, boolean reverse, int[] pixelLine) {
    for (int i = 7; i >= 0; i--) {
        int mask = (1 << i);
        int p = 2 * ((data2 & mask) == 0 ? 0 : 1) + ((data1 & mask) == 0 ? 0 : 1);
        if (reverse) {
            pixelLine[i] = p;
        } else {
            pixelLine[7 - i] = p;
        }
    }
    return pixelLine;
}
import eu.rekawek.coffeegb.AddressSpace;
import eu.rekawek.coffeegb.Gameboy;
import eu.rekawek.coffeegb.debug.Command;
import eu.rekawek.coffeegb.debug.CommandPattern;
import eu.rekawek.coffeegb.debug.CommandPattern.ParsedCommandLine;
import eu.rekawek.coffeegb.gpu.Gpu;
import eu.rekawek.coffeegb.gpu.GpuRegister;
import eu.rekawek.coffeegb.gpu.Lcdc;
import eu.rekawek.coffeegb.gpu.TileAttributes;
import eu.rekawek.coffeegb.gui.SwingDisplay;
import eu.rekawek.coffeegb.memory.MemoryRegisters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import static eu.rekawek.coffeegb.cpu.BitUtils.toSigned;
import static eu.rekawek.coffeegb.gpu.Fetcher.zip;
package eu.rekawek.coffeegb.debug.command.ppu;




public class ShowBackground implements Command {

    private static final Logger LOG = LoggerFactory.getLogger(ShowBackground.class);

    public enum Type {
        WINDOW, BACKGROUND;
    }

    private static final CommandPattern PATTERN_BACKGROUND = CommandPattern.Builder
            .create("ppu show background")
            .withDescription("display the background tiles")
            .build();

    private static final CommandPattern PATTERN_WINDOW = CommandPattern.Builder
            .create("ppu show window")
            .withDescription("display the window tiles")
            .build();


    private final Gameboy gameboy;

    private volatile boolean windowPresent;

    private final Type type;

    public ShowBackground(Gameboy gameboy, Type type) {
        this.gameboy = gameboy;
        this.type = type;
    }

    @Override
    public CommandPattern getPattern() {
        return type == Type.WINDOW ? PATTERN_WINDOW : PATTERN_BACKGROUND;
    }

    @Override
    public void run(ParsedCommandLine commandLine) {
        if (windowPresent) {
            System.out.println("Window already present");
            return;
        }

        SwingUtilities.invokeLater(() -> {
            BackgroundTiles panel = new BackgroundTiles(gameboy.getGpu());
            panel.setPreferredSize(new Dimension(272 * 2, 272 * 2));

            new Thread(panel).start();

            Runnable panelTick = panel::tick;

            JFrame mainWindow = new JFrame(type == Type.BACKGROUND ? "Background tiles" : "Window tiles");
            mainWindow.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            mainWindow.setLocationRelativeTo(null);
            mainWindow.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosed(WindowEvent e) {
                    super.windowClosed(e);
                    gameboy.unregisterTickListener(panelTick);
                    panel.doStop = true;
                    synchronized (panel) {
                        panel.notify();
                    }
                    windowPresent = false;
                }
            });

            mainWindow.setContentPane(panel);
            mainWindow.setResizable(false);
            mainWindow.setVisible(true);
            mainWindow.pack();

            gameboy.registerTickListener(panelTick);

            windowPresent = true;
        });
    }

    public class BackgroundTiles extends JPanel implements Runnable {

        private boolean doRefresh;

        private boolean doStop;

        private final BufferedImage img;

        private final GraphicsConfiguration gfxConfig;

        private final Gpu gpu;

        private Gpu.Mode lastMode;

        public BackgroundTiles(Gpu gpu) {
            super();
            gfxConfig = GraphicsEnvironment.
                    getLocalGraphicsEnvironment().getDefaultScreenDevice().
                    getDefaultConfiguration();
            img = gfxConfig.createCompatibleImage(272 * 2, 272 * 2);
            this.gpu = gpu;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.drawImage(img, 0, 0, 272 * 2, 272 * 2, null);
            g2d.dispose();
        }

        private void drawBackground() {
            Graphics2D g2d = img.createGraphics();
            g2d.clearRect(0, 0, 272 * 2, 272 * 2);

            Lcdc lcdc = gpu.getLcdc();
            AddressSpace videoRam0 = gpu.getVideoRam0();
            AddressSpace videoRam1 = gpu.getVideoRam1();
            MemoryRegisters reg = gpu.getRegisters();