use crate::device;
use crate::ui;

pub fn check_pending_interrupts(device: &mut device::Device) {
    if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
        & (device::cop0::COP0_STATUS_IE
            | device::cop0::COP0_STATUS_EXL
            | device::cop0::COP0_STATUS_ERL))
        != device::cop0::COP0_STATUS_IE
    {
        // interrupts disabled globally, or error/exception is already set
        return;
    }

    if device.mi.regs[device::mi::MI_INTR_REG] & device.mi.regs[device::mi::MI_INTR_MASK_REG] != 0 {
        device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_IP2;
        device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &=
            !device::cop0::COP0_CAUSE_EXCCODE_MASK;
    }

    if (device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG]
        & device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG]
        & device::cop0::COP0_CAUSE_IP_MASK)
        == 0
    {
        // interrupt disabled, or no pending interrupts
        return;
    }

    if device.cpu.cop0.is_event {
        interrupt_exception(device);
    } else {
        device::events::create_event(device, device::events::EVENT_TYPE_INT, 0);
    }
}

pub fn interrupt_exception(device: &mut device::Device) {
    exception_general(device, 0x180);
}

pub fn floating_point_exception(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_FPE;

    exception_general(device, 0x180)
}

pub fn trap_exception(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TR;

    exception_general(device, 0x180)
}

pub fn syscall_exception(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_SYS;

    exception_general(device, 0x180)
}

pub fn break_exception(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_BP;

    exception_general(device, 0x180)
}

pub fn reserved_exception(device: &mut device::Device, cop: u64) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_RI | cop;

    exception_general(device, 0x180)
}

pub fn cop_unusable_exception(device: &mut device::Device, cop: u64) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_CPU | cop;

    exception_general(device, 0x180)
}

pub fn tlb_miss_exception(
    device: &mut device::Device,
    address: u64,
    access_type: device::memory::AccessType,
) {
    if access_type == device::memory::AccessType::Read {
        device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBL
    } else {
        device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] = device::cop0::COP0_CAUSE_EXCCODE_TLBS
    }

    device.cpu.cop0.regs[device::cop0::COP0_BADVADDR_REG] = address;
    device::memory::masked_write_64(
        &mut device.cpu.cop0.regs[device::cop0::COP0_CONTEXT_REG],
        address >> 9,
        device::cop0::COP0_CONTEXT_BADVPN2_MASK,
    );
    device::memory::masked_write_64(
        &mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
        address >> 9,
        device::cop0::COP0_XCONTEXT_BADVPN2_MASK,
    );
    device::memory::masked_write_64(
        &mut device.cpu.cop0.regs[device::cop0::COP0_XCONTEXT_REG],
        address >> 31,
        device::cop0::COP0_XCONTEXT_REGION_MASK,
    );
    device::memory::masked_write_64(
        &mut device.cpu.cop0.regs[device::cop0::COP0_ENTRYHI_REG],
        address,
        0xFFFFE000,
    );

    let mut vector_offset = 0x180;
    let mut valid = true;
    for i in device.cpu.cop0.tlb_entries {
        if address & !3 >= i.start_even && address & !3 <= i.end_even {
            valid = i.v_even != 0;
            if valid && access_type == device::memory::AccessType::Write && i.d_even == 0 {
                device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
                    device::cop0::COP0_CAUSE_EXCCODE_MOD;
                valid = false;
            }
            break;
        }
        if address & !3 >= i.start_odd && address & !3 <= i.end_odd {
            valid = i.v_odd != 0;
            if valid && access_type == device::memory::AccessType::Write && i.d_odd == 0 {
                device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] =
                    device::cop0::COP0_CAUSE_EXCCODE_MOD;
                valid = false;
            }
            break;
        }
    }

    if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0
        && valid
    {
        vector_offset = 0;
    }

    exception_general(device, vector_offset)
}

pub fn reset_event(device: &mut device::Device) {
    device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_IP4;

    device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_ERL
        | device::cop0::COP0_STATUS_SR
        | device::cop0::COP0_STATUS_BEV;
    device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] &= !device::cop0::COP0_STATUS_TS;

    device.cpu.cop0.regs[device::cop0::COP0_ERROREPC_REG] = device.cpu.pc;
    device.cpu.pc = 0xBFC00000;
    device.cpu.branch_state.state = device::cpu::State::Step;
    device.rsp.regs2[device::rsp_interface::SP_PC_REG] = 0;

    device::pif::reset_pif(device, true);

    ui::video::onscreen_message("Game reset", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
}

fn exception_general(device: &mut device::Device, vector_offset: u32) {
    if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_EXL == 0 {
        device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] = device.cpu.pc;
        if device::cpu::in_delay_slot(device) {
            device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] |= device::cop0::COP0_CAUSE_BD;
            device.cpu.cop0.regs[device::cop0::COP0_EPC_REG] -= 4;
        } else {
            device.cpu.cop0.regs[device::cop0::COP0_CAUSE_REG] &= !device::cop0::COP0_CAUSE_BD;
        }
    }

    device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] |= device::cop0::COP0_STATUS_EXL;

    if device.cpu.cop0.regs[device::cop0::COP0_STATUS_REG] & device::cop0::COP0_STATUS_BEV == 0 {
        device.cpu.pc = device::cpu_instructions::se32((0x80000000 + vector_offset) as i32);
    } else {
        device.cpu.pc = device::cpu_instructions::se32((0xBFC00200 + vector_offset) as i32);
    }

    device.cpu.branch_state.state = device::cpu::State::Exception;

    device::cop0::add_cycles(device, 2);
}
