#include "mem.h"
#include "sm83.h"
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
#include "APU.h"

#include "mbc_none.h"
#include "mbc1.h"
#include "mbc3.h"

#define DIV hram[0x104]  // 0xff04 - 0xfe00
#define TIMA hram[0x105]  // 0xff05 - 0xfe00
#define TMA hram[0x106]  // 0xff06 - 0xfe00
#define TAC hram[0x107]  // 0xff07 - 0xfe00
namespace mem
{
    void (*resetMbc)(void);
    uint8_t (*readRom)(uint16_t);
    void (*writeRom)(uint16_t, uint8_t);
    uint8_t (*readRam)(uint16_t);
    void (*writeRam)(uint16_t, uint8_t);
    void (*tick)(void) = nullptr;;

    uint8_t bootrom[256];
    uint8_t *rom;
    uint8_t vram[8192];
    uint8_t wram[8192];
    uint8_t hram[512];

    uint8_t tags[65536];

    char *title = nullptr;
    uint8_t mapper_type = 0;
    uint32_t rom_size = 0;
    uint32_t ram_size = 0;
    uint16_t timer = 0;

    uint16_t dma_address = 0;
    uint8_t dma_pos = 160;
    uint16_t dma_dots = 0;

    uint16_t div_counter = 0;
    uint16_t timer_counter = 0;

    void init(uint8_t* rom, const int size)
    {
        title = (char*)&rom[0x134];
        mapper_type = rom[0x147];
        rom_size = 32768 * (1 << rom[0x148]);
        int sizes[] = { 0, 0, 8, 32, 128, 64};
        ram_size = sizes[rom[0x149]] * 1024;

        switch (mapper_type)
        {
            case 0x00:
                mbc_none::init(rom, rom_size, ram_size);
                mem::resetMbc = mbc_none::reset;
                mem::readRom = mbc_none::readRom;
                mem::writeRom = mbc_none::writeRom;
                mem::readRam = mbc_none::readRam;
                mem::writeRam = mbc_none::writeRam;
                break;
            case 0x01:
            case 0x02:
            case 0x03:
                mbc1::init(rom, rom_size, ram_size);
                mem::resetMbc = mbc1::reset;
                mem::readRom = mbc1::readRom;
                mem::writeRom = mbc1::writeRom;
                mem::readRam = mbc1::readRam;
                mem::writeRam = mbc1::writeRam;
                break;
            case 0x0f:
            case 0x10:
            case 0x11:
            case 0x12:
            case 0x13:
                mbc3::init(rom, rom_size, ram_size);
                mem::resetMbc = mbc3::reset;
                mem::readRom = mbc3::readRom;
                mem::writeRom = mbc3::writeRom;
                mem::readRam = mbc3::readRam;
                mem::writeRam = mbc3::writeRam;
                mem::tick = mbc3::tick;
                break;
        };

        APU::init();
    }

    void reset()
    {
        FILE *f = fopen("dmg_boot.bin", "rb");
        if (!f) { printf("ABORTING: 'dmg_boot.bin' not found!\n"); exit(1); }
        fseek(f, 0, SEEK_END);
        const int size = ftell(f);
        fseek(f, 0, SEEK_SET);
        fread(bootrom, size, 1, f);
        fclose(f);

        for (int i=0; i<8192; ++i) { vram[i] = 0; }
        for (int i=0; i<8192; ++i) { wram[i] = 0; }
        for (int i=0; i<512; ++i) { hram[i] = 0; }
        for (int i=0; i<65536; ++i) { tags[i] = MEMTAG_NONE; }

        resetMbc();
        APU::reset();
    }

    uint8_t getKeypad(uint8_t value)
    {
        const uint8_t *keys = SDL_GetKeyboardState(NULL);
        value = value & 0xf0;
        if (value & 0x10) {
            if (!keys[SDL_SCANCODE_RETURN]) value = value | 0x8;
            if (!keys[SDL_SCANCODE_SPACE]) value = value | 0x4;
            if (!keys[SDL_SCANCODE_Z]) value = value | 0x2;
            if (!keys[SDL_SCANCODE_X]) value = value | 0x1;
        } else if (value & 0x20) {
            if (!keys[SDL_SCANCODE_DOWN]) value = value | 0x8;
            if (!keys[SDL_SCANCODE_UP]) value = value | 0x4;
            if (!keys[SDL_SCANCODE_LEFT]) value = value | 0x2;
            if (!keys[SDL_SCANCODE_RIGHT]) value = value | 0x1;
        } else {
            value = value | 0x0f;
        }
        return value;
    }

    uint8_t readMem(uint16_t address)
    {
        if (address < 0x8000) {
            if ( (address < 0x0100) && ((hram[0x150]&0x01)==0) ) return bootrom[address];
            return readRom(address);
        } else if (address < 0xA000) {
            return vram[address - 0x8000];
        } else if (address < 0xC000) {
            return readRam(address);
        } else if (address < 0xE000) {
            return wram[address - 0xC000];
        } else if (address < 0xFE00) {
            return wram[address - 0xE000];
        } else {
            if (address==0xFF00) {
                hram[address - 0XFE00] = getKeypad(hram[address - 0XFE00]);
            } else if (address>=0xFF10 && address<=0xFF3F) { return APU::readRegister(address); }
            return hram[address - 0XFE00];
        }
    }

    void writeMem(uint16_t address, uint8_t value)
    {
        if (address < 0x8000) {
            writeRom(address, value);
        } else if (address < 0xA000) {
            vram[address - 0x8000] = value;
        } else if (address < 0xC000) {
            writeRam(address, value);
        } else if (address < 0xE000) {
            wram[address - 0xC000] = value;
        } else if (address < 0xFE00) {
            wram[address - 0xE000] = value;
        } else {
            if ( (address==0xFF50) && ((value&0x01) != 1) ) {
                return;  //Only allow disabling boot room
            } else if ( (address==0xFF00) ) {
                value = value & 0x30;
            } else if ( (address==0xFF04) ) {
                hram[address-0xFE00] = 0;
                return;
            } else if ( (address==0xFF0F) ) {                   // IF
                hram[address-0xFE00] = value;
                sm83::processInterrupts();
                return;
            } else if (address>=0xFF10 && address<=0xFF3F) {    // APU
                APU::writeRegister(address, value);
                return;
            } else if ( (address==0xFF46) ) {                   // OAM DMA
                mem::init_dma_transfer(value);
            }
            hram[address - 0xFE00] = value;
        }
    }

    uint8_t getTag(uint16_t address)
    {
        return tags[address];
    }

    void setTag(uint16_t address, uint8_t value)
    {
        tags[address] = value;
    }

    void saveState(FILE* f)
    {

    }

    void loadState(FILE* f)
    {

    }

    uint8_t *rawVram()
    {
        return vram;
    }

    uint8_t *rawHram(uint16_t address)
    {
        return &hram[address-0xfe00];
    }


    void init_dma_transfer(uint8_t source)
    {
        dma_address = source << 8;
        dma_pos = 0;
        dma_dots = 0;
    }

    uint16_t timer_frequencies[4] { 256*4, 4*4, 16*4, 64*4 };
    void update_mapped(const uint32_t dt)
    {
        timer+=dt;
        if (timer >= 4194304 ) {
            timer -= 4194304;
            if (mem::tick) mem::tick();
        }
        // DIV Divider register (0xFF04)  (val com a timer bàsic)
        div_counter += dt;
        if (div_counter>=256) {
            div_counter -= 256;
            bool bitset = DIV&0x10;
            DIV++;
            if (bitset && !(DIV&0x10)) APU::incDIVAPU();
        }
        
        // Timer
        if (TAC&0x4) { // if bit 3 of mem(0xff07) is 1, timer enabled
            uint16_t freq = timer_frequencies[TAC&0x03];
            timer_counter += dt;
            if (timer_counter>=freq) {
                timer_counter -= freq;
                if (TIMA<255)
                    TIMA++;
                else {
                    TIMA = TMA;
                    sm83::interrupt(INTERRUPT_TIMER);
                }
            }

        }

        // APU
        APU::update(dt);

        // OAM DMA
        if (dma_pos<160) {
            dma_dots += dt;
            while (dma_dots >= 4 && dma_pos<160) {
                dma_dots -= 4;
                hram[dma_pos] = mem::readMem(dma_address|dma_pos);
                dma_pos++;
            }
        }
    }

}
