mod trace;
mod traced;

use crate::core::*;
use crate::func::*;
use trace::*;
pub use traced::Traced;

pub struct TracedDiagnostic;

pub struct TracedClass;

trait WithTrace: Sized {
    fn with_trace(self, t: TraceBox) -> Traced<Self>;
}

impl<A> WithTrace for A {
    fn with_trace(self, t: TraceBox) -> Traced<Self> {
        Traced { a: self, t }
    }
}

impl<A> Traced<A> {
    pub fn wrapped(self, event: &str) -> Self {
        Traced {
            a: self.a,
            t: self.t.wrapped(event),
        }
    }

    pub fn after_resolution(self) -> Self {
        self.after(TraceBox::resolution())
    }

    fn after(self, t: TraceBox) -> Self {
        Traced {
            a: self.a,
            t: self.t.after(t),
        }
    }

    fn before(self, t: TraceBox) -> Self {
        Traced {
            a: self.a,
            t: self.t.before(t),
        }
    }

    pub fn length(&self) -> usize {
        self.t.length()
    }

    pub fn width(&self) -> usize {
        self.t.width()
    }
}

impl WeakFunctor for TracedClass {
    type F<'a, A: 'a> = Traced<A>;
}

impl Functor for TracedClass {
    fn fmap<'a, A: 'a, B: 'a>(f: impl 'a + FnOnce(A) -> B, fa: Self::F<'a, A>) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        f(fa.a).with_trace(fa.t)
    }

    fn replace<'a, A: 'a, B: 'a>(fa: Self::F<'a, A>, b: B) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        b.with_trace(fa.t)
    }

    fn void<'a, A: 'a>(fa: Self::F<'a, A>) -> Self::F<'a, ()>
    where
        Self: 'a,
    {
        ().with_trace(fa.t)
    }
}

impl ApplicativeSeq for TracedClass {
    fn seq<'a, A: 'a, B: 'a>(
        ff: Self::F<'a, impl 'a + FnOnce(A) -> B>,
        fa: Self::F<'a, A>,
    ) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        (ff.a)(fa.a).with_trace(TraceBox::parallel(ff.t, fa.t))
    }
}

impl ApplicativeLA2 for TracedClass {
    fn la2<'a, A: 'a, B: 'a, C: 'a>(
        f: impl 'a + FnOnce(A, B) -> C,
        fa: Self::F<'a, A>,
        fb: Self::F<'a, B>,
    ) -> Self::F<'a, C>
    where
        Self: 'a,
    {
        f(fa.a, fb.a).with_trace(TraceBox::parallel(fa.t, fb.t))
    }
}

impl ApplicativeTuple for TracedClass {
    fn tuple<'a, A: 'a, B: 'a>((fa, fb): (Self::F<'a, A>, Self::F<'a, B>)) -> Self::F<'a, (A, B)>
    where
        Self: 'a,
    {
        (fa.a, fb.a).with_trace(TraceBox::parallel(fa.t, fb.t))
    }
}

impl Applicative for TracedClass {
    fn pure<'a, A: 'a>(a: A) -> Self::F<'a, A> {
        a.with_trace(TraceBox::pure())
    }

    fn discard_first<'a, A: 'a, B: 'a>(fa: Self::F<'a, A>, fb: Self::F<'a, B>) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        fb.a.with_trace(TraceBox::parallel(fa.t, fb.t))
    }

    fn discard_second<'a, A: 'a, B: 'a>(fa: Self::F<'a, A>, fb: Self::F<'a, B>) -> Self::F<'a, A>
    where
        Self: 'a,
    {
        fa.a.with_trace(TraceBox::parallel(fa.t, fb.t))
    }
}

impl Monad for TracedClass {
    fn bind<'a, A: 'a, B: 'a>(
        fa: Self::F<'a, A>,
        f: impl 'a + FnOnce(A) -> Self::F<'a, B>,
    ) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        f(fa.a).after(fa.t)
    }

    fn ibind<'a, A: 'a, B: 'a>(
        mut a: A,
        mut f: impl 'a + FnMut(A) -> Self::F<'a, IState<A, B>>,
    ) -> Self::F<'a, B>
    where
        Self: 'a,
    {
        let mut t = TraceBox::pure();
        loop {
            let fa = f(a);
            t = fa.t.after(t);
            match fa.a {
                IState::Pending(next_a) => a = next_a,
                IState::Done(b) => return b.with_trace(t),
            }
        }
    }

    fn join<'a, A: 'a>(ffa: Self::F<'a, Self::F<'a, A>>) -> Self::F<'a, A>
    where
        Self::F<'a, A>: 'a,
        Self: 'a,
    {
        ffa.a.after(ffa.t)
    }
}

impl Diagnostic<TracedClass> for TracedDiagnostic {
    fn after<A>(fa: Traced<A>, event: &str) -> Traced<A> {
        fa.after(TraceBox::event(event))
    }

    fn before<A>(fa: Traced<A>, event: &str) -> Traced<A> {
        fa.before(TraceBox::event(event))
    }

    fn wrapped<A>(fa: Traced<A>, event: &str) -> Traced<A> {
        fa.wrapped(event)
    }
}