#include "CPU.hpp"

#include <spider/runtime/native/machine.hpp>

#include <spider/runtime/memory/RAM.hpp>
#include <spider/runtime/memory/Types.hpp>

#include <spider/runtime/reel/InstrReel.hpp>

#if __cplusplus >= 202002L
#include <bit>
#endif

namespace spider {

    CPU::CPU()
        : RA{}, RB{}, RC{}, RD{},
          RX{}, RY{}, R0{}, R1{},
          R2{}, R3{}, R4{}, R5{},
          R6{}, R7{}, R8{}, R9{},
          RF{}, RI{}, RS{}, RZ{},
          RE{}, RN{}, RV{}, RM{},
          ALU0{}, ALU1{},
        _dst(nullptr), _src(nullptr),
        _opcode(0), _addrm(0), _size(0),
        _store(0), _post(&CPU::imp),
        _ram(nullptr), _reel(nullptr) {
    }

    CPU::~CPU() {}

    // Setup & Configuration //

    void CPU::hookRAM(RAM* ram) {
        this->_ram = ram;
    }

    void CPU::hookInstrReel(InstrReel* reel) {
        this->_reel = reel;
    }

    constexpr u64 CPU::getFlag(u64 mask) {
        if (!mask) return 0;
#if __cplusplus >= 202002L
        return (RF & mask) >> std::countr_zero(mask);
#elif defined(SPIDER_COMPILER_GCC_LIKE)
        return (RF & mask) >> __builtin_ctzll(mask);
#elif defined(SPIDER_COMPILER_MSVC)
        return (RF & mask) >> _BitScanForward64(mask);
#else
        // If you have reached this part,
        // please come up with a better alternative.
        u64 bits = RF & mask;
        while (mask && (mask >>= 1)) bits >>= 1;
        return bits;
#endif
    }

    // Interaction with Reel //

    CPU::Fn CPU::addrModes[] = {
        &CPU::imm, &CPU::abs,
        &CPU::reg, &CPU::ind,
        &CPU::ptr, &CPU::idx,
        &CPU::sca, &CPU::dis
    };

    void CPU::fetchInstr() {
        u16 i = _reel->readU16(RI);
        _opcode = (i >> 7) & 0x1FF;
        _addrm = (i >> 2) & 0x1F;
        _size = i & 0x3;
        RI += 2;
    }

    void CPU::fetchOperDst() {
        // Move the operand ptrs
        _alu = &ALU0;
        _opers[1] = _opers[0];

        // call specific addressing mode
        (this->*(CPU::addrModes[_addrm]))();
    }

    void CPU::fetchOperSrc() {
        // set ALU
        _alu = &ALU1;

        // call specific addressing mode
        (this->*(CPU::addrModes[_addrm]))();

        // modify the _addrm register
        _addrm >>= 3;
        _addrm++;
    }

    void CPU::execute() {
        (this->*(CPU::addrModes[_opcode]))();
    }

    // Addressing Modes //

    /**
     * Implied Addressing Mode
     */
    void CPU::imp() {
        // Nothing //
    }

    /**
     * Immediate Addressing Mode
     */
    void CPU::imm() {
        switch(_size) {
            case 0b00:
                _alu->_u8 = _reel->readU8(RI);
                break;
            case 0b01:
                _alu->_u16 = _reel->readU16(RI);
                break;
            case 0b10:
                _alu->_u32 = _reel->readU32(RI);
                break;
            case 0b11:
                _alu->_u64 = _reel->readU64(RI);
                break;
        }

        _opers[0] = _alu;
        _post = &CPU::imp;
        RI += 1 << _size;
    }

    /**
     * Absolute Addressing Mode
     */
    void CPU::abs() { // TODO cache ptr size
        u8 dat_size = 1 << _size;
        u8 ptr_size = 1 << getFlag(CPU::FLAG_MEMORY_MODE);
        u64 ptr = 0;

        if(ptr_size + dat_size > _ram->size()) return; // TODO: avoid overflow

        switch(ptr_size) {
            case 1:
                ptr = _reel->readU8(RI);
                break;
            case 2:
                ptr = _reel->readU16(RI);
                break;
            case 4:
                ptr = _reel->readU32(RI);
                break;
            case 8:
                ptr = _reel->readU64(RI);
                break;
        }
        
        switch(_size) {
            case 0b00:
                spider::loadLE(&_alu->_u8, &(*_ram)[ptr]);
                break;
            case 0b01:
                spider::loadLE(&_alu->_u16, &(*_ram)[ptr]);
                break;
            case 0b10:
                spider::loadLE(&_alu->_u32, &(*_ram)[ptr]);
                break;
            case 0b11:
                spider::loadLE(&_alu->_u64, &(*_ram)[ptr]);
                break;
        }

        RI += dat_size;
    }

    /**
     * Register Addressing Mode
     */
    void CPU::reg() {
        
    }

    /**
     * Indrect Addressing Mode
     */
    void CPU::ind() {}

    /**
     * Pointer Addressing Mode
     */
    void CPU::ptr() {}

    /**
     * Indexed Addressing Mode
     */
    void CPU::idx() {}

    /**
     * Scaled Addressing Mode
     */
    void CPU::sca() {}

    /**
     * Displaced Addressing Mode
     */
    void CPU::dis() {}

    /**
     * Post Write Action
     */
    void CPU::psw() {}

}
