use colored::Colorize;
use std::ops::Not;

/// Information used to display the panic messages.
#[derive(Debug, Default)]
pub struct Information {
    /// Messages of each failed assertion.
    pub(crate) messages: Vec<String>,

    /// The debug representation of the left part.
    /// Example: (1 + 1) => `2`.
    pub left_dbg: String,

    /// Explanation of the assertion. Must be set with the `because`
    /// method of an assertion.
    pub(crate) explanation: Option<&'static str>,

    /// Collect all the information taken from the code generation.
    /// See [`FromMacro`](struct.FromMacro) for more details.
    pub(crate) from_macro: Option<FromMacro>,
}

impl Information {
    pub fn new(from_macro: Option<FromMacro>, left_dbg: String) -> Self {
        Information {
            messages: Default::default(),
            explanation: Default::default(),
            left_dbg,
            from_macro,
        }
    }

    /// This function throws a panic of custom type.
    /// Before that, the first time it is called,
    /// it sets the callback for printing the panic message.
    fn panic(msg: String) -> ! {
        use std::panic::{set_hook, PanicInfo};
        use std::sync::Once;

        struct FluidPanic(String);

        /// Prints the panic message:
        ///     - If the info is `FluidPanic`, it prints the custom
        ///       colored message;
        ///     - If the info is another type, it only prints the
        ///       info as is.
        fn panic_message(info: &PanicInfo) {
            match info.payload().downcast_ref::<FluidPanic>() {
                Some(p) => print!("{}", p.0),
                None => println!("{}", info),
            }
        }

        static SET_HOOK: Once = Once::new();

        SET_HOOK.call_once(|| set_hook(Box::new(panic_message)));
        panic!(FluidPanic(msg))
    }
}

impl Drop for Information {
    fn drop(&mut self) {
        // If self.messages contains a message, there is at least one test that failed.
        if self.messages.is_empty().not() {
            let header = if let Some(ref from_macro) = self.from_macro {
                let case = from_macro
                    .case
                    .map(|s| format!(", for the case {}", s.bold()))
                    .unwrap_or_default();
                format!(
                    "\rThe test failed at {}{}:",
                    from_macro.location.bold(),
                    case
                )
                .red()
            } else {
                "The test failed:".red()
            };
            let explanation = self
                .explanation
                .map(|s| format!("This test should pass because {}.\n", s))
                .unwrap_or_default();
            let message = self.messages.join("\n\n");
            let msg = format!("{}\n{}\n{}", header, message, explanation);

            Self::panic(msg.bright_white().to_string())
        }
    }
}

/// Information taken from the macros:FromMacro
///
/// - stringified and location, either from the custom attributes or
/// the `fact_` macro;
/// - case if this is a `theory` test.
#[derive(Debug)]
pub struct FromMacro {
    /// The litteral representation of the left part.
    /// Example: (1 + 1) => `1 + 1`.
    pub stringified: &'static str,

    /// Location of the line.
    pub location: &'static str,

    /// The current case of a `theory` test (set with the corresponding
    /// custom attribute).
    pub case: Option<&'static str>,
}
