use super::{
    super::{attachment::*, cause::*, error::*, implementation::*, problems::*},
    implementation::*,
    into::*,
};

use std::{any::*, env::*, error::*, fmt, io};

/// Environment variable to control the auto-attachment of a [Location](std::panic::Location).
///
/// Enabled when set to anything other than "0".
pub const AUTO_ATTACH_LOCATION: &str = "PROBLEMO_LOCATION";

/// Environment variable to control the auto-attachment of a backtrace.
///
/// Enabled when set to anything other than "0".
pub const AUTO_ATTACH_BACKTRACE: &str = "PROBLEMO_BACKTRACE";

/// Separator for [Problem] [Display](fmt::Display).
pub const PROBLEM_DISPLAY_SEPARATOR: &str = ": ";

//
// Problem
//

/// A problem is a simple wrapper for an error causation chain.
///
/// Each link in the chain is a [Cause], which is itself a simple wrapper for a [CapturedError].
/// Each cause can have any number of attachments.
///
/// Though you can iterate a problem directly to access its causes, often it is easier to use one
/// of the many convenience functions in the [ProblemImplementation] and [Causes] traits.
///
/// Note that this type does not itself implement [Error] directly, but you can use
/// [into_error](Problem::into_error) if compatibility is needed.
///
/// For more information and usage examples see the
/// [Problemo home page](https://codeberg.org/tliron/problemo).
#[derive(Debug, Eq, PartialEq, PartialOrd)]
pub struct Problem {
    /// Causes in order of causation from top to root.
    pub causes: Deque<Cause>,
}

impl Problem {
    /// Constructor.
    #[track_caller]
    pub fn new(error: CapturedError) -> Self {
        auto_attach(Self {
            causes: [Cause::new(error)].into(),
        })
    }

    /// Add support for [Error].
    ///
    /// Take care to avoid adding it into a [Problem]'s causation chain.
    pub fn into_error(self) -> ProblemAsError {
        self.into()
    }

    /// Adds the error to the top of the causation chain.
    pub fn via<ErrorT>(mut self, error: ErrorT) -> Self
    where
        ErrorT: 'static + Error + Send + Sync,
    {
        self.add_top(error.into());
        self
    }

    /// Attach to the top cause.
    pub fn with<AttachmentT>(mut self, attachment: AttachmentT) -> Self
    where
        AttachmentT: Any + Send + Sync,
    {
        self.attach(attachment);
        self
    }

    /// Attach only if we don't already have one of the same type.
    pub fn with_once<AttachmentT>(mut self, attachment: AttachmentT) -> Self
    where
        AttachmentT: Any + Send + Sync,
    {
        self.attach_once(attachment);
        self
    }

    /// Attach to the top cause if [Some].
    pub fn maybe_with<AttachmentT>(mut self, attachment: Option<AttachmentT>) -> Self
    where
        AttachmentT: Any + Send + Sync,
    {
        if let Some(attachment) = attachment {
            self.attach(attachment);
        }
        self
    }

    /// Attach to the top cause if [Some] and we don't already have one of the same type.
    pub fn maybe_with_once<AttachmentT>(mut self, attachment: Option<AttachmentT>) -> Self
    where
        AttachmentT: Any + Send + Sync,
    {
        if let Some(attachment) = attachment {
            self.attach_once(attachment);
        }
        self
    }

    /// Attach a [Location](std::panic::Location) (via
    /// [Location::caller](std::panic::Location::caller)) if we don't already have one.
    #[track_caller]
    pub fn with_location(mut self) -> Self {
        self.attach_location_once();
        self
    }

    /// Attach a backtrace if we don't already have one.
    pub fn with_backtrace(mut self) -> Self {
        self.attach_backtrace_once();
        self
    }

    /// Flatten.
    ///
    /// Will return a single [Problems] in which each [Problems] in the causation chain is unpacked
    /// in place, recursively.
    ///
    /// If there are no contained [Problems] will return a [Problems] that just contains self.
    pub fn flatten(self) -> Problems {
        let mut problems = Problems::default();
        problems.add(self);
        problems.flatten()
    }
}

impl IntoProblem for Problem {
    fn into_problem(self) -> Problem {
        self
    }
}

impl Causes for Problem {
    fn problem(&self) -> &Problem {
        self
    }

    fn iter_causes(&self) -> impl Iterator<Item = &Cause> {
        self.into_iter()
    }
}

impl Attachments for Problem {
    fn attachments(&self) -> impl Iterator<Item = &CapturedAttachment> {
        self.into_iter().flat_map(|cause| cause.attachments.iter())
    }
}

impl AttachmentsMut for Problem {
    fn attachments_mut(&mut self) -> impl Iterator<Item = &mut CapturedAttachment> {
        self.into_iter()
            .flat_map(|cause| cause.attachments.iter_mut())
    }

    fn attach<AttachmentT>(&mut self, attachment: AttachmentT)
    where
        AttachmentT: Any + Send + Sync,
    {
        if let Some(cause) = self.top_mut() {
            cause.attach(attachment);
        }
    }
}

impl fmt::Display for Problem {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let mut iterator = self.into_iter().peekable();
        while let Some(cause) = iterator.next() {
            write!(formatter, "{}", cause.error)?;
            if iterator.peek().is_some() {
                write!(formatter, "{}", PROBLEM_DISPLAY_SEPARATOR)?;
            }
        }
        Ok(())
    }
}

impl Default for Problem {
    #[track_caller]
    fn default() -> Self {
        auto_attach(Self {
            causes: Default::default(),
        })
    }
}

impl<ErrorT> From<ErrorT> for Problem
where
    ErrorT: 'static + Error + Send + Sync,
{
    #[track_caller]
    fn from(error: ErrorT) -> Self {
        Self::new(error.into())
    }
}

impl From<Problem> for io::Error {
    fn from(problem: Problem) -> Self {
        io::Error::new(io::ErrorKind::Other, problem.into_error())
    }
}

// Utils

#[track_caller]
fn auto_attach(mut problem: Problem) -> Problem {
    if env_var_enabled(AUTO_ATTACH_LOCATION) {
        problem = problem.with_location();
    }

    if env_var_enabled(AUTO_ATTACH_BACKTRACE) {
        problem = problem.with_backtrace();
    }

    problem
}

fn env_var_enabled(name: &'static str) -> bool {
    var_os(name).map(|var| var != "0").unwrap_or(false)
}
