using System.Runtime.CompilerServices;
using static System.Runtime.CompilerServices.MethodImplOptions;

namespace InnovEnergy.Lib.Utils;

public static class ExceptionHandling
{
    // TODO: https://blog.stephencleary.com/2020/06/a-new-pattern-for-exception-logging.html

    public static Try<T> Try<T>(Func<T> func)
    {
        Object wrapped;

        try
        {
            wrapped = func()!;
        }
        catch (Exception e)
        {
            wrapped = e;
        }

        return new Try<T>(wrapped);
    }


    public static async Task<Try<T>> Try<T>(Func<Task<T>> func)
    {
        Object wrapped;

        try
        {
            wrapped = (await func())!;
        }
        catch (Exception e)
        {
            wrapped = e;
        }

        return new Try<T>(wrapped);
    }


    public static Try<R> ThenTry<T, R>(this Try<T> @try, Func<T, Try<R>> func)
    {
        var wrapped = @try.Wrapped;

        if (wrapped is Exception e)
            return new Try<R>(e);

        return func((T) wrapped);
    }


    public static Try<R> ThenTry<T, R>(this Try<T> @try, Func<T, R> func)
    {
        var wrapped = @try.Wrapped;

        if (wrapped is Exception e)
            return new Try<R>(e);

        return Try(() => func((T) wrapped));
    }


    public static Try<R> TryApply<T,R>(this T t, Func<T, R> func)
    {
        return Try(() => func(t));
    }

    [MethodImpl(AggressiveInlining | AggressiveOptimization)]
    public static Try<R> TryApply<T1, T2, R>(this (T1 p1, T2 p2) t, Func<T1, T2, R> f)
    {
        return Try(() => f(t.p1, t.p2));
    }

    [MethodImpl(AggressiveInlining | AggressiveOptimization)]
    public static Try<R> TryApply<T1, T2, T3, R>(this (T1 p1, T2 p2, T3 p3) t, Func<T1, T2, T3, R> f)
    {
        return Try(() => f(t.p1, t.p2, t.p3));
    }


    // public static Try<Boolean> Try(Action action)
    // {
    //     Boolean Func()
    //     {
    //         action();
    //         return true;
    //     }
    //
    //     return Try(Func);
    // }


    // public static Try<Boolean> TryApply<T>(this T t, Action<T> action)
    // {
    //     return Try(() => action(t));
    // }


    public static Try<T> OnErrorDo<T>(this Try<T> @try, Action<Exception> onError)
    {
        if (@try.Wrapped is Exception originalException)
        {
            try
            {
                onError(originalException);
            }
            catch (Exception handlerException)                      // make absolutely sure no exception can escape,
            {                                                       // even if the handler fails
                const String msg = nameof(OnErrorDo) + " handler failed";
                var ex = new AggregateException(msg, handlerException, originalException);
                return new Try<T>(ex);
            }
        }

        return @try;
    }

    public static Try<T> OnErrorLog<T>(this Try<T> @try, String msg)  // TODO: Func<String>
    {
        if (@try.Wrapped is Exception e)
            return new Try<T>(new Exception(msg, e));

        return @try;
    }

    public static T Catch<T>(this Try<T> @try, T onError)
    {
        var wrapped = @try.Wrapped;

        if (wrapped is Exception)
            return onError;

        return (T) wrapped;
    }

    public static Try<T> Catch<T>(this Try<T> @try, Func<Exception, T> onError)
    {
        var wrapped = @try.Wrapped;

        if (wrapped is Exception e)
            return Try(() => onError(e));

        return @try;
    }

    public static Boolean Succeeds(Action action)
    {
        try
        {
            action();
            return true;
        }
        catch
        {
            return false;
        }
    }

    public static Boolean Succeeds<T>(Func<T> func)
    {
        try
        {
            func();
            return true;
        }
        catch
        {
            return false;
        }
    }

}

public readonly struct Try<T>  // phantom type
{
    internal readonly Object Wrapped;

    internal Try(Object wrapped)
    {
        Wrapped = wrapped;
    }
}