// Copyright 2020 the .NET Foundation
// Licensed under the MIT License.

//! Generic error handling for the WWT aligner frontend, copied from Tectonic.
//!
//! This crate provides a generic boxed error type, plus supporting utilities.
//! In particular:
//!
//! - The basic `Error` type is an `anyhow` 1.x boxed Error
//! - The `atry!` macro allows simple structured annotations to be added to `?`
//!   operations
//! - The `a_ok_or!` macro allows for annotations to `Option::ok_or_else` calls.

use std::{error, fmt, result::Result as StdResult};

/// The generic error type, for complex operations that can fail for a wide
/// range of reasons. This type is a reexport of the `anyhow` 1.x series Error
/// type.
///
/// There’s an appeal to not explicitly committing ourselves to using this
/// particular error implementation, but the `anyhow` error type has a
/// sufficient number of special methods and traits that it would be pretty
/// tedious to re-implement them all while pretending that we're using some
/// different type.
pub use anyhow::Error;

/// A preloaded result type where the error type is our generic error type.
pub type Result<T> = StdResult<T, Error>;

/// A simple annotated message that can be attached to errors using the
/// `anyhow::Context` methods, or be used as an error type itself. The
/// recommended way to use this for error context is using `atry!` or related
/// macros.
///
/// The `std::fmt::Display` of this type yields its primary “message”. Consumers
/// that are aware of this type can obtain additional context through its
/// `notes()` method.
#[derive(Debug, Default)]
pub struct AnnotatedMessage {
    /// The main message.
    message: String,

    /// Additional annotations that can be displayed after the primary message.
    notes: Vec<String>,
}

impl AnnotatedMessage {
    /// Set the primary message associated with this annotated report.
    #[allow(dead_code)]
    pub fn set_message<T: fmt::Display>(&mut self, m: T) {
        self.message = format!("{}", m);
    }

    /// Add an additional note to be associated with this annotated report.
    #[allow(dead_code)]
    pub fn add_note<T: fmt::Display>(&mut self, n: T) {
        self.notes.push(format!("{}", n));
    }

    /// Obtain the set of notes associated with this report.
    pub fn notes(&self) -> &[String] {
        &self.notes[..]
    }
}

impl fmt::Display for AnnotatedMessage {
    fn fmt(&self, f: &mut fmt::Formatter) -> StdResult<(), fmt::Error> {
        write!(f, "{}", self.message)
    }
}

impl error::Error for AnnotatedMessage {}

/// "Annotated try” — like `try!`, but with the ability to add extended context
/// to the error message. This tries to provide a bit more syntactic sugar than
/// anyhow's `with_context()`, and it supports our AnnotatedMessage context type.
///
/// # Example
///
/// ```
/// # use tectonic_errors::{atry, Result};
/// # fn my_fallible_operation(a: bool, b: bool) -> Result<()> { Ok(()) }
/// # fn my_caller() -> Result<()> {
/// # let arg = true;
/// # let option = true;
/// let ok_val = atry!(
///     my_fallible_operation(arg, option);
///     ["the operation on `{}` failed", arg]
///     (note "option was `{}`; maybe you should choose a better value next time", option)
/// );
/// # Ok(())
/// # }
/// ```
///
/// This is equivalent to `let ok_val = my_fallible_operation(arg)?;`, but if the
/// operation fails, the returned error will have a message formatted as per the
/// format expression in square brackets, with attached "note" text formatted as
/// per the parenthesized `note` expression. There may be zero, one, or many notes
/// attached.
#[macro_export]
macro_rules! atry {
    (@aa $ar:ident [ $($inner:tt)+ ] ) => {
        $ar.set_message(format!($($inner)+));
    };

    (@aa $ar:ident ( note $($inner:tt)+ ) ) => {
        $ar.add_note(format!($($inner)+));
    };

    ($op:expr ; $( $annotation:tt )+) => {{
        use anyhow::Context;
        $op.with_context(|| {
            let mut ar = $crate::errors::AnnotatedMessage::default();
            $(
                atry!(@aa ar $annotation);
            )+
            ar
        })?
    }};
}

/// "annotated ok_or” — like `Option::ok_or_else()?`, but with the ability to
/// add extended context to the error. This yields an `AnnotatedMessage` as its
/// error type.
#[macro_export]
macro_rules! a_ok_or {
    ($option:expr ; $( $annotation:tt )+) => {{
        use $crate::atry;
        $option.ok_or_else(|| {
            let mut ar = $crate::errors::AnnotatedMessage::default();
            $(
                atry!(@aa ar $annotation);
            )+
            ar
        })?
    }};
}

pub fn report(r: Result<i32>) -> i32 {
    let err = match r {
        Ok(c) => return c,
        Err(e) => e,
    };

    let mut notes = Vec::new();

    eprintln!("");
    eprintln!("fatal error: {}", err);

    if let Some(ann) = err.downcast_ref::<AnnotatedMessage>() {
        notes.extend(ann.notes());
    }

    err.chain().skip(1).for_each(|cause| {
        eprintln!("caused by: {}", cause);

        if let Some(ann) = cause.downcast_ref::<AnnotatedMessage>() {
            notes.extend(ann.notes());
        }
    });

    for note in &notes {
        eprintln!("");
        eprintln!("note: {}", note);
    }

    1
}

#[cfg(test)]
mod test {
    use super::*;

    fn atry_eval() -> Result<()> {
        let fine: Result<usize> = Ok(0);
        let display = "a string";

        atry!(
            fine;
            ["message {}", display]
            (note "but what about {}", display)
        );
        Ok(())
    }

    #[test]
    fn atry_compile() {
        atry_eval().unwrap();
    }

    fn a_ok_or_eval() -> Result<()> {
        let fine: Option<usize> = Some(0);
        let display = "a string";

        a_ok_or!(
            fine;
            ["message {}", display]
            (note "but what about {}", display)
        );
        Ok(())
    }

    #[test]
    fn a_ok_or_compile() {
        a_ok_or_eval().unwrap();
    }
}
