#[cfg(test)]
mod tests {
    use crate::{Logger, logger};

    #[test]
    fn log() {
        let x = 30;
        let mut logger = Logger::new("MAIN", std::io::stdout());
        logger.options.time_unit = logger::TimeUnit::Microseconds;
        logger.info("We will be testing a fibonnaci function");
        logger.info(format!("Starting fibonnaci({})", x));
        logger.timer_start("wrap");
        logger.timer_start("fibonnaci");
        let y = fibo(x);
        logger.timer_log_and_stop("fibonnaci");
        logger.timer_log_and_stop("wrap");
        logger.info(format!("The result of the fibonnaci function until {} is : {}", x, y));
    }

    fn fibo(n: usize) -> usize {
        if n < 3 {
            1
        } else {
            fibo_alt(1, 1, n)
        }
    }

    fn fibo_alt(a: usize, b: usize, n: usize) -> usize {
        if n == 2 {
            b
        } else {
            fibo_alt(b, a + b, n - 1)
        }
    }
}

pub use crate::logger::Logger;

pub mod logger {
    use std::{collections::HashMap, fmt::Display, io::stderr, time::{SystemTime, Duration}};
    use crate::modules::{self, Module};

    /// A modular and powerful logger
    ///
    /// # Example
    ///
    /// ```
    /// use rusty_logger::{logger, Logger};
    ///
    /// let mut logger = Logger::new("EXAMPLE", std::io::stdout());
    ///
    /// logger.options.time_unit = logger::TimeUnit::Nanoseconds;
    ///
    /// logger.info("We will be timing a for loop");
    /// logger.timer_start("for loop");
    /// for i in 0..1000 {
    ///     // Do something...
    /// }
    /// logger.timer_log_and_stop("for loop");
    /// logger.info("The for loop ended successfully");
    /// ```
    pub struct Logger<T: std::io::Write> {
        pub options: Options,
        timer: Timer,
        modules : Vec<Box<dyn Module>>,
        output: T,
    }

    pub struct LogMessage<'a> {
        pub content: &'a str,
        pub msg_type: LogType,
        pub options: &'a Options,
    }

    /// A struct that stores the core data of a logger
    ///
    /// You may use the default configuration or make
    /// your own with the `homemade` function depending
    /// on your needs
    pub struct Options {
        pub name: String,
        pub time_unit: TimeUnit,
        pub colors: bool,
    }

    struct Timer {
        timers: HashMap<String, (SystemTime, SystemTime, bool)>,
    }

    pub enum LogType {
        Info,
        Warning,
        Error,
        Time,
    }

    #[derive(Copy, Clone)]
    pub enum TimeUnit {
        Nanoseconds,
        Microseconds,
        Milliseconds,
    }

    impl Options {
        pub fn new(s: &str) -> Options {
            Options {
                name: s.to_string(),
                colors: true,
                time_unit: TimeUnit::Milliseconds,
            }
        }
    }

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

    impl Timer {
        pub fn new() -> Timer {
            Timer {
                timers: HashMap::new(),
            }
        }

        fn start(&mut self, msg: &str) {
            self.timers.insert(msg.to_string(), (SystemTime::now(), SystemTime::now(), false));
        }

        fn get(&self, msg: &str) -> Option<Duration> {
            self.timers.get(msg).map(|time| time.0.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
        }

        fn stop(&mut self, msg: &str) -> Option<Duration> {
            self.timers.remove(msg).map(|time| time.0.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
        }

        fn reset(&mut self, msg: &str) -> Option<Duration> {
            self.timers.insert(msg.to_string(), (SystemTime::now(), SystemTime::now(), false)).map(|time| time.0.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
        }

        fn pause(&mut self, msg: &str) -> Option<Duration> {
            if self.timers.contains_key(msg) {
                let timer = self.timers.entry(msg.to_string()).or_insert((
                    SystemTime::now(),
                    SystemTime::now(),
                    false,
                ));
                if timer.2 {
                    return None;
                }
                timer.1 = SystemTime::now();
                timer.2 = true;
                Some(timer.0.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
            } else {
                None
            }
        }

        fn resume(&mut self, msg: &str) -> Option<Duration>{
            if self.timers.contains_key(msg) {
                let timer = self.timers.entry(msg.to_string()).or_insert((
                    SystemTime::now(),
                    SystemTime::now(),
                    false,
                ));
                if timer.2 {
                    timer.2 = false;
                    timer.0 = timer
                        .0
                        .checked_add(timer.1.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
                        .unwrap_or_else(SystemTime::now);
                }
                Some(timer.0.elapsed().unwrap_or_else(|_| Duration::from_micros(0)))
            } else {
                None
            }
        }
    }

    impl<T: std::io::Write> Logger<T> {
        /// Creates a new Logger with default values and the given name
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        /// logger.info("This is a new logger named 'name' !");
        /// ```
        pub fn new(s: &str, output: T) -> Self {
            let modules: Vec<Box<dyn Module>> = vec!(Box::new(modules::Name{}), Box::new(modules::Time{}) , Box::new(modules::Type{}));
            Logger {
                options: Options::new(s),
                timer: Timer::new(),
                modules,
                output,
            }
        }

        /// Creates a new Logger with custom options and modules
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::{
        ///     logger, Logger,
        ///     modules::{self, Module},
        /// };
        ///
        /// let options = logger::Options::new("homemade logger");
        /// 
        /// let modules: Vec<Box<dyn Module>> = vec!(Box::new(modules::Name{}), Box::new(modules::Time{}) , Box::new(modules::Type{}));
        ///
        /// let mut logger = Logger::homemade(std::io::stdout(), options, modules);
        /// 
        /// logger.info("This is a new logger named 'name' which has 'options' as its options !");
        /// ```
        pub fn homemade(output: T, options: Options, modules: Vec<Box<dyn Module>>) -> Logger<T> {
            Logger {
                options,
                timer: Timer::new(),
                modules,
                output,
            }
        }

        /// Formats the message with the current logger options
        /// and then prints it. It is a private function and is
        /// only used by the other internal functions to print
        /// the message.
        ///
        /// It goes through the options of the logger it is
        /// called on and formats the message accordingly.
        fn log(&mut self, msg: String, msg_type: LogType) {
            let mut output = String::new();

            let lm = LogMessage {
                content: &msg[..],
                msg_type,
                options: &self.options,
            };

            self.modules.iter_mut().for_each(|module| {
                output.push_str(&module.print(&lm));
            });

            match writeln!(self.output, "{}: {}", output, msg) {
                std::result::Result::Err(err) => Logger::static_log(err, LogType::Error, stderr()),
                std::result::Result::Ok(_) => {}
            }
        }

        /// Static implementation for the log function
        ///
        /// Mainly used to print internal error messages
        /// but can also be used by an end user
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::{Logger, logger::LogType};
        ///
        /// Logger::static_log("Printing on the fly", LogType::Info, std::io::stdout());
        /// ```
        pub fn static_log<D: Display>(msg: D, msg_type: LogType, output: T) {
            let mut logger = Logger::new("STATIC", output);
            logger.log(msg.to_string(), msg_type);
        }

        /// Logs the given message to the output
        /// as an information
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        /// logger.info("Here is some information I want to log");
        /// ```
        pub fn info<D: Display>(&mut self, msg: D) {
            self.log(msg.to_string(), LogType::Info);
        }

        /// Logs the given message to the output
        /// as a warning
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        /// logger.warn("Here is a warning");
        /// ```
        pub fn warn<D: Display>(&mut self, msg: D) {
            self.log(msg.to_string(), LogType::Warning);
        }

        /// Logs the given message to the output
        /// as an error
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        /// logger.info("Some error happened");
        /// ```
        pub fn error<D: Display>(&mut self, msg: D) {
            self.log(msg.to_string(), LogType::Error);
        }

        /// Starts a new timer with the given name
        /// and the actual time
        pub fn timer_start(&mut self, msg: &str) {
            self.timer.start(msg);
        }

        /// Gets the duration from when the timer
        /// with the given name was started
        pub fn timer_get(&self, msg: &str) -> Option<Duration> {
            self.timer.get(msg)
        }

        /// Stops the timer with the given name
        /// and returns it as a duration
        pub fn timer_stop(&mut self, msg: &str) -> Option<Duration> {
            self.timer.stop(msg)
        }

        /// Resets the timer with the given name
        /// and returns the duration of the timer
        /// before being reset as a duration
        pub fn timer_reset(&mut self, msg: &str) -> Option<Duration> {
            self.timer.reset(msg)
        }

        /// Pauses the timer with the given name
        /// and returns the duration of the timer
        /// before pausing it as a duration
        pub fn timer_pause(&mut self, msg: &str) -> Option<Duration> {
            self.timer.pause(msg)
        }

        /// Resumes the timer with the given name
        pub fn timer_resume(&mut self, msg: &str) -> Option<Duration> {
            self.timer.resume(msg)
        }

        /// Logs the time elapsed between the
        /// start/reset of the timer and now
        /// **without** reseting it
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        ///
        /// logger.timer_start("new_timer");
        ///
        /// std::thread::sleep(std::time::Duration::from_millis(1));
        ///
        /// logger.timer_log("new_timer");
        /// ```
        pub fn timer_log(&mut self, msg: &str) -> Option<Duration> {
            if let Some(time) = self.timer.get(msg) {
                match self.options.time_unit {
                    TimeUnit::Nanoseconds => {
                        self.log(format!("{} - {}ns", msg, time.as_nanos()), LogType::Time);
                    }
                    TimeUnit::Microseconds => {
                        self.log(format!("{} - {}μs", msg, time.as_micros()), LogType::Time);
                    }
                    TimeUnit::Milliseconds => {
                        self.log(format!("{} - {}ms", msg, time.as_millis()), LogType::Time);
                    }
                }
                Some(time)
            } else {
                Logger::static_log(msg, LogType::Error, stderr());
                None
            }
        }

        /// Logs the time elapsed between the
        /// start/reset of the timer and now
        /// and then stops it
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        ///
        /// logger.timer_start("new_timer");
        ///
        /// std::thread::sleep(std::time::Duration::from_millis(1));
        ///
        /// logger.timer_log_and_stop("new_timer");
        /// ```
        pub fn timer_log_and_stop(&mut self, msg: &str) -> Option<Duration> {
            self.timer_log(msg);
            self.timer_stop(msg)
        }

        /// Logs the time elapsed between the
        /// start/reset of the timer and now
        /// and then resets it
        ///
        /// # Example
        ///
        /// ```
        /// use rusty_logger::Logger;
        ///
        /// let mut logger = Logger::new("name", std::io::stdout());
        ///
        /// logger.timer_start("new_timer");
        ///
        /// std::thread::sleep(std::time::Duration::from_millis(1));
        ///
        /// logger.timer_log_and_reset("new_timer");
        ///
        /// std::thread::sleep(std::time::Duration::from_millis(1));
        ///
        /// logger.timer_log_and_stop("new_timer");
        /// ```
        pub fn timer_log_and_reset(&mut self, msg: &str) -> Option<Duration> {
            self.timer_log(msg);
            self.timer_reset(msg)
        }
    }
}

pub mod modules {
    use crate::logger::{LogType, LogMessage};
    use colored::{Color, Colorize};
    use chrono::Utc;

    /// Trait that allows your struct to be implemented
    /// as a logger module
    ///
    /// # Example
    ///
    /// ```
    /// use rusty_logger::modules::Module;
    /// use rusty_logger::logger::LogMessage;
    ///
    /// struct IncrementalModule {n: usize};
    ///
    /// impl Module for IncrementalModule {
    ///     fn print(&mut self, msg: &LogMessage) -> String {
    ///         self.n = self.n + 1;
    ///         (self.n - 1).to_string()
    ///     }
    /// }
    /// ```
    pub trait Module {
        fn print(&mut self, msg: &LogMessage) -> String;
    }

    /// A simple module to display the name of the logger
    pub struct Name;

    impl Module for Name {
        fn print(&mut self, msg: &LogMessage) -> String {
            if msg.options.colors {
                format!("[{}]", msg.options.name.color(Color::Blue))
            } else {
                format!("[{}]", msg.options.name)
            }
        }
    }

    /// A simple module to display the type of the message
    pub struct Type;

    impl Module for Type {
        fn print(&mut self, msg: &LogMessage) -> String {
            let s = if msg.options.colors {
                match msg.msg_type {
                    LogType::Error => "ERROR".color(Color::Red),
                    LogType::Warning => "WARN".color(Color::Yellow),
                    LogType::Info => "INFO".color(Color::Green),
                    LogType::Time => "TIME".color(Color::Cyan),
                }.to_string()
            } else {
                match msg.msg_type {
                    LogType::Error => "ERROR",
                    LogType::Warning => "WARN",
                    LogType::Info => "INFO",
                    LogType::Time => "TIME",
                }.to_string()
            };
            format!("[{}]", s)
        }
    }

    /// A simple module to display the time of the log
    pub struct Time;

    impl Module for Time {
        fn print(&mut self, msg: &LogMessage) -> String {
            if msg.options.colors {
                format!("[{}]", Utc::now().format("%T").to_string().color(Color::Magenta))
            } else {
                format!("[{}]", Utc::now().format("%T").to_string())
            }
        }
    }
}
