public class Data {
    protected Data() {}

    public Data(ROM rom) {}

    public Data(ROM rom, int offset) {
        this(rom.seekAndGet(offset));
    }
} @Getter
@ToString
public class ConnectionData extends Data {
    private long numConnsPtr;
    private long dataPtr;

    private List<Connection> connections;

    public ConnectionData(ROM rom) {
        numConnsPtr = rom.getPointer(true);
        dataPtr = rom.getPointer(true);

        connections = new ArrayList<>();
        loadConnections(rom);
    }

    private void loadConnections(ROM rom) {
        rom.seek(BitConverter.shortenPointer(dataPtr));

        for (int i = 0; i < numConnsPtr; i++) {
            connections.add(new Connection(rom));
        }
    }
} @Getter
public class ROMData {
    public static final int BANKS = 42;

    private String romName;
    private int itemData;
    private int attackNames;
    private int tmData;
    private int totalTMsAndHMs;
    private int totalTMs;
    private int itemImgData;
    private int itemsNumber;
    private int attacksNumber;
    private int moveTutorAttacks;
    private int moveTutorAttacksNumber;
    private int pokemonNames;
    private int pokemonsNumber;
    private int nationalDexTable;
    private int secondDexTable;
    private int pokedexData;
    private int dexEntriesNumber;
    private int regionDexEntriesNumber;
    private int pokemonData;
    private int abilityNames;
    private int abilitiesNumber;
    private int pointerOfMapBanksPointer;

    // Should always have a length of 42
    private int[] bankPointers;
    private int[] mapsInBanksNumber;

    private int mapLabelData;
    private int mapLabelsNumber;

    // TODO Add all the tilesets

    private int pokemonFrontSprites;
    private int pokemonBackSprites;
    private int pokemonNormalPal;
    private int pokemonShinyPal;
    private int iconPointerTable;
    private int iconPalTable;
    private int cryTable;
    private int cryTable2;
    private int cryCoversionTable;
    private int footPrintTable;
    private int pokemonAttackTable;
    private int pokemonEvolutions;
    private int tmhmcCompatibility;
    private int tmhmlLenPerPoke;
    private int moveTutorCompatibility;
    private int enemyYTable;
    private int playerYTable;
    private int enemyAltitudeTable;
    private int attackData;
    private int attackDescriptionTable;
    private int abilityDescriptionTable;
    private int attackAnimationTable;
    private int iconPals;
    private int jamboLearnableMovesTerm;
    private int startSearchingForSpaceOffset;
    private int freeSpaceSearchInterval;
    private int evolutionsPerPokeNumber;
    private int evolutionTypesNumber;

    // Should have a length of `evolutionTypesNumber`
    private String[] evolutionNames;
    private EvolutionParam[] evolutionParams;

    private int eggMoveTable;
    private int eggMoveTableLimiter;
    private int habitatTable;
    private int itemAnimationTable;
    private int trainerTable;
    private int trainersNumber;
    private int trainerClasses;
    private int trainerClassesNumber;
    private int trainerImageTable;
    private int trainerImagesNumber;
    private int trainerPaletteTable;
    private int trainerMoneyTable;
    private int dexSizeTrainerSprite;
    private int tradeData;
    private int tradesNumber;
    private int pokedexAlphabetTable;
    private int pokedexLightestTable;
    private int pokedexSmallestTable;
    private int pokedexTypeTable;

    private int mainPalCount;
    private int mainBlocks;
    @Setter
    private int localBlocks;
    private int mainTilesetSize;
    private int localTilesetSize;
    private int mainTilesetHeight;
    private int localTilesetHeight;

    public ROMData(Wini store, String header) {
        DataLoader loader = new DataLoader(store, header);

        romName = loader.getString("ROMName");
        itemData = loader.getPointer("ItemData");
        attackNames = loader.getPointer("AttackNames");
        tmData = loader.getPointer("TMData");
        totalTMsAndHMs = loader.getInt("TotalTMsPlusHMs");
        totalTMs = loader.getInt("TotalTMs");
        itemImgData = loader.getPointer("ItemIMGData");
        itemsNumber = loader.getInt("NumberOfItems");
        attacksNumber = loader.getInt("NumberOfAttacks");
        moveTutorAttacks = loader.getPointer("MoveTutorAttacks");
        moveTutorAttacksNumber = loader.getInt("NumberOfMoveTutorAttacks");
        pokemonNames = loader.getPointer("PokemonNames");
        pokemonsNumber = loader.getInt("NumberOfPokemon");
        nationalDexTable = loader.getPointer("NationalDexTable");
        secondDexTable = loader.getPointer("SecondDexTable");
        pokedexData = loader.getPointer("PokedexData");
        dexEntriesNumber = loader.getInt("NumberOfDexEntries");
        regionDexEntriesNumber = loader.getInt("NumberOfRegionDex");
        pokemonData = loader.getPointer("PokemonData") + 28;
        abilityNames = loader.getPointer("AbilityNames");
        abilitiesNumber = loader.getInt("NumberOfAbilities");

        pointerOfMapBanksPointer = loader.getPointer("Pointer2PointersToMapBanks");

        bankPointers = new int[BANKS];
        mapsInBanksNumber = new int[BANKS];

        for (int i = 0; i < BANKS; i++) {
            bankPointers[i] = loader.getPointer("OriginalBankPointer" + i);
            mapsInBanksNumber[i] = loader.getInt("NumberOfMapsInBank" + i) + 1; // Convert to 1-index based
        }

        mapLabelData = loader.getPointer("MapLabelData");
        mapLabelsNumber = loader.getInt("NumberOfMapLabels");

        // TODO Load tiles in tileset

        pokemonFrontSprites = loader.getPointer("PokemonFrontSprites");
        pokemonBackSprites = loader.getPointer("PokemonBackSprites");
        pokemonNormalPal = loader.getPointer("PokemonNormalPal");
        pokemonShinyPal = loader.getPointer("PokemonShinyPal");
        iconPointerTable = loader.getPointer("IconPointerTable");
        iconPalTable = loader.getPointer("IconPalTable");
        cryTable = loader.getPointer("CryTable");
        cryTable2 = loader.getPointer("CryTable2");
        cryCoversionTable = loader.getPointer("CryConversionTable");
        footPrintTable = loader.getPointer("FootPrintTable");
        pokemonAttackTable = loader.getPointer("PokemonAttackTable");
        pokemonEvolutions = loader.getPointer("PokemonEvolutions");
        tmhmcCompatibility = loader.getPointer("TMHMCompatibility");
        tmhmlLenPerPoke = loader.getInt("TMHMLenPerPoke");
        moveTutorCompatibility = loader.getPointer("MoveTutorCompatibility");
        enemyYTable = loader.getPointer("EnemyYTable");
        playerYTable = loader.getPointer("PlayerYTable");
        enemyAltitudeTable = loader.getPointer("EnemyAltitudeTable");
        attackData = loader.getPointer("AttackData");
        attackDescriptionTable = loader.getPointer("AttackDescriptionTable");
        abilityDescriptionTable = loader.getPointer("AbilityDescriptionTable");
        attackAnimationTable = loader.getPointer("AttackAnimationTable");
        iconPals = loader.getPointer("IconPals");
        jamboLearnableMovesTerm = loader.getPointer("JamboLearnableMovesTerm");
        startSearchingForSpaceOffset = loader.getPointer("StartSearchingForSpaceOffset");
        freeSpaceSearchInterval = loader.getInt("FreeSpaceSearchInterval");
        evolutionsPerPokeNumber = loader.getInt("NumberOfEvolutionsPerPokemon");
        evolutionTypesNumber = loader.getInt("NumberOfEvolutionTypes");
        evolutionParams = new EvolutionParam[evolutionTypesNumber];

        for (int i = 0; i < evolutionTypesNumber; i++) {
            String param = loader.getString("Evolution" + i + "Param");
            evolutionParams[i] = EvolutionParam.byConfig(param);
        }

        eggMoveTable = loader.getPointer("EggMoveTable");
        eggMoveTableLimiter = loader.getPointer("EggMoveTableLimiter");
        habitatTable = loader.getPointer("HabitatTable");
        itemAnimationTable = loader.getPointer("ItemAnimationTable");
        trainerTable = loader.getPointer("TrainerTable");
        trainersNumber = loader.getInt("NumberOfTrainers");
        trainerClasses = loader.getPointer("TrainerClasses");
        trainerClassesNumber = loader.getInt("NumberOfTrainerClasses");
        trainerImageTable = loader.getPointer("TrainerImageTable");
        trainerImagesNumber = loader.getInt("NumberOfTrainerImages");
        trainerPaletteTable = loader.getPointer("TrainerPaletteTable");
        trainerMoneyTable = loader.getPointer("TrainerMoneyTable");
        dexSizeTrainerSprite = loader.getInt("DexSizeTrainerSprite");
        tradeData = loader.getPointer("TradeData");
        tradesNumber = loader.getInt("NumberOfTrades");
        pokedexAlphabetTable = loader.getPointer("PokedexAlphabetTable");
        pokedexLightestTable = loader.getPointer("PokedexLightestTable");
        pokedexSmallestTable = loader.getPointer("PokedexSmallestTable");
        pokedexTypeTable = loader.getPointer("PokedexTypeTable");

        mainPalCount = loader.getPointer("MainTSPalCount");
        mainBlocks = loader.getPointer("MainTSBlocks");
        localBlocks = loader.getPointer("LocalTSBlocks");
        mainTilesetSize = loader.getPointer("MainTSSize");
        localTilesetSize = loader.getPointer("LocalTSSize");
        mainTilesetHeight = loader.getPointer("MainTSHeight");
        localTilesetHeight = loader.getPointer("LocalTSHeight");
    }

    public ROMData(File file, String header) throws IOException {
        this(new Wini(file), header);
    }

    public ROMData(String header) throws IOException {
        this(
            new Wini(
                ROMData.class.getClassLoader().getResourceAsStream("roms.ini")
            ),
            header
        );
    }

    public ROMData(ROM rom) throws IOException {
        this(rom.getGameCode());
    }
} public class BankLoader {
    private static int[] bankPointers;
    private static int[] mapsInBanksNum;

    public static int getMapHeaderPointer(ROM rom, ROMData data, int bank, int map) {
        if (bankPointers == null) {
            loadPointers(data);
        }

        // Perform bounds validation
        if (bank >= bankPointers.length) {
            throw new IllegalArgumentException("Bank number is too high. Got: " + bank + ", expected less than " + bankPointers.length);
        }

        if (map >= mapsInBanksNum[bank]) {
            throw new IllegalArgumentException("Map number for bank " + bank + " is too hight. Got: " + map + ", expected less than " + mapsInBanksNum[bank]);
        }

        int bankOffset = bankPointers[bank] + (map * 4);

        return rom.getPointerAsInt(bankOffset);
    }

    private static void loadPointers(ROMData data) {
        bankPointers = data.getBankPointers();
        mapsInBanksNum = data.getMapsInBanksNumber();
    }
} public interface ROM {
    /**
     * Retrieves this ROM's bytes
     */
    byte[] getData();

    /**
     * Initializes this ROM's bytes. Only called once
     */
    void setData(byte[] bytes);

    int getInternalOffset();
    void setInternalOffset(int offset);

    /**
     * You can default it to {@code (byte) 0xFF}
     */
    byte getFreeSpaceByte();

    default void addInternalOffset(int add) {
        setInternalOffset(getInternalOffset() + add);
    }

    /**
     * Read bytes from the ROM from given offset into an array of a given size
     * @param offset Offset in ROM as hex string
     * @param size Amount of bytes to grab
     */
    default byte[] readBytes(String offset, int size) {
        int offs = convertOffsetToInt(offset);
        return readBytes(offs, size);
    }

    /**
     * Read bytes from the ROM from given offset into an array of a given size
     * Use {@link BitConverter#getBytes(byte[], int, int)}
     * @param offset Offset in ROM
     * @param size Amount of bytes to grab
     */
    default byte[] readBytes(int offset, int size) {
        return BitConverter.getBytes(getData(), offset, size);
    }

    default byte[] readBytes(int size) {
        int offset = getInternalOffset();

        byte[] result = BitConverter.getBytes(getData(), offset, size);

        addInternalOffset(size);

        return result;
    }

    /**
     * Reads a byte from an offset
     * @param offset Offset to read from
     */
    default byte readByte(int offset) {
        return readBytes(offset,1)[0];
    }

    default byte readByte() {
        byte t = getData()[getInternalOffset()];
        addInternalOffset(1);

        return t;
    }

    /**
     * Reads a byte from an offset
     * @param offset Offset to read from
     */
    default int readByteAsInt(int offset) {
        return BitConverter.toInts(readBytes(offset,1))[0];
    }

    /**
     * Reads a byte from an internal offset
     */
    default int readByteAsInt() {
        return readByteAsInt(getInternalOffset());
    }

    default long readLong(int offset) {
        byte[] t = readBytes(offset, 4);

        return BitConverter.toInt32(t);
    }

    default long readLong() {
        return readLong(getInternalOffset());
    }

    /**
     * Reads a 16 bit word from an offset
     * @param offset Offset to read from
     */
    default int readWord(int offset) {
        int[] words = BitConverter.toInts(readBytes(offset,2));

        return ((words[1] << 8) + (words[0]));
    }

    /**
     * Reads a 16 bit word from the internalOffset
     */
    default int readWord() {
        int word = readWord(getInternalOffset());

        addInternalOffset(2);

        return word;
    }

    default void writeWord(int offset, int toWrite) {
        int[] bytes = new int[] {toWrite & 0xFF, (toWrite & 0xFF00) >> 8};
        byte[] nBytes = BitConverter.toBytes(bytes);

        writeBytes(offset, nBytes);
    }

    default void writeWord(int toWrite) {
        writeWord(getInternalOffset(), toWrite);
        addInternalOffset(2);
    }

    void writeByte(byte value, int offset);

    /**
     * Write an array of bytes to the ROM at a given offset
     * @param offset Offset to write the bytes at
     * @param bytesToWrite Bytes to write to the ROM
     */
    default void writeBytes(int offset, byte[] bytesToWrite) {
        for (int count = 0; count < bytesToWrite.length; count++) {
            try {
                writeByte(bytesToWrite[count], offset);
                offset++;
            } catch (Exception e) {
                String message = String.format("Tried to write out of bounds (%s)", offset);
                System.out.println(message);
            }
        }
    }

    default void writeByte(byte b) {
        writeByte(b, getInternalOffset());
        addInternalOffset(1);
    }

    /**
     * Write an array of bytes to the ROM at a given offset
     * @param bytesToWrite Bytes to write to the ROM
     */
    default void writeBytes(byte[] bytesToWrite) {
        writeBytes(getInternalOffset(), bytesToWrite);
        addInternalOffset(bytesToWrite.length);
    }

    /**
     * Convert a string offset i.e 0x943BBD into a decimal
     * Used for directly accessing the ROM byte array
     * @param offset Offset to convert to an integer
     * @return The offset as an int
     */
    default int convertOffsetToInt(String offset) {
        return Integer.parseInt(offset, 16);
    }

    /**
     * Validate the file loaded based on a given byte and offset
     * @param offset Offset to check in the ROM
     * @param validByte Byte to check it with
     * @return
     */
    default boolean validateROM(int offset, byte validByte) {
        return getData()[offset] == validByte;
    }

    void setCurrentHeader(byte[] romHeader);
    byte[] getCurrentHeader();

    /**
     * Retrieve the header of the ROM, based on offset and size
     * Identical to readBytesFromROM just with a different name
     */
    @Deprecated
    default byte[] getROMHeader(String headerOffset, int headerSize) {
        byte[] header = readBytes(headerOffset, headerSize);
        setCurrentHeader(header);

        return header;
    }

    /**
     * Return a string of the friendly ROM header based on the current ROM
     */
    default String getFriendlyHeader() {
        return HeaderNames.get(new String(getCurrentHeader()));
    }

    /**
     * Read a structure of data from the ROM at a given offset, a set number of times, with a set structure size
     * For example returning the names of Pokemon into an ArrayList of bytes
     * @param offset Offset to read the structure from
     * @param amount Amount to read
     * @param maxStructSize Maximum structure size
     */
    default List<byte[]> loadArrayOfStructuredData(int offset, int amount, int maxStructSize) {
        List<byte[]> data = new ArrayList<>();
        int offs = offset & 0x1FFFFFF;

        for (int count = 0; count < amount; count++) {
            byte[] tempByte = new byte[maxStructSize];

            for (int c2 = 0; c2 < tempByte.length; c2++) {
                tempByte[c2] = getData()[offs];
                offs++;
            }

            data.add(tempByte);
        }

        return data;
    }

    /**
     * Reads ASCII text from the ROM
     * @param offset The offset to read from
     * @param length The amount of text to read
     * @return The text as a String object
     */
    default String readText(int offset, int length) {
        return new String(BitConverter.getBytes(getData(), offset, length));
    }

    default String readPokeText(int length) {
        String text = readPokeText(getInternalOffset(), length);
        addInternalOffset(length);

        return text;
    }

    default String readPokeText(int offset, int length) {
        // If length set, return converted
        if (length > -1) {
            return PokeText.toAscii(BitConverter.getBytes(getData(), offset, length));
        }

        // Grab till end of text
        byte b = 0x0;
        int i = 0;

        while (b != -1) {
            b = getData()[offset + i];
            i++;
        }

        return PokeText.toAscii(BitConverter.getBytes(getData(), offset, i));
    }

    /**
     * Gets a pointer at an offset
     * @param offset Offset to get the pointer from
     * @param fullPointer Whether we should fetch the full 32 bit pointer or the 24 bit byte[] friendly version.
     * @return Pointer as a long
     */
    default long getPointer(int offset, boolean fullPointer) {
        byte[] data = BitConverter.getBytes(getData(), offset, 4);

        if(!fullPointer) {
            data[3] = 0;
        }

        return BitConverter.toInt32(data);
    }

    /**
     * Gets a 24 bit pointer in the ROM as an integer.
     * @param offset Offset to get the pointer from
     * @return Pointer as a Long
     */
    default long getPointer(int offset) {
        return getPointer(offset,false) & 0x1FFFFFF;
    }

    /**
     * Gets a pointer in the ROM as an integer.
     * Does not support 32 bit pointers due to Java's integer size ({@link Integer#MAX_VALUE}) not being long enough.
     * @param offset Offset to get the pointer from
     * @return Pointer as an Integer
     */
    default int getPointerAsInt(int offset) {
        return (int) getPointer(offset,false);
    }

    default long getSignedLong(boolean fullPointer) {
        byte[] data = BitConverter.getBytes(getData(), getInternalOffset(), 4);

        if(!fullPointer) {
            data[3] = 0;
        }

        addInternalOffset(4);

        long pointer = BitConverter.toInt32(data);
        return data[3] > 0x7F ? ~pointer : pointer;
    }

    /**
     * Reverses and writes a pointer to the ROM
     * @param pointer Pointer to write
     * @param offset Offset to write it at
     */
    default void writePointer(long pointer, int offset) {
        byte[] bytes = BitConverter.getBytes(pointer);
        writeBytes(offset, bytes);
    }

    /**
     * Reverses and writes a pointer to the ROM. Assumes pointer is ROM memory and appends 08 to it.
     * @param pointer Pointer to write (appends 08 automatically)
     * @param offset Offset to write it at
     */
    default void writePointer(int pointer, int offset) {
        byte[] bytes = BitConverter.getBytes(pointer);

        bytes = BitConverter.reverseBytes(bytes);
        bytes[3] = 0x08;

        writeBytes(offset, bytes);
    }

    /**
     * Gets the game code from the ROM, ie BPRE for US Pkmn Fire Red
     * @see #getGame()
     */
    String getGameCode();

    /**
     * Gets the Game from the ROM, ie {@link Game#FIRE_RED} for US Pokémon Fire Red
     */
    Game getGame();

    /**
     * Gets the game text from the ROM, ie POKEMON FIRE for US Pkmn Fire Red
     */
    String getGameText();

    /**
     * Gets the game creator ID as a String, ie '01' is GameFreak's Company ID
     */
    String getGameCreatorId();

    /**
     * Sets the game code, the game creator and the game creator ID
     */
    void setGameVars(String code, String name, String maker);

    /**
     * Update some ROM flags depending on the Pokémon game
     */
    default void updateFlags() {
        String code = getGameCode();

        if (code.equals("BPRE")) {
            // Is this a function pointer?
            if (readByte(0x082903) == 0x8) {
                setDNPokePatchAdded(true);
            }

            // Is interdepth's RTC in there?
            if (readByte(0x427) == 0x8) {
                setRTCAdded(true);
            }
        } else if (code.equals("BPEE")) {
            setRTCAdded(true);

            if (readByte(0x0B4C7F) == 0x8) {
                setDNPokePatchAdded(true);
            }
        }
    }

    // Flags getters and setters
    boolean isDNPokePatchAdded();

    boolean isRTCAdded();

    void setDNPokePatchAdded(boolean added);

    void setRTCAdded(boolean added);

    /**
     * Gets a pointer at an offset
     * @param fullPointer Whether we should fetch the full 32 bit pointer or the 24 bit byte[] friendly version.
     * @return Pointer as a Long
     */
    default long getPointer(boolean fullPointer) {
        byte[] data = BitConverter.getBytes(getData(), getInternalOffset(), 4);

        if (!fullPointer && data[3] >= 0x8) {
            data[3] -= 0x8;
        }

        addInternalOffset(4);
        return BitConverter.toInt32(data);
    }

    /**
     * Gets a 24 bit pointer in the ROM as an integer.
     * @return Pointer as a Long
     */
    default long getPointer() {
        return getPointer(false);
    }

    /**
     * Gets a pointer in the ROM as an integer.
     * Does not support 32 bit pointers due to Java's integer size not being long enough.
     * @return Pointer as an Integer
     */
    default int getPointerAsInt() {
        return (int) getPointer(false);
    }

    /**
     * Reverses and writes a pointer to the ROM
     * @param pointer Pointer to write
     */
    default void writePointer(long pointer) {
        byte[] bytes = BitConverter.reverseBytes(BitConverter.getBytes(pointer));

        writeBytes(getInternalOffset(), bytes);
        addInternalOffset(4);
    }

    default void writeSignedPointer(long pointer) {
        byte[] bytes = BitConverter.reverseBytes(BitConverter.getBytes(pointer));

        writeBytes(getInternalOffset(), bytes);
        addInternalOffset(4);
    }

    /**
     * Reverses and writes a pointer to the ROM. Assumes pointer is ROM memory and appends 08 to it.
     * @param pointer Pointer to write (appends 08 automatically)
     */
    default void writePointer(int pointer) {
        byte[] bytes = BitConverter.reverseBytes(BitConverter.getBytes(pointer));
        bytes[3] += 0x8;

        writeBytes(getInternalOffset(), bytes);
        addInternalOffset(4);
    }

    default void seek(int offset) {
        if(offset > 0x08000000) {
            offset &= 0x1FFFFFF;
        }

        setInternalOffset(offset);
    }

    // Hacky way to make ROM, offset constructors work
    default ROM seekAndGet(int offset) {
        seek(offset);
        return this;
    }

    default int findFreespace(long freeSpaceSize, int startLoc, boolean asmSafe) {
        byte free = getFreeSpaceByte();
        byte[] searching = new byte[(int) freeSpaceSize];

        for(int i = 0; i < freeSpaceSize; i++) {
            searching[i] = free;
        }

        int numMatches = 0;
        int freespace = -1;

        byte[] bytes = getData();

        for(int i = startLoc; i < bytes.length; i++) {
            byte b = bytes[i];
            byte c = searching[numMatches];

            if(b == c) {
                numMatches++;

                if(numMatches == searching.length - 1) {
                    freespace = i - searching.length + 2;
                    break;
                }
            } else {
                numMatches = 0;
            }
        }

        return freespace;
    }

    default int findFreespace(int length, boolean asmSafe) {
        return findFreespace(length, 0, asmSafe);
    }

    default int findFreespace(long freeSpaceStart, int startLoc) {
        return findFreespace(freeSpaceStart, startLoc, false);
    }

    default int findFreespace(int length) {
        return findFreespace(length, 0, false);
    }

    default void floodBytes(int offset, byte b, int length) {
        if(offset > 0x1FFFFFF) {
            return;
        }

        for(int i = offset; i < offset + length; i++) {
            writeByte(b, i);
        }
    }

    default void repoint(int original, int newPointer, int lookForNum) {
        original |= 0x8000000;

        byte[] searching = BitConverter.reverseBytes(BitConverter.getBytes(original));

        int numMatches = 0;
        int totalMatches = 0;
        int offset;

        byte[] bytes = getData();

        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            byte c = searching[numMatches];

            if (b == c) {
                numMatches++;

                if (numMatches == searching.length - 1) {
                    offset = i - searching.length + 2;

                    this.seek(offset);
                    this.writePointer(newPointer);

                    // TODO Should we log the offsets?
                    //System.out.println(BitConverter.toHexString(offset));

                    totalMatches++;

                    if (totalMatches == lookForNum) {
                        break;
                    }

                    numMatches = 0;
                }
            } else {
                numMatches = 0;
            }
        }

        System.out.println("Found " + totalMatches + " occurrences of the pointer specified.");
    }

    default void repoint(int original, int newPointer) {
        repoint(original, newPointer, -1);
    }
} @Getter
public class SpritesHeader extends Data {
    private byte npcAmount;
    private byte exitsAmount;
    private byte triggersAmount;
    private byte signsAmount;

    private long npcPtr;
    private long exitsPtr;
    private long triggersPtr;
    private long signsPtr;

    public SpritesHeader(ROM rom) {
        npcAmount = rom.readByte();
        exitsAmount = rom.readByte();
        triggersAmount = rom.readByte();
        signsAmount = rom.readByte();

        npcPtr = rom.getPointer();
        exitsPtr = rom.getPointer();
        triggersPtr = rom.getPointer();
        signsPtr = rom.getPointer();
    }
} public class SpritesExitManager extends SpriteManager<SpriteExit> {
    public SpritesExitManager(ROM rom, int count) {
        for (int i = 0; i < count; i++) {
            sprites.add(new SpriteExit(rom));
        }
    }

    public SpritesExitManager(ROM rom, int offset, int count) {
        super(rom, offset, count);
    }
} public class SpritesNPCManager extends SpriteManager<SpriteNPC> {
    public SpritesNPCManager(ROM rom, int count) {
        for (int i = 0; i < count; i++) {
            sprites.add(new SpriteNPC(rom));
        }
    }

    public SpritesNPCManager(ROM rom, int offset, int count) {
        // TODO Make sure this in super calls constructor above
        super(rom, offset, count);
    }
} public class SpritesSignManager extends SpriteManager<SpriteSign> {
    public SpritesSignManager(ROM rom, int count) {
        for (int i = 0; i < count; i++) {
            sprites.add(new SpriteSign(rom));
        }
    }

    public SpritesSignManager(ROM rom, int offset, int count) {
        super(rom, offset, count);
    }
} public class TriggerManager extends SpriteManager<Trigger> {
    public TriggerManager(ROM rom, int count) {
        for (int i = 0; i < count; i++) {
            sprites.add(new Trigger(rom));
        }
    }

    public TriggerManager(ROM rom, int offset, int count) {
        super(rom, offset, count);
    }

    // Getter mapping, triggers aren't sprites
    public List<Trigger> getTriggers() {
        return sprites;
    }
} public class TilesetCache {
    private static Map<Integer, Tileset> cache = new HashMap<>();

    public static Tileset get(ROM rom, ROMData data, int offset) {
        if (cache.containsKey(offset)) {
            return cache.get(offset);
        }

        Tileset tileset = Tileset.load(rom, data, offset);
        cache.put(offset, tileset);

        return tileset;
    }

    public static void switchTileset(ROM rom, ROMData data, MapData mapData) {
        Tileset global = get(rom, data, mapData.getGlobalTilesetPtr());
        Tileset local = get(rom, data, mapData.getLocalTilesetPtr());

        for (int j = 0; j < Tileset.MAX_TIME; j++) {
            for (int i = data.getMainPalCount() - 1; i < 13; i++) {
                global.getPalette(j)[i] = local.getPalette(j)[i];
            }

            local.setPalettes(j, global.getPalette(j));
        }

        global.renderPalettedTiles();
        local.renderPalettedTiles();

        global.startTileLoaders();
        local.startTileLoaders();
    }
} public class BitConverter {
    protected BitConverter() {}

    public static long toInt32(byte[] bytes) {
        // AB CD EF 08 -> 08EFCDAB
        return (long) ((bytes[0] & 0xFF) + ((bytes[1] & 0xFF) << 8) + ((bytes[2] & 0xFF) << 16) + ((bytes[3] & 0xFF) << 24));
    }

    public static int shortenPointer(long pointer) {
        return (int)(pointer & 0x1FFFFFF);
    }

    public static byte[] getBytes(long i) {
        return new byte[] {
            (byte)((i & 0xFF000000) >> 24),
            (byte)((i & 0x00FF0000) >> 16),
            (byte)((i & 0x0000FF00) >> 8),
            (byte)((i & 0x000000FF))
        };
    }

    public static int[] getInts(long i) {
        return new int[] {
            (int)((i & 0xFF000000) >> 24),
            (int)((i & 0x00FF0000) >> 16),
            (int)((i & 0x0000FF00) >> 8),
            (int)((i & 0x000000FF))
        };
    }

    public static byte[] reverseBytes(byte[] bytes) {
        byte[] result = new byte[bytes.length];

        for (int i = 0; i < bytes.length; i++) {
            result[i] = bytes[bytes.length - 1 - i];
        }

        return result;
    }

    public static byte[] getBytes(byte[] array, int offset, int length) {
        byte[] result = new byte[length];

        for (int i = 0; i < length; i++) {
            try {
                result[i] = array[offset + i];
            } catch (ArrayIndexOutOfBoundsException e) {
                String message = String.format("Tried to read outside of bounds (%05X, %s)", offset, i);

                System.out.println(message);
            }
        }

        return result;
    }

    public static int[] grabBytesAsInts(byte[] array, int offset, int length) {
        if (length > array.length) {
            length = array.length;
        }

        int[] result = new int[length];

        for (int i = 0; i < length; i++) {
            result[i] = (array[offset + i] & 0xFF);
        }

        return result;
    }

    public static byte[] putBytes(byte[] array, byte[] toPut, int offset) {
        for (int i = 0; i < toPut.length; i++) {
            array[offset + i] = toPut[i];
        }

        return array;
    }

    public static int[] toInts(byte[] array) {
        return grabBytesAsInts(array, 0, array.length);
    }

    public static String toHexString(int loc) {
        return toHexString(loc, false);
    }

    // Use absolute value to prevent negative bytes

    public static String toHexString(int loc, boolean spacing) {
        if (spacing) {
            return String.format("%02X", Math.abs(loc));
        } else {
            return String.format("%X", Math.abs(loc));
        }
    }

    public static String toDwordString(int b, boolean spacing) {
        if (spacing) {
            return String.format("%06X", Math.abs(b));
        } else {
            return String.format("%X", Math.abs(b));
        }
    }

    public static String byteToStringNoZero(int b) {
        if(b != 0) {
            return String.format("%X", Math.abs(b));
        } else {
            return "";
        }
    }


    public static byte[] toBytes(int[] data) {
        byte[] result = new byte[data.length];

        for (int i = 0; i < result.length; i++) {
            result[i] = (byte)(data[i]);
        }

        return result;
    }

    public static boolean zeroedOut(int... words) {
        for (int i : words) {
            if (i != 0) {
                return false;
            }
        }

        return true;
    }

    public static boolean isBitSet(byte value, int index) {
        return (value & (1 << index)) != 0;
    }

    /**
     * Left-to-right bit range of 2 bytes
     * @param from The bits from 0 (left)
     * @param to The bits from 0 (left)
     */
    public static int getBitRange(int value, int from, int to) {
        int fromA = 16 - to;
        to = 16 - from;

        from = fromA;

        int bits = to - from;
        int rightShifted = value >>> from;
        int mask = (1 << bits) - 1;

        return rightShifted & mask;
    }
}
import lombok.Getter;
import lombok.ToString;
import me.hugmanrique.pokedata.Data;
import me.hugmanrique.pokedata.connections.ConnectionData;
import me.hugmanrique.pokedata.loaders.ROMData;
import me.hugmanrique.pokedata.maps.banks.BankLoader;
import me.hugmanrique.pokedata.roms.ROM;
import me.hugmanrique.pokedata.sprites.SpritesHeader;
import me.hugmanrique.pokedata.sprites.exits.SpritesExitManager;
import me.hugmanrique.pokedata.sprites.npcs.SpritesNPCManager;
import me.hugmanrique.pokedata.sprites.signs.SpritesSignManager;
import me.hugmanrique.pokedata.sprites.triggers.TriggerManager;
import me.hugmanrique.pokedata.tiles.TilesetCache;
import me.hugmanrique.pokedata.utils.BitConverter;
package me.hugmanrique.pokedata.maps;


/**
 * Bulbapedia article missing
 * As the map data class contains a lot of values, each "section" is
 * separated into child classes
 *
 * Data structure gathered by @shinyquagsire23,
 * Extracted from MEH (https://github.com/shinyquagsire23/MEH)
 * Thanks!
 *
 * @author shinyquagsire23, Hugmanrique
 * @since 30/04/2017
 */
@Getter
@ToString
public class Map extends Data {
    private MapHeader header;
    private ConnectionData connections;
    private SpritesHeader sprites;

    private SpritesNPCManager npcManager;
    private SpritesSignManager signManager;
    private TriggerManager triggerManager;
    private SpritesExitManager exitManager;

    private MapData data;
    private MapTileData tileData;

    public Map(ROM rom, ROMData data) {