/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

//! Control Flow for Correctness-Critical applications
//!
//! https://sled.rs/errors.html
//!
//! Where the sled.rs library uses `Result<Result<A, Error>, FatalError>`, these are a little hard to
//! work with. This library works out a set of utility functions that allow us to work with the
//! nested errors in the same way as a regular Result.
use std::{error::Error, fmt};

/// Implement this trait for the application's fatal errors.
///
/// Fatal errors should be things that should trigger an application shutdown. Applications should
/// not try to recover from fatal errors, but simply bring the app to the safest shutdown point and
/// report the best possible information to the user.
///
/// Examples: database corruption, or the database is unavailable in an application that cannot
/// function without it. Graphics environment cannot be initialized in a GUI app.
///
/// Applications should generally have only one FatalError type. There are no handling utilities
/// for Fatal conditions, so Fatal conditions must be handled through an ordinary `match`
/// statement.
pub trait FatalError: Error {}

/// Flow<A, FE, E> represents a return value that might be a success, might be a fatal error, or
/// might be a normal handleable error.
pub enum Flow<A, FE, E> {
    /// The operation was successful
    Ok(A),
    /// The operation encountered a fatal error. These should be bubbled up to a level that can
    /// safely shut the application down.
    Fatal(FE),
    /// Ordinary errors. These should be handled and the application should recover gracefully.
    Err(E),
}

impl<A, FE, E> Flow<A, FE, E> {
    /// Apply an infallible function to a successful value.
    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),
        }
    }

    /// Apply a potentially fallible function to a successful value.
    ///
    /// Like `Result.and_then`, the mapping function can itself fail.
    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),
        }
    }

    /// Map a normal error from one type to another. This is useful for converting an error from
    /// one type to another, especially in re-throwing an underlying error. `?` syntax does not
    /// work with `Flow`, so you will likely need to use this a lot.
    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)),
        }
    }

    /// Provide a function to use to recover from (or simply re-throw) an error.
    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),
        }
    }
}

/// Convert from a normal `Result` type to a `Flow` type. The error condition for a `Result` will
/// be treated as `Flow::Err`, never `Flow::Fatal`.
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),
        }
    }
}

impl<A, FE, E> fmt::Debug for Flow<A, FE, E>
where
    A: fmt::Debug,
    FE: fmt::Debug,
    E: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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)),
        }
    }
}

impl<A, FE, E> PartialEq for Flow<A, FE, E>
where
    A: PartialEq,
    FE: PartialEq,
    E: PartialEq,
{
    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,
        }
    }
}

/// Convenience function to create an ok value.
pub fn ok<A, FE: FatalError, E: Error>(val: A) -> Flow<A, FE, E> {
    Flow::Ok(val)
}

/// Convenience function to create an error value.
pub fn error<A, FE: FatalError, E: Error>(err: E) -> Flow<A, FE, E> {
    Flow::Err(err)
}

/// Convenience function to create a fatal value.
pub fn fatal<A, FE: FatalError, E: Error>(err: FE) -> Flow<A, FE, E> {
    Flow::Fatal(err)
}

/// Return early from the current function if the value is a fatal error.
#[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]
/// Return early from the current function is the value is an error.
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
        }
    }

    #[test]
    fn it_can_map_things() {
        let success: Flow<i32, FatalError, Error> = ok(15);
        assert_eq!(ok(16), success.map(|v| v + 1));
    }

    #[test]
    fn it_can_chain_success() {
        let success: Flow<i32, FatalError, Error> = ok(15);
        assert_eq!(ok(16), success.and_then(|v| ok(v + 1)));
    }

    #[test]
    fn it_can_handle_an_error() {
        let failure: Flow<i32, FatalError, Error> = error(Error::Error);
        assert_eq!(
            ok::<i32, FatalError, Error>(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(_) => 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();
    }
}
