public interface AddressSpace {

    boolean accepts(int address);

    void setByte(int address, int value);

    int getByte(int address);

} public class InterruptManager implements AddressSpace {

    public enum InterruptType {
        VBlank(0x0040), LCDC(0x0048), Timer(0x0050), Serial(0x0058), P10_13(0x0060);

        private final int handler;

        InterruptType(int handler) {
            this.handler = handler;
        }

        public int getHandler() {
            return handler;
        }
    }

    private final boolean gbc;

    private boolean ime;

    private int interruptFlag = 0xe1;

    private int interruptEnabled;

    private int pendingEnableInterrupts = -1;

    private int pendingDisableInterrupts = -1;

    public InterruptManager(boolean gbc) {
        this.gbc = gbc;
    }

    public void enableInterrupts(boolean withDelay) {
        pendingDisableInterrupts = -1;
        if (withDelay) {
            if (pendingEnableInterrupts == -1) {
                pendingEnableInterrupts = 1;
            }
        } else {
            pendingEnableInterrupts = -1;
            ime = true;
        }
    }

    public void disableInterrupts(boolean withDelay) {
        pendingEnableInterrupts = -1;
        if (withDelay && gbc) {
            if (pendingDisableInterrupts == -1) {
                pendingDisableInterrupts = 1;
            }
        } else {
            pendingDisableInterrupts = -1;
            ime = false;
        }
    }

    public void requestInterrupt(InterruptType type) {
        interruptFlag = interruptFlag | (1 << type.ordinal());
    }

    public void clearInterrupt(InterruptType type) {
        interruptFlag = interruptFlag & ~(1 << type.ordinal());
    }

    public void onInstructionFinished() {
        if (pendingEnableInterrupts != -1) {
            if (pendingEnableInterrupts-- == 0) {
                enableInterrupts(false);
            }
        }
        if (pendingDisableInterrupts != -1) {
            if (pendingDisableInterrupts-- == 0) {
                disableInterrupts(false);
            }
        }
    }

    public boolean isIme() {
        return ime;
    }

    public boolean isInterruptRequested() {
        return (interruptFlag & interruptEnabled) != 0;
    }

    public boolean isHaltBug() {
        return (interruptFlag & interruptEnabled & 0x1f) != 0 && !ime;
    }

    @Override
    public boolean accepts(int address) {
        return address == 0xff0f || address == 0xffff;
    }

    @Override
    public void setByte(int address, int value) {
        switch (address) {
            case 0xff0f:
                interruptFlag = value | 0xe0;
                break;

            case 0xffff:
                interruptEnabled = value;
                break;
        }
    }

    @Override
    public int getByte(int address) {
        switch (address) {
            case 0xff0f:
                return interruptFlag;

            case 0xffff:
                return  interruptEnabled;

            default:
                return 0xff;
        }
    }
} public enum InterruptType {
    VBlank(0x0040), LCDC(0x0048), Timer(0x0050), Serial(0x0058), P10_13(0x0060);

    private final int handler;

    InterruptType(int handler) {
        this.handler = handler;
    }

    public int getHandler() {
        return handler;
    }
} public class SpeedMode implements AddressSpace {

    private boolean currentSpeed;

    private boolean prepareSpeedSwitch;

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

    @Override
    public void setByte(int address, int value) {
        prepareSpeedSwitch = (value & 0x01) != 0;
    }

    @Override
    public int getByte(int address) {
        return (currentSpeed ? (1 << 7) : 0) | (prepareSpeedSwitch ? (1 << 0) : 0) | 0b01111110;
    }

    boolean onStop() {
        if (prepareSpeedSwitch) {
            currentSpeed = !currentSpeed;
            prepareSpeedSwitch = false;
            return true;
        } else {
            return false;
        }
    }

    public int getSpeedMode() {
        return currentSpeed ? 2 : 1;
    }
} public interface GpuPhase {

    boolean tick();

} public class HBlankPhase implements GpuPhase {

    private int ticks;

    public HBlankPhase start(int ticksInLine) {
        this.ticks = ticksInLine;
        return this;
    }

    @Override
    public boolean tick() {
        ticks++;
        return ticks < 456;
    }

} public class OamSearch implements GpuPhase {

    private enum State {
        READING_Y, READING_X;
    }

    public static class SpritePosition {

        private final int x;

        private final int y;

        private final int address;

        public SpritePosition(int x, int y, int address) {
            this.x = x;
            this.y = y;
            this.address = address;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public int getAddress() {
            return address;
        }
    }

    private final AddressSpace oemRam;

    private final MemoryRegisters registers;

    private final SpritePosition[] sprites;

    private final Lcdc lcdc;

    private int spritePosIndex;

    private State state;

    private int spriteY;

    private int spriteX;

    private int i;

    public OamSearch(AddressSpace oemRam, Lcdc lcdc, MemoryRegisters registers) {
        this.oemRam = oemRam;
        this.registers = registers;
        this.lcdc = lcdc;
        this.sprites = new SpritePosition[10];
    }

    public OamSearch start() {
        spritePosIndex = 0;
        state = State.READING_Y;
        spriteY = 0;
        spriteX = 0;
        i = 0;
        for (int j = 0; j < sprites.length; j++) {
            sprites[j] = null;
        }
        return this;
    }

    @Override
    public boolean tick() {
        int spriteAddress = 0xfe00 + 4 * i;
        switch (state) {
            case READING_Y:
                spriteY = oemRam.getByte(spriteAddress);
                state = State.READING_X;
                break;

            case READING_X:
                spriteX = oemRam.getByte(spriteAddress + 1);
                if (spritePosIndex < sprites.length && between(spriteY, registers.get(GpuRegister.LY) + 16, spriteY + lcdc.getSpriteHeight())) {
                    sprites[spritePosIndex++] = new SpritePosition(spriteX, spriteY, spriteAddress);
                }
                i++;
                state = State.READING_Y;
                break;
        }
        return i < 40;
    }

    public SpritePosition[] getSprites() {
        return sprites;
    }

    private static boolean between(int from, int x, int to) {
        return from <= x && x < to;
    }

} public class PixelTransfer implements GpuPhase {

    private final PixelFifo fifo;

    private final Fetcher fetcher;

    private final Display display;

    private final MemoryRegisters r;

    private final Lcdc lcdc;

    private final boolean gbc;

    private SpritePosition[] sprites;

    private int droppedPixels;

    private int x;

    private boolean window;

    public PixelTransfer(AddressSpace videoRam0, AddressSpace videoRam1, AddressSpace oemRam, Display display, Lcdc lcdc, MemoryRegisters r, boolean gbc, ColorPalette bgPalette, ColorPalette oamPalette) {
        this.r = r;
        this.lcdc = lcdc;
        this.gbc = gbc;
        if (gbc) {
            this.fifo = new ColorPixelFifo(lcdc, display, bgPalette, oamPalette);
        } else {
            this.fifo = new DmgPixelFifo(display, lcdc, r);
        }
        this.fetcher = new Fetcher(fifo, videoRam0, videoRam1, oemRam, lcdc, r, gbc);
        this.display = display;

    }

    public PixelTransfer start(SpritePosition[] sprites) {
        this.sprites = sprites;
        droppedPixels = 0;
        x = 0;
        window = false;

        fetcher.init();
        if (gbc || lcdc.isBgAndWindowDisplay()) {
            startFetchingBackground();
        } else {
            fetcher.fetchingDisabled();
        }
        return this;
    }

    @Override
    public boolean tick() {
        fetcher.tick();
        if (lcdc.isBgAndWindowDisplay() || gbc) {
            if (fifo.getLength() <= 8) {
                return true;
            }
            if (droppedPixels < r.get(SCX) % 8) {
                fifo.dropPixel();
                droppedPixels++;
                return true;
            }
            if (!window && lcdc.isWindowDisplay() && r.get(LY) >= r.get(WY) && x == r.get(WX) - 7) {
                window = true;
                startFetchingWindow();
                return true;
            }
        }

        if (lcdc.isObjDisplay()) {
            if (fetcher.spriteInProgress()) {
                return true;
            }
            boolean spriteAdded = false;
            for (int i = 0; i < sprites.length; i++) {
                SpritePosition s = sprites[i];
                if (s == null) {
                    continue;
                }
                if (x == 0 && s.getX() < 8) {
                    if (!spriteAdded) {
                        fetcher.addSprite(s, 8 - s.getX(), i);
                        spriteAdded = true;
                    }
                    sprites[i] = null;
                } else if (s.getX() - 8 == x) {
                    if (!spriteAdded) {
                        fetcher.addSprite(s, 0, i);
                        spriteAdded = true;
                    }
                    sprites[i] = null;
                }
                if (spriteAdded) {
                    return true;
                }
            }
        }

        fifo.putPixelToScreen();
        if (++x == 160) {
            return false;
        }
        return true;
    }

    private void startFetchingBackground() {
        int bgX = r.get(SCX) / 0x08;
        int bgY = (r.get(SCY) + r.get(LY)) % 0x100;

        fetcher.startFetching(lcdc.getBgTileMapDisplay() + (bgY / 0x08) * 0x20, lcdc.getBgWindowTileData(), bgX, lcdc.isBgWindowTileDataSigned(), bgY % 0x08);
    }

    private void startFetchingWindow() {
        int winX = (this.x - r.get(WX) + 7) / 0x08;
        int winY = r.get(LY) - r.get(WY);

        fetcher.startFetching(lcdc.getWindowTileMapDisplay() + (winY / 0x08) * 0x20, lcdc.getBgWindowTileData(), winX, lcdc.isBgWindowTileDataSigned(), winY % 0x08);
    }

} public class VBlankPhase implements GpuPhase {

    private int ticks;

    public VBlankPhase start() {
        ticks = 0;
        return this;
    }

    @Override
    public boolean tick() {
        return ++ticks < 456;
    }
} public class Dma implements AddressSpace {

    private final AddressSpace addressSpace;

    private final AddressSpace oam;

    private final SpeedMode speedMode;

    private boolean transferInProgress;

    private boolean restarted;

    private int from;

    private int ticks;

    private int regValue = 0xff;

    public Dma(AddressSpace addressSpace, AddressSpace oam, SpeedMode speedMode) {
        this.addressSpace = new DmaAddressSpace(addressSpace);
        this.speedMode = speedMode;
        this.oam = oam;
    }

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

    public void tick() {
        if (transferInProgress) {
            if (++ticks >= 648 / speedMode.getSpeedMode()) {
                transferInProgress = false;
                restarted = false;
                ticks = 0;
                for (int i = 0; i < 0xa0; i++) {
                    oam.setByte(0xfe00 + i, addressSpace.getByte(from + i));
                }
            }
        }
    }

    @Override
    public void setByte(int address, int value) {
        from = value * 0x100;
        restarted = isOamBlocked();
        ticks = 0;
        transferInProgress = true;
        regValue = value;
    }

    @Override
    public int getByte(int address) {
        return regValue;
    }

    public boolean isOamBlocked() {
        return restarted || (transferInProgress && ticks >= 5);
    }
} 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 class Ram implements AddressSpace {

    private int[] space;

    private int length;

    private int offset;

    public Ram(int offset, int length) {
        this.space = new int[length];
        this.length = length;
        this.offset = offset;
    }

    private Ram(int offset, int length, Ram ram) {
        this.offset = offset;
        this.length = length;
        this.space = ram.space;
    }

    public static Ram createShadow(int offset, int length, Ram ram) {
        return new Ram(offset, length, ram);
    }

    @Override
    public boolean accepts(int address) {
        return address >= offset && address < offset + length;
    }

    @Override
    public void setByte(int address, int value) {
        space[address - offset] = value;
    }

    @Override
    public int getByte(int address) {
        int index = address - offset;
        if (index < 0 || index >= space.length) {
            throw new IndexOutOfBoundsException("Address: " + address);
        }
        return space[index];
    }
} 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;
    }

}
import eu.rekawek.coffeegb.AddressSpace;
import eu.rekawek.coffeegb.cpu.InterruptManager;
import eu.rekawek.coffeegb.cpu.InterruptManager.InterruptType;
import eu.rekawek.coffeegb.cpu.SpeedMode;
import eu.rekawek.coffeegb.gpu.phase.GpuPhase;
import eu.rekawek.coffeegb.gpu.phase.HBlankPhase;
import eu.rekawek.coffeegb.gpu.phase.OamSearch;
import eu.rekawek.coffeegb.gpu.phase.PixelTransfer;
import eu.rekawek.coffeegb.gpu.phase.VBlankPhase;
import eu.rekawek.coffeegb.memory.Dma;
import eu.rekawek.coffeegb.memory.MemoryRegisters;
import eu.rekawek.coffeegb.memory.Ram;
import static eu.rekawek.coffeegb.gpu.GpuRegister.*;
package eu.rekawek.coffeegb.gpu;



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) {