use pyo3::conversion::IntoPy;
use pyo3::prelude::*;
use pyo3::types::IntoPyDict;
use pyo3::types::{PyAny, PyDict, PyTuple};
use pyo3::GILGuard;

/// Create HashMap.
#[macro_export]
macro_rules! pyo3_hash {
    ($py:expr) => {{
        use pyo3::{prelude::Py, types::PyAny};
        use std::collections::HashMap;
        HashMap::<&str, Py<PyAny>>::new().into_py_dict($py)
    }};
}

/// Binding to create *args.
#[macro_export]
macro_rules! args {
    ($py:expr, $($var:expr), +) => {{
        use pyo3::{types::PyTuple, ToPyObject};
        PyTuple::new($py, &[$($var.to_object($py)), +])
    }};
}

/// Binding to add to existing *args.
#[macro_export]
macro_rules! add_to_args {
    ($py:expr, $tuple:expr, $($var:expr), +) => {{
        use pyo3::{types::PyTuple, ToPyObject};
        let mut args = vec![$($var.to_object($py)), +];
        for element in $tuple.iter() {
            args.push(element.to_object($py));
        }
        PyTuple::new($py, args)
    }};
}

/// Binding to add to existing *args at the end.
#[macro_export]
macro_rules! add_to_args_after {
    ($py:expr, $tuple:expr, $($var:expr), +) => {{
        use pyo3::{types::PyTuple, ToPyObject};
        let mut args = vec![];
        for element in $tuple.iter() {
            args.push(element.to_object($py));
        }
        $(args.push($var.to_object($py)))+
        PyTuple::new($py, args)
    }};
}

/// Binding to create **kwargs.
#[macro_export]
macro_rules! kwargs {
    ($py:expr, $(($key:expr, $value:expr)), *) => {{
        use pyo3::{ToPyObject, types::IntoPyDict};
        let mut hash = $crate::pyo3_hash!($py);
        $(
            let value = $value.to_object($py);
            hash.set_item($key, value).unwrap();
        )*
        hash
    }};
}

/// Binding to create **kwargs from variable without need of key.
#[macro_export]
macro_rules! kwargs_auto {
    ($py:expr, $($var:expr), *) => {
        [$((stringify!($var), $var.to_object($py))), *].into_py_dict($py)
    };
    ($py:expr) => {
        pyo3_hash!()
    };
}

/// Binding to add var to HashMap if is some.
#[macro_export]
macro_rules! add_hashmap {
    ($py:expr, &$hashmap:expr, $var:expr) => {
        if let Some(var) = $var {
            $hashmap.insert(stringify!($var), var.to_object($py));
        }
    };
}

/// Binding to create **kwargs and add var if is some.
#[macro_export]
macro_rules! kwargs_filtered {
    ($py:expr, $($var:expr), +) => {
        {
            let hash = pyo3_hash!($py);
            $(
                if let Some(var) = $var {
                hash.set_item(stringify!($var), var.to_object($py)).unwrap();
            };
            )+
            hash
        }
    };
}

/// Structure interface to plot graphs.
pub struct Graph {
    /// The Python GIL.
    gil: GILGuard,
}

impl Graph {
    /// Constructor.
    pub fn new(nrows: usize, ncols: usize) -> Self {
        let mut graph = Self {
            gil: Python::acquire_gil(),
        };
        graph.add_module_to_path();
        graph.initialize_figure(nrows, ncols);
        graph
    }

    /// Binding to access the Python marker.
    pub fn py(&self) -> Python {
        self.gil.python()
    }

    /// Binding to run code.
    pub fn run(&self, code: &str, globals: Option<&PyDict>, locals: Option<&PyDict>) {
        self.py().run(code, globals, locals).unwrap();
    }

    /// Binding to eval code.
    pub fn eval(&self, code: &str, globals: Option<&PyDict>, locals: Option<&PyDict>) -> &PyAny {
        self.py().eval(code, globals, locals).unwrap()
    }

    /// Binding to import a module.
    pub fn import(&self, name: &str) -> &PyModule {
        self.py().import(name).unwrap()
    }

    /// Add current module to path for relative imports.
    fn add_module_to_path(&mut self) {
        let sys = self.import("sys");
        let os = self.import("os");
        let locals = kwargs_auto!(self.py(), sys, os);
        self.run("sys.path.insert(0, os.getcwd())", None, Some(locals));
    }

    /// Complete initialization.
    fn initialize_figure(&mut self, nrows: usize, ncols: usize) {
        self.run(r#"from src import Interface"#, None, None);
        self.run("interface = Interface()", None, None);
        let locals = kwargs_auto!(self.py(), nrows, ncols);
        self.run("interface.build(nrows, ncols)", None, Some(locals));
    }

    /// Binding to instance of Interface.
    fn interface(&self) -> &PyAny {
        self.eval("interface", None, None).extract().unwrap()
    }

    /// Binding to method0 for Interface.
    pub fn method0(&self, name: &str) -> &PyAny {
        self.interface().call_method0(name).unwrap()
    }

    /// Binding to method1 for Interface.
    pub fn method1(&self, name: &str, args: impl IntoPy<Py<PyTuple>>) -> &PyAny {
        self.interface().call_method1(name, args).unwrap()
    }

    /// Binding to method2 for Interface.
    pub fn method2(&self, name: &str, args: impl IntoPy<Py<PyTuple>>, kwargs: &PyDict) -> &PyAny {
        self.interface()
            .call_method(name, args, Some(kwargs))
            .unwrap()
    }

    /// Set title.
    pub fn set_title(&self, title: &str) {
        self.method1("set_title", (title,));
    }

    /// Set xlabel.
    pub fn set_xlabel(&self, index: usize, label: &str) {
        self.method1("set_xlabel", (index, label));
    }

    /// Set ylabel.
    pub fn set_ylabel(&self, index: usize, label: &str) {
        self.method1("set_ylabel", (index, label));
    }

    /// Set axis limits.
    pub fn set_axis_limits(
        &self,
        index: usize,
        xmin: Option<f64>,
        xmax: Option<f64>,
        ymin: Option<f64>,
        ymax: Option<f64>,
    ) {
        let py = self.py();
        let limits = kwargs_filtered!(py, xmin, xmax, ymin, ymax);
        self.method2("set_axis_limits", (index,), limits);
    }

    /// Add a title to the legend.
    pub fn set_legend_title(&self, index: usize, title: &str) {
        self.method1("set_legend_title", (index, title));
    }

    /// Add a plot.
    pub fn plot(&self, index: usize, args: &PyTuple, kwargs: &PyDict) {
        let newargs = add_to_args!(self.py(), args, index);
        self.method2("plot", newargs, kwargs);
    }

    /// To save the figure.
    pub fn save(&self, name: &str, dpi: Option<usize>) {
        self.method1("save", (name, dpi));
    }

    /// Display the figure in interactive mode.
    pub fn show(&self, block: bool) {
        self.method1("show", (block,));
    }
}
