use std::borrow::Cow;
use std::collections::HashMap;
use icicle_cpu::mem::{Mapping, MemError, perm};
use icicle_cpu::{Cpu, ExceptionCode, ValueSource, VmExit};
use pyo3::prelude::*;
use icicle_vm;
use pyo3::exceptions::*;
use target_lexicon;
use indexmap::IndexMap;
use target_lexicon::Architecture;
use sleigh_runtime::NamedRegister;

// References:
// - https://pyo3.rs/main/conversions/tables
// - https://pyo3.rs/main/class

struct X86FlagsRegHandler {
    pub eflags: pcode::VarNode,
}

impl icicle_cpu::RegHandler for X86FlagsRegHandler {
    fn read(&mut self, cpu: &mut Cpu) {
        let eflags = icicle_vm::x86::eflags(cpu);
        cpu.write_var::<u32>(self.eflags, eflags);
    }

    fn write(&mut self, cpu: &mut Cpu) {
        let eflags = cpu.read_var::<u32>(self.eflags);
        icicle_vm::x86::set_eflags(cpu, eflags);
    }
}

#[pyclass(eq, eq_int, module = "icicle")]
#[derive(Clone, Debug, PartialEq)]
pub enum MemoryProtection {
    NoAccess,
    ReadOnly,
    ReadWrite,
    ExecuteOnly,
    ExecuteRead,
    ExecuteReadWrite,
}

#[pyclass(eq, eq_int, module = "icicle")]
#[derive(Clone, Debug, PartialEq)]
pub enum RunStatus {
    /// The VM is still running.
    Running,

    /// The VM exited because it reached instruction count limit.
    InstructionLimit,

    /// The VM exited because it reached a breakpoint.
    Breakpoint,

    /// The VM exited because the interrupt flag was set.
    Interrupted,

    /// The VM has halted.
    Halt,

    /// Killed by an environment specific mechanism.
    Killed,

    /// A deadlock was detected.
    Deadlock,

    /// MMU was unable to allocate memory for an operation.
    OutOfMemory,

    /// Internal error where the emulator reached unimplemented code.
    Unimplemented,

    /// The VM exited due to a unhandled exception.
    UnhandledException,
}

#[pyclass(eq, eq_int, module = "icicle")]
#[derive(Clone, Debug, PartialEq)]
pub enum MemoryExceptionCode {
    Unallocated,
    Unmapped,
    UnmappedRegister,
    Uninitialized,
    ReadViolation,
    WriteViolation,
    ExecViolation,
    ReadWatch,
    WriteWatch,
    Unaligned,
    OutOfMemory,
    SelfModifyingCode,
    AddressOverflow,
    Unknown,
}

impl From<MemError> for MemoryExceptionCode {
    fn from(value: MemError) -> Self {
        match value {
            MemError::Unallocated => MemoryExceptionCode::Unallocated,
            MemError::Unmapped => MemoryExceptionCode::Unmapped,
            MemError::UnmappedRegister => MemoryExceptionCode::UnmappedRegister,
            MemError::Uninitalized => MemoryExceptionCode::Uninitialized,
            MemError::ReadViolation => MemoryExceptionCode::ReadViolation,
            MemError::WriteViolation => MemoryExceptionCode::WriteViolation,
            MemError::ExecViolation => MemoryExceptionCode::ExecViolation,
            MemError::ReadWatch => MemoryExceptionCode::ReadWatch,
            MemError::WriteWatch => MemoryExceptionCode::WriteWatch,
            MemError::Unaligned => MemoryExceptionCode::Unaligned,
            MemError::OutOfMemory => MemoryExceptionCode::OutOfMemory,
            MemError::SelfModifyingCode => MemoryExceptionCode::SelfModifyingCode,
            MemError::AddressOverflow => MemoryExceptionCode::AddressOverflow,
            MemError::Unknown => MemoryExceptionCode::Unknown,
        }
    }
}

#[pyclass(eq, eq_int, module = "icicle", name = "ExceptionCode")]
#[derive(Clone, Debug, PartialEq)]
pub enum ExceptionCodePy {
    NoException = 0x0000,

    InstructionLimit = 0x0001,
    Halt = 0x0002,
    Sleep = 0x0003,
    SoftwareBreakpoint = 0x0004,

    Syscall = 0x0101,
    CpuStateChanged = 0x0102,
    DivisionException = 0x0103,

    ReadUnmapped = 0x0201,
    ReadPerm = 0x0202,
    ReadUnaligned = 0x0203,
    ReadWatch = 0x0204,
    ReadUninitialized = 0x0205,

    WriteUnmapped = 0x0301,
    WritePerm = 0x0302,
    WriteWatch = 0x0303,
    WriteUnaligned = 0x0304,

    ExecViolation = 0x0401,
    SelfModifyingCode = 0x0402,
    ExecUnaligned = 0x0403,

    OutOfMemory = 0x0501,
    AddressOverflow = 0x0502,

    InvalidInstruction = 0x1001,
    UnknownInterrupt = 0x1002,
    UnknownCpuID = 0x1003,
    InvalidOpSize = 0x1004,
    InvalidFloatSize = 0x1005,
    CodeNotTranslated = 0x1006,
    ShadowStackOverflow = 0x1007,
    ShadowStackInvalid = 0x1008,
    InvalidTarget = 0x1009,
    UnimplementedOp = 0x100a,

    ExternalAddr = 0x2001,
    Environment = 0x2002,

    JitError = 0x3001,
    InternalError = 0x3002,
    UnmappedRegister = 0x3003,

    UnknownError,
}

impl From<ExceptionCode> for ExceptionCodePy {
    fn from(value: ExceptionCode) -> Self {
        match value {
            ExceptionCode::None => ExceptionCodePy::NoException,
            ExceptionCode::InstructionLimit => ExceptionCodePy::InstructionLimit,
            ExceptionCode::Halt => ExceptionCodePy::Halt,
            ExceptionCode::Sleep => ExceptionCodePy::Sleep,
            ExceptionCode::SoftwareBreakpoint => ExceptionCodePy::SoftwareBreakpoint,
            ExceptionCode::Syscall => ExceptionCodePy::Syscall,
            ExceptionCode::CpuStateChanged => ExceptionCodePy::CpuStateChanged,
            ExceptionCode::DivisionException => ExceptionCodePy::DivisionException,
            ExceptionCode::ReadUnmapped => ExceptionCodePy::ReadUnmapped,
            ExceptionCode::ReadPerm => ExceptionCodePy::ReadPerm,
            ExceptionCode::ReadUnaligned => ExceptionCodePy::ReadUnaligned,
            ExceptionCode::ReadWatch => ExceptionCodePy::ReadWatch,
            ExceptionCode::ReadUninitialized => ExceptionCodePy::ReadUninitialized,
            ExceptionCode::WriteUnmapped => ExceptionCodePy::WriteUnmapped,
            ExceptionCode::WritePerm => ExceptionCodePy::WritePerm,
            ExceptionCode::WriteWatch => ExceptionCodePy::WriteWatch,
            ExceptionCode::WriteUnaligned => ExceptionCodePy::WriteUnaligned,
            ExceptionCode::ExecViolation => ExceptionCodePy::ExecViolation,
            ExceptionCode::SelfModifyingCode => ExceptionCodePy::SelfModifyingCode,
            ExceptionCode::ExecUnaligned => ExceptionCodePy::ExecUnaligned,
            ExceptionCode::OutOfMemory => ExceptionCodePy::OutOfMemory,
            ExceptionCode::AddressOverflow => ExceptionCodePy::AddressOverflow,
            ExceptionCode::InvalidInstruction => ExceptionCodePy::InvalidInstruction,
            ExceptionCode::UnknownInterrupt => ExceptionCodePy::UnknownInterrupt,
            ExceptionCode::UnknownCpuID => ExceptionCodePy::UnknownCpuID,
            ExceptionCode::InvalidOpSize => ExceptionCodePy::InvalidOpSize,
            ExceptionCode::InvalidFloatSize => ExceptionCodePy::InvalidFloatSize,
            ExceptionCode::CodeNotTranslated => ExceptionCodePy::CodeNotTranslated,
            ExceptionCode::ShadowStackOverflow => ExceptionCodePy::ShadowStackOverflow,
            ExceptionCode::ShadowStackInvalid => ExceptionCodePy::ShadowStackInvalid,
            ExceptionCode::InvalidTarget => ExceptionCodePy::InvalidTarget,
            ExceptionCode::UnimplementedOp => ExceptionCodePy::UnimplementedOp,
            ExceptionCode::ExternalAddr => ExceptionCodePy::ExternalAddr,
            ExceptionCode::Environment => ExceptionCodePy::Environment,
            ExceptionCode::JitError => ExceptionCodePy::JitError,
            ExceptionCode::InternalError => ExceptionCodePy::InternalError,
            ExceptionCode::UnmappedRegister => ExceptionCodePy::UnmappedRegister,
            ExceptionCode::UnknownError => ExceptionCodePy::UnknownError,
        }
    }
}

#[allow(non_snake_case)]
fn raise_MemoryException(message: String, e: MemError) -> PyErr {
    Python::with_gil(|py| {
        let icicle = py.import("icicle").unwrap();
        let exception = icicle.getattr("MemoryException").unwrap();
        let args = (message, MemoryExceptionCode::from(e));
        let inst = exception.call1(args).unwrap();
        PyErr::from_value(inst)
    })
}

fn convert_protection(protection: MemoryProtection) -> u8 {
    match protection {
        MemoryProtection::NoAccess => perm::NONE,
        MemoryProtection::ReadOnly => perm::READ,
        MemoryProtection::ReadWrite => perm::READ | perm::WRITE,
        MemoryProtection::ExecuteOnly => perm::EXEC,
        MemoryProtection::ExecuteRead => perm::EXEC | perm::READ,
        MemoryProtection::ExecuteReadWrite => perm::EXEC | perm::READ | perm::WRITE,
    }
}

#[pyclass(unsendable, module = "icicle")]
pub struct Icicle {
    architecture: String,
    vm: icicle_vm::Vm,
    regs: HashMap<String, NamedRegister>,
}

fn reg_find<'a>(i: &'a Icicle, name: &str) -> PyResult<&'a NamedRegister> {
    let sleigh = &i.vm.cpu.arch.sleigh;
    match sleigh.get_reg(name) {
        None => {
            i.regs.get(name.to_lowercase().as_str())
                .ok_or(
                    PyKeyError::new_err(format!("Register not found: {name}"))
                )
        }
        Some(r) => Ok(r),
    }
}

#[pymethods]
impl Icicle {
    #[getter]
    pub fn get_icount_limit(&mut self) -> u64 {
        self.vm.icount_limit
    }

    #[setter]
    pub fn set_icount_limit(&mut self, value: u64) {
        self.vm.icount_limit = value;
    }

    #[getter]
    pub fn get_icount(&mut self) -> u64 {
        self.vm.cpu.icount
    }

    #[setter]
    pub fn set_icount(&mut self, value: u64) {
        self.vm.cpu.icount = value;
    }

    #[getter]
    pub fn get_exception_code(&self) -> ExceptionCodePy {
        ExceptionCode::from_u32(self.vm.cpu.exception.code).into()
    }

    #[getter]
    pub fn get_exception_value(&self) -> u64 {
        self.vm.cpu.exception.value
    }

    #[getter]
    pub fn get_architecture(&self) -> String {
        self.architecture.to_string()
    }

    #[getter]
    pub fn get_pc(&self) -> u64 {
        self.vm.cpu.read_pc()
    }

    #[setter]
    pub fn set_pc(&mut self, address: u64) {
        self.vm.cpu.write_pc(address)
    }

    #[getter]
    pub fn get_sp(&mut self) -> u64 {
        self.vm.cpu.read_reg(self.vm.cpu.arch.reg_sp)
    }

    #[setter]
    pub fn set_sp(&mut self, address: u64) {
        self.vm.cpu.write_reg(self.vm.cpu.arch.reg_sp, address)
    }

    #[new]
    #[pyo3(signature = (
        architecture,
        jit = true,
        jit_mem = true,
        shadow_stack = false,
        recompilation = true,
        track_uninitialized = false,
        optimize_instructions = true,
        optimize_block = false,
        tracing = false,
    ))]
    pub fn new(
        architecture: String,
        jit: bool,
        jit_mem: bool,
        shadow_stack: bool,
        recompilation: bool,
        track_uninitialized: bool,
        optimize_instructions: bool,
        optimize_block: bool,
        tracing: bool,
    ) -> PyResult<Self> {
        // Prevent mixing '_' and '-'
        if architecture.split("-").count() != 1 {
            return Err(
                PyException::new_err(format!("Bad architecture format: {architecture}"))
            );
        }

        // TODO: support instantiating this multiple times
        if tracing {
            if tracing_subscriber::fmt()
                .with_max_level(tracing::Level::DEBUG)
                .with_target(false)
                .try_init().is_err() {
            }
        }

        // Set up the CPU state for the target triple
        let mut config = icicle_vm::cpu::Config::from_target_triple(
            format!("{architecture}-none").as_str()
        );
        if config.triple.architecture == target_lexicon::Architecture::Unknown {
            return Err(
                PyException::new_err(format!("Unknown architecture: {architecture}"))
            );
        }

        // Configuration
        config.enable_jit = jit;
        config.enable_jit_mem = jit_mem;
        config.enable_shadow_stack = shadow_stack;
        config.enable_recompilation = recompilation;
        config.track_uninitialized = track_uninitialized;
        config.optimize_instructions = optimize_instructions;
        config.optimize_block = optimize_block;

        let mut vm = icicle_vm::build(&config)
            .map_err(|e| {
                PyException::new_err(format!("VM build error: {e}"))
            })?;

        // Populate the lowercase register map
        let mut regs = HashMap::new();
        let sleigh = &vm.cpu.arch.sleigh;
        for reg in &sleigh.named_registers {
            let name = sleigh.get_str(reg.name);
            regs.insert(name.to_lowercase(), reg.clone());
        }

        // Special handling for x86 flags
        match config.triple.architecture {
            Architecture::X86_32(_) | Architecture::X86_64 | Architecture::X86_64h => {
                let eflags = sleigh.get_reg("eflags").unwrap().var;
                let reg_handler = X86FlagsRegHandler { eflags };
                vm.cpu.add_reg_handler(eflags.id, Box::new(reg_handler));
            }
            _ => {}
        }

        Ok(Icicle {
            architecture,
            vm,
            regs,
        })
    }

    pub fn __str__(&mut self) -> String {
        let arch = &self.vm.cpu.arch;
        let endianness = if arch.sleigh.big_endian {
            "big endian"
        } else {
            "little endian"
        };
        format!("Icicle VM for {0:?} ({endianness})", self.architecture)
    }

    pub fn mem_map(&mut self, address: u64, size: u64, protection: MemoryProtection) -> PyResult<()> {
        let init_perm = if self.vm.cpu.mem.track_uninitialized { perm::NONE } else { perm::INIT };
        let mapping = Mapping {
            perm: convert_protection(protection) | init_perm,
            value: 0,
        };
        if self.vm.cpu.mem.map_memory_len(address, size, mapping) {
            Ok(())
        } else {
            Err(
                raise_MemoryException(
                    format!("Failed to map memory {address:X}[{size:X}]"),
                    MemError::Unknown,
                )
            )
        }
    }

    pub fn mem_unmap(&mut self, address: u64, size: u64) -> PyResult<()> {
        if self.vm.cpu.mem.unmap_memory_len(address, size) {
            Ok(())
        } else {
            Err(
                raise_MemoryException(
                    format!("Failed to unmap memory {address:X}[{size:X}]"),
                    MemError::Unknown,
                )
            )
        }
    }

    pub fn mem_protect(&mut self, address: u64, size: usize, protection: MemoryProtection) -> PyResult<()> {
        self.vm.cpu.mem.update_perm(address, size as u64, convert_protection(protection))
            .map_err(|e| {
                raise_MemoryException(
                    format!("Failed to protect memory {address:X}[{size:X}]"),
                    e,
                )
            })?;
        Ok(())
    }

    pub fn mem_read(&mut self, address: u64, size: usize) -> PyResult<Cow<[u8]>> {
        // Allocate a buffer
        let mut buffer = Vec::with_capacity(size);
        buffer.resize(size, 0);

        // Read the memory
        self.vm.cpu.mem.read_bytes(address, &mut buffer[..], perm::NONE)
            .map_err(|e| {
                raise_MemoryException(
                    format!("Failed to read memory {address:X}[{size:X}]"),
                    e,
                )
            })?;
        Ok(Cow::Owned(buffer))
    }

    pub fn mem_write(&mut self, address: u64, data: Vec<u8>) -> PyResult<()> {
        let size = data.len();
        self.vm.cpu.mem.write_bytes(address, &data[..], perm::NONE)
            .map_err(|e| {
                raise_MemoryException(
                    format!("Failed to write memory {address:X}[{size:X}]"),
                    e,
                )
            })
    }

    pub fn reg_list(&self) -> PyResult<IndexMap<String, (u32, u8)>> {
        let mut result = IndexMap::new();
        let sleigh = &self.vm.cpu.arch.sleigh;
        for reg in &sleigh.named_registers {
            let name = sleigh.get_str(reg.name);
            result.insert(name.to_string(), (reg.offset, reg.var.size));
        }
        Ok(result)
    }

    pub fn reg_offset(&self, name: &str) -> PyResult<u32> {
        Ok(reg_find(self, name)?.offset)
    }

    pub fn reg_size(&self, name: &str) -> PyResult<u8> {
        Ok(reg_find(self, name)?.var.size)
    }

    pub fn reg_read(&mut self, name: &str) -> PyResult<u64> {
        Ok(self.vm.cpu.read_reg(reg_find(self, name)?.var))
    }

    pub fn reg_write(&mut self, name: &str, value: u64) -> PyResult<()> {
        let var = reg_find(self, name)?.var;
        if var == self.vm.cpu.arch.reg_pc {
            self.vm.cpu.write_pc(value);
        } else {
            self.vm.cpu.write_reg(var, value);
        }
        Ok(())
    }

    pub fn reset(&mut self) {
        self.vm.reset();
    }

    pub fn run(&mut self) -> RunStatus {
        match self.vm.run() {
            VmExit::Running => RunStatus::Running,
            VmExit::InstructionLimit => RunStatus::InstructionLimit,
            VmExit::Breakpoint => RunStatus::Breakpoint,
            VmExit::Interrupted => RunStatus::Interrupted,
            VmExit::Halt => RunStatus::Halt,
            VmExit::Killed => RunStatus::Killed,
            VmExit::Deadlock => RunStatus::Deadlock,
            VmExit::OutOfMemory => RunStatus::OutOfMemory,
            VmExit::Unimplemented => RunStatus::Unimplemented,
            VmExit::UnhandledException(_) => RunStatus::UnhandledException,
        }
    }

    pub fn run_until(&mut self, address: u64) -> RunStatus {
        let breakpoint_added = self.vm.add_breakpoint(address);
        let status = self.run();
        if breakpoint_added {
            self.vm.remove_breakpoint(address);
        }
        status
    }

    pub fn step(&mut self, count: u64) -> RunStatus {
        let old_limit = self.vm.icount_limit;
        self.vm.icount_limit = self.vm.cpu.icount.saturating_add(count);
        let status = self.run();
        self.vm.icount_limit = old_limit;
        status
    }

    pub fn add_breakpoint(&mut self, address: u64) -> bool {
        self.vm.add_breakpoint(address)
    }

    pub fn remove_breakpoint(&mut self, address: u64) -> bool {
        self.vm.remove_breakpoint(address)
    }
}

#[pyfunction]
pub fn architectures() -> PyResult<Vec<&'static str>> {
    Ok(vec![
        "i686",
        "x86_64",
        "aarch64",
    ])
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn icicle(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(architectures, m)?)?;
    m.add_class::<Icicle>()?;
    m.add_class::<MemoryProtection>()?;
    m.add_class::<MemoryExceptionCode>()?;
    m.add_class::<RunStatus>()?;
    m.add_class::<ExceptionCodePy>()?;
    Ok(())
}