package app;

// Java imports
import java.nio.charset.*;
import java.util.*;

// Cartridge ROM module
public class ROM {

    // Instance fields
    private byte[] data;   // Binary data
    private int    format; // File format of loaded file



    ///////////////////////////////////////////////////////////////////////////
    //                               Constants                               //
    ///////////////////////////////////////////////////////////////////////////

    // Formats
    public static final int RAW = 0;
    public static final int ISX = 1;



    ///////////////////////////////////////////////////////////////////////////
    //                                Classes                                //
    ///////////////////////////////////////////////////////////////////////////

    // ISX code segments
    private static class Code {
        int address; // CPU address
        int offset;  // Position within file
        int size;    // Number of bytes
        Code(int o, int a, int s) { address = a; offset =  o; size = s; }
    }



    ///////////////////////////////////////////////////////////////////////////
    //                             Constructors                              //
    ///////////////////////////////////////////////////////////////////////////

    // Default constructor
    ROM(byte[] data) {

        // Attempt to decode as ISX
        try { isxDecode(data); return; } catch (Exception e) { }

        // Attempt to decode as raw
        try { rawDecode(data); return; } catch (Exception e) { }

        // Unable to identify the file contents
        throw new RuntimeException();
    }



    ///////////////////////////////////////////////////////////////////////////
    //                            Public Methods                             //
    ///////////////////////////////////////////////////////////////////////////

    // Retrieve the game code from the ROM header
    public String getGameCode() {
        var bytes = new byte[4];
        System.arraycopy(data, data.length - 517, bytes, 0, 4);
        return new String(bytes, StandardCharsets.ISO_8859_1);
    }

    // Retrieve the file format of the loaded ROM file
    public int getFormat() {
        return format;
    }

    // Retrieve the maker code from the ROM header
    public String getMaker() {
        var bytes = new byte[2];
        System.arraycopy(data, data.length - 519, bytes, 0, 2);
        return new String(bytes, StandardCharsets.ISO_8859_1);
    }

    // Retrieve the number of bytes in the ROM data
    public int getSize() {
        return data.length;
    }

    // Retrieve the game title from the ROM header
    public String getTitle() {
        return ShiftJIS.decode(data, data.length - 544, 20).trim();
    }

    // Retrieve the version number from the ROM header
    public int getVersion() {
        return data[data.length - 513] & 0xFF;
    }

    // Produce a byte array containing the ROM contents
    public byte[] toByteArray() {
        return data;
    }



    ///////////////////////////////////////////////////////////////////////////
    //                            Private Methods                            //
    ///////////////////////////////////////////////////////////////////////////

    // Decode the file data as ISX
    private void isxDecode(byte[] data) {
        var codes = isxParse(data); // ISX code segments
        int head  = -1; // Latest address in bottom half of ROM address space
        int tail  = -1; // Earliest address in upper half of ROM address space

        // Process all code segments
        for (var code : codes) {
            int     end    = code.address + code.size - 1;
            int     start  = code.address;
            boolean isHead = end   >= 0; // Lower half of CPU address range
            boolean isTail = start <  0; // Upper half of CPU address range

            // The segment spans the middle of the ROM address space
            if (isHead && isTail) {
                head = 0x7FFFFF;
                tail = 0x800000;
                break;
            }

            // Track the segment's position within the ROM address space
            end   &= 0x00FFFFFF;
            start &= 0x00FFFFFF;
            if (isHead && (head == -1 || head < end  )) head = end;
            if (isTail && (tail == -1 || tail > start)) tail = start;
        }

        // Determine the required ROM size
        int size    = 1024;
        int minSize = (head == -1 ? 0 : head + 1) +
            (tail == -1 ? 0 : 0x01000000 - tail);
        if (minSize == 0 || tail == -1 && (minSize - 1 & minSize) != 0)
            throw new RuntimeException();
        for (; size < minSize; size <<= 1);

        // Transfer the code segments into the ROM buffer
        this.data = new byte[size];
        for (var code : codes)
            System.arraycopy(
                data     , code.offset,
                this.data, code.address & size - 1,
                code.size
            );

        // The ROM is valid
        format = ISX;
    }

    // Parse the code records from the ISX data
    private Code[] isxParse(byte[] data) {
        int count;
        int offset = 0;
        var ret    = new ArrayList<Code>();

        // Check for an extended ISX header
        if (readInt(data, 0, 3) == 0x585349) // ASCII "ISX"
            offset = 32;

        // Process records
        while (offset != data.length)
        switch (data[offset++] & 0xFF) {

            // SNES code
            case 0x01:
                if ((data[offset++] & 0xFF) >= 0x80) // Bank
                    offset++; // BankHigh
                offset += 2; // Address
                offset += 2 + readInt(data, offset, 2); // Data
                break;

            // SNES range
            case 0x03:
                // Count, { Bank, StartAddress, EndAddress, Type }
                offset += 2 + readInt(data, offset, 2) * 6;
                break;

            // SNES symbol
            case 0x04:
                count = readInt(data, offset, 2); offset += 2;
                for (; count > 0; count--) // Name, Flags, Bank, Address
                    offset += 5 + readInt(data, offset, 1);
                break;

            // Virtual Boy code
            case 0x11:
                int address = readInt(data, offset, 4); offset += 4;
                int size    = readInt(data, offset, 4); offset += 4;
                if ((address & 0x07000000) != 0x07000000 || size < 0 ||
                    (address & 0x00FFFFFF) + size > 0x01000000)
                    throw new RuntimeException();
                ret.add(new Code(offset, address, size));
                offset += size;
                break;

            // Virtual Boy range
            case 0x13:
                // Count, { StartAddress, EndAddress, Type }
                offset += 2 + readInt(data, offset, 2) * 9;
                break;

            // Virtual Boy symbol
            case 0x14:
                count = readInt(data, offset, 2); offset += 2;
                for (; count > 0; count--) // Name, Flags, Address
                    offset += 7 + readInt(data, offset, 1);
                break;

            // System
            case 0x20: case 0x21: case 0x22:
                offset += 4 + readInt(data, offset, 4); // Debug, undocumented
                break;

            // Invalid record type
            default:
                throw new RuntimeException();
        }

        return ret.toArray(new Code[ret.size()]);
    }

    // Decode the file as raw
    private void rawDecode(byte[] data) {

        // Validate length
        if (
            data.length < 1024       || // Exception handlers and ROM header
            data.length > 0x00FFFFFF || // 24-bit bus width
            (data.length & data.length - 1) != 0 // Must be a power of 2
        ) throw new RuntimeException();

        // The ROM is valid
        this.data = data;
        format    = RAW;
    }

    // Read an integer from a byte array
    private static int readInt(byte[] data, int offset, int size) {
        int ret = 0;
        for (size--; size >= 0; size--)
            ret = ret << 8 | data[offset + size] & 0xFF;
        return ret;
    }

}