#ifndef MEM_H
#define MEM_H

#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

#include "app/emu/debug/log.h"
#include "app/emu/memory/dma_channel.h"

// LoROM (Mode 20):

// Banks   Offset        Purpose
// 00-3F   0000-1FFF     LowRAM (shadowed from 7E)
//         2000-2FFF     PPU1, APU
//         3000-3FFF     SFX, DSP, etc.
//         4000-41FF     Controller
//         4200-5FFF     PPU2, DMA, etc.
//         6000-7FFF     Expansion RAM (reserved)
//         8000-FFFF     32k ROM Chunk
// 40-7C   0000-7FFF     32k ROM Chunk
//         8000-FFFF     32k ROM Chunk
// 7D      0000-FFFF     SRAM
// 7E      0000-1FFF     LowRAM
//         2000-FFFF     System RAM
// 7F      0000-FFFF     System RAM

namespace yaze {
namespace app {
namespace emu {
namespace memory {

enum RomSpeed { SLOW_ROM = 0x00, FAST_ROM = 0x07 };

enum BankSize { LOW_ROM = 0x00, HI_ROM = 0x01 };

enum RomType {
  ROM_DEFAULT = 0x00,
  ROM_RAM = 0x01,
  ROM_SRAM = 0x02,
  ROM_DSP1 = 0x03,
  ROM_DSP1_RAM = 0x04,
  ROM_DSP1_SRAM = 0x05,
  FX = 0x06
};

enum RomSize {
  SIZE_2_MBIT = 0x08,
  SIZE_4_MBIT = 0x09,
  SIZE_8_MBIT = 0x0A,
  SIZE_16_MBIT = 0x0B,
  SIZE_32_MBIT = 0x0C
};

enum SramSize {
  NO_SRAM = 0x00,
  SRAM_16_KBIT = 0x01,
  SRAM_32_KBIT = 0x02,
  SRAM_64_KBIT = 0x03
};

enum CountryCode {
  JAPAN = 0x00,
  USA = 0x01,
  EUROPE_OCEANIA_ASIA = 0x02,
  // ... and other countries
};

enum License {
  INVALID = 0,
  NINTENDO = 1,
  ZAMUSE = 5,
  CAPCOM = 8,
  // ... and other licenses
};

class RomInfo {
 public:
  std::string title;
  RomSpeed romSpeed;
  BankSize bankSize;
  RomType romType;
  RomSize romSize;
  SramSize sramSize;
  CountryCode countryCode;
  License license;
  uint8_t version;
  uint16_t checksumComplement;
  uint16_t checksum;
  uint16_t nmiVblVector;
  uint16_t resetVector;
};

typedef struct CpuCallbacks {
  std::function<uint8_t(uint32_t)> read_byte;
  std::function<void(uint32_t, uint8_t)> write_byte;
  std::function<void(bool waiting)> idle;
} CpuCallbacks;

constexpr uint32_t kROMStart = 0x008000;
constexpr uint32_t kROMSize = 0x200000;
constexpr uint32_t kRAMStart = 0x7E0000;
constexpr uint32_t kRAMSize = 0x20000;

/**
 * @brief Memory interface
 */
class Memory {
 public:
  virtual ~Memory() = default;
  virtual uint8_t ReadByte(uint32_t address) const = 0;
  virtual uint16_t ReadWord(uint32_t address) const = 0;
  virtual uint32_t ReadWordLong(uint32_t address) const = 0;
  virtual std::vector<uint8_t> ReadByteVector(uint32_t address,
                                              uint16_t length) const = 0;

  virtual void WriteByte(uint32_t address, uint8_t value) = 0;
  virtual void WriteWord(uint32_t address, uint16_t value) = 0;
  virtual void WriteLong(uint32_t address, uint32_t value) = 0;

  virtual void PushByte(uint8_t value) = 0;
  virtual uint8_t PopByte() = 0;
  virtual void PushWord(uint16_t value) = 0;
  virtual uint16_t PopWord() = 0;
  virtual void PushLong(uint32_t value) = 0;
  virtual uint32_t PopLong() = 0;

  virtual uint16_t SP() const = 0;
  virtual void SetSP(uint16_t value) = 0;

  virtual void ClearMemory() = 0;

  virtual uint8_t operator[](int i) const = 0;
  virtual uint8_t at(int i) const = 0;

  virtual uint8_t open_bus() const = 0;
  virtual void set_open_bus(uint8_t value) = 0;

  virtual bool hdma_init_requested() const = 0;
  virtual bool hdma_run_requested() const = 0;
  virtual void init_hdma_request() = 0;
  virtual void run_hdma_request() = 0;
  virtual void set_hdma_run_requested(bool value) = 0;
  virtual void set_hdma_init_requested(bool value) = 0;
  virtual void set_pal_timing(bool value) = 0;
  virtual void set_h_pos(uint16_t value) = 0;
  virtual void set_v_pos(uint16_t value) = 0;

  // get h_pos and v_pos
  virtual auto h_pos() const -> uint16_t = 0;
  virtual auto v_pos() const -> uint16_t = 0;
  // get pal timing
  virtual auto pal_timing() const -> bool = 0;
};

/**
 * @class MemoryImpl
 * @brief Implementation of the Memory interface for emulating memory in a SNES
 * system.
 *
 */
class MemoryImpl : public Memory, public Loggable {
 public:
  uint32_t romSize;
  uint32_t sramSize;
  void Initialize(const std::vector<uint8_t>& romData, bool verbose = false);

  uint16_t GetHeaderOffset() {
    uint8_t mapMode = memory_[(0x00 << 16) + 0xFFD5];
    uint16_t offset;

    switch (mapMode & 0x07) {
      case 0:  // LoROM
        offset = 0x7FC0;
        break;
      case 1:  // HiROM
        offset = 0xFFC0;
        break;
      case 5:  // ExHiROM
        offset = 0x40;
        break;
      default:
        throw std::invalid_argument(
            "Unable to locate supported ROM mapping mode in the provided ROM "
            "file. Please try another ROM file.");
    }

    return offset;
  }

  memory::RomInfo ReadRomHeader();

  uint8_t cart_read(uint8_t bank, uint16_t adr);
  void cart_write(uint8_t bank, uint16_t adr, uint8_t val);

  uint8_t cart_readLorom(uint8_t bank, uint16_t adr);
  void cart_writeLorom(uint8_t bank, uint16_t adr, uint8_t val);

  uint8_t cart_readHirom(uint8_t bank, uint16_t adr);
  uint8_t cart_readExHirom(uint8_t bank, uint16_t adr);

  void cart_writeHirom(uint8_t bank, uint16_t adr, uint8_t val);

  uint8_t ReadByte(uint32_t address) const override {
    uint32_t mapped_address = GetMappedAddress(address);
    return memory_.at(mapped_address);
  }
  uint16_t ReadWord(uint32_t address) const override {
    uint32_t mapped_address = GetMappedAddress(address);
    return static_cast<uint16_t>(memory_.at(mapped_address)) |
           (static_cast<uint16_t>(memory_.at(mapped_address + 1)) << 8);
  }
  uint32_t ReadWordLong(uint32_t address) const override {
    uint32_t mapped_address = GetMappedAddress(address);
    return static_cast<uint32_t>(memory_.at(mapped_address)) |
           (static_cast<uint32_t>(memory_.at(mapped_address + 1)) << 8) |
           (static_cast<uint32_t>(memory_.at(mapped_address + 2)) << 16);
  }
  std::vector<uint8_t> ReadByteVector(uint32_t address,
                                      uint16_t length) const override {
    uint32_t mapped_address = GetMappedAddress(address);
    return std::vector<uint8_t>(memory_.begin() + mapped_address,
                                memory_.begin() + mapped_address + length);
  }

  void WriteByte(uint32_t address, uint8_t value) override {
    uint32_t mapped_address = GetMappedAddress(address);
    memory_[mapped_address] = value;
  }
  void WriteWord(uint32_t address, uint16_t value) override {
    uint32_t mapped_address = GetMappedAddress(address);
    memory_.at(mapped_address) = value & 0xFF;
    memory_.at(mapped_address + 1) = (value >> 8) & 0xFF;
  }
  void WriteLong(uint32_t address, uint32_t value) override {
    uint32_t mapped_address = GetMappedAddress(address);
    memory_.at(mapped_address) = value & 0xFF;
    memory_.at(mapped_address + 1) = (value >> 8) & 0xFF;
    memory_.at(mapped_address + 2) = (value >> 16) & 0xFF;
  }

  // Stack operations
  void PushByte(uint8_t value) override {
    if (SP_ > 0x0100) {
      memory_.at(SP_--) = value;
    } else {
      // Handle stack underflow
      std::cout << "Stack underflow!" << std::endl;
      throw std::runtime_error("Stack underflow!");
    }
  }

  uint8_t PopByte() override {
    if (SP_ < 0x1FF) {
      return memory_.at(++SP_);
    } else {
      // Handle stack overflow
      std::cout << "Stack overflow!" << std::endl;
      throw std::runtime_error("Stack overflow!");
    }
  }

  void PushWord(uint16_t value) override {
    PushByte(value >> 8);
    PushByte(value & 0xFF);
  }

  uint16_t PopWord() override {
    uint8_t low = PopByte();
    uint8_t high = PopByte();
    return (static_cast<uint16_t>(high) << 8) | low;
  }

  void PushLong(uint32_t value) override {
    PushByte(value >> 16);
    PushByte(value >> 8);
    PushByte(value & 0xFF);
  }

  uint32_t PopLong() override {
    uint8_t low = PopByte();
    uint8_t mid = PopByte();
    uint8_t high = PopByte();
    return (static_cast<uint32_t>(high) << 16) |
           (static_cast<uint32_t>(mid) << 8) | low;
  }

  // Stack Pointer access.
  uint16_t SP() const override { return SP_; }
  auto mutable_sp() -> uint16_t& { return SP_; }
  void SetSP(uint16_t value) override { SP_ = value; }
  void ClearMemory() override { std::fill(memory_.begin(), memory_.end(), 0); }

  uint8_t at(int i) const override { return memory_[i]; }
  uint8_t operator[](int i) const override {
    if (i > memory_.size()) {
      std::cout << i << " out of bounds \n";
      return memory_[0];
    }
    return memory_[i];
  }

  auto size() const { return memory_.size(); }
  auto begin() const { return memory_.begin(); }
  auto end() const { return memory_.end(); }
  auto data() const { return memory_.data(); }
  void set_open_bus(uint8_t value) override { open_bus_ = value; }
  auto open_bus() const -> uint8_t override { return open_bus_; }
  auto hdma_init_requested() const -> bool override {
    return hdma_init_requested_;
  }
  auto hdma_run_requested() const -> bool override {
    return hdma_run_requested_;
  }
  void init_hdma_request() override { hdma_init_requested_ = true; }
  void run_hdma_request() override { hdma_run_requested_ = true; }
  void set_hdma_run_requested(bool value) override {
    hdma_run_requested_ = value;
  }
  void set_hdma_init_requested(bool value) override {
    hdma_init_requested_ = value;
  }
  void set_pal_timing(bool value) override { pal_timing_ = value; }
  void set_h_pos(uint16_t value) override { h_pos_ = value; }
  void set_v_pos(uint16_t value) override { v_pos_ = value; }
  auto h_pos() const -> uint16_t override { return h_pos_; }
  auto v_pos() const -> uint16_t override { return v_pos_; }
  auto pal_timing() const -> bool override { return pal_timing_; }

  auto dma_state() -> uint8_t& { return dma_state_; }
  void set_dma_state(uint8_t value) { dma_state_ = value; }
  auto dma_channels() -> DmaChannel* { return channel; }

  // Define memory regions
  std::vector<uint8_t> rom_;
  std::vector<uint8_t> ram_;

 private:
  uint32_t GetMappedAddress(uint32_t address) const;

  bool verbose_ = false;

  // DMA requests
  bool hdma_run_requested_ = false;
  bool hdma_init_requested_ = false;

  bool pal_timing_ = false;

  // Frame timing
  uint16_t h_pos_ = 0;
  uint16_t v_pos_ = 0;

  // Dma State
  uint8_t dma_state_ = 0;

  // Dma Channels
  DmaChannel channel[8];

  // Open bus
  uint8_t open_bus_ = 0;

  // Stack Pointer
  uint16_t SP_ = 0;

  // Cart Type
  uint8_t type_ = 1;

  // Memory (64KB)
  std::vector<uint8_t> memory_;
};

void DrawSnesMemoryMapping(const MemoryImpl& memory);

}  // namespace memory
}  // namespace emu
}  // namespace app
}  // namespace yaze

#endif  // MEM_H