#ifndef YAZE_APP_EMU_CPU_H_
#define YAZE_APP_EMU_CPU_H_

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <unordered_map>
#include <vector>

#include "app/core/common.h"
#include "app/emu/cpu/clock.h"
#include "app/emu/cpu/internal/opcodes.h"
#include "app/emu/debug/log.h"
#include "app/emu/memory/memory.h"

namespace yaze {
namespace app {
namespace emu {

class InstructionEntry {
 public:
  // Constructor
  InstructionEntry(uint32_t addr, uint8_t op, const std::string& ops,
                   const std::string& instr)
      : address(addr), opcode(op), operands(ops), instruction(instr) {}

  // Getters for the class members
  uint32_t GetAddress() const { return address; }
  uint8_t GetOpcode() const { return opcode; }
  const std::string& GetOperands() const { return operands; }
  const std::string& GetInstruction() const { return instruction; }

  uint32_t address;         // Memory address of the instruction
  uint8_t opcode;           // Opcode of the instruction
  std::string operands;     // Operand(s) of the instruction, if any
  std::string instruction;  // Human-readable instruction text
};

class Cpu : public Loggable, public core::ExperimentFlags {
 public:
  explicit Cpu(memory::Memory& mem, Clock& vclock,
               memory::CpuCallbacks& callbacks)
      : memory(mem), clock(vclock), callbacks_(callbacks) {}
  void Reset(bool hard = false);

  void RunOpcode();

  void ExecuteInstruction(uint8_t opcode);
  void LogInstructions(uint16_t PC, uint8_t opcode, uint16_t operand,
                       bool immediate, bool accumulator_mode);

  void UpdatePC(uint8_t instruction_length) { PC += instruction_length; }
  void UpdateClock(int delta_time) { clock.UpdateClock(delta_time); }

  void SetIrq(bool state) { irq_wanted_ = state; }
  void Nmi() { nmi_wanted_ = true; }

  uint8_t GetInstructionLength(uint8_t opcode);

  std::vector<uint32_t> breakpoints_;
  std::vector<InstructionEntry> instruction_log_;

  // ======================================================
  // Interrupt Vectors
  // Emulation mode, e = 1      Native mode, e = 0
  //
  // 0xFFFE,FF - IRQ/BRK        0xFFEE,EF  - IRQ
  // 0xFFFC,FD - RESET
  // 0xFFFA,FB - NMI            0xFFEA,EB  - NMI
  // 0xFFF8,F9 - ABORT          0xFFE8,E9  - ABORT
  //                            0xFFE6,E7  - BRK
  // 0xFFF4,F5 - COP            0xFFE4,E5  - COP
  void DoInterrupt();

  // ======================================================
  // Registers

  uint16_t A = 0;               // Accumulator
  uint16_t X = 0;               // X index register
  uint16_t Y = 0;               // Y index register
  uint16_t D = 0;               // Direct Page register
  uint8_t DB = 0;               // Data Bank register
  uint8_t PB = 0;               // Program Bank register
  uint16_t PC = 0;              // Program Counter
  uint8_t E = 1;                // Emulation mode flag
  uint8_t status = 0b00110000;  // Processor Status (P)

  // Mnemonic 	Value 	Binary 	Description
  // N 	      #$80 	10000000 	Negative
  // V 	      #$40 	01000000 	Overflow
  // M 	      #$20 	00100000 	Accumulator size (0 = 16-bit, 1 = 8-bit)
  // X 	      #$10 	00010000 	Index size (0 = 16-bit, 1 = 8-bit)
  // D 	      #$08 	00001000 	Decimal
  // I 	      #$04 	00000100 	IRQ disable
  // Z 	      #$02 	00000010 	Zero
  // C 	      #$01 	00000001 	Carry
  // E 			                  6502 emulation mode
  // B 	      #$10 	00010000 	Break (emulation mode only)

  void SetFlags(uint8_t val) {
    status = val;
    if (E) {
      SetAccumulatorSize(true);
      SetIndexSize(true);
      SetSP(SP() & 0xFF | 0x100);
    }
    if (GetIndexSize()) {
      X &= 0xff;
      Y &= 0xff;
    }
  }

  void SetZN(uint16_t value, bool byte) {
    if (byte) {
      SetZeroFlag((value & 0xff) == 0);
      SetNegativeFlag(value & 0x80);
    } else {
      SetZeroFlag(value == 0);
      SetNegativeFlag(value & 0x8000);
    }
  }

  // Setting flags in the status register
  bool m() { return GetAccumulatorSize() ? 1 : 0; }
  bool xf() { return GetIndexSize() ? 1 : 0; }
  int GetAccumulatorSize() const { return status & 0x20; }
  int GetIndexSize() const { return status & 0x10; }
  void SetAccumulatorSize(bool set) { SetFlag(0x20, set); }
  void SetIndexSize(bool set) { SetFlag(0x10, set); }

  // Set individual flags
  void SetNegativeFlag(bool set) { SetFlag(0x80, set); }
  void SetOverflowFlag(bool set) { SetFlag(0x40, set); }
  void SetBreakFlag(bool set) { SetFlag(0x10, set); }
  void SetDecimalFlag(bool set) { SetFlag(0x08, set); }
  void SetInterruptFlag(bool set) { SetFlag(0x04, set); }
  void SetZeroFlag(bool set) { SetFlag(0x02, set); }
  void SetCarryFlag(bool set) { SetFlag(0x01, set); }

  // Get individual flags
  bool GetNegativeFlag() const { return GetFlag(0x80); }
  bool GetOverflowFlag() const { return GetFlag(0x40); }
  bool GetBreakFlag() const { return GetFlag(0x10); }
  bool GetDecimalFlag() const { return GetFlag(0x08); }
  bool GetInterruptFlag() const { return GetFlag(0x04); }
  bool GetZeroFlag() const { return GetFlag(0x02); }
  bool GetCarryFlag() const { return GetFlag(0x01); }

  enum class AccessType { Control, Data };

  uint8_t ReadOpcode() { return ReadByte((PB << 16) | PC++); }

  uint16_t ReadOpcodeWord(bool int_check = false) {
    uint8_t value = ReadOpcode();
    if (int_check) CheckInt();
    return value | (ReadOpcode() << 8);
  }

  // Memory access routines
  uint8_t ReadByte(uint32_t address) { return callbacks_.read_byte(address); }
  uint16_t ReadWord(uint32_t address, uint32_t address_high,
                    bool int_check = false) {
    uint8_t value = ReadByte(address);
    if (int_check) CheckInt();
    uint8_t value2 = ReadByte(address_high);
    return value | (value2 << 8);
  }
  uint32_t ReadWordLong(uint32_t address) {
    uint8_t value = ReadByte(address);
    uint8_t value2 = ReadByte(address + 1);
    uint8_t value3 = ReadByte(address + 2);
    return value | (value2 << 8) | (value3 << 16);
  }

  void WriteByte(uint32_t address, uint8_t value) {
    callbacks_.write_byte(address, value);
  }

  void WriteWord(uint32_t address, uint32_t address_high, uint16_t value,
                 bool reversed = false, bool int_check = false) {
    if (reversed) {
      callbacks_.write_byte(address_high, value >> 8);
      if (int_check) CheckInt();
      callbacks_.write_byte(address, value & 0xFF);
    } else {
      callbacks_.write_byte(address, value & 0xFF);
      if (int_check) CheckInt();
      callbacks_.write_byte(address_high, value >> 8);
    }
  }
  void WriteLong(uint32_t address, uint32_t value) {
    callbacks_.write_byte(address, value & 0xFF);
    callbacks_.write_byte(address + 1, (value >> 8) & 0xFF);
    callbacks_.write_byte(address + 2, value >> 16);
  }

  void PushByte(uint8_t value) {
    callbacks_.write_byte(SP(), value);
    SetSP(SP() - 1);
    if (E) SetSP((SP() & 0xff) | 0x100);
  }
  void PushWord(uint16_t value, bool int_check = false) {
    PushByte(value >> 8);
    if (int_check) CheckInt();
    PushByte(value & 0xFF);
  }
  void PushLong(uint32_t value) {  // Push 24-bit value
    PushByte(value >> 16);
    PushWord(value & 0xFFFF);
  }

  uint8_t PopByte() {
    SetSP(SP() + 1);
    if (E) SetSP((SP() & 0xff) | 0x100);
    return ReadByte(SP());
  }
  uint16_t PopWord(bool int_check = false) {
    uint8_t low = PopByte();
    if (int_check) CheckInt();
    return low | (PopByte() << 8);
  }
  uint32_t PopLong() {  // Pop 24-bit value
    uint32_t low = PopWord();
    uint32_t high = PopByte();
    return (high << 16) | low;
  }

  void DoBranch(bool check) {
    if (!check) CheckInt();
    uint8_t value = ReadOpcode();
    if (check) {
      CheckInt();
      callbacks_.idle(false);  // taken branch: 1 extra cycle
      PC += (int8_t)value;
    }
  }

  void set_int_delay(bool delay) { int_delay_ = delay; }

  // Addressing Modes

  // Effective Address:
  //    Bank: Data Bank Register if locating data
  //          Program Bank Register if transferring control
  //    High: Second operand byte
  //    Low:  First operand byte
  //
  // LDA addr
  uint32_t Absolute(uint32_t* low);

  // Effective Address:
  //    The Data Bank Register is concatened with the 16-bit operand
  //    the 24-bit result is added to the X Index Register
  //    based on the emulation mode (16:X=0, 8:X=1)
  //
  // LDA addr, X
  uint32_t AbsoluteIndexedX();
  uint32_t AdrAbx(uint32_t* low, bool write);

  // Effective Address:
  //    The Data Bank Register is concatened with the 16-bit operand
  //    the 24-bit result is added to the Y Index Register
  //    based on the emulation mode (16:Y=0, 8:Y=1)
  //
  // LDA addr, Y
  uint32_t AbsoluteIndexedY();
  uint32_t AdrAby(uint32_t* low, bool write);

  void AdrImp();
  uint32_t AdrIdx(uint32_t* low);

  uint32_t AdrIdp(uint32_t* low);
  uint32_t AdrIdy(uint32_t* low, bool write);
  uint32_t AdrIdl(uint32_t* low);
  uint32_t AdrIly(uint32_t* low);
  uint32_t AdrIsy(uint32_t* low);
  uint32_t Immediate(uint32_t* low, bool xFlag);

  // Effective Address:
  //    Bank:             Program Bank Register (PBR)
  //    High/low:         The Indirect Address
  //    Indirect Address: Located in the Program Bank at the sum of
  //                      the operand double byte and X based on the
  //                      emulation mode
  // JMP (addr, X)
  uint16_t AbsoluteIndexedIndirect();

  // Effective Address:
  //    Bank:             Program Bank Register (PBR)
  //    High/low:         The Indirect Address
  //    Indirect Address: Located in Bank Zero, at the operand double byte
  //
  // JMP (addr)
  uint16_t AbsoluteIndirect();

  // Effective Address:
  //   Bank/High/Low: The 24-bit Indirect Address
  //   Indirect Address: Located in Bank Zero, at the operand double byte
  //
  // JMP [addr]
  uint32_t AbsoluteIndirectLong();

  // Effective Address:
  //    Bank: Third operand byte
  //    High: Second operand byte
  //    Low:  First operand byte
  //
  // LDA long
  uint32_t AbsoluteLong();
  uint32_t AdrAbl(uint32_t* low);

  // Effective Address:
  //   The 24-bit operand is added to X based on the emulation mode
  //
  // LDA long, X
  uint32_t AbsoluteLongIndexedX();
  uint32_t AdrAlx(uint32_t* low);

  // Source Effective Address:
  //    Bank: Second operand byte
  //    High/Low: The 16-bit value in X, if X is 8-bit high byte is 0
  //
  // Destination Effective Address:
  //    Bank: First operand byte
  //    High/Low: The 16-bit value in Y, if Y is 8-bit high byte is 0
  //
  // Length:
  //    The number of bytes to be moved: 16-bit value in Acculumator C plus 1.
  //
  // MVN src, dst
  void BlockMove(uint16_t source, uint16_t dest, uint16_t length);

  // Effective Address:
  //    Bank:     Zero
  //    High/low: Direct Page Register plus operand byte
  //
  // LDA dp
  uint16_t DirectPage();
  uint32_t AdrDp(uint32_t* low);

  // Effective Address:
  //    Bank:     Zero
  //    High/low: Direct Page Register plus operand byte plus X
  //              based on the emulation mode
  //
  // LDA dp, X
  uint16_t DirectPageIndexedX();
  uint32_t AdrDpx(uint32_t* low);

  // Effective Address:
  //    Bank:     Zero
  //    High/low: Direct Page Register plus operand byte plus Y
  //              based on the emulation mode
  // LDA dp, Y
  uint16_t DirectPageIndexedY();
  uint32_t AdrDpy(uint32_t* low);

  // Effective Address:
  // Bank:      Data bank register
  // High/low:  The indirect address
  // Indirect Address: Located in the direct page at the sum of the direct page
  // register, the operand byte, and X based on the emulation mode in bank zero.
  //
  // LDA (dp, X)
  uint16_t DirectPageIndexedIndirectX();

  // Effective Address:
  // Bank:     Data bank register
  // High/low: The 16-bit indirect address
  // Indirect Address: The operand byte plus the direct page register in bank
  // zero.
  //
  // LDA (dp)
  uint16_t DirectPageIndirect();

  // Effective Address:
  //    Bank/High/Low:    The 24-bit indirect address
  //    Indirect address: The operand byte plus the direct page
  //                   register in bank zero.
  //
  // LDA [dp]
  uint32_t DirectPageIndirectLong();

  // Effective Address:
  //    Found by concatenating the data bank to the double-byte
  //    indirect address, then adding Y based on the emulation mode.
  //
  // Indirect Address: Located in the Direct Page at the sum of the direct page
  //                   register and the operand byte, in bank zero.
  //
  // LDA (dp), Y
  uint16_t DirectPageIndirectIndexedY();

  // Effective Address:
  //    Found by adding to the triple-byte indirect address Y based on the
  //    emulation mode. Indrect Address: Located in the Direct Page at the sum
  //    of the direct page register and the operand byte in bank zero.
  // Indirect Address:
  //    Located in the Direct Page at the sum of the direct page register and
  //    the operand byte in bank zero.
  //
  // LDA (dp), Y
  uint32_t DirectPageIndirectLongIndexedY();

  // 8-bit data: Data Operand Byte
  // 16-bit data 65816 native mode m or x = 0
  //   Data High: Second Operand Byte
  //   Data Low:  First Operand Byte
  //
  // LDA #const
  uint16_t Immediate(bool index_size = false);

  uint16_t StackRelative();
  uint32_t AdrSr(uint32_t* low);

  // Effective Address:
  //    The Data Bank Register is concatenated to the Indirect Address;
  //    the 24-bit result is added to Y (16 bits if x = 0; else 8 bits)
  // Indirect Address:
  //    Located at the 16-bit sum of the 8-bit operand and the 16-bit stack
  //    pointer
  //
  // LDA (sr, S), Y
  uint32_t StackRelativeIndirectIndexedY();

  // ======================================================
  // Instructions

  // ADC: Add with carry
  void ADC(uint16_t operand);

  // AND: Logical AND
  void AND(uint32_t address, bool immediate = false);
  void ANDAbsoluteLong(uint32_t address);

  // ASL: Arithmetic shift left
  void ASL(uint16_t address);

  // BCC: Branch if carry clear
  void BCC(int8_t offset);

  // BCS: Branch if carry set
  void BCS(int8_t offset);

  // BEQ: Branch if equal
  void BEQ(int8_t offset);

  // BIT: Bit test
  void BIT(uint16_t address);

  // BMI: Branch if minus
  void BMI(int8_t offset);

  // BNE: Branch if not equal
  void BNE(int8_t offset);

  // BPL: Branch if plus
  void BPL(int8_t offset);

  // BRA: Branch always
  void BRA(int8_t offset);

  // BRK: Force interrupt
  void BRK();

  // BRL: Branch always long
  void BRL(int16_t offset);

  // BVC: Branch if overflow clear
  void BVC(int8_t offset);

  // BVS: Branch if overflow set
  void BVS(int8_t offset);

  // CLC: Clear carry flag
  void CLC();

  // CLD: Clear decimal mode
  void CLD();

  // CLI: Clear interrupt disable bit
  void CLI();

  // CLV: Clear overflow flag
  void CLV();

  // CMP: Compare
  void CMP(uint32_t address, bool immediate = false);

  // COP: Coprocessor enable
  void COP();

  // CPX: Compare X register
  void CPX(uint32_t address, bool immediate = false);

  // CPY: Compare Y register
  void CPY(uint32_t address, bool immediate = false);

  // DEC: Decrement memory
  void DEC(uint32_t address, bool accumulator = false);

  // DEX: Decrement X register
  void DEX();

  // DEY: Decrement Y register
  void DEY();

  // EOR: Exclusive OR
  void EOR(uint32_t address, bool immediate = false);

  // INC: Increment memory
  void INC(uint32_t address, bool accumulator = false);

  // INX: Increment X register
  void INX();

  // INY: Increment Y register
  void INY();

  // JMP: Jump
  void JMP(uint16_t address);

  // JML: Jump long
  void JML(uint16_t address);

  // JSR: Jump to subroutine
  void JSR(uint16_t address);

  // JSL: Jump to subroutine long
  void JSL(uint16_t address);

  // LDA: Load accumulator
  void LDA(uint16_t address, bool immediate = false, bool direct_page = false,
           bool data_bank = false);

  // LDX: Load X register
  void LDX(uint16_t address, bool immediate = false);

  // LDY: Load Y register
  void LDY(uint16_t address, bool immediate = false);

  // LSR: Logical shift right
  void LSR(uint16_t address, bool accumulator = false);

  // MVN: Block move next
  void MVN();

  // MVP: Block move previous
  void MVP();

  // NOP: No operation
  void NOP();

  // ORA: Logical inclusive OR
  void ORA(uint32_t low, uint32_t high);

  // PEA: Push effective absolute address
  void PEA();

  // PEI: Push effective indirect address
  void PEI();

  // PER: Push effective relative address
  void PER();

  // PHA: Push accumulator
  void PHA();

  // PHB: Push data bank register
  void PHB();

  // PHD: Push direct page register
  void PHD();

  // PHK: Push program bank register
  void PHK();

  // PHP: Push processor status (flags)
  void PHP();

  // PHX: Push X register
  void PHX();

  // PHY: Push Y register
  void PHY();

  // PLA: Pull accumulator
  void PLA();

  // PLB: Pull data bank register
  void PLB();

  // PLD: Pull direct page register
  void PLD();

  // PLP: Pull processor status (flags)
  void PLP();

  // PLX: Pull X register
  void PLX();

  // PLY: Pull Y register
  void PLY();

  // REP: Reset processor status bits
  void REP();

  // ROL: Rotate left
  void ROL(uint32_t address, bool accumulator = false);

  // ROR: Rotate right
  void ROR(uint32_t address, bool accumulator = false);

  // RTI: Return from interrupt
  void RTI();

  // RTL: Return from subroutine long
  void RTL();

  // RTS: Return from subroutine
  void RTS();

  // SBC: Subtract with carry
  void SBC(uint32_t operand, bool immediate = false);

  // SEC: Set carry flag
  void SEC();

  // SED: Set decimal mode
  void SED();

  // SEI: Set interrupt disable status
  void SEI();

  // SEP: Set processor status bits
  void SEP();

  // STA: Store accumulator
  void STA(uint32_t address);

  // STP: Stop the processor
  void STP();

  // STX: Store X register
  void STX(uint16_t address);

  // STY: Store Y register
  void STY(uint16_t address);

  // STZ: Store zero
  void STZ(uint16_t address);

  // TAX: Transfer accumulator to X
  void TAX();

  // TAY: Transfer accumulator to Y
  void TAY();

  // TCD: Transfer 16-bit accumulator to direct page register
  void TCD();

  // TCS: Transfer 16-bit accumulator to stack pointer
  void TCS();

  // TDC: Transfer direct page register to 16-bit accumulator
  void TDC();

  // TRB: Test and reset bits
  void TRB(uint16_t address);

  // TSB: Test and set bits
  void TSB(uint16_t address);

  // TSC: Transfer stack pointer to 16-bit accumulator
  void TSC();

  // TSX: Transfer stack pointer to X
  void TSX();

  // TXA: Transfer X to accumulator
  void TXA();

  // TXS: Transfer X to stack pointer
  void TXS();

  // TXY: Transfer X to Y
  void TXY();

  // TYA: Transfer Y to accumulator
  void TYA();

  // TYX: Transfer Y to X
  void TYX();

  // WAI: Wait for interrupt
  void WAI();

  // WDM: Reserved for future expansion
  void WDM();

  // XBA: Exchange B and A
  void XBA();

  // XCE: Exchange carry and emulation bits
  void XCE();

  void And(uint32_t low, uint32_t high);
  void Eor(uint32_t low, uint32_t high);
  void Adc(uint32_t low, uint32_t high);
  void Sbc(uint32_t low, uint32_t high);
  void Cmp(uint32_t low, uint32_t high);
  void Cpx(uint32_t low, uint32_t high);
  void Cpy(uint32_t low, uint32_t high);
  void Bit(uint32_t low, uint32_t high);
  void Lda(uint32_t low, uint32_t high);
  void Ldx(uint32_t low, uint32_t high);
  void Ldy(uint32_t low, uint32_t high);
  void Sta(uint32_t low, uint32_t high);
  void Stx(uint32_t low, uint32_t high);
  void Sty(uint32_t low, uint32_t high);
  void Stz(uint32_t low, uint32_t high);
  void Ror(uint32_t low, uint32_t high);
  void Rol(uint32_t low, uint32_t high);
  void Lsr(uint32_t low, uint32_t high);
  void Asl(uint32_t low, uint32_t high);
  void Inc(uint32_t low, uint32_t high);
  void Dec(uint32_t low, uint32_t high);
  void Tsb(uint32_t low, uint32_t high);
  void Trb(uint32_t low, uint32_t high);

  uint16_t SP() const { return memory.SP(); }
  void SetSP(uint16_t value) { memory.SetSP(value); }

  bool IsBreakpoint(uint32_t address) {
    return std::find(breakpoints_.begin(), breakpoints_.end(), address) !=
           breakpoints_.end();
  }
  void SetBreakpoint(uint32_t address) { breakpoints_.push_back(address); }
  void ClearBreakpoint(uint32_t address) {
    breakpoints_.erase(
        std::remove(breakpoints_.begin(), breakpoints_.end(), address),
        breakpoints_.end());
  }
  void ClearBreakpoints() {
    breakpoints_.clear();
    breakpoints_.shrink_to_fit();
  }
  auto GetBreakpoints() { return breakpoints_; }

  void CheckInt() {
    int_wanted_ =
        (nmi_wanted_ || (irq_wanted_ && !GetInterruptFlag())) && !int_delay_;
    int_delay_ = false;
  }

  auto mutable_log_instructions() -> bool* { return &log_instructions_; }

 private:
  void compare(uint16_t register_value, uint16_t memory_value) {
    uint16_t result;
    if (GetIndexSize()) {
      // 8-bit mode
      uint8_t result8 = static_cast<uint8_t>(register_value) -
                        static_cast<uint8_t>(memory_value);
      result = result8;
      SetNegativeFlag(result & 0x80);  // Negative flag for 8-bit
    } else {
      // 16-bit mode
      result = register_value - memory_value;
      SetNegativeFlag(result & 0x8000);  // Negative flag for 16-bit
    }
    SetZeroFlag(result == 0);                      // Zero flag
    SetCarryFlag(register_value >= memory_value);  // Carry flag
  }

  void SetFlag(uint8_t mask, bool set) {
    if (set) {
      status |= mask;  // Set the bit
    } else {
      status &= ~mask;  // Clear the bit
    }
  }

  bool GetFlag(uint8_t mask) const { return (status & mask) != 0; }

  bool log_instructions_ = false;

  bool waiting_ = false;
  bool stopped_ = false;

  bool irq_wanted_ = false;
  bool nmi_wanted_ = false;
  bool reset_wanted_ = false;
  bool int_wanted_ = false;
  bool int_delay_ = false;

  memory::CpuCallbacks callbacks_;
  memory::Memory& memory;
  Clock& clock;
};

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

#endif  // YAZE_APP_EMU_CPU_H_