#ifndef ADDRESSSPACE_HPP
#define ADDRESSSPACE_HPP
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

#include "defines.hpp"

class AddressSpace {
  bool bootromLoaded = true;
  Byte bootrom[BOOTROM_SIZE] = {0};
  std::vector<Byte> game;
  bool testing;
  Byte testRam[0xFFFF];
  Byte *cartridgeRam = nullptr;

public:
  AddressSpace() {
    // Initialize the memory to zero
    memoryLayout = {};
  }

  struct {
    Byte *romBank0;           //[ROM_BANK_SIZE] Mapped to 0x0000
    Byte *romBankSwitch;      //[ROM_BANK_SIZE] Mapped to 0x4000
    Byte vram[0x2000];        // Mapped to 0x8000
    Byte *externalRam;        //[0x2000]; Mapped to 0xA000
    Byte memoryBank1[0x1000]; // Mapped to 0xC000
    Byte memoryBank2[0x1000]; // Mapped to 0xD000
    Byte oam[0xA0];           // Mapped to 0xFE00
    Byte notUsable[0x60];     // Mapped to 0xFEA0
    // General purpose hardware registers
    Byte JOYP = 0xCF;
    Byte SB;
    Byte SC = 0x7E;
    Byte DIV;
    // Timer registers
    Byte TIMA;
    Byte TMA;
    Byte TAC = 0xF8;
    // interrupt flag and enable
    Byte IF = 0xE1;
    // Sound registers
    Byte NR10;
    Byte NR11;
    Byte NR12;
    Byte NR13;
    Byte NR14;
    Byte NR20; // not used
    Byte NR21;
    Byte NR22;
    Byte NR23;
    Byte NR24;
    Byte NR30;
    Byte NR31;
    Byte NR32;
    Byte NR33;
    Byte NR34;
    Byte NR40; // unused
    Byte NR41;
    Byte NR42;
    Byte NR43;
    Byte NR44;
    Byte NR50;
    Byte NR51;
    Byte NR52;
    Byte waveRam[0x10];
    // PPU registers
    Byte LCDC;
    Byte STAT;
    Byte SCY;
    Byte SCX;
    Byte LY;
    Byte LYC;
    Byte DMA;
    Byte BGP;
    Byte OBP0;
    Byte OBP1;
    Byte WY;
    Byte WX;
    Byte specialRam[0x7F]; // Mapped to 0xFF80
    Byte IE;               // Mapped to 0xFFFF
  } memoryLayout{};

  void unmapBootrom();
  void mapBootrom();
  bool getBootromState() const;
  void loadBootrom(const std::string &filename);
  void loadGame(const std::string &filename);

  void determineMBCInfo();
  Byte *MBCWrite(Word address);
  // prevents seg faults when programs with no MBC try to write to ROM
  Byte dummyVal = 0;

  // called after every op
  void MBCUpdate();

  void loadRomBank();
  void createRamBank();
  void loadRamBank();
  MBCType_enum MBC = {};
  uint32_t romSize = 0;
  uint32_t romBanks = 0;
  uint32_t externalRamSize = 0;
  uint32_t externalRamBanks = 0;

  bool dmaTransferRequested = false;
  void dmaTransfer();

  // MBC registers
  // Selected ROM Bank in MBC1 = (Secondary Bank << 5) + ROM Bank
  Byte selectedRomBank = 0;
  Byte romBankRegister = 0x00;
  // 2 bit register acts as secondary rom bank register or ram bank number in
  // MBC1
  Byte twoBitBankRegister = 0x0;
  Byte selectedExternalRamBank = 0;
  Byte romRamSelect = 0x00;
  // 0xA if enabled in MBC1/3, else off
  // in MBC2 also the rom bank number
  // in MBC3 also timer enable
  // reading ram while disabled is undefined behaviour
  Byte ramEnable = 0x00;
  // MBC3
  Byte latchClockData = 0x00;
  Byte ramBankRTCRegister = 0x00;
  // RTC registers
  enabledRTCRegister_enum enabledRTCRegister = NONE;
  Byte RTCSeconds = 0x0;
  Byte RTCMinutes = 0x0;
  Byte RTCHours = 0x0;
  Byte RTCDayLower = 0x0;
  Byte RTCDayHigher = 0x0;

  void setTesting(bool state);

  // read
  Byte operator[](const Word address) const {
    if (testing)
      return testRam[address];
    if (address < 0x0100 && bootromLoaded)
      return bootrom[address];
    else if (address < 0x4000)
      return memoryLayout.romBank0[address];
    else if (address < 0x8000)
      return memoryLayout.romBankSwitch[address - 0x4000];
    else if (address < 0xA000)
      return memoryLayout.vram[address - 0x8000];
    else if (address < 0xC000) {
      if (enabledRTCRegister != NONE) {
        switch (enabledRTCRegister) {
        case RTCS:
          return RTCSeconds;
        case RTCM:
          return RTCMinutes;
        case RTCH:
          return RTCHours;
        case RTCDL:
          return RTCDayLower;
        case RTCDH:
          return RTCDayHigher;
        }
      }
      if (externalRamSize == 0)
        return 0xFF;
      // MBC2 echos 15 times and only stores lower 4 bits
      else if (MBC == MBC2 || MBC == MBC2Battery)
        return memoryLayout.externalRam[(address - 0xA000) & 0x01FF] | 0xF0;
      else
        return memoryLayout.externalRam[address - 0xA000];
    } else if (address < 0xD000)
      return memoryLayout.memoryBank1[address - 0xC000];
    else if (address < 0xE000)
      return memoryLayout.memoryBank2[address - 0xD000];
    else if (address < 0xFE00)
      return memoryLayout.memoryBank1[address - 0xE000];
    else if (address < 0xFEA0)
      return memoryLayout.oam[address - 0xFE00];
    else if (address < 0xFF00) {
      if ((memoryLayout.STAT & 0x03) == 2 || (memoryLayout.STAT & 0x03) == 3)
        return 0xFF;
      return 0x00;
    } else if (address < 0xFF80)
      switch (address) {
      case 0xFF00:
        return memoryLayout.JOYP;
      case 0xFF01:
        return memoryLayout.SB;
      case 0xFF02:
        return memoryLayout.SC;
      case 0xFF04:
        return memoryLayout.DIV;
      case 0xFF05:
        return memoryLayout.TIMA;
      case 0xFF06:
        return memoryLayout.TMA;
      case 0xFF07:
        return memoryLayout.TAC | 0xF8;
      case 0xFF0F:
        return memoryLayout.IF | 0xE0;
      case 0xFF10:
        return memoryLayout.NR10;
      case 0xFF11:
        return memoryLayout.NR11;
      case 0xFF12:
        return memoryLayout.NR12;
      case 0xFF13:
        return memoryLayout.NR13;
      case 0xFF14:
        return memoryLayout.NR14;
      case 0xFF16:
        return memoryLayout.NR21;
      case 0xFF17:
        return memoryLayout.NR22;
      case 0xFF18:
        return memoryLayout.NR23;
      case 0xFF19:
        return memoryLayout.NR24;
      case 0xFF1A:
        return memoryLayout.NR30;
      case 0xFF1B:
        return memoryLayout.NR31;
      case 0xFF1C:
        return memoryLayout.NR32;
      case 0xFF1D:
        return memoryLayout.NR33;
      case 0xFF1E:
        return memoryLayout.NR34;
      case 0xFF20:
        return memoryLayout.NR41;
      case 0xFF21:
        return memoryLayout.NR42;
      case 0xFF22:
        return memoryLayout.NR43;
      case 0xFF23:
        return memoryLayout.NR44;
      case 0xFF24:
        return memoryLayout.NR50;
      case 0xFF25:
        return memoryLayout.NR51;
      case 0xFF26:
        return memoryLayout.NR52;
      // PPU registers
      case 0xFF40:
        return memoryLayout.LCDC;
      case 0xFF41:
        return memoryLayout.STAT;
      case 0xFF42:
        return memoryLayout.SCY;
      case 0xFF43:
        return memoryLayout.SCX;
      case 0xFF44:
        return memoryLayout.LY;
      case 0xFF45:
        return memoryLayout.LYC;
      case 0xFF46:
        return memoryLayout.DMA;
      case 0xFF47:
        return memoryLayout.BGP;
      case 0xFF48:
        return memoryLayout.OBP0;
      case 0xFF49:
        return memoryLayout.OBP1;
      case 0xFF4A:
        return memoryLayout.WY;
      case 0xFF4B:
        return memoryLayout.WX;
      default:
        if (address >= 0xFF30 && address <= 0xFF3F) {
          return memoryLayout.waveRam[address - 0xFF30];
        }
        return 0xFF;
      }
    else if (address < 0xFFFF)
      return memoryLayout.specialRam[address - 0xFF80];
    // 0xFFFF
    return memoryLayout.IE;
  }

  // write
  Byte &operator[](const Word address) {
    dummyVal = 0xFF;
    if (testing)
      return testRam[address];
    else if (address < 0x0100 && bootromLoaded)
      return bootrom[address];
    else if (address < 0x8000)
      return (*MBCWrite(address));
    else if (address < 0xA000)
      return memoryLayout.vram[address - 0x8000];
    else if (address < 0xC000) {
      if (externalRamSize == 0)
        return dummyVal;
      return memoryLayout.externalRam[address - 0xA000];
    } else if (address < 0xD000)
      return memoryLayout.memoryBank1[address - 0xC000];
    else if (address < 0xE000)
      return memoryLayout.memoryBank2[address - 0xD000];
    else if (address < 0xFE00)
      return memoryLayout.memoryBank1[address - 0xE000];
    else if (address < 0xFEA0)
      return memoryLayout.oam[address - 0xFE00];
    else if (address < 0xFF00)
      return memoryLayout.notUsable[address - 0xFEA0];
    else if (address < 0xFF80)
      switch (address) {
      case 0xFF00:
        return memoryLayout.JOYP;
      case 0xFF01:
        return memoryLayout.SB;
      case 0xFF02:
        return memoryLayout.SC;
      case 0xFF04:
        memoryLayout.DIV = 0;
        return dummyVal;
      // Timer registers
      case 0xFF05:
        return memoryLayout.TIMA;
      case 0xFF06:
        return memoryLayout.TMA;
      case 0xFF07:
        return memoryLayout.TAC;
      case 0xFF0F:
        return memoryLayout.IF;
      case 0xFF10:
        return memoryLayout.NR10;
      case 0xFF11:
        return memoryLayout.NR11;
      case 0xFF12:
        return memoryLayout.NR12;
      case 0xFF13:
        return memoryLayout.NR13;
      case 0xFF14:
        return memoryLayout.NR14;
      case 0xFF16:
        return memoryLayout.NR21;
      case 0xFF17:
        return memoryLayout.NR22;
      case 0xFF18:
        return memoryLayout.NR23;
      case 0xFF19:
        return memoryLayout.NR24;
      case 0xFF1A:
        return memoryLayout.NR30;
      case 0xFF1B:
        return memoryLayout.NR31;
      case 0xFF1C:
        return memoryLayout.NR32;
      case 0xFF1D:
        return memoryLayout.NR33;
      case 0xFF1E:
        return memoryLayout.NR34;
      case 0xFF20:
        return memoryLayout.NR41;
      case 0xFF21:
        return memoryLayout.NR42;
      case 0xFF22:
        return memoryLayout.NR43;
      case 0xFF23:
        return memoryLayout.NR44;
      case 0xFF24:
        return memoryLayout.NR50;
      case 0xFF25:
        return memoryLayout.NR51;
      case 0xFF26:
        return memoryLayout.NR52;
      case 0xFF40:
        return memoryLayout.LCDC;
      case 0xFF41:
        return memoryLayout.STAT;
      case 0xFF42:
        return memoryLayout.SCY;
      case 0xFF43:
        return memoryLayout.SCX;
      case 0xFF44:
        dummyVal = memoryLayout.LY;
        return dummyVal;
      case 0xFF45:
        return memoryLayout.LYC;
      case 0xFF46:
        dmaTransferRequested = true;
        return memoryLayout.DMA;
      case 0xFF47:
        return memoryLayout.BGP;
      case 0xFF48:
        return memoryLayout.OBP0;
      case 0xFF49:
        return memoryLayout.OBP1;
      case 0xFF4A:
        return memoryLayout.WY;
      case 0xFF4B:
        return memoryLayout.WX;
      default:
        if (address >= 0xFF30 && address <= 0xFF3F) {
          return memoryLayout.waveRam[address - 0xFF30];
        }
        return dummyVal;
      }
    else if (address < 0xFFFF)
      return memoryLayout.specialRam[address - 0xFF80];
    // 0xFFFF
    return memoryLayout.IE;
  }
};

#endif // ADDRESSSPACE_HPP
