use {
    anstream::{print, println},
    owo_colors::*,
    problemo::*,
    std::{any::*, error::*, fmt},
};

// In this example we will be extending our errors with an additional trait.

// The challenge is that we want the error to still work as a std Error while
// allowing us to use the trait-extended functionality.

// The trick is to 1) use a supertrait wrapper, and 2) provide access to the inner
// error as an Any.

// This technique isn't unique to Problemo, but we'll show how it can be
// integrated quite nicely.

// See examples/function_attachment.rs for a different approach.

fn read_file(path: &str) -> Result<String, Problem> {
    std::fs::read_to_string(path).via(ReadError::new(path.into()).pretty())
}

fn main() {
    if let Err(problem) = read_file("non-existing.txt") {
        for cause in &problem {
            print!("• ");
            match cause.error.downcast_ref::<PrettyErrorRef>() {
                // Handle pretty errors
                Some(pretty_error) => {
                    pretty_error.pretty_print();

                    // We can still access the concrete error type
                    if let Some(_read_error) = pretty_error.downcast_ref::<ReadError>() {
                        println!("  (This is a ReadError)");
                    }
                }

                // Handle non-pretty errors
                None => println!("{}", cause.error),
            }
        }
    }
}

//
// ReadError
//

#[derive(Debug)]
pub struct ReadError {
    pub path: String,
}

impl ReadError {
    pub fn new(path: String) -> Self {
        Self { path }
    }
}

impl fmt::Display for ReadError {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "could not read from: {}'", self.path)
    }
}

impl Error for ReadError {}

impl Pretty for ReadError {
    fn pretty_print(&self) {
        println!("could not read from: {}", self.path.red().underline());
    }
}

//
// Pretty
//

/// Pretty.
pub trait Pretty {
    /// Pretty print.
    fn pretty_print(&self);
}

//
// AsAnyRef
//

/// As an [Any] reference.
pub trait AsAnyRef {
    /// As an [Any] reference.
    ///
    /// Implementations that do not support it should return [None].
    fn as_any_ref(&self) -> Option<&dyn Any>;
}

impl<ErrorT> AsAnyRef for ErrorT
where
    ErrorT: 'static + Error,
{
    fn as_any_ref(&self) -> Option<&dyn Any> {
        Some(self)
    }
}

//
// PrettyError
//

/// Pretty error (supertrait).
pub trait PrettyError: AsAnyRef + Error + Pretty {}

// Blanket implementation of the supertrait:

impl<ErrorT> PrettyError for ErrorT where ErrorT: 'static + Error + Pretty {}

//
// PrettyErrorRef
//

/// Common reference type for [PrettyError].
pub struct PrettyErrorRef(Box<dyn PrettyError + Send + Sync>);

impl PrettyErrorRef {
    /// Constructor.
    pub fn new<ErrorT>(error: ErrorT) -> Self
    where
        ErrorT: 'static + Error + Pretty + Send + Sync,
    {
        Self(Box::new(error))
    }

    /// As a concrete type reference.
    pub fn downcast_ref<AnyT>(&self) -> Option<&AnyT>
    where
        AnyT: 'static,
    {
        self.as_ref()
            .as_any_ref()
            .and_then(|any| any.downcast_ref())
    }
}

impl AsRef<dyn 'static + PrettyError + Send + Sync> for PrettyErrorRef {
    fn as_ref(&self) -> &(dyn 'static + PrettyError + Send + Sync) {
        self.0.as_ref()
    }
}

impl fmt::Debug for PrettyErrorRef {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        fmt::Debug::fmt("Box<dyn PrettyError + Send + Sync>", formatter)
    }
}

impl fmt::Display for PrettyErrorRef {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(self.as_ref(), formatter)
    }
}

impl Error for PrettyErrorRef {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.as_ref().source()
    }
}

impl Pretty for PrettyErrorRef {
    fn pretty_print(&self) {
        self.as_ref().pretty_print();
    }
}

//
// IntoPrettyErrorRef
//

/// Convert into a [PrettyErrorRef].
pub trait IntoPrettyErrorRef {
    /// Convert into a [PrettyErrorRef].
    fn pretty(self) -> PrettyErrorRef;
}

impl<ErrorT> IntoPrettyErrorRef for ErrorT
where
    ErrorT: 'static + Error + Pretty + Send + Sync,
{
    fn pretty(self) -> PrettyErrorRef {
        PrettyErrorRef::new(self)
    }
}
