// Copyright 2015, Tobias Hermann and the FunctionalPlus contributors.
// https://github.com/Dobiasd/FunctionalPlus
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)

#pragma once

#include <fplus/function_traits.hpp>
#include <fplus/maybe.hpp>

#include <fplus/internal/asserts/functions.hpp>
#include <fplus/internal/composition.hpp>
#include <fplus/internal/invoke.hpp>

#include <cassert>
#include <functional>
#include <memory>

namespace fplus {

template <typename Ok, typename Error>
class result;

template <typename Ok, typename Error>
result<Ok, Error> ok(const Ok& val);

template <typename Ok, typename Error>
result<Ok, Error> error(const Error& error);

// Can hold a value of type Ok or an error of type Error.
template <typename Ok, typename Error>
class result {
public:
    bool is_ok() const { return static_cast<bool>(ptr_ok_); }
    bool is_error() const { return static_cast<bool>(ptr_error_); }
    const Ok& unsafe_get_ok() const
    {
        check_either_or_invariant();
        assert(is_ok());
        return *ptr_ok_;
    }
    const Error& unsafe_get_error() const
    {
        check_either_or_invariant();
        assert(is_error());
        return *ptr_error_;
    }
    typedef Ok ok_t;
    typedef Error error_t;

    result(const result<Ok, Error>& other)
        : ptr_ok_(other.is_ok() ? ptr_ok(new Ok(other.unsafe_get_ok())) : ptr_ok())
        , ptr_error_(other.is_error() ? ptr_error(new Error(other.unsafe_get_error())) : ptr_error())
    {
        check_either_or_invariant();
    }
    result<Ok, Error>& operator=(const result<Ok, Error>& other)
    {
        ptr_ok_ = other.is_ok() ? ptr_ok(new Ok(other.unsafe_get_ok())) : ptr_ok();
        ptr_error_ = other.is_error() ? ptr_error(new Error(other.unsafe_get_error())) : ptr_error();
        return *this;
    }

private:
    void check_either_or_invariant() const
    {
        assert(is_ok() != is_error());
    }
    result()
        : ptr_ok_(ptr_ok())
        , ptr_error_(ptr_error())
    {
    }
    typedef std::unique_ptr<Ok> ptr_ok;
    typedef std::unique_ptr<Error> ptr_error;
    friend result<Ok, Error> ok<Ok, Error>(const Ok& ok);
    friend result<Ok, Error> error<Ok, Error>(const Error& error);
    ptr_ok ptr_ok_;
    ptr_error ptr_error_;
};

// API search type: is_ok : Result a b -> Bool
// fwd bind count: 0
// Is not error?
template <typename Ok, typename Error>
bool is_ok(const result<Ok, Error>& result)
{
    return result.is_ok();
}

// API search type: is_error : Result a b -> Bool
// fwd bind count: 0
// Is not OK?
template <typename Ok, typename Error>
bool is_error(const result<Ok, Error>& result)
{
    return !is_ok(result);
}

// API search type: unsafe_get_ok : Result a b -> a
// fwd bind count: 0
// Crashes if result is error!
template <typename Ok, typename Error>
Ok unsafe_get_ok(const result<Ok, Error>& result)
{
    return result.unsafe_get_ok();
}

// API search type: unsafe_get_error : Result a b -> b
// fwd bind count: 0
// Crashes if result is ok!
template <typename Ok, typename Error>
Error unsafe_get_error(const result<Ok, Error>& result)
{
    return result.unsafe_get_error();
}

// API search type: ok_with_default : (a, Result a b) -> a
// fwd bind count: 1
// Get the value from a result or the default in case it is error.
template <typename Ok, typename Error>
Ok ok_with_default(const Ok& defaultValue, const result<Ok, Error>& result)
{
    if (is_ok(result))
        return unsafe_get_ok(result);
    return defaultValue;
}

// API search type: ok : a -> Result a b
// fwd bind count: 0
// Wrap a value in a result as a Ok.
template <typename Ok, typename Error>
result<Ok, Error> ok(const Ok& val)
{
    result<Ok, Error> x;
    x.ptr_ok_.reset(new Ok(val));
    return x;
}

// API search type: error : b -> Result a b
// fwd bind count: 0
// Construct an error of a certain result type.
template <typename Ok, typename Error>
result<Ok, Error> error(const Error& error)
{
    result<Ok, Error> x;
    x.ptr_error_.reset(new Error(error));
    return x;
}

// API search type: to_maybe : Result a b -> Maybe a
// fwd bind count: 0
// Convert ok to just, error to nothing.
template <typename Ok, typename Error>
maybe<Ok> to_maybe(const result<Ok, Error>& result)
{
    if (is_ok(result))
        return just<Ok>(unsafe_get_ok(result));
    else
        return nothing<Ok>();
}

// API search type: from_maybe : (b, Maybe a) -> Result a b
// fwd bind count: 1
// Convert just to ok, nothing to error.
template <typename Error, typename Ok>
result<Ok, Error> from_maybe(const Error& err, const maybe<Ok>& maybe)
{
    if (is_just(maybe))
        return ok<Ok, Error>(unsafe_get_just(maybe));
    else
        return error<Ok, Error>(err);
}

// API search type: throw_on_error : (e, Result a b) -> a
// fwd bind count: 1
// Throws the given exception in case of error.
// Return ok value if ok.
template <typename E, typename Ok, typename Error>
Ok throw_on_error(const E& e, const result<Ok, Error>& result)
{
    if (is_error(result))
        throw e;
    return unsafe_get_ok(result);
}

// API search type: throw_type_on_error : Result a b -> a
// Throws the given exception type constructed with error value if error.
// Return ok value if ok.
template <typename E, typename Ok, typename Error>
Ok throw_type_on_error(const result<Ok, Error>& result)
{
    if (is_error(result))
        throw E(unsafe_get_error(result));
    return unsafe_get_ok(result);
}

// True if ok values are the same or if errors are the same.
template <typename Ok, typename Error>
bool operator==(const result<Ok, Error>& x, const result<Ok, Error>& y)
{
    if (is_ok(x) && is_ok(y))
        return unsafe_get_ok(x) == unsafe_get_ok(y);
    if (is_error(x) && is_error(y))
        return unsafe_get_error(x) == unsafe_get_error(y);
    return false;
}

// False if ok values are the same or if both errors are the same.
template <typename Ok, typename Error>
bool operator!=(const result<Ok, Error>& x, const result<Ok, Error>& y)
{
    return !(x == y);
}

// API search type: lift_result : ((a -> b), Result a c) -> Result b c
// fwd bind count: 1
// Lifts a function into the result functor.
// A function that for example was able to convert and int into a string,
// now can convert a result<int, Err> into a result<string, Err>.
// An error stays the same error, regardless of the conversion.
template <typename Error, typename F, typename A>
auto lift_result(F f, const result<A, Error>& r)
{
    internal::trigger_static_asserts<internal::unary_function_tag, F, A>();

    using B = std::decay_t<internal::invoke_result_t<F, A>>;

    if (is_ok(r))
        return ok<B, Error>(internal::invoke(f, unsafe_get_ok(r)));
    return error<B, Error>(unsafe_get_error(r));
}

// API search type: lift_result_both : ((a -> c), (b -> d), Result a b) -> Result c d
// fwd bind count: 2
// Lifts two functions into the result functor.
template <typename F, typename G, typename A, typename B>
auto lift_result_both(F f, G g, const result<A, B>& r)
{
    internal::trigger_static_asserts<internal::unary_function_tag, F, A>();
    internal::trigger_static_asserts<internal::unary_function_tag, G, B>();

    using C = std::decay_t<internal::invoke_result_t<F, A>>;
    using D = std::decay_t<internal::invoke_result_t<G, B>>;

    if (is_ok(r))
        return ok<C, D>(internal::invoke(f, unsafe_get_ok(r)));
    return error<C, D>(internal::invoke(g, unsafe_get_error(r)));
}

// API search type: unify_result : ((a -> c), (b -> c), Result a b) -> c
// fwd bind count: 2
// Extracts the value (Ok or Error) from a Result
// as defined by the two given functions.
template <typename F, typename G, typename A, typename B>
auto unify_result(F f, G g, const result<A, B>& r)
{
    internal::trigger_static_asserts<internal::unary_function_tag, F, A>();
    internal::trigger_static_asserts<internal::unary_function_tag, G, B>();
    static_assert(std::is_same<internal::invoke_result_t<F, A>,
                      internal::invoke_result_t<G, B>>::value,
        "Both functions must return the same type.");
    if (is_ok(r))
        return internal::invoke(f, unsafe_get_ok(r));
    return internal::invoke(g, unsafe_get_error(r));
}

// API search type: join_result : Result (Result a b) b -> Result a b
// Flattens a nested result.
// join_result(Ok Ok x) == Ok x
// join_result(Ok Error e) == Error e
// join_result(Error e) == Error e
template <typename OK, typename Error>
result<OK, Error> join_result(const result<result<OK, Error>, Error>& r)
{
    if (is_ok(r))
        return unsafe_get_ok(r);
    else
        return error<OK, Error>(r.unsafe_get_error());
}

// API search type: and_then_result : ((a -> Result c b), (Result a b)) -> Result c b
// fwd bind count: 1
// Monadic bind.
// Returns the error if the result is an error.
// Otherwise return the result of applying
// the function to the ok value of the result.
template <typename Ok, typename Error, typename F>
auto and_then_result(F f, const result<Ok, Error>& r)
{
    internal::trigger_static_asserts<internal::unary_function_tag, F, Ok>();

    using FOut = std::decay_t<internal::invoke_result_t<F, Ok>>;
    static_assert(std::is_same<Error, typename FOut::error_t>::value,
        "Error type must stay the same.");
    if (is_ok(r))
        return internal::invoke(f, unsafe_get_ok(r));
    else
        return error<typename FOut::ok_t, typename FOut::error_t>(r.unsafe_get_error());
}

// API search type: compose_result : ((a -> Result b c), (b -> Result d c)) -> (a -> Result d c)
// Left-to-right Kleisli composition of monads.
// It is possible to compose a variadic number of callables.
// The first callable can take a variadic number of parameters.
template <typename... Callables>
auto compose_result(Callables&&... callables)
{
    auto bind_result = [](auto f, auto g) {
        return [f = std::move(f), g = std::move(g)](auto&&... args) {
            internal::trigger_static_asserts<internal::check_arity_tag,
                decltype(f),
                decltype(args)...>();
#if defined(_MSC_VER) && _MSC_VER >= 1920 // in VS2019, compilation with /permissive- breaks with 'using' syntax below
            struct FOut : std::decay_t<
                              internal::invoke_result_t<decltype(f), decltype(args)...>> {
            };
#else
            using FOut = std::decay_t<
                internal::invoke_result_t<decltype(f), decltype(args)...>>;
#endif

            internal::trigger_static_asserts<internal::unary_function_tag,
                decltype(g),
                typename FOut::ok_t>();
#if defined(_MSC_VER) && _MSC_VER >= 1920 // in VS2019, compilation with /permissive- breaks with 'using' syntax below
            struct GOut : std::decay_t<
                              internal::invoke_result_t<decltype(g), typename FOut::ok_t>> {
            };
#else
            using GOut = std::decay_t<
                internal::invoke_result_t<decltype(g), typename FOut::ok_t>>;
#endif
            static_assert(std::is_same<typename FOut::error_t,
                              typename GOut::error_t>::value,
                "Error type must stay the same.");

            auto resultB = internal::invoke(f, std::forward<decltype(args)>(args)...);
            if (is_ok(resultB))
                return internal::invoke(g, unsafe_get_ok(resultB));
            return error<typename GOut::ok_t, typename GOut::error_t>(
                unsafe_get_error(resultB));
        };
    };
    return internal::compose_binary_lift(bind_result,
        std::forward<Callables>(callables)...);
}

// API search type: compose_first_error_and_result : ((a -> Result b c), (a -> Result d e)) -> (a -> (Maybe c, Result d e))
// Run two result-returning functions on the same input independently.
// The first contributes only its potential error (as a Maybe), its Ok
// value is discarded. The second's result is forwarded unchanged.
// The second function is always called, regardless of whether the first
// one failed. Useful for ancillary checks (logging, metrics, validation,
// audit, cache lookup, ...) that may fail but should not block the main
// computation.
template <typename First, typename Second>
auto compose_first_error_and_result(First first, Second second)
{
    return [first = std::move(first), second = std::move(second)](const auto& x) {
        auto first_result = internal::invoke(first, x);
        using FirstErr = typename std::decay_t<decltype(first_result)>::error_t;
        auto first_err = is_error(first_result)
            ? just<FirstErr>(unsafe_get_error(first_result))
            : nothing<FirstErr>();
        return std::make_pair(std::move(first_err), internal::invoke(second, x));
    };
}
} // namespace fplus
