use crate::{device, savestates};

pub const COP0_INDEX_REG: usize = 0;
const COP0_RANDOM_REG: usize = 1;
pub const COP0_ENTRYLO0_REG: usize = 2;
pub const COP0_ENTRYLO1_REG: usize = 3;
pub const COP0_CONTEXT_REG: usize = 4;
pub const COP0_PAGEMASK_REG: usize = 5;
const COP0_WIRED_REG: usize = 6;
//const COP0_UNUSED_7: usize = 7;
pub const COP0_BADVADDR_REG: usize = 8;
pub const COP0_COUNT_REG: usize = 9;
pub const COP0_ENTRYHI_REG: usize = 10;
const COP0_COMPARE_REG: usize = 11;
pub const COP0_STATUS_REG: usize = 12;
pub const COP0_CAUSE_REG: usize = 13;
pub const COP0_EPC_REG: usize = 14;
const COP0_PREVID_REG: usize = 15;
const COP0_CONFIG_REG: usize = 16;
pub const COP0_LLADDR_REG: usize = 17;
//const COP0_WATCHLO_REG: usize = 18;
//const COP0_WATCHHI_REG: usize = 19;
pub const COP0_XCONTEXT_REG: usize = 20;
//const COP0_UNUSED_21: usize = 21;
//const COP0_UNUSED_22: usize = 22;
//const COP0_UNUSED_23: usize = 23;
//const COP0_UNUSED_24: usize = 24;
//const COP0_UNUSED_25: usize = 25;
//const COP0_PARITYERR_REG: usize = 26;
//const COP0_CACHEERR_REG: usize = 27;
pub const COP0_TAGLO_REG: usize = 28;
//const COP0_TAGHI_REG: usize = 29;
pub const COP0_ERROREPC_REG: usize = 30;
//const COP0_UNUSED_31: usize = 31;
pub const COP0_REGS_COUNT: usize = 32;

pub const COP0_STATUS_IE: u64 = 1 << 0;
pub const COP0_STATUS_EXL: u64 = 1 << 1;
pub const COP0_STATUS_ERL: u64 = 1 << 2;
pub const COP0_STATUS_SR: u64 = 1 << 20;
pub const COP0_STATUS_TS: u64 = 1 << 21;
pub const COP0_STATUS_BEV: u64 = 1 << 22;
pub const COP0_STATUS_FR: u64 = 1 << 26;
pub const COP0_STATUS_CU1: u64 = 1 << 29;
pub const COP0_STATUS_CU2: u64 = 1 << 30;

//const COP0_CAUSE_EXCCODE_INTR: u64 = 0 << 2;
pub const COP0_CAUSE_EXCCODE_MOD: u64 = 1 << 2;
pub const COP0_CAUSE_EXCCODE_TLBL: u64 = 2 << 2;
pub const COP0_CAUSE_EXCCODE_TLBS: u64 = 3 << 2;
//const COP0_CAUSE_EXCCODE_ADEL: u64 = 4 << 2;
//const COP0_CAUSE_EXCCODE_ADES: u64 = 5 << 2;
pub const COP0_CAUSE_EXCCODE_SYS: u64 = 8 << 2;
pub const COP0_CAUSE_EXCCODE_BP: u64 = 9 << 2;
pub const COP0_CAUSE_EXCCODE_RI: u64 = 10 << 2;
pub const COP0_CAUSE_EXCCODE_CPU: u64 = 11 << 2;
pub const COP0_CAUSE_EXCCODE_TR: u64 = 13 << 2;
pub const COP0_CAUSE_EXCCODE_FPE: u64 = 15 << 2;
pub const COP0_CAUSE_IP2: u64 = 1 << 10;
pub const COP0_CAUSE_IP4: u64 = 1 << 12;
pub const COP0_CAUSE_IP7: u64 = 1 << 15;
pub const COP0_CAUSE_BD: u64 = 1 << 31;

pub const COP0_CAUSE_CE1: u64 = 1 << 28;
pub const COP0_CAUSE_CE2: u64 = 1 << 29;

pub const COP0_CAUSE_IP_MASK: u64 = 0b00000000000000001111111100000000;
pub const COP0_CAUSE_EXCCODE_MASK: u64 = 0x1F << 2;
pub const COP0_CONTEXT_BADVPN2_MASK: u64 = 0b00000000011111111111111111110000;
pub const COP0_XCONTEXT_BADVPN2_MASK: u64 = 0b01111111111111111111111111110000;
pub const COP0_XCONTEXT_REGION_MASK: u64 = 0b110000000000000000000000000000000;

const COP0_INDEX_REG_MASK: u64 = 0b10000000000000000000000000111111;
//const COP0_RANDOM_REG_MASK: u64 = 0b00000000000000000000000000111111;
const COP0_ENTRYLO_REG_MASK: u64 =
    0b0000000000000000000000000000000000111111111111111111111111111111;
const COP0_CONTEXT_REG_MASK: u64 =
    0b1111111111111111111111111111111111111111100000000000000000000000;
const COP0_PAGEMASK_REG_MASK: u64 = 0b00000001111111111110000000000000;
const COP0_WIRED_REG_MASK: u64 = 0b00000000000000000000000000111111;
const COP0_ENTRYHI_REG_MASK: u64 =
    0b1100000000000000000000001111111111111111111111111110000011111111;
const COP0_STATUS_REG_MASK: u64 = 0b11111111010101111111111111111111;
const COP0_CAUSE_REG_MASK: u64 = 0b00000000000000000000001100000000;
const COP0_CONFIG_REG_MASK: u64 = 0b00001111000000001000000000001111;
const COP0_LLADDR_REG_MASK: u64 = 0b11111111111111111111111111111111;
const COP0_WATCHLO_REG_MASK: u64 = 0b11111111111111111111111111111011;
const COP0_WATCHHI_REG_MASK: u64 = 0b00000000000000000000000000001111;
const COP0_XCONTEXT_REG_MASK: u64 =
    0b1111111111111111111111111111111000000000000000000000000000000000;
const COP0_PARITYERR_REG_MASK: u64 = 0b00000000000000000000000011111111;
const COP0_TAGLO_REG_MASK: u64 = 0b00001111111111111111111111000000;

#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cop0 {
    pub reg_latch: u64,
    pub regs: [u64; COP0_REGS_COUNT],
    pub reg_write_masks: [u64; COP0_REGS_COUNT],
    #[serde(skip, default = "savestates::default_instructions")]
    pub instrs: [fn(&mut device::Device, u32); 32],
    #[serde(skip, default = "savestates::default_instructions")]
    pub instrs2: [fn(&mut device::Device, u32); 32],
    pub tlb_lut_r: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
    pub tlb_lut_w: rustc_hash::FxHashMap<u64, device::tlb::TlbLut>,
    pub tlb_entries: [device::tlb::TlbEntry; 32],
    pub is_event: bool,
}

fn mfc0(device: &mut device::Device, opcode: u32) {
    device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] = device::cpu_instructions::se32(
        (get_control_registers(device, device::cpu_instructions::rd(opcode))) as u32 as i32,
    )
}

fn dmfc0(device: &mut device::Device, opcode: u32) {
    device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] =
        get_control_registers(device, device::cpu_instructions::rd(opcode))
}

fn mtc0(device: &mut device::Device, opcode: u32) {
    device::cop0::set_control_registers(
        device,
        device::cpu_instructions::rd(opcode),
        device::cpu_instructions::se32(
            device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize] as u32 as i32,
        ),
    )
}

fn dmtc0(device: &mut device::Device, opcode: u32) {
    device::cop0::set_control_registers(
        device,
        device::cpu_instructions::rd(opcode),
        device.cpu.gpr[device::cpu_instructions::rt(opcode) as usize],
    )
}

fn tlbr(device: &mut device::Device, _opcode: u32) {
    device::tlb::read(device, device.cpu.cop0.regs[COP0_INDEX_REG])
}

fn tlbwi(device: &mut device::Device, _opcode: u32) {
    device::tlb::write(device, device.cpu.cop0.regs[COP0_INDEX_REG])
}

fn tlbwr(device: &mut device::Device, _opcode: u32) {
    let random = set_random_register(device);
    device::tlb::write(device, random)
}

fn tlbp(device: &mut device::Device, _opcode: u32) {
    device::tlb::probe(device)
}

fn eret(device: &mut device::Device, _opcode: u32) {
    if device.cpu.cop0.regs[COP0_STATUS_REG] & COP0_STATUS_ERL != 0 {
        device.cpu.pc = device.cpu.cop0.regs[COP0_ERROREPC_REG];
        device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_ERL
    } else {
        device.cpu.pc = device.cpu.cop0.regs[COP0_EPC_REG];
        device.cpu.cop0.regs[COP0_STATUS_REG] &= !COP0_STATUS_EXL
    }
    device.cpu.branch_state.state = device::cpu::State::Exception;
    device.cpu.llbit = false;
    device::exceptions::check_pending_interrupts(device)
}

fn execute_cp0(device: &mut device::Device, opcode: u32) {
    device.cpu.cop0.instrs2[(opcode & 0x3F) as usize](device, opcode)
}

pub fn syscall(device: &mut device::Device, _opcode: u32) {
    device::exceptions::syscall_exception(device)
}

pub fn break_(device: &mut device::Device, _opcode: u32) {
    device::exceptions::break_exception(device)
}

pub fn reserved(device: &mut device::Device, _opcode: u32) {
    device::exceptions::reserved_exception(device, 0);
}

pub fn emux_xdetect(_device: &mut device::Device, _opcode: u32) {}

fn get_control_registers(device: &device::Device, index: u32) -> u64 {
    match index as usize {
        COP0_COUNT_REG => device.cpu.cop0.regs[index as usize] >> 1,
        COP0_RANDOM_REG => set_random_register(device),
        7 | 21 | 22 | 23 | 24 | 25 | 31 => device.cpu.cop0.reg_latch,
        _ => device.cpu.cop0.regs[index as usize],
    }
}

fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64) {
    device.cpu.cop0.reg_latch = data;
    match index as usize {
        COP0_COUNT_REG => {
            data &= 0xFFFFFFFF;
            data <<= 1;
            device::events::translate_events(device, device.cpu.cop0.regs[COP0_COUNT_REG], data);
            device.cpu.cop0.regs[COP0_COUNT_REG] = data;
            return;
        }
        COP0_WIRED_REG => device.cpu.cop0.regs[COP0_RANDOM_REG] = 31,
        COP0_COMPARE_REG => {
            data &= 0xFFFFFFFF;
            let current_count = (device.cpu.cop0.regs[COP0_COUNT_REG] >> 1) & 0xFFFFFFFF;
            let mut compare_event_diff = (data as u32).wrapping_sub(current_count as u32);

            if compare_event_diff == 0 {
                compare_event_diff += u32::MAX;
            }

            device::events::create_event(
                device,
                device::events::EVENT_TYPE_COMPARE,
                (compare_event_diff as u64) << 1,
            );
            device.cpu.cop0.regs[COP0_CAUSE_REG] &= !COP0_CAUSE_IP7;
        }
        COP0_STATUS_REG
            if data & COP0_STATUS_FR != device.cpu.cop0.regs[index as usize] & COP0_STATUS_FR =>
        {
            device::cop1::set_fgr_registers(device, data)
        }
        _ => {}
    }
    device::memory::masked_write_64(
        &mut device.cpu.cop0.regs[index as usize],
        data,
        device.cpu.cop0.reg_write_masks[index as usize],
    );
    device::exceptions::check_pending_interrupts(device);
}

pub fn compare_event(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP7;
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_EXCCODE_MASK;

    device::events::create_event_at(
        device,
        device::events::EVENT_TYPE_COMPARE,
        device.cpu.next_event_count + (u32::MAX as u64),
    );
    device::exceptions::check_pending_interrupts(device);
}

fn set_random_register(device: &device::Device) -> u64 {
    if device.cpu.cop0.regs[COP0_WIRED_REG] > 31 {
        (u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG]) & 0x3F
    } else {
        (u64::MAX - device.cpu.cop0.regs[COP0_COUNT_REG])
            % (32 - device.cpu.cop0.regs[COP0_WIRED_REG])
            + device.cpu.cop0.regs[COP0_WIRED_REG]
    }
}

pub fn add_cycles(device: &mut device::Device, cycles: u64) {
    device.cpu.cop0.regs[COP0_COUNT_REG] += cycles // COUNT_REG is shifted right 1 bit when read by MFC0
}

pub fn map_instructions(device: &mut device::Device) {
    device.cpu.cop0.instrs = [
        device::cop0::mfc0,         // 0
        device::cop0::dmfc0,        // 1
        device::cop0::reserved,     // 2
        device::cop0::reserved,     // 3
        device::cop0::mtc0,         // 4
        device::cop0::dmtc0,        // 5
        device::cop0::reserved,     // 6
        device::cop0::reserved,     // 7
        device::cop0::reserved,     // 8
        device::cop0::reserved,     // 9
        device::cop0::reserved,     // 10
        device::cop0::reserved,     // 11
        device::cop0::reserved,     // 12
        device::cop0::reserved,     // 13
        device::cop0::reserved,     // 14
        device::cop0::reserved,     // 15
        device::cop0::execute_cp0,  // 16
        device::cop0::reserved,     // 17
        device::cop0::reserved,     // 18
        device::cop0::reserved,     // 19
        device::cop0::emux_xdetect, // 20
        device::cop0::reserved,     // 21
        device::cop0::reserved,     // 22
        device::cop0::reserved,     // 23
        device::cop0::reserved,     // 24
        device::cop0::reserved,     // 25
        device::cop0::reserved,     // 26
        device::cop0::reserved,     // 27
        device::cop0::reserved,     // 28
        device::cop0::reserved,     // 29
        device::cop0::reserved,     // 30
        device::cop0::reserved,     // 31
    ];

    device.cpu.cop0.instrs2 = [
        device::cop0::reserved, // 0
        device::cop0::tlbr,     // 1
        device::cop0::tlbwi,    // 2
        device::cop0::reserved, // 3
        device::cop0::reserved, // 4
        device::cop0::reserved, // 5
        device::cop0::tlbwr,    // 6
        device::cop0::reserved, // 7
        device::cop0::tlbp,     // 8
        device::cop0::reserved, // 9
        device::cop0::reserved, // 10
        device::cop0::reserved, // 11
        device::cop0::reserved, // 12
        device::cop0::reserved, // 13
        device::cop0::reserved, // 14
        device::cop0::reserved, // 15
        device::cop0::reserved, // 16
        device::cop0::reserved, // 17
        device::cop0::reserved, // 18
        device::cop0::reserved, // 19
        device::cop0::reserved, // 20
        device::cop0::reserved, // 21
        device::cop0::reserved, // 22
        device::cop0::reserved, // 23
        device::cop0::eret,     // 24
        device::cop0::reserved, // 25
        device::cop0::reserved, // 26
        device::cop0::reserved, // 27
        device::cop0::reserved, // 28
        device::cop0::reserved, // 29
        device::cop0::reserved, // 30
        device::cop0::reserved, // 31
    ];
}

pub fn init(device: &mut device::Device) {
    device.cpu.cop0.reg_write_masks = [
        COP0_INDEX_REG_MASK,
        0, // Random, read only
        COP0_ENTRYLO_REG_MASK,
        COP0_ENTRYLO_REG_MASK,
        COP0_CONTEXT_REG_MASK,
        COP0_PAGEMASK_REG_MASK,
        COP0_WIRED_REG_MASK,
        0,               // 7
        0,               // BadVAddr, read only
        u32::MAX as u64, // count
        COP0_ENTRYHI_REG_MASK,
        u32::MAX as u64, // compare
        COP0_STATUS_REG_MASK,
        COP0_CAUSE_REG_MASK,
        u64::MAX, // EPC
        0,        // previd, read only
        COP0_CONFIG_REG_MASK,
        COP0_LLADDR_REG_MASK,
        COP0_WATCHLO_REG_MASK,
        COP0_WATCHHI_REG_MASK,
        COP0_XCONTEXT_REG_MASK,
        0, // 21
        0, // 22
        0, // 23
        0, // 24
        0, // 25
        COP0_PARITYERR_REG_MASK,
        0, // cache error
        COP0_TAGLO_REG_MASK,
        0,        // taghi
        u64::MAX, // ErrorPC
        0,        // 31
    ];

    map_instructions(device);

    // taken from VR4300 manual
    device.cpu.cop0.regs[COP0_RANDOM_REG] = 0b00000000000000000000000000011111;
    device.cpu.cop0.regs[COP0_CONFIG_REG] = 0b01110000000001101110010001100000;
    device.cpu.cop0.regs[COP0_STATUS_REG] = 0b00000000010000000000000000000100;
    device.cpu.cop0.regs[COP0_PREVID_REG] = 0b00000000000000000000101100100010;
    device.cpu.cop0.regs[COP0_EPC_REG] = 0b11111111111111111111111111111111;
    device.cpu.cop0.regs[COP0_ERROREPC_REG] = 0b11111111111111111111111111111111;
    device.cpu.cop0.regs[COP0_BADVADDR_REG] = 0xFFFFFFFF;
    device.cpu.cop0.regs[COP0_CONTEXT_REG] = 0x7FFFF0;

    device::events::create_event(device, device::events::EVENT_TYPE_COMPARE, u32::MAX as u64)
}
