#![allow(non_camel_case_types)]
#![allow(improper_ctypes)]

#[cfg(target_arch = "aarch64")]
#[derive(Copy, Clone)]
#[allow(dead_code)]
pub struct __m128i(std::arch::aarch64::int64x2_t);
#[cfg(target_arch = "aarch64")]
include!(concat!(env!("OUT_DIR"), "/simd_bindings.rs"));
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;

const N64_EXTENSIONS_UNCOMPRESSED: [&str; 8] =
    ["n64", "v64", "z64", "bin", "N64", "V64", "Z64", "BIN"];

use rand::{Rng, SeedableRng};

use crate::{cheats, netplay, retroachievements, savestates, ui};
use std::io::Read;

pub mod ai;
pub mod cache;
pub mod cart;
pub mod controller;
pub mod cop0;
pub mod cop1;
pub mod cop2;
pub mod cpu;
pub mod cpu_instructions;
pub mod events;
pub mod exceptions;
pub mod fpu_instructions;
pub mod is_viewer;
pub mod memory;
pub mod mi;
pub mod pi;
pub mod pif;
pub mod rdp;
pub mod rdram;
pub mod ri;
pub mod rsp_cpu;
pub mod rsp_interface;
pub mod rsp_su_instructions;
pub mod rsp_vu_instructions;
pub mod si;
pub mod tlb;
pub mod unmapped;
pub mod vi;

pub fn run_game(
    device: &mut Device,
    rom_contents: &[u8],
    game_settings: ui::GameSettings,
    ra_config: retroachievements::RAConfig,
    netplay_config: Option<netplay::NetplayConfig>,
) {
    device.cpu.overclock = game_settings.overclock;
    if game_settings.disable_expansion_pak {
        device.rdram.size = 0x400000;
    }
    if let Some(slot) = game_settings.load_savestate_slot {
        device.ui.storage.save_state_slot = slot;
        device.savestate.load_state = true;
    }

    cart::rom::init(device, rom_contents); // cart needs to come before rdram

    // rdram pointer is shared with parallel-rdp and retroachievements
    rdram::init(device);

    ui::video::init(device, netplay_config.is_some());
    ui::audio::init(device);
    ui::input::init(&mut device.ui);

    // must be after video init
    if let Some(netplay_config) = &netplay_config {
        device.netplay = netplay::init(
            device,
            netplay_config,
            cart::rom::is_system_pal(rom_contents),
        );
        if device.netplay.is_none() {
            ui::input::close(&mut device.ui);
            ui::audio::close(&mut device.ui);
            ui::video::close(&device.ui);
            return;
        }
    }

    init_rng_rtc(device);

    // must be after video init
    let (discord_watch_tx, discord_handle) = if ra_config.enabled && device.netplay.is_none() {
        retroachievements::load_game(rom_contents, rom_contents.len(), ra_config.rich_presence)
    } else {
        (None, None)
    };

    mi::init(device);
    pif::init(device);
    if device.ui.config.input.emulate_vru && device.netplay.is_none() {
        controller::vru::init(device);
    }
    memory::init(device);
    cache::init(device);
    rsp_interface::init(device);
    rdp::init(device);
    vi::init(device);
    cpu::init(device);

    ui::storage::init(&mut device.ui, &device.cart.rom);
    ui::storage::load_saves(&mut device.ui, &mut device.netplay);
    ui::storage::format_saves(device);

    if !retroachievements::get_hardcore() {
        cheats::init(device, game_settings.cheats);
    }

    cpu::run(device);

    retroachievements::unload_game(discord_watch_tx, discord_handle);

    ui::storage::write_saves(device);

    if let Some(netplay) = &mut device.netplay {
        netplay::close(netplay);
    }

    ui::input::close(&mut device.ui);
    ui::audio::close(&mut device.ui);
    ui::video::close(&device.ui);
}

fn set_rng() -> rand::rngs::Xoshiro256PlusPlus {
    rand::rngs::Xoshiro256PlusPlus::try_from_rng(&mut rand::rngs::SysRng).unwrap()
}

fn init_rng_rtc(device: &mut Device) {
    let mut rng_seed = set_rng().next_u64();
    if let Some(netplay) = &mut device.netplay {
        if netplay.player_number == 0 {
            netplay::send_rng(netplay, rng_seed);
        } else {
            rng_seed = netplay::receive_rng(netplay);
        }
    }
    device.rng = rand::rngs::Xoshiro256PlusPlus::seed_from_u64(rng_seed);

    device.cart.rtc_timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    if let Some(netplay) = &mut device.netplay {
        if netplay.player_number == 0 {
            netplay::send_rtc(netplay, device.cart.rtc_timestamp);
        } else {
            device.cart.rtc_timestamp = netplay::receive_rtc(netplay);
        }
    }
}

fn swap_rom(contents: Vec<u8>) -> Option<Vec<u8>> {
    let test = u32::from_be_bytes(contents[0..4].try_into().unwrap());
    if test == 0x80371240 {
        // z64
        Some(contents)
    } else if test == 0x37804012 {
        // v64
        let mut data: Vec<u8> = vec![0; contents.len()];
        for i in (0..contents.len()).step_by(2) {
            let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap());
            data[i..i + 2].copy_from_slice(&temp.to_be_bytes());
        }
        Some(data)
    } else if test == 0x40123780 {
        // n64
        let mut data: Vec<u8> = vec![0; contents.len()];
        for i in (0..contents.len()).step_by(4) {
            let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap());
            data[i..i + 4].copy_from_slice(&temp.to_be_bytes());
        }
        Some(data)
    } else {
        None
    }
}

pub fn get_rom_contents(file_path: &std::path::PathBuf) -> Option<Vec<u8>> {
    let mut contents = vec![];
    if file_path
        .extension()
        .unwrap_or_default()
        .eq_ignore_ascii_case("zip")
    {
        #[cfg(target_os = "android")]
        let zip_file = ui::android::get_file_from_uri(file_path).unwrap();
        #[cfg(not(target_os = "android"))]
        let zip_file = std::fs::File::open(file_path).unwrap();
        let mut archive = zip::ZipArchive::new(zip_file).unwrap();
        for i in 0..archive.len() {
            let mut file = archive.by_index(i).unwrap();
            if let Some(extension) = file.enclosed_name().unwrap().extension()
                && N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
            {
                file.read_to_end(&mut contents)
                    .expect("could not read zip file");
                break;
            }
        }
    } else if file_path
        .extension()
        .unwrap_or_default()
        .eq_ignore_ascii_case("7z")
    {
        #[cfg(target_os = "android")]
        let sevenz_file = ui::android::get_file_from_uri(file_path).unwrap();
        #[cfg(not(target_os = "android"))]
        let sevenz_file = std::fs::File::open(file_path).unwrap();
        let mut archive =
            sevenz_rust2::ArchiveReader::new(sevenz_file, sevenz_rust2::Password::empty()).unwrap();

        let mut found = false;
        archive
            .for_each_entries(
                &mut |entry: &sevenz_rust2::ArchiveEntry, reader: &mut dyn std::io::Read| {
                    if !found
                        && let Some(extension) = std::path::PathBuf::from(entry.name()).extension()
                        && N64_EXTENSIONS_UNCOMPRESSED.contains(&extension.to_str().unwrap())
                    {
                        reader
                            .read_to_end(&mut contents)
                            .expect("could not read zip file");
                        found = true;
                    } else {
                        //skip other files
                        std::io::copy(reader, &mut std::io::sink())?;
                    }
                    Ok(true)
                },
            )
            .expect("ok");
    } else {
        #[cfg(target_os = "android")]
        let mut file = ui::android::get_file_from_uri(file_path).unwrap();
        #[cfg(not(target_os = "android"))]
        let mut file = std::fs::File::open(file_path).unwrap();
        file.read_to_end(&mut contents).unwrap();
    }

    if contents.is_empty() {
        None
    } else {
        swap_rom(contents)
    }
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct SpeedLimiter {
    pub limit_freq: u64,
    #[serde(skip)]
    #[serde(default = "std::time::Instant::now")]
    pub limit_freq_check: std::time::Instant,
    pub frame_counter: u64,
    pub enabled: bool,
    pub min_wait_time: std::time::Duration,
    #[serde(skip)]
    pub next_pace_deadline: Option<std::time::Instant>,
}

#[derive(serde::Serialize, serde::Deserialize)]
pub struct Device {
    #[serde(skip)]
    pub netplay: Option<netplay::Netplay>,
    #[serde(skip)]
    pub ui: ui::Ui,
    pub speed_limiter: SpeedLimiter,
    pub byte_swap: usize,
    pub savestate: savestates::Savestate,
    pub cpu: cpu::Cpu,
    pub pif: pif::Pif,
    pub cart: cart::Cart,
    pub memory: memory::Memory,
    pub rsp: rsp_interface::Rsp,
    pub rdp: rdp::Rdp,
    pub rdram: rdram::Rdram,
    pub mi: mi::Mi,
    pub pi: pi::Pi,
    pub vi: vi::Vi,
    pub ai: ai::Ai,
    pub si: si::Si,
    pub ri: ri::Ri,
    pub rng: rand::rngs::Xoshiro256PlusPlus,
    pub vru: controller::vru::Vru,
    pub transferpaks: [controller::transferpak::TransferPak; 4],
    pub cheats: cheats::Cheats,
}

pub fn zero_m128i() -> __m128i {
    unsafe { _mm_setzero_si128() }
}

impl Device {
    pub fn clone_state(&mut self, device: &Device) {
        self.rng.clone_from(&device.rng);
        self.cpu.clone_from(&device.cpu);
        self.pif.clone_from(&device.pif);
        self.cart.clone_from(&device.cart);
        self.memory.clone_from(&device.memory);
        self.rsp.clone_from(&device.rsp);
        self.rdp.clone_from(&device.rdp);
        self.rdram.clone_from(&device.rdram);
        self.mi.clone_from(&device.mi);
        self.pi.clone_from(&device.pi);
        self.vi.clone_from(&device.vi);
        self.ai.clone_from(&device.ai);
        self.si.clone_from(&device.si);
        self.ri.clone_from(&device.ri);
        self.vru.clone_from(&device.vru);
        self.transferpaks.clone_from(&device.transferpaks);
        self.cheats.clone_from(&device.cheats);
    }

    pub fn new(with_ui: bool) -> Box<Device> {
        let mut byte_swap: usize = 0;
        let test: [u8; 4] = [1, 2, 3, 4];
        // if the host computer is little endian, that means the RDRAM will be stored as little endian
        // when accessing bytes in RDRAM, we need to swap them by XORing with the byte_swap value (3)
        if u32::from_le_bytes(test) == u32::from_ne_bytes(test) {
            byte_swap = 3;
        }
        Box::new(Device {
            netplay: None,
            ui: if with_ui {
                ui::Ui::new()
            } else {
                ui::Ui::default()
            },
            speed_limiter: SpeedLimiter {
                limit_freq: 2,
                limit_freq_check: std::time::Instant::now(),
                frame_counter: 0,
                enabled: true,
                min_wait_time: std::time::Duration::from_secs(1),
                next_pace_deadline: None,
            },
            byte_swap,
            savestate: savestates::Savestate {
                save_state: false,
                load_state: false,
                load_rewind: false,
                last_rewind_saved: 0.0,
                rewind_pool: std::sync::Arc::new(tokio::sync::Mutex::new(
                    std::collections::BTreeMap::new(),
                )),
            },
            cpu: cpu::Cpu {
                cop0: cop0::Cop0 {
                    regs: [0; cop0::COP0_REGS_COUNT],
                    reg_write_masks: [0; cop0::COP0_REGS_COUNT],
                    reg_latch: 0,
                    is_event: false,
                    instrs: [cop0::reserved; 32],
                    instrs2: [cop0::reserved; 32],
                    tlb_lut_w: rustc_hash::FxHashMap::default(),
                    tlb_lut_r: rustc_hash::FxHashMap::default(),
                    tlb_entries: [tlb::TlbEntry {
                        mask: 0,
                        vpn2: 0,
                        region: 0,
                        g: 0,
                        asid: 0,
                        pfn_even: 0,
                        c_even: 0,
                        d_even: 0,
                        v_even: 0,
                        pfn_odd: 0,
                        c_odd: 0,
                        d_odd: 0,
                        v_odd: 0,
                        start_even: 0,
                        end_even: 0,
                        phys_even: 0,
                        start_odd: 0,
                        end_odd: 0,
                        phys_odd: 0,
                    }; 32],
                },
                cop1: cop1::Cop1 {
                    fcr0: 0,
                    fcr31: 0,
                    //#[cfg(target_arch = "x86_64")]
                    //flush_mode: 0,
                    fgr32: [[0; 4]; 32],
                    fgr64: [[0; 8]; 32],
                    instrs: [cop1::reserved; 32],
                    b_instrs: [cop1::reserved; 4],
                    s_instrs: [cop1::reserved; 64],
                    d_instrs: [cop1::reserved; 64],
                    w_instrs: [cop1::reserved; 64],
                    l_instrs: [cop1::reserved; 64],
                },
                cop2: cop2::Cop2 {
                    instrs: [cop2::reserved; 32],
                    reg_latch: 0,
                },
                branch_state: cpu::BranchState {
                    state: cpu::State::Step,
                    pc: 0,
                },
                gpr: [0; 32],
                clock_rate: 0,
                pc: 0xBFC00000,
                pc_phys: 0,
                llbit: false,
                overclock: false,
                lo: 0,
                hi: 0,
                running: false,
                instrs: [cop0::reserved; 64],
                special_instrs: [cop0::reserved; 64],
                regimm_instrs: [cop0::reserved; 32],
                events: [events::Event {
                    enabled: false,
                    count: u64::MAX,
                }; events::EVENT_TYPE_COUNT],
                next_event_count: u64::MAX,
                next_event: events::EVENT_TYPE_NONE,
            },
            pif: pif::Pif {
                rom: [0; 1984],
                ram: [0; 64],
                channels: [pif::PifChannel {
                    tx: None,
                    tx_buf: None,
                    rx: None,
                    rx_buf: None,
                    process: None,
                    pak_handler: None,
                    change_pak: controller::PakType::None,
                }; 5],
            },
            cart: cart::Cart {
                rom: Vec::new(),
                is_viewer_buffer: vec![0; 0xFFFF],
                pal: false,
                latch: 0,
                cic_seed: 0,
                rtc_timestamp: 0,
                rtc: cart::AfRtc { control: 0x0200 },
                sc64: cart::sc64::Sc64 {
                    regs: [0; cart::sc64::SC64_REGS_COUNT],
                    regs_locked: true,
                    cfg: [0; cart::sc64::SC64_CFG_COUNT],
                    sector: 0,
                    buffer: vec![0; 8192],
                    writeback_sector: vec![0; 256],
                    usb_buffer: vec![],
                },
                flashram: cart::sram::Flashram {
                    status: 0,
                    erase_page: 0,
                    page_buf: [0xff; 128],
                    silicon_id: [cart::sram::FLASHRAM_TYPE_ID, cart::sram::MX29L1100_ID],
                    mode: cart::sram::FlashramMode::ReadArray,
                },
            },
            memory: memory::Memory {
                fast_read: [unmapped::read_mem_fast; 0x2000],
                memory_map_read: [unmapped::read_mem; 0x2000],
                memory_map_write: [unmapped::write_mem; 0x2000],
                icache: [cache::ICache {
                    valid: false,
                    index: 0,
                    tag: 0,
                    words: [0; 8],
                    instruction: [cop0::reserved; 8],
                }; 512],
                dcache: [cache::DCache {
                    valid: false,
                    dirty: false,
                    tag: 0,
                    index: 0,
                    words: [0; 4],
                }; 512],
            },
            rdram: rdram::Rdram {
                mem: vec![],
                size: 0x800000,
                regs: [[0; rdram::RDRAM_REGS_COUNT]; 4],
            },
            rsp: rsp_interface::Rsp {
                cpu: rsp_cpu::Cpu {
                    instructions: [rsp_cpu::Instructions {
                        func: rsp_su_instructions::reserved,
                        opcode: 0,
                    }; 0x1000 / 4],
                    last_instruction_type: rsp_cpu::InstructionType::Su,
                    instruction_type: rsp_cpu::InstructionType::Su,
                    pipeline_full: false,
                    branch_state: rsp_cpu::BranchState {
                        state: cpu::State::Step,
                        pc: 0,
                    },
                    broken: false,
                    running: false,
                    halted: false,
                    sync_point: false,
                    cycle_counter: 0,
                    gpr: [0; 32],
                    vpr: unsafe { [_mm_setzero_si128(); 32] },
                    reciprocals: [0; 512],
                    inverse_square_roots: [0; 512],
                    divdp: false,
                    divin: 0,
                    divout: 0,
                    shuffle: unsafe { [_mm_setzero_si128(); 16] },
                    vcol: unsafe { _mm_setzero_si128() },
                    vcoh: unsafe { _mm_setzero_si128() },
                    vccl: unsafe { _mm_setzero_si128() },
                    vcch: unsafe { _mm_setzero_si128() },
                    vce: unsafe { _mm_setzero_si128() },
                    accl: unsafe { _mm_setzero_si128() },
                    accm: unsafe { _mm_setzero_si128() },
                    acch: unsafe { _mm_setzero_si128() },
                    special_instrs: [rsp_su_instructions::reserved; 64],
                    regimm_instrs: [rsp_su_instructions::reserved; 32],
                    cop0_instrs: [rsp_su_instructions::reserved; 32],
                    cop2_instrs: [rsp_su_instructions::reserved; 32],
                    lwc2_instrs: [rsp_su_instructions::reserved; 32],
                    swc2_instrs: [rsp_su_instructions::reserved; 32],
                    instrs: [rsp_su_instructions::reserved; 64],
                    vec_instrs: [rsp_vu_instructions::reserved; 64],
                },
                regs: [0; rsp_interface::SP_REGS_COUNT],
                regs2: [0; rsp_interface::SP_REGS2_COUNT],
                mem: [0; 0x2000],
                last_status_value: 0,
                run_after_dma: false,
                fifo: [rsp_interface::RspDma {
                    dir: rsp_interface::DmaDir::None,
                    length: 0,
                    memaddr: 0,
                    dramaddr: 0,
                }; 2],
            },
            rdp: rdp::Rdp {
                regs_dpc: [0; rdp::DPC_REGS_COUNT],
                regs_dps: [0; rdp::DPS_REGS_COUNT],
                wait_frozen: false,
                last_status_value: 0,
            },
            mi: mi::Mi {
                regs: [0; mi::MI_REGS_COUNT],
            },
            pi: pi::Pi {
                regs: [0; pi::PI_REGS_COUNT],
            },
            ai: ai::Ai {
                regs: [0; ai::AI_REGS_COUNT],
                last_read: 0,
                freq: 33600,
                delayed_carry: false,
                fifo: [ai::AiDma {
                    address: 0,
                    length: 0,
                    duration: 0,
                }; 2],
            },
            si: si::Si {
                regs: [0; si::SI_REGS_COUNT],
                dma_dir: si::DmaDir::None,
            },
            ri: ri::Ri {
                regs: [0; ri::RI_REGS_COUNT],
                ram_init: false,
            },
            vi: vi::Vi {
                regs: [0; vi::VI_REGS_COUNT],
                clock: 0,
                field: 0,
                delay: 0,
                count_per_scanline: 0,
                frame_time: 0.0,
                elapsed_time: 0.0,
            },
            vru: controller::vru::Vru {
                status: 0,
                voice_state: 0,
                load_offset: 0,
                voice_init: 0,
                word_buffer: [0; 40],
                words: Vec::new(),
                talking: false,
                word_mappings: rustc_hash::FxHashMap::default(),
            },
            rng: set_rng(),
            transferpaks: [
                controller::transferpak::TransferPak::default(),
                controller::transferpak::TransferPak::default(),
                controller::transferpak::TransferPak::default(),
                controller::transferpak::TransferPak::default(),
            ],
            cheats: cheats::Cheats {
                cheats: vec![],
                boot: true,
                enabled: false,
            },
        })
    }
}
