use std::error::Error;

pub trait FatalError: Error {}

pub enum Flow<A, FE, E> {
    Ok(A),
    Fatal(FE),
    Err(E),
}

impl<A, FE, E> Flow<A, FE, E> {
    pub fn map<B, O>(self, mapper: O) -> Flow<B, FE, E>
    where
        O: FnOnce(A) -> B,
    {
        match self {
            Flow::Ok(val) => Flow::Ok(mapper(val)),
            Flow::Fatal(err) => Flow::Fatal(err),
            Flow::Err(err) => Flow::Err(err),
        }
    }

    pub fn and_then<B, O>(self, handler: O) -> Flow<B, FE, E>
    where
        O: FnOnce(A) -> Flow<B, FE, E>,
    {
        match self {
            Flow::Ok(val) => handler(val),
            Flow::Fatal(err) => Flow::Fatal(err),
            Flow::Err(err) => Flow::Err(err),
        }
    }

    pub fn map_err<F, O>(self, mapper: O) -> Flow<A, FE, F>
    where
        O: FnOnce(E) -> F,
    {
        match self {
            Flow::Ok(val) => Flow::Ok(val),
            Flow::Fatal(err) => Flow::Fatal(err),
            Flow::Err(err) => Flow::Err(mapper(err)),
        }
    }

    pub fn or_else<O, F>(self, handler: O) -> Flow<A, FE, F>
    where
        O: FnOnce(E) -> Flow<A, FE, F>,
    {
        match self {
            Flow::Ok(val) => Flow::Ok(val),
            Flow::Fatal(err) => Flow::Fatal(err),
            Flow::Err(err) => handler(err),
        }
    }
}

impl<A, FE, E> From<Result<A, E>> for Flow<A, FE, E> {
    fn from(r: Result<A, E>) -> Self {
        match r {
            Ok(val) => Flow::Ok(val),
            Err(err) => Flow::Err(err),
        }
    }
}

pub fn ok<A, FE: FatalError, E: Error>(val: A) -> Flow<A, FE, E> {
    Flow::Ok(val)
}

pub fn error<A, FE: FatalError, E: Error>(err: E) -> Flow<A, FE, E> {
    Flow::Err(err)
}

pub fn fatal<A, FE: FatalError, E: Error>(err: FE) -> Flow<A, FE, E> {
    Flow::Fatal(err)
}

#[macro_export]
macro_rules! return_fatal {
    ($x:expr) => {
        match $x {
            Flow::Fatal(err) => return Flow::Fatal(err),
            Flow::Err(err) => Err(err),
            Flow::Ok(val) => Ok(val),
        }
    };
}

#[macro_export]
macro_rules! return_error {
    ($x:expr) => {
        match $x {
            Flow::Ok(val) => val,
            Flow::Err(err) => return Flow::Err(err),
            Flow::Fatal(err) => return Flow::Fatal(err),
        }
    };
}

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

    #[derive(Debug, Error)]
    enum FatalError {
        #[error("A fatal error occurred")]
        FatalError,
    }
    impl super::FatalError for FatalError {}
    impl PartialEq for FatalError {
        fn eq(&self, rhs: &Self) -> bool {
            true
        }
    }

    #[derive(Debug, Error)]
    enum Error {
        #[error("an error occurred")]
        Error,
    }
    impl PartialEq for Error {
        fn eq(&self, rhs: &Self) -> bool {
            true
        }
    }

    impl PartialEq for Flow<i32, FatalError, Error> {
        fn eq(&self, rhs: &Self) -> bool {
            match (self, rhs) {
                (Flow::Ok(val), Flow::Ok(rhs)) => val == rhs,
                (Flow::Err(_), Flow::Err(_)) => true,
                (Flow::Fatal(_), Flow::Fatal(_)) => true,
                _ => false,
            }
        }
    }

    impl std::fmt::Debug for Flow<i32, FatalError, Error> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            match self {
                Flow::Ok(val) => f.write_fmt(format_args!("Flow::Ok {}", val)),
                Flow::Err(err) => f.write_fmt(format_args!("Flow::Err {:?}", err)),
                Flow::Fatal(err) => f.write_fmt(format_args!("Flow::Fatal {:?}", err)),
            }
        }
    }

    #[test]
    fn it_can_map_things() {
        let success = ok(15);
        assert_eq!(ok(16), success.map(|v| v + 1));
    }

    #[test]
    fn it_can_chain_success() {
        let success = ok(15);
        assert_eq!(ok(16), success.and_then(|v| ok(v + 1)));
    }

    #[test]
    fn it_can_handle_an_error() {
        let failure = error(Error::Error);
        assert_eq!(ok(16), failure.or_else(|_| ok(16)));
    }

    #[test]
    fn early_exit_on_fatal() {
        fn ok_func() -> Flow<i32, FatalError, Error> {
            let value = return_fatal!(ok::<i32, FatalError, Error>(15));
            match value {
                Ok(_) => ok(14),
                Err(err) => error(err),
            }
        }

        fn err_func() -> Flow<i32, FatalError, Error> {
            let value = return_fatal!(error::<i32, FatalError, Error>(Error::Error));
            match value {
                Ok(_) => panic!("shouldn't have gotten here"),
                Err(err) => ok(0),
            }
        }

        fn fatal_func() -> Flow<i32, FatalError, Error> {
            return_fatal!(fatal::<i32, FatalError, Error>(FatalError::FatalError));
            panic!("failed to bail");
        }

        fatal_func();
        assert_eq!(ok_func(), ok(14));
        assert_eq!(err_func(), ok(0));
    }

    #[test]
    fn it_can_early_exit_on_all_errors() {
        fn ok_func() -> Flow<i32, FatalError, Error> {
            let value = return_error!(ok::<i32, FatalError, Error>(15));
            assert_eq!(value, 15);
            ok(14)
        }

        fn err_func() -> Flow<i32, FatalError, Error> {
            return_error!(error::<i32, FatalError, Error>(Error::Error));
            panic!("failed to bail");
        }

        fn fatal_func() -> Flow<i32, FatalError, Error> {
            return_error!(fatal::<i32, FatalError, Error>(FatalError::FatalError));
            panic!("failed to bail");
        }

        fatal_func();
        assert_eq!(ok_func(), ok(14));
        err_func();
    }
}