#include "app/emu/snes.h"

#include <cstdint>
#include <memory>
#include <string>
#include <thread>

#include "app/emu/audio/apu.h"
#include "app/emu/audio/spc700.h"
#include "app/emu/cpu/clock.h"
#include "app/emu/cpu/cpu.h"
#include "app/emu/debug/debugger.h"
#include "app/emu/memory/dma.h"
#include "app/emu/memory/memory.h"
#include "app/emu/video/ppu.h"
#include "app/rom.h"

namespace yaze {
namespace app {
namespace emu {

namespace {
void input_latch(Input* input, bool value) {
  input->latch_line_ = value;
  if (input->latch_line_) input->latched_state_ = input->current_state_;
}

uint8_t input_read(Input* input) {
  if (input->latch_line_) input->latched_state_ = input->current_state_;
  uint8_t ret = input->latched_state_ & 1;
  input->latched_state_ >>= 1;
  input->latched_state_ |= 0x8000;
  return ret;
}
}  // namespace

void SNES::Init(std::vector<uint8_t>& rom_data) {
  // Initialize the CPU, PPU, and APU
  ppu_.Init();
  apu_.Init();

  // Load the ROM into memory and set up the memory mapping
  memory_.Initialize(rom_data);
  Reset(true);

  running_ = true;
}

void SNES::Reset(bool hard) {
  cpu_.Reset(hard);
  apu_.Reset();
  ppu_.Reset();
  memory::dma::Reset(&memory_);
  input1.latch_line_ = false;
  input2.latch_line_ = false;
  input1.latched_state_ = 0;
  input2.latched_state_ = 0;
  if (hard) memset(ram, 0, sizeof(ram));
  ram_adr_ = 0;
  memory_.set_h_pos(0);
  memory_.set_v_pos(0);
  frames_ = 0;
  cycles_ = 0;
  sync_cycle_ = 0;
  apu_catchup_cycles_ = 0.0;
  h_irq_enabled_ = false;
  v_irq_enabled_ = false;
  nmi_enabled_ = false;
  h_timer_ = 0x1ff * 4;
  v_timer_ = 0x1ff;
  in_nmi_ = false;
  irq_condition_ = false;
  in_irq_ = false;
  in_vblank_ = false;
  memset(port_auto_read_, 0, sizeof(port_auto_read_));
  auto_joy_read_ = false;
  auto_joy_timer_ = 0;
  ppu_latch_ = false;
  multiply_a_ = 0xff;
  multiply_result_ = 0xFE01;
  divide_a_ = 0xffFF;
  divide_result_ = 0x101;
  fast_mem_ = false;
  memory_.set_open_bus(0);
  next_horiz_event = 16;
  InitAccessTime(false);
}

void SNES::RunFrame() {
  while (in_vblank_) {
    cpu_.RunOpcode();
  }
  uint32_t frame = frames_;
  while (!in_vblank_ && frame == frames_) {
    cpu_.RunOpcode();
  }
}

void SNES::CatchUpApu() { apu_.RunCycles(cycles_); }

void SNES::HandleInput() {
  memset(port_auto_read_, 0, sizeof(port_auto_read_));
  // latch controllers
  input_latch(&input1, true);
  input_latch(&input2, true);
  input_latch(&input1, false);
  input_latch(&input2, false);
  for (int i = 0; i < 16; i++) {
    uint8_t val = input_read(&input1);
    port_auto_read_[0] |= ((val & 1) << (15 - i));
    port_auto_read_[2] |= (((val >> 1) & 1) << (15 - i));
    val = input_read(&input2);
    port_auto_read_[1] |= ((val & 1) << (15 - i));
    port_auto_read_[3] |= (((val >> 1) & 1) << (15 - i));
  }
}

void SNES::RunCycle() {
  cycles_ += 2;

  // check for h/v timer irq's
  bool condition = ((v_irq_enabled_ || h_irq_enabled_) &&
                    (memory_.v_pos() == v_timer_ || !v_irq_enabled_) &&
                    (memory_.h_pos() == h_timer_ || !h_irq_enabled_));

  if (!irq_condition_ && condition) {
    in_irq_ = true;
    cpu_.SetIrq(true);
  }
  irq_condition_ = condition;

  // increment position; must come after irq checks! (hagane, cybernator)
  memory_.set_h_pos(memory_.h_pos() + 2);

  // handle positional stuff
  if (memory_.h_pos() == next_horiz_event) {
    switch (memory_.h_pos()) {
      case 16: {
        next_horiz_event = 512;
        if (memory_.v_pos() == 0) memory_.init_hdma_request();
      } break;
      case 512: {
        next_horiz_event = 1104;
        // render the line halfway of the screen for better compatibility
        if (!in_vblank_ && memory_.v_pos() > 0) ppu_.RunLine(memory_.v_pos());
      } break;
      case 1104: {
        if (!in_vblank_) memory_.run_hdma_request();
        if (!memory_.pal_timing()) {
          // line 240 of odd frame with no interlace is 4 cycles shorter
          next_horiz_event = (memory_.v_pos() == 240 && !ppu_.even_frame &&
                              !ppu_.frame_interlace)
                                 ? 1360
                                 : 1364;
        } else {
          // line 311 of odd frame with interlace is 4 cycles longer
          next_horiz_event = (memory_.v_pos() != 311 || ppu_.even_frame ||
                              !ppu_.frame_interlace)
                                 ? 1364
                                 : 1368;
        }
      } break;
      case 1360:
      case 1364:
      case 1368: {  // this is the end (of the h-line)
        next_horiz_event = 16;

        memory_.set_h_pos(0);
        memory_.set_v_pos(memory_.v_pos() + 1);
        if (!memory_.pal_timing()) {
          // even interlace frame is 263 lines
          if ((memory_.v_pos() == 262 &&
               (!ppu_.frame_interlace || !ppu_.even_frame)) ||
              memory_.v_pos() == 263) {
            memory_.set_v_pos(0);
            frames_++;
          }
        } else {
          // even interlace frame is 313 lines
          if ((memory_.v_pos() == 312 &&
               (!ppu_.frame_interlace || !ppu_.even_frame)) ||
              memory_.v_pos() == 313) {
            memory_.set_v_pos(0);
            frames_++;
          }
        }

        // end of hblank, do most memory_.v_pos()-tests
        bool starting_vblank = false;
        if (memory_.v_pos() == 0) {
          // end of vblank
          in_vblank_ = false;
          in_nmi_ = false;
          ppu_.HandleFrameStart();
        } else if (memory_.v_pos() == 225) {
          // ask the ppu if we start vblank now or at memory_.v_pos() 240
          // (overscan)
          starting_vblank = !ppu_.CheckOverscan();
        } else if (memory_.v_pos() == 240) {
          // if we are not yet in vblank, we had an overscan frame, set
          // starting_vblank
          if (!in_vblank_) starting_vblank = true;
        }
        if (starting_vblank) {
          // catch up the apu at end of emulated frame (we end frame @ start of
          // vblank)
          CatchUpApu();
          // notify dsp of frame-end, because sometimes dma will extend much
          // further past vblank (or even into the next frame) Megaman X2
          // (titlescreen animation), Tales of Phantasia (game demo), Actraiser
          // 2 (fade-in @ bootup)
          apu_.dsp().NewFrame();
          // we are starting vblank
          ppu_.HandleVblank();
          in_vblank_ = true;
          in_nmi_ = true;
          if (auto_joy_read_) {
            // TODO: this starts a little after start of vblank
            auto_joy_timer_ = 4224;
            HandleInput();
          }
          if (nmi_enabled_) {
            cpu_.Nmi();
          }
        }
      } break;
    }
  }
  // handle auto_joy_read_-timer
  if (auto_joy_timer_ > 0) auto_joy_timer_ -= 2;
}

void SNES::RunCycles(int cycles) {
  if (memory_.h_pos() + cycles >= 536 && memory_.h_pos() < 536) {
    // if we go past 536, add 40 cycles for dram refersh
    cycles += 40;
  }
  for (int i = 0; i < cycles; i += 2) {
    RunCycle();
  }
}

void SNES::SyncCycles(bool start, int sync_cycles) {
  int count = 0;
  if (start) {
    sync_cycle_ = cycles_;
    count = sync_cycles - (cycles_ % sync_cycles);
  } else {
    count = sync_cycles - ((cycles_ - sync_cycle_) % sync_cycles);
  }
  RunCycles(count);
}

uint8_t SNES::ReadBBus(uint8_t adr) {
  if (adr < 0x40) {
    return ppu_.Read(adr, ppu_latch_);
  }
  if (adr < 0x80) {
    CatchUpApu();  // catch up the apu before reading
    return apu_.out_ports_[adr & 0x3];
  }
  if (adr == 0x80) {
    uint8_t ret = ram[ram_adr_++];
    ram_adr_ &= 0x1ffff;
    return ret;
  }
  return memory_.open_bus();
}

uint8_t SNES::ReadReg(uint16_t adr) {
  switch (adr) {
    case 0x4210: {
      uint8_t val = 0x2;  // CPU version (4 bit)
      val |= in_nmi_ << 7;
      in_nmi_ = false;
      return val | (memory_.open_bus() & 0x70);
    }
    case 0x4211: {
      uint8_t val = in_irq_ << 7;
      in_irq_ = false;
      cpu_.SetIrq(false);
      return val | (memory_.open_bus() & 0x7f);
    }
    case 0x4212: {
      uint8_t val = (auto_joy_timer_ > 0);
      val |= (memory_.h_pos() < 4 || memory_.h_pos() >= 1096) << 6;
      val |= in_vblank_ << 7;
      return val | (memory_.open_bus() & 0x3e);
    }
    case 0x4213: {
      return ppu_latch_ << 7;  // IO-port
    }
    case 0x4214: {
      return divide_result_ & 0xff;
    }
    case 0x4215: {
      return divide_result_ >> 8;
    }
    case 0x4216: {
      return multiply_result_ & 0xff;
    }
    case 0x4217: {
      return multiply_result_ >> 8;
    }
    case 0x4218:
    case 0x421a:
    case 0x421c:
    case 0x421e: {
      return port_auto_read_[(adr - 0x4218) / 2] & 0xff;
    }
    case 0x4219:
    case 0x421b:
    case 0x421d:
    case 0x421f: {
      return port_auto_read_[(adr - 0x4219) / 2] >> 8;
    }
    default: {
      return memory_.open_bus();
    }
  }
}

uint8_t SNES::Rread(uint32_t adr) {
  uint8_t bank = adr >> 16;
  adr &= 0xffff;
  if (bank == 0x7e || bank == 0x7f) {
    return ram[((bank & 1) << 16) | adr];  // ram
  }
  if (bank < 0x40 || (bank >= 0x80 && bank < 0xc0)) {
    if (adr < 0x2000) {
      return ram[adr];  // ram mirror
    }
    if (adr >= 0x2100 && adr < 0x2200) {
      return ReadBBus(adr & 0xff);  // B-bus
    }
    if (adr == 0x4016) {
      return input_read(&input1) | (memory_.open_bus() & 0xfc);
    }
    if (adr == 0x4017) {
      return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c;
    }
    if (adr >= 0x4200 && adr < 0x4220) {
      return ReadReg(adr);  // internal registers
    }
    if (adr >= 0x4300 && adr < 0x4380) {
      return memory::dma::Read(&memory_, adr);  // dma registers
    }
  }
  // read from cart
  return memory_.cart_read(bank, adr);
}

uint8_t SNES::Read(uint32_t adr) {
  uint8_t val = Rread(adr);
  memory_.set_open_bus(val);
  return val;
}

void SNES::WriteBBus(uint8_t adr, uint8_t val) {
  if (adr < 0x40) {
    ppu_.Write(adr, val);
    return;
  }
  if (adr < 0x80) {
    CatchUpApu();  // catch up the apu before writing
    apu_.in_ports_[adr & 0x3] = val;
    return;
  }
  switch (adr) {
    case 0x80: {
      ram[ram_adr_++] = val;
      ram_adr_ &= 0x1ffff;
      break;
    }
    case 0x81: {
      ram_adr_ = (ram_adr_ & 0x1ff00) | val;
      break;
    }
    case 0x82: {
      ram_adr_ = (ram_adr_ & 0x100ff) | (val << 8);
      break;
    }
    case 0x83: {
      ram_adr_ = (ram_adr_ & 0x0ffff) | ((val & 1) << 16);
      break;
    }
  }
}

void SNES::WriteReg(uint16_t adr, uint8_t val) {
  switch (adr) {
    case 0x4200: {
      auto_joy_read_ = val & 0x1;
      if (!auto_joy_read_) auto_joy_timer_ = 0;
      h_irq_enabled_ = val & 0x10;
      v_irq_enabled_ = val & 0x20;
      if (!h_irq_enabled_ && !v_irq_enabled_) {
        in_irq_ = false;
        cpu_.SetIrq(false);
      }
      // if nmi is enabled while in_nmi_ is still set, immediately generate nmi
      if (!nmi_enabled_ && (val & 0x80) && in_nmi_) {
        cpu_.Nmi();
      }
      nmi_enabled_ = val & 0x80;
      cpu_.set_int_delay(true);
      break;
    }
    case 0x4201: {
      if (!(val & 0x80) && ppu_latch_) {
        // latch the ppu
        ppu_.LatchHV();
      }
      ppu_latch_ = val & 0x80;
      break;
    }
    case 0x4202: {
      multiply_a_ = val;
      break;
    }
    case 0x4203: {
      multiply_result_ = multiply_a_ * val;
      break;
    }
    case 0x4204: {
      divide_a_ = (divide_a_ & 0xff00) | val;
      break;
    }
    case 0x4205: {
      divide_a_ = (divide_a_ & 0x00ff) | (val << 8);
      break;
    }
    case 0x4206: {
      if (val == 0) {
        divide_result_ = 0xffff;
        multiply_result_ = divide_a_;
      } else {
        divide_result_ = divide_a_ / val;
        multiply_result_ = divide_a_ % val;
      }
      break;
    }
    case 0x4207: {
      h_timer_ = (h_timer_ & 0x100) | val;
      break;
    }
    case 0x4208: {
      h_timer_ = (h_timer_ & 0x0ff) | ((val & 1) << 8);
      break;
    }
    case 0x4209: {
      v_timer_ = (v_timer_ & 0x100) | val;
      break;
    }
    case 0x420a: {
      v_timer_ = (v_timer_ & 0x0ff) | ((val & 1) << 8);
      break;
    }
    case 0x420b: {
      memory::dma::StartDma(&memory_, val, false);
      break;
    }
    case 0x420c: {
      memory::dma::StartDma(&memory_, val, true);
      break;
    }
    case 0x420d: {
      fast_mem_ = val & 0x1;
      break;
    }
    default: {
      break;
    }
  }
}

void SNES::Write(uint32_t adr, uint8_t val) {
  memory_.set_open_bus(val);
  uint8_t bank = adr >> 16;
  adr &= 0xffff;
  if (bank == 0x7e || bank == 0x7f) {
    ram[((bank & 1) << 16) | adr] = val;  // ram
  }
  if (bank < 0x40 || (bank >= 0x80 && bank < 0xc0)) {
    if (adr < 0x2000) {
      ram[adr] = val;  // ram mirror
    }
    if (adr >= 0x2100 && adr < 0x2200) {
      WriteBBus(adr & 0xff, val);  // B-bus
    }
    if (adr == 0x4016) {
      input_latch(&input1, val & 1);  // input latch
      input_latch(&input2, val & 1);
    }
    if (adr >= 0x4200 && adr < 0x4220) {
      WriteReg(adr, val);  // internal registers
    }
    if (adr >= 0x4300 && adr < 0x4380) {
      memory::dma::Write(&memory_, adr, val);  // dma registers
    }
  }

  // write to cart
  memory_.cart_write(bank, adr, val);
}

int SNES::GetAccessTime(uint32_t adr) {
  uint8_t bank = adr >> 16;
  adr &= 0xffff;
  if ((bank < 0x40 || (bank >= 0x80 && bank < 0xc0)) && adr < 0x8000) {
    // 00-3f,80-bf:0-7fff
    if (adr < 0x2000 || adr >= 0x6000) return 8;  // 0-1fff, 6000-7fff
    if (adr < 0x4000 || adr >= 0x4200) return 6;  // 2000-3fff, 4200-5fff
    return 12;                                    // 4000-41ff
  }
  // 40-7f,co-ff:0000-ffff, 00-3f,80-bf:8000-ffff
  return (fast_mem_ && bank >= 0x80) ? 6
                                     : 8;  // depends on setting in banks 80+
}

uint8_t SNES::CpuRead(uint32_t adr) {
  cpu_.set_int_delay(false);
  const int cycles = access_time[adr] - 4;
  memory::dma::HandleDma(this, &memory_, cycles);
  RunCycles(cycles);
  uint8_t rv = Read(adr);
  memory::dma::HandleDma(this, &memory_, 4);
  RunCycles(4);
  return rv;
}

void SNES::CpuWrite(uint32_t adr, uint8_t val) {
  cpu_.set_int_delay(false);
  const int cycles = access_time[adr];
  memory::dma::HandleDma(this, &memory_, cycles);
  RunCycles(cycles);
  Write(adr, val);
}

void SNES::CpuIdle(bool waiting) {
  cpu_.set_int_delay(false);
  memory::dma::HandleDma(this, &memory_, 6);
  RunCycles(6);
}

void SNES::SetSamples(int16_t* sample_data, int wanted_samples) {
  apu_.dsp().GetSamples(sample_data, wanted_samples, memory_.pal_timing());
}

void SNES::SetPixels(uint8_t* pixel_data) { ppu_.PutPixels(pixel_data); }

void SNES::SetButtonState(int player, int button, bool pressed) {
  // set key in controller
  if (player == 1) {
    if (pressed) {
      input1.current_state_ |= 1 << button;
    } else {
      input1.current_state_ &= ~(1 << button);
    }
  } else {
    if (pressed) {
      input2.current_state_ |= 1 << button;
    } else {
      input2.current_state_ &= ~(1 << button);
    }
  }
}

void SNES::InitAccessTime(bool recalc) {
  int start = (recalc) ? 0x800000 : 0;  // recalc only updates fast rom
  access_time.resize(0x1000000);
  for (int i = start; i < 0x1000000; i++) {
    access_time[i] = GetAccessTime(i);
  }
}

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