#include <gb/gb.hpp>

namespace GameBoy {

PPU ppu;
#include "io.cpp"
#include "dmg.cpp"
#include "cgb.cpp"
#include "serialization.cpp"

auto PPU::Enter() -> void {
  while(true) scheduler.synchronize(), ppu.main();
}

auto PPU::main() -> void {
  if(!status.displayEnable) {
    for(uint n : range(160 * 144)) screen[n] = Model::GameBoy() ? 0 : 0x7fff;
    Thread::step(154 * 456);
    synchronize(cpu);
    scheduler.exit(Scheduler::Event::Frame);
    return;
  }

  status.lx = 0;
  if(Model::SuperGameBoy()) superGameBoy->lcdScanline();

  if(status.ly <= 143) {
    status.mode = 2;
    scanline();
    step(92);

    status.mode = 3;
    for(auto n : range(160)) {
      run();
      step(1);
    }

    status.mode = 0;
    cpu.hblank();
    step(204);
  } else {
    status.mode = 1;
    step(456);
  }

  status.ly++;

  if(status.ly == 144) {
    cpu.raise(CPU::Interrupt::Vblank);
    scheduler.exit(Scheduler::Event::Frame);
  }

  if(status.ly == 154) {
    status.ly = 0;
  }
}

auto PPU::stat() -> void {
  bool irq = status.irq;

  status.irq  = status.interruptHblank && status.mode == 0;
  status.irq |= status.interruptVblank && status.mode == 1;
  status.irq |= status.interruptOAM    && status.mode == 2;
  status.irq |= status.interruptLYC    && coincidence();

  if(!irq && status.irq) cpu.raise(CPU::Interrupt::Stat);
}

auto PPU::coincidence() -> bool {
  uint ly = status.ly;
  if(ly == 153 && status.lx >= 92) ly = 0;  //LYC=0 triggers early during LY=153
  return ly == status.lyc;
}

auto PPU::refresh() -> void {
  if(!Model::SuperGameBoy()) Emulator::video.refresh(screen, 160 * sizeof(uint32), 160, 144);
}

auto PPU::step(uint clocks) -> void {
  while(clocks--) {
    stat();
    if(status.dmaActive) {
      uint hi = status.dmaClock++;
      uint lo = hi & (cpu.status.speedDouble ? 1 : 3);
      hi >>= cpu.status.speedDouble ? 1 : 2;
      if(lo == 0) {
        if(hi == 0) {
          //warm-up
        } else if(hi == 161) {
          //cool-down; disable
          status.dmaActive = false;
        } else {
          oam[hi - 1] = bus.read(status.dmaBank << 8 | hi - 1);
        }
      }
    }

    status.lx++;
    Thread::step(1);
    synchronize(cpu);
  }
}

auto PPU::hflip(uint data) const -> uint {
  return (data & 0x8080) >> 7 | (data & 0x4040) >> 5
       | (data & 0x2020) >> 3 | (data & 0x1010) >> 1
       | (data & 0x0808) << 1 | (data & 0x0404) << 3
       | (data & 0x0202) << 5 | (data & 0x0101) << 7;
}

auto PPU::power() -> void {
  create(Enter, 4 * 1024 * 1024);

  if(Model::GameBoyColor()) {
    scanline = {&PPU::scanlineCGB, this};
    run = {&PPU::runCGB, this};
  } else {
    scanline = {&PPU::scanlineDMG, this};
    run = {&PPU::runDMG, this};
  }

  for(uint n = 0x8000; n <= 0x9fff; n++) bus.mmio[n] = this;  //VRAM
  for(uint n = 0xfe00; n <= 0xfe9f; n++) bus.mmio[n] = this;  //OAM

  bus.mmio[0xff40] = this;  //LCDC
  bus.mmio[0xff41] = this;  //STAT
  bus.mmio[0xff42] = this;  //SCY
  bus.mmio[0xff43] = this;  //SCX
  bus.mmio[0xff44] = this;  //LY
  bus.mmio[0xff45] = this;  //LYC
  bus.mmio[0xff46] = this;  //DMA
  bus.mmio[0xff47] = this;  //BGP
  bus.mmio[0xff48] = this;  //OBP0
  bus.mmio[0xff49] = this;  //OBP1
  bus.mmio[0xff4a] = this;  //WY
  bus.mmio[0xff4b] = this;  //WX

  if(Model::GameBoyColor()) {
  bus.mmio[0xff4f] = this;  //VBK
  bus.mmio[0xff68] = this;  //BGPI
  bus.mmio[0xff69] = this;  //BGPD
  bus.mmio[0xff6a] = this;  //OBPI
  bus.mmio[0xff6b] = this;  //OBPD
  }

  for(auto& n : vram) n = 0x00;
  for(auto& n : oam) n = 0x00;
  for(auto& n : bgp) n = 0x00;
  for(auto& n : obp[0]) n = 3;
  for(auto& n : obp[1]) n = 3;
  for(auto& n : bgpd) n = 0x0000;
  for(auto& n : obpd) n = 0x0000;

  memory::fill(&status, sizeof(Status));

  for(auto& n : screen) n = 0;

  bg.color = 0;
  bg.palette = 0;
  bg.priority = 0;

  ob.color = 0;
  ob.palette = 0;
  ob.priority = 0;

  for(auto& s : sprite) {
    s.x = 0;
    s.y = 0;
    s.tile = 0;
    s.attr = 0;
    s.data = 0;
  }
  sprites = 0;

  background.attr = 0;
  background.data = 0;

  window.attr = 0;
  window.data = 0;
}

}
