use clap::ArgMatches;
use std::any::Any;
use std::error::Error as StdError;

pub mod xoshiro256starstar;

use libloading::{Library, Symbol};

pub type Error = Box<dyn StdError + Send + Sync + 'static>;

#[derive(Default)]
pub struct PluginManager {
    plugins: Vec<Box<dyn Plugin>>,
    loaded_libraries: Vec<Library>,
}

impl PluginManager {
    pub fn new() -> Self {
        Self {
            plugins: Vec::new(),
            loaded_libraries: Vec::new(),
        }
    }

    pub(crate) unsafe fn load_single_plugin<'help>(
        &mut self,
        app: clap::App<'help>,
        path: &str,
    ) -> Result<clap::App<'help>, (clap::App<'help>, Error)> {
        type PluginCreate<'help> =
            unsafe fn(*const clap::App<'help>) -> (*mut clap::App<'help>, *mut dyn Plugin, *mut Error);

        let lib = match Library::new(path) {
            Ok(l) => l,
            Err(e) => return Err((app, e.to_string().into())),
        };

        self.loaded_libraries.push(lib); // Save library so it persists in memory
        let lib = self.loaded_libraries.last().unwrap();

        let constructor: Symbol<PluginCreate> = match lib.get(b"_plugin_create") {
            Ok(cons) => cons,
            Err(e) => return Err((app, e.to_string().into())),
        };

        let (app, boxed_raw, error) = constructor(Box::into_raw(Box::new(app)));
        let app = *Box::from_raw(app);

        if boxed_raw.is_null() {
            return Err((app, *Box::from_raw(error)));
        }
        let plugin = Box::from_raw(boxed_raw);
        self.plugins.push(plugin);

        Ok(app)
    }

    pub fn build(&self) -> Result<Vec<Box<dyn WorkerSpec + 'static>>, Error> {
        let mut specs = Vec::<Box<dyn WorkerSpec + 'static>>::new();
        for plugin in &self.plugins {
            if plugin.enabled() {
                specs.extend(plugin.get_worker_specs());
            }
        }
        Ok(specs)
    }

    pub fn process_options(&mut self, matches: &ArgMatches) -> Result<usize, Error> {
        let mut count = 0usize;
        self.plugins.iter_mut().for_each(|plugin| {
            count += match plugin.process_option(matches) {
                Ok(n) => n,
                Err(e) => {
                    eprintln!(
                        "WARNING: Failed processing options for {} (ignore if you do not intend to use): {}",
                        plugin.name(),
                        e
                    );
                    0
                }
            }
        });
        Ok(count)
    }

    pub fn has_specs(&self) -> bool {
        !self.plugins.is_empty()
    }

    // New method to access plugin names
    pub fn plugin_names(&self) -> Vec<&'static str> {
        self.plugins.iter().map(|plugin| plugin.name()).collect()
    }
}

pub trait Plugin: Any + Send + Sync {
    fn name(&self) -> &'static str;
    fn enabled(&self) -> bool;
    fn get_worker_specs(&self) -> Vec<Box<dyn WorkerSpec>>;
    fn process_option(&mut self, matches: &ArgMatches) -> Result<usize, Error>;
}

pub trait WorkerSpec: Any + Send + Sync {
    fn id(&self) -> String;
    fn build(&self) -> Box<dyn Worker>;
}

pub trait Worker {
    fn id(&self) -> String;
    fn load_block_constants(&mut self, hash_header: &[u8; 72], target: &[u64; 4]);
    fn calculate_hash(&mut self, nonces: Option<&Vec<u64>>, nonce_mask: u64, nonce_fixed: u64, timestamp: u64);
    fn sync(&self) -> Result<(), Error>;
    fn get_workload(&self) -> usize;
    fn copy_output_to(&mut self, nonces: &mut Vec<u64>) -> Result<(), Error>;
}

pub fn load_plugins<'help>(
    app: clap::App<'help>,
    paths: &[String],
) -> Result<(clap::App<'help>, PluginManager), Error> {
    let mut factory = PluginManager::new();
    let mut app = app;
    for path in paths {
        app = unsafe {
            factory.load_single_plugin(app, path.as_str()).unwrap_or_else(|(app, e)| {
                eprintln!("WARNING: Failed loading plugin {} (ignore if you do not intend to use): {}", path, e);
                app
            })
        };
    }
    Ok((app, factory))
}

#[macro_export]
macro_rules! declare_plugin {
    ($plugin_type:ty, $constructor:path, $args:ty) => {
        use clap::Args;
        #[no_mangle]
        pub unsafe extern "C" fn _plugin_create(
            app: *mut clap::App,
        ) -> (*mut clap::App, *mut dyn $crate::Plugin, *const $crate::Error) {
            let constructor: fn() -> Result<$plugin_type, $crate::Error> = $constructor;

            let object = match constructor() {
                Ok(obj) => obj,
                Err(e) => {
                    return (
                        app,
                        std::mem::MaybeUninit::zeroed().assume_init(),
                        Box::into_raw(Box::new(e)),
                    );
                }
            };

            let boxed: Box<dyn $crate::Plugin> = Box::new(object);
            let boxed_app = Box::new(<$args>::augment_args(unsafe { *Box::from_raw(app) }));
            (Box::into_raw(boxed_app), Box::into_raw(boxed), std::ptr::null::<Error>())
        }
    };
}