#include <ares/ares.hpp>
#include "hg51b.hpp"

namespace ares {

#include "registers.cpp"
#include "instruction.cpp"
#include "instructions.cpp"
#include "serialization.cpp"
#include "disassembler.cpp"

auto HG51B::lock() -> void {
  io.lock = 1;
}

auto HG51B::halt() -> void {
  io.halt = 1;
}

auto HG51B::wait(n24 address) -> u32 {
  if(isROM(address)) return 1 + io.wait.rom;
  if(isRAM(address)) return 1 + io.wait.ram;
  return 1;
}

auto HG51B::main() -> void {
  if(io.lock) return step(1);
  if(io.suspend.enable) return suspend();
  if(io.cache.enable) return cache(), void();
  if(io.dma.enable) return dma();
  if(io.halt) return step(1);
  return execute();
}

auto HG51B::step(u32 clocks) -> void {
  if(io.bus.enable) {
    if(io.bus.pending > clocks) {
      io.bus.pending -= clocks;
    } else {
      io.bus.enable = 0;
      io.bus.pending = 0;
      if(io.bus.reading) io.bus.reading = 0, r.mdr = read(io.bus.address);
      if(io.bus.writing) io.bus.writing = 0, write(io.bus.address, r.mdr);
    }
  }
}

auto HG51B::execute() -> void {
  if(!cache()) return halt();
  auto opcode = programRAM[io.cache.page][r.pc];
  advance();
  step(1);
  instructionTable[opcode]();
}

auto HG51B::advance() -> void {
  if(++r.pc == 0) {
    if(io.cache.page == 1) return halt();
    io.cache.page = 1;
    if(io.cache.lock[io.cache.page]) return halt();
    r.pb = r.p;
    if(!cache()) return halt();
  }
}

auto HG51B::suspend() -> void {
  if(!io.suspend.duration) return step(1);  //indefinite
  step(io.suspend.duration);
  io.suspend.duration = 0;
  io.suspend.enable = 0;
}

auto HG51B::cache() -> bool {
  n24 address = io.cache.base + r.pb * 512;

  //try to use the current page ...
  if(io.cache.address[io.cache.page] == address) return io.cache.enable = 0, true;
  //if it's not valid, try to use the other page ...
  io.cache.page ^= 1;
  if(io.cache.address[io.cache.page] == address) return io.cache.enable = 0, true;
  //if it's not valid, try to load into the other page ...
  if(io.cache.lock[io.cache.page]) io.cache.page ^= 1;
  //if it's locked, try to load into the first page ...
  if(io.cache.lock[io.cache.page]) return io.cache.enable = 0, false;

  io.cache.address[io.cache.page] = address;
  for(u32 offset : range(256)) {
    step(wait(address));
    programRAM[io.cache.page][offset].byte(0) = read(address++);
    programRAM[io.cache.page][offset].byte(1) = read(address++);
  }
  return io.cache.enable = 0, true;
}

auto HG51B::dma() -> void {
  for(u32 offset : range(io.dma.length)) {
    n24 source = io.dma.source + offset;
    n24 target = io.dma.target + offset;

    if(isROM(source) && isROM(target)) return lock();
    if(isRAM(source) && isRAM(target)) return lock();

    step(wait(source));
    auto data = read(source);

    step(wait(target));
    write(target, data);
  }

  io.dma.enable = 0;
}

auto HG51B::running() const -> bool {
  return io.cache.enable || io.dma.enable || io.bus.pending || !io.halt;
}

auto HG51B::busy() const -> bool {
  return io.cache.enable || io.dma.enable || io.bus.pending;
}

auto HG51B::power() -> void {
  r = {};
  io = {};
}

}
