#![doc = include_str!("../README.md")]
#![feature(fn_traits, unboxed_closures, tuple_trait)]
#![no_std]

#[macro_use]
extern crate alloc;

use alloc::vec::Vec;
use core::{
    any::type_name,
    fmt::{self, Debug, Display},
    marker::Tuple,
};

#[derive(Debug, Clone, PartialEq)]
#[doc = include_str!("../README.md")]
pub struct FnLogger<F, Args, Output> {
    func: F,
    logs: Vec<(Args, Output)>,
}

impl<F, Args, Output> FnLogger<F, Args, Output> {
    /// Create a new instance of [`FnLogger`]
    pub const fn new(func: F) -> Self {
        FnLogger { func, logs: vec![] }
    }
}

impl<F, Args, Output> FnOnce<Args> for FnLogger<F, Args, Output>
where
    F: FnOnce<Args, Output = Output>,
    Args: Tuple,
{
    type Output = Output;

    extern "rust-call" fn call_once(self, args: Args) -> Output {
        self.func.call_once(args)
    }
}

impl<F, Args, Output> FnMut<Args> for FnLogger<F, Args, Output>
where
    F: FnMut<Args, Output = Output>,
    Args: Tuple + Clone,
    Output: Clone,
{
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Output {
        let out = self.func.call_mut(args.clone());
        self.logs.push((args, out.clone()));
        out
    }
}

impl<F, Args, Output> Display for FnLogger<F, Args, Output>
where
    Args: Debug,
    Output: Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let delim = if f.alternate() { ", " } else { "\n" };
        let fn_name = type_name::<F>();

        for (i, (args, out)) in self.logs.iter().enumerate() {
            write!(f, "{fn_name}{args:?} -> {out:?}")?;

            if i != self.logs.len() - 1 {
                write!(f, "{delim}")?;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::string::ToString;
    use core::any::type_name_of_val;

    #[test]
    fn simple_add() {
        fn add(a: i32, b: i32) -> i32 {
            a + b
        }

        let mut log = FnLogger::new(add);

        let x = log(1, 2);
        assert_eq!(x, 3);

        let y = log(3, 4);
        assert_eq!(y, 7);

        let fn_name = type_name_of_val(&add);

        assert_eq!(
            log.to_string(),
            format!(
                "{fn_name}(1, 2) -> 3
{fn_name}(3, 4) -> 7"
            )
        );
    }
}
