use std::{
    fmt::{self, Arguments, Debug},
    str::FromStr,
    sync::{
        OnceLock,
        atomic::{AtomicU8, Ordering},
    },
};

mod cli_logger;
pub use cli_logger::*;

static LOGGER: OnceLock<&'static dyn Log> = OnceLock::new();

/// Sets the logger for the running program. This can only be done once and cannot be reversed
///
/// # Errors
///
/// Errors if a logger is already set, returns the set logger
pub fn set_logger(logger: &'static dyn Log) -> Result<(), &'static dyn Log> {
    LOGGER.set(logger)
}

/// Gets the logger for the running program, if any
pub fn get_logger() -> Option<&'static dyn Log> {
    LOGGER.get().copied()
}

#[derive(Debug, thiserror::Error)]
pub enum LogLevelParseError {
    #[error("Invalid log level: {0}")]
    InvalidInput(String),
}

/// A level filter for any incoming logs to filter logs that should be disabled
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LevelFilter {
    Off,
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

/// The log level of something.
///
/// This is used in logging to prevent logs which should be filtered out by the [`LevelFilter`]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
    Error = 1,
    Warn,
    Info,
    Debug,
    Trace,
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogLevel::Error => f.write_str("Error"),
            LogLevel::Warn => f.write_str("Warn"),
            LogLevel::Info => f.write_str("Info"),
            LogLevel::Debug => f.write_str("Debug"),
            LogLevel::Trace => f.write_str("Trace"),
        }
    }
}

impl FromStr for LevelFilter {
    type Err = LogLevelParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "off" | "quiet" => Ok(LevelFilter::Off),
            "error" => Ok(LevelFilter::Error),
            "warn" => Ok(LevelFilter::Warn),
            "info" => Ok(LevelFilter::Info),
            "debug" => Ok(LevelFilter::Debug),
            "trace" => Ok(LevelFilter::Trace),
            _ => Err(LogLevelParseError::InvalidInput(s.to_owned())),
        }
    }
}

static MAX_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
/// The max log level of a program. Defaults to [`LevelFilter::Off`]
///
/// This function does not have to be used if you want custom log filtering logic. See [`Log::enabled()`] for more info
pub fn set_max_log_level(level: LevelFilter) {
    MAX_LOG_LEVEL.store(level as u8, Ordering::Relaxed);
}

/// A basic function to check if a [`LogLevel`] is filtered out by the max log level's [`LevelFilter`]
///
/// This function does not have to be used if you want custom log filtering logic. See [`Log::enabled()`] for more info
pub fn can_log(level: LogLevel) -> bool {
    MAX_LOG_LEVEL.load(Ordering::Relaxed) >= level as u8
}

pub trait Log: Sync + Debug {
    /// This function should return true for the given log level if its allowed to be logged
    fn enabled(&self, level: LogLevel) -> bool {
        can_log(level)
    }

    /// This function should provide an unchecked log function, like calling `println!()`
    ///
    /// See [`Self::log()`] for a version that uses [`Self::enabled()`] to only log a string if logging is enabled
    fn log_unchecked(&self, level: LogLevel, msg: Arguments);

    /// Logs a string if allowed
    fn log(&self, level: LogLevel, msg: Arguments) {
        if self.enabled(level) {
            self.log_unchecked(level, msg);
        }
    }
}

/// Attempts to log format args to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! log {
    ($level:expr, $($arg:tt)*) => {{
        if let Some(logger) = $crate::log::get_logger()
            && logger.enabled($level) {
                logger.log_unchecked($level, format_args!($($arg)*))
        }
    }};
}

/// Attempts to log format args as an error to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! error {
    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Error, $($arg)*) };
}

/// Attempts to log format args as a warning to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! warn {
    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Warn, $($arg)*) };
}

/// Attempts to log format args as info to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! info {
    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Info, $($arg)*) };
}

/// Attempts to log format args as debug to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! debug {
    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Debug, $($arg)*) };
}

/// Attempts to log format args as a trace to the current logger lazily
///
/// If the logger rejects the log or none is set, the args will not be parsed
#[macro_export]
macro_rules! trace {
    ($($arg:tt)*) => { $crate::log!($crate::log::LogLevel::Trace, $($arg)*) };
}
