#include <cart.h>
#include <common.h>
#include <string.h>
#ifdef __windows__
#include <windows.h>
#endif

static cart_context ctx;

bool cart_need_save () {
    return ctx.need_save;
}

bool cart_mbc1() {
    return BETWEEN(ctx.header->type, 1, 3);
}

bool cart_mbc2() {
    return BETWEEN(ctx.header->type, 5, 6);
}

bool cart_mbc3() {
    return BETWEEN(ctx.header->type, 15, 19);
}

bool cart_mbc5() {
    return BETWEEN(ctx.header->type, 25, 30);
}

bool cart_mbc() {
    return cart_mbc1() || cart_mbc2() || cart_mbc3() || cart_mbc5();
}

bool cart_battery() {
    return ctx.header->type == 3 || BETWEEN(ctx.header->type, 15, 16) || ctx.header->type == 19 || ctx.header->type == 6 || ctx.header->type == 26 || ctx.header->type == 29;
}

static const char *ROM_TYPES[] = {
    "ROM ONLY",
    "MBC1",
    "MBC1+RAM",
    "MBC1+RAM+BATTERY",
    "0X04 ???",
    "MBC2",
    "MBC2+BATTERY",
    "0X07 ???",
    "ROM+RAM 1",
    "ROM+RAM+BATTERY 1",
    "0X0A ???",
    "MMM01",
    "MMM01+RAM",
    "MMM01+RAM+BATTERY",
    "0X0E ???",
    "MBC3+TIMER+BATTERY",
    "MBC3+TIMER+RAM+BATTERY 2",
    "MBC3",
    "MBC3+RAM 2",
    "MBC3+RAM+BATTERY 2",
    "0X14 ???",
    "0X15 ???",
    "0X16 ???",
    "0X17 ???",
    "0X18 ???",
    "MBC5",
    "MBC5+RAM",
    "MBC5+RAM+BATTERY",
    "MBC5+RUMBLE",
    "MBC5+RUMBLE+RAM",
    "MBC5+RUMBLE+RAM+BATTERY",
    "0X1F ???",
    "MBC6",
    "0X21 ???",
    "MBC7+SENSOR+RUMBLE+RAM+BATTERY "
};

static const char *LIC_CODE[0xA5] = {
    [0x00] = "None",
    [0x01] = "Nintendo R&D1",
    [0x08] = "Capcom",
    [0x13] = "Electronic Arts",
    [0x18] = "Hudson Soft",
    [0x19] = "b-ai",
    [0x20] = "kss",
    [0x22] = "pow",
    [0x24] = "PCM Complete",
    [0x25] = "san-x",
    [0x28] = "Kemco Japan",
    [0x29] = "seta",
    [0x30] = "Viacom",
    [0x31] = "Nintendo",
    [0x32] = "Bandai",
    [0x33] = "Ocean/Acclaim",
    [0x34] = "Konami",
    [0x35] = "Hector",
    [0x37] = "Taito",
    [0x38] = "Hudson",
    [0x39] = "Banpresto",
    [0x41] = "Ubi Soft",
    [0x42] = "Atlus",
    [0x44] = "Malibu",
    [0x46] = "angel",
    [0x47] = "Bullet-Proof",
    [0x49] = "irem",
    [0x50] = "Absolute",
    [0x51] = "Acclaim",
    [0x52] = "Activision",
    [0x53] = "American sammy",
    [0x54] = "Konami",
    [0x55] = "Hi tech entertainment",
    [0x56] = "LJN",
    [0x57] = "Matchbox",
    [0x58] = "Mattel",
    [0x59] = "Milton Bradley",
    [0x60] = "Titus",
    [0x61] = "Virgin",
    [0x64] = "LucasArts",
    [0x67] = "Ocean",
    [0x69] = "Electronic Arts",
    [0x70] = "Infogrames",
    [0x71] = "Interplay",
    [0x72] = "Broderbund",
    [0x73] = "sculptured",
    [0x75] = "sci",
    [0x78] = "THQ",
    [0x79] = "Accolade",
    [0x80] = "misawa",
    [0x83] = "lozc",
    [0x86] = "Tokuma Shoten Intermedia",
    [0x87] = "Tsukuda Original",
    [0x91] = "Chunsoft",
    [0x92] = "Video system",
    [0x93] = "Ocean/Acclaim",
    [0x95] = "Varie",
    [0x96] = "Yonezawa/s’pal",
    [0x97] = "Kaneko",
    [0x99] = "Pack in soft",
    [0xA4] = "Konami (Yu-Gi-Oh!)"
};

rom_header *get_rom_header() {
    return ctx.header;
}

const char *cart_type_name() {
    if(ctx.header->type <= 0x22) {
        return ROM_TYPES[ctx.header->type];
    }

    return "UNKNOWN";
}

const char *cart_lic_name() {
    if(ctx.header->new_lic_code <= 0xA4) {
        return LIC_CODE[ctx.header->lic_code];
    }

    return "UNKNOWN";
}

void cart_setup_bamking() {
    for (int i=0; i<16; i++) {
        ctx.ram_banks[i] = 0;

        if((ctx.header->ram_size == 2 && i == 0) || 
            (ctx.header->ram_size == 3 && i < 4) ||
            (ctx.header->ram_size == 5 && i < 8) ||
            (ctx.header->ram_size == 4)) {
            ctx.ram_banks[i] = malloc(0x2000);
            memset(ctx.ram_banks[i], 0, 0x2000);
        }
    }

    if(cart_mbc2()) {
        ctx.ram_banks[0] = malloc(0x2000);
        memset(ctx.ram_banks[0], 0, 0x2000);
    }

    ctx.ram_bank = ctx.ram_banks[0];
    ctx.rom_bank_x = ctx.rom_data + 0x4000;
    ctx.rom_bank_x2 = ctx.rom_data;
}

bool cart_init() {
      char filename[1024];
    u32 rom_size;
    u8 *rom_data;
    rom_header *header;

    //mbc1 data
    bool ram_enabled;
    bool ram_banking;

    u8 *rom_bank_x;
    u8 *rom_bank_x2;
    u8 banking_mode;

    u8 rom_bank_value;
    u8 rom_bank_value_2;
    u8 ram_bank_value;

    u8 *ram_bank;
    u8 *ram_banks[16];

    //battery
    bool battery;
    bool need_save;

    ctx.ram_enabled = 0;
    ctx.ram_banking = 0;
    ctx.rom_bank_x = ctx.rom_data;
    ctx.rom_bank_x2 = ctx.rom_data;
    ctx.banking_mode = 0;
    ctx.rom_bank_value = 1;
    ctx.rom_bank_value_2 = 0;
    ctx.ram_bank_value = 0;
    cart_setup_bamking();
    ctx.need_save = 0;
    if(ctx.battery) {
        cart_battery_load();
    }
}

bool cart_load(char *cart) {
    snprintf(ctx.filename, sizeof(ctx.filename), "%s", cart);
    FILE *fp = fopen(cart, "r");
    if (!fp) {
        printf("Failed to open: %s\n", cart);
        return false;
    }

    printf("Opened: %s\n", cart),

    fseek(fp, 0, SEEK_END);
    ctx.rom_size = ftell(fp);

    rewind(fp);

    ctx.rom_data = malloc(ctx.rom_size);
    fread(ctx.rom_data, ctx.rom_size, 1, fp);
    fclose(fp);

    ctx.header = (rom_header *)(ctx.rom_data + 0x100);
    ctx.header->title[15] = 0;
    ctx.battery = cart_battery();
    ctx.need_save = false;

    printf("Cartridge Loaded:\n");
    printf("\t Title    : %s\n", ctx.header->title);
    printf("\t Type     : %2.2X (%s)\n", ctx.header->type, cart_type_name());
    printf("\t ROM Size : %d KB\n", 32 << ctx.header->rom_size);
    printf("\t RAM Size : %2.2X\n", ctx.header->ram_size);
    printf("\t LIC Code : %2.2X (%s)\n", ctx.header->lic_code, cart_lic_name());
    printf("\t ROM Vers : %2.2X\n", ctx.header->version);

    cart_setup_bamking();

    u16 x = 0;
    for (u16 i=0X0134; i<=0X014C; i++) {
        x = x - ctx.rom_data[i] -1;
    }

    printf("\t Checksum : %2.2X (%s)\n", ctx.header->checksum, (x & 0xFF) == ctx.header->checksum ? "PASSED" : "Failed");

    if((x & 0xFF) != ctx.header->checksum) {
        fprintf(stderr, "WARNING!!! The header checksum does not match! ROM may be corrupt or invalid!\n");
    }

    if(cart_mbc1() && ctx.header->rom_size >= 5) {
        int count = 0;
        for(int i = 1; i < 4; i++) {
            u32 start_addr = (i << 18);
            bool same = true;
            for(int j = 0x104; j < 0x134; j++) {
                if(ctx.rom_data[start_addr | j] != ctx.rom_data[j]) {
                    same = false;
                    break;
                }
            }
            if(same)
                count++;
        }
        if(count >= 2) {
            ctx.is_multicart = true;
        }
    }

    if(ctx.battery) {
        cart_battery_load();
    }

    
    return true;
}

u8 cart_read(u16 address){
    if(cart_mbc1() && ctx.header->rom_size >= 5 && address < 0x4000) {
        return ctx.rom_bank_x2[address];
    }

    if(!cart_mbc() || address < 0x4000) {
        return ctx.rom_data[address];
    }

    if((address & 0xE000) == 0xA000) {
        if(cart_mbc2())
            address &= 0xE1FF;
        if (!ctx.ram_enabled) {
            return 0xFF;
        }

        if(!ctx.ram_bank) {
            return 0xFF;
        }

        return cart_mbc2() ? ctx.ram_bank[address - 0xA000] | 0xF0 : ctx.ram_bank[address - 0xA000];
    }

    return ctx.rom_bank_x[address - 0x4000];

}

void cart_write(u16 address, u8 value){
    //printf("Cart Write: %04X\n", address);
    if(!cart_mbc()) {
        return;
    }

    if(cart_mbc2()) {
        if(address < 0x4000){
            if(address & 0x100) {
                value &= 0b1111;
                if(value == 0) {
                    value = 1;
                }
                switch(ctx.header->rom_size) {
                    case 0: value &= 0b1; break;
                    case 1: value &= 0b11; break;
                    case 2: value &= 0b111; break;
                    default: break;
                }
                ctx.rom_bank_value = value;
                ctx.rom_bank_x = ctx.rom_data + (0x4000 * ctx.rom_bank_value);
            } else {
                ctx.ram_enabled = ((value & 0xF) == 0xA);
            }
        }
    } else {
        if(address < 0x2000) {
            ctx.ram_enabled = ((value & 0xF) == 0xA);
        }

        if((address & 0xE000) == 0x2000) {
            //rom bank
            if(cart_mbc3()) {
                value &= 0b1111111;
                if(value == 0) {
                    value = 1;
                }
                ctx.rom_bank_value = value;
                ctx.rom_bank_x = ctx.rom_data + (0x4000 * ctx.rom_bank_value);
            } else if(cart_mbc5()) {
                if(address < 0x3000){
                    switch(ctx.header->rom_size) {
                        case 0: value &= 0b1; break;
                        case 1: value &= 0b11; break;
                        case 2: value &= 0b111; break;
                        case 3: value &= 0b1111; break;
                        case 4: value &= 0b11111; break;
                        case 5: value &= 0b111111; break;
                        case 6: value &= 0b1111111; break;
                        default: break;
                    }
                    ctx.rom_bank_value = value;
                    ctx.rom_bank_x = ctx.rom_data + (0x4000 * ctx.rom_bank_value + ctx.rom_bank_value_2);
                } else {
                    if(ctx.header->rom_size == 8) {
                        ctx.rom_bank_value_2 = (value & 0b1) << 8;
                        ctx.rom_bank_x = ctx.rom_data + (0x4000 * ctx.rom_bank_value + ctx.rom_bank_value_2);
                    }
                }
            } else {
                //printf("Bank Value: %d\n", value);
                value &= 0b11111;
                if(value == 0) {
                    value = 1;
                }
                if(ctx.is_multicart)
                    value &= 0b1111;
                //printf("Bank Value after filter: %d\n", value);
                switch(ctx.header->rom_size) {
                    case 0: value &= 0b1; break;
                    case 1: value &= 0b11; break;
                    case 2: value &= 0b111; break;
                    case 3: value &= 0b1111; break;
                    default: break;
                }
                ctx.rom_bank_value = value;
                if(ctx.header->rom_size >= 5) {
                    ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value+ctx.rom_bank_value_2));
                    //printf("(REG1 >5) Banked %d\n", ctx.rom_bank_value+ctx.rom_bank_value_2);
                    return;
                }
                ctx.rom_bank_x = ctx.rom_data + (0x4000 * ctx.rom_bank_value);
                //printf("(REG1 <5) Banked %d\n", ctx.rom_bank_value);
            }
        }

        if((address & 0xE000) == 0x4000) {
            //ram bank number
            if(cart_mbc1() && ctx.header->rom_size >= 5) {
                if(ctx.header->rom_size == 5)
                    value &= 0b1;
                ctx.rom_bank_value_2 = (value & 0b11) << 5;
                if(ctx.is_multicart)
                    ctx.rom_bank_value_2 = (value & 0b11) << 4;
                ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value+ctx.rom_bank_value_2));
                //printf("(REG2 >5) Banked %d\n", ctx.rom_bank_value+ctx.rom_bank_value_2);
                if(ctx.banking_mode)
                    ctx.rom_bank_x2 = ctx.rom_data + (0x4000 * (ctx.rom_bank_value_2));
                return;
            }
            ctx.ram_bank_value = value & 0b11;
            if(cart_mbc5()) 
                ctx.ram_bank_value = value & 0xF;
            if(ctx.ram_banking || cart_mbc5()) {
                if(cart_need_save()) {
                    cart_battery_save();
                }
                if((ctx.header->ram_size == 2 && ctx.ram_bank_value == 0) || 
                    (ctx.header->ram_size == 3 && ctx.ram_bank_value < 4) ||
                    (ctx.header->ram_size == 5 && ctx.ram_bank_value < 8) ||
                    (ctx.header->ram_size == 4)) {
                    ctx.ram_bank = ctx.ram_banks[ctx.ram_bank_value];
                }
                
            }
        }

        if((address & 0xE000) == 0x6000) {
            //banking mode select
            if(cart_mbc1()){
                ctx.banking_mode = value & 1;

                ctx.ram_banking = ctx.banking_mode;

                if(cart_mbc1() && ctx.header->rom_size >= 5) {
                    if(ctx.banking_mode) {
                        ctx.rom_bank_value_2 = (value & 0b11) << 5;
                        ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value+ctx.rom_bank_value_2));
                        ctx.rom_bank_x2 = ctx.rom_data + (0x4000 * (ctx.rom_bank_value_2));
                    } else {
                        ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value));
                        ctx.rom_bank_x2 = ctx.rom_data;
                    }
                    return;
                }

                if (ctx.ram_banking) {
                    if(cart_need_save()) {
                        cart_battery_save();
                    }
                    if((ctx.header->ram_size == 2 && ctx.ram_bank_value == 0) || 
                        (ctx.header->ram_size == 3 && ctx.ram_bank_value < 4) ||
                        (ctx.header->ram_size == 5 && ctx.ram_bank_value < 8) ||
                        (ctx.header->ram_size == 4)) {
                        ctx.ram_bank = ctx.ram_banks[ctx.ram_bank_value];
                    }
                } else {
                    ctx.ram_bank = ctx.ram_banks[0];
                }
            }
        }
    }

    

    if((address & 0xE000) == 0xA000) {
        if(cart_mbc2())
            address &= 0xE1FF;

        if(!ctx.ram_enabled){
            return;
        }

        if(!ctx.ram_bank)
            return;

        ctx.ram_bank[address - 0xA000] = cart_mbc2() ? value & 0xF : value;

        if(ctx.battery) {
            ctx.need_save = true;
        }
    }
}

bool cart_battery_load(){
#ifdef __windows__
    char *filename = strrchr(ctx.filename, '\\');
#else
    char *filename = strrchr(ctx.filename, '/');
#endif
    filename++;
    char fn[1048];
#ifdef __windows__
    char *profile = getenv("USERPROFILE");
    sprintf(fn, "%s/Documents/gbemu/saves/%s.battery", profile, filename);
#else
    char *profile = getenv("HOME");
    sprintf(fn, "%s/.config/gbemu/saves/%s.battery", profile, filename);
#endif
    printf("loading %s\n", fn);
    FILE *fp = fopen(fn, "rb");

    if(!fp) {
        printf("No battery save found.");
        return true;
    }

    fread(ctx.ram_bank, 0x2000, 1, fp);
    fclose(fp);
}

bool cart_battery_save(){
#ifdef __windows__
    char *filename = strrchr(ctx.filename, '\\');
#else
    char *filename = strrchr(ctx.filename, '/');
#endif
    filename++;
    char fn[1048];
#ifdef __windows__
    char *profile = getenv("USERPROFILE");
    sprintf(fn, "%s/Documents/gbemu/saves/%s.battery", profile, filename);
#else
    char *profile = getenv("HOME");
    sprintf(fn, "%s/.config/gbemu/saves/%s.battery", profile, filename);
#endif
    FILE *fp = fopen(fn, "wb");

    if(!fp) {
        fprintf(stderr, "unable to open: %s\n", fn);
    }

    fwrite(ctx.ram_bank, 0x2000, 1, fp);
    fclose(fp);

}

u8 cart_get_rom_bank() {
    return ctx.rom_bank_value;
}

void cart_load_state(const cart_state* state){
    ctx.ram_enabled = state->ram_enabled; 
    ctx.ram_banking = state->ram_banking;
    ctx.banking_mode = state->banking_mode;
    ctx.rom_bank_value = state->rom_bank_value;
    ctx.rom_bank_value_2 = state->rom_bank_value_2;
    ctx.ram_bank_value = state->ram_bank_value;
    ctx.need_save = state->need_save;

    for(int i = 0; i < 16; i++) {
        if(ctx.ram_banks[i]){
            memcpy(ctx.ram_banks[i], &state->ram_banks[i], 0x2000);
        }
    }

    if(ctx.banking_mode) {
        ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value+ctx.rom_bank_value_2));
        ctx.rom_bank_x2 = ctx.rom_data + (0x4000 * (ctx.rom_bank_value_2));
    } else {
        ctx.rom_bank_x = ctx.rom_data + (0x4000 * (ctx.rom_bank_value));
        ctx.rom_bank_x2 = ctx.rom_data;
    }

    if(ctx.ram_banking) {
        ctx.ram_bank = ctx.ram_banks[ctx.ram_bank_value];
    } else {
        ctx.ram_bank = ctx.ram_banks[0];
    }

    if(ctx.battery) {
        cart_battery_load();
    }

}

void cart_save_state(cart_state* state){
    state->ram_enabled = ctx.ram_enabled;
    state->ram_banking = ctx.ram_banking;
    state->banking_mode = ctx.banking_mode;
    state->rom_bank_value = ctx.rom_bank_value;
    state->rom_bank_value_2 = ctx.rom_bank_value_2;
    state->ram_bank_value = ctx.ram_bank_value;
    state->need_save = ctx.need_save;

    for (int i=0; i<16; i++) {
        if(ctx.ram_banks[i]) {
            memcpy((&state->ram_banks[i]), ctx.ram_banks[i], 0x2000);
        }
    }
}

cart_context *cart_get_context(){
    return &ctx;
}
