use std::{collections::HashMap, convert::TryInto};

use super::{Executor, LoadModuleError, UnloadModuleError};
use crate::{
    engine::EngineRef,
    module::{DispatchError, Module},
};
use tempfile::tempdir;
use uuid::Uuid;

pub struct NativeExecutor {
    engine: Option<EngineRef>,
    modules: HashMap<Uuid, Box<NativeModule>>,
}

impl Default for NativeExecutor {
    fn default() -> Self {
        Self::new()
    }
}

impl NativeExecutor {
    pub fn new() -> Self {
        Self {
            engine: None,
            modules: HashMap::new(),
        }
    }
}

impl Executor for NativeExecutor {
    fn set_engine(&mut self, engine: EngineRef) {
        self.engine = Some(engine);
    }

    fn name(&self) -> &'static str {
        "native"
    }

    fn load_module(
        &mut self,
        module_definition: arora_types::module::low::ModuleDefinition,
    ) -> Result<Box<dyn Module>, LoadModuleError> {
        let tmp_dir = tempdir().map_err(|err| {
            LoadModuleError::Internal(format!(
                "failed put module in a temporary directory: {}",
                err
            ))
        })?;

        let tmp_file_path = tmp_dir
            .path()
            .join(format!("{}.bin", module_definition.header.name));

        std::fs::write(&tmp_file_path, module_definition.executable).map_err(|err| {
            LoadModuleError::Internal(format!("failed to write module to file: {}", err))
        })?;

        let lib = unsafe {
            libloading::Library::new(tmp_file_path).map_err(|err| {
                LoadModuleError::Internal(format!("failed to load module: {}", err))
            })?
        };

        Ok(Box::new(NativeModule { lib }))
    }

    fn unload_module(&mut self, module_id: uuid::Uuid) -> Result<(), super::UnloadModuleError> {
        if let Some(module) = self.modules.remove(&module_id) {
            module.lib.close().map_err(|err| {
                UnloadModuleError::Internal(format!("failed to unload module: {}", err))
            })
        } else {
            Err(UnloadModuleError::ModuleNotFound)
        }
    }
}

struct NativeModule {
    lib: libloading::Library,
}

impl Module for NativeModule {
    fn dispatch(
        &mut self,
        function_id: &uuid::Uuid,
        arg: &[u8],
    ) -> Result<Box<[u8]>, DispatchError> {
        unsafe {
            let func: libloading::Symbol<unsafe extern "C" fn(usize) -> usize> = self
                .lib
                .get(
                    format!(
                        "arora_function_{}",
                        function_id.to_string().replace('-', "_")
                    )
                    .as_bytes(),
                )
                .map_err(|err| DispatchError::Internal {
                    message: format!("failed to get function {}: {}", function_id, err),
                })?;

            let arg_address = arg.as_ptr() as usize;
            let res_address = func(arg_address);
            let res_ptr = res_address as *mut u8;
            let size_buf = std::slice::from_raw_parts(res_ptr, 4);
            let size = u32::from_be_bytes(size_buf.try_into().unwrap());
            let res_buf: *const [u8] = std::ptr::slice_from_raw_parts(res_ptr, size as usize);
            Ok(Box::from_raw(res_buf as *mut [u8]))
        }
    }
}
