/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.commons.functional;

import java.io.Serializable;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.function.Supplier;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import org.springframework.util.function.ThrowingConsumer;
import org.springframework.util.function.ThrowingFunction;

import org.apache.causeway.commons.internal.exceptions._Exceptions;

import lombok.SneakyThrows;

/**
 * The {@link Try} type represents a value of one of two possible types (a disjoint union)
 * of {@link Success} or {@link Failure}.
 * <p>
 * Factory methods {@link Try#success(Object)} and {@link Try#failure(Throwable)}
 * correspond to the two possible values.
 * <p>
 * Follows the <em>Railway Pattern</em>, that is, once failed, stays failed.
 * @see Railway
 *
 * @since 2.0 {@index}
 */
public sealed interface Try<T>
permits Try.Success, Try.Failure {

    // -- FACTORIES

    /** success case with no value */
    public static <T> Try<T> empty() {
        return success(null);
    }

    public static <T> Try<T> call(final @NonNull Callable<T> callable) {
        try {
            return success(callable.call());
        } catch (Throwable e) {
            return failure(e);
        }
    }

    public static Try<Void> run(final @NonNull ThrowingRunnable runnable) {
        try {
            runnable.run();
            return success(null);
        } catch (Throwable e) {
            return failure(e);
        }
    }

    public static <T> Success<T> success(final @Nullable T value) {
        return new Success<>(value);
    }

    public static <T> Failure<T> failure(final @NonNull Throwable throwable) {
        return new Failure<T>(throwable);
    }

    // -- PREDICATES

    boolean isSuccess();
    boolean isFailure();

    // -- ACCESSORS

    /**
     * Optionally returns the contained {@code value} based on presence,
     * that is, if this is a {@link Success} and the value is not {@code null}.
     */
    Optional<T> getValue();
    /**
     * Optionally returns the contained {@code failure} based on presence,
     * that is, if this is a {@link Failure}.
     */
    Optional<Throwable> getFailure();

    // -- PEEKING

    /**
     * If this is a {@link Success}, peeks into the {@code value} wrapped in an {@link Optional}.
     * <p>
     * If given valueConsumer throws an exception, a failed {@link Try} is returned.
     */
    Try<T> ifSuccess(final @NonNull ThrowingConsumer<Optional<T>> valueConsumer);
    /**
     * If this is a {@link Success} peeks into the (null-able) {@code value}.
     * <p>
     * If given valueConsumer throws an exception, a failed {@link Try} is returned.
     * @apiNote If preceded with a call to {@link #mapEmptyToFailure()},
     *      the success value - as passed over to the valueConsumer - is guaranteed non-null.
     */
    Try<T> ifSuccessAsNullable(final @NonNull ThrowingConsumer<T> valueConsumer);
    /**
     * If this is a {@link Failure}, peeks into the {@code failure}.
     * <p>
     * If given exceptionConsumer throws an exception, a failed {@link Try} is returned.
     */
    Try<T> ifFailure(final @NonNull ThrowingConsumer<Throwable> exceptionConsumer);

    // -- FAIL EARLY

    /** Throws the contained failure if any. */
    Try<T> ifFailureFail();
    /** Throws {@link NoSuchElementException} if {@code value} is {@code null}. */
    Try<T> ifAbsentFail();

    // -- MAPPING

    /**
     * If this is a {@link Success}, maps this {@link Try} to another,
     * by calling the successMapper with the {@code value} wrapped by an {@link Optional}.
     * Otherwise if this is a {@link Failure}, acts as identity operator,
     * though implementations may return a new instance.
     * <p>
     * If given successMapper throws an exception, a failed {@link Try} is returned.
     */
    <R> Try<R> mapSuccess(@NonNull ThrowingFunction<Optional<T>, R> successMapper);
    /**
     * If this is a {@link Success}, maps this {@link Try} to another,
     * by calling the successMapper with the {@code value} (which may be null).
     * Otherwise if this is a {@link Failure}, acts as identity operator,
     * though implementations may return a new instance.
     * <p>
     * If given successMapper throws an exception, a failed {@link Try} is returned.
     * @apiNote If preceded with a call to {@link #mapEmptyToFailure()},
     *      the success value - as passed over to the successMapper - is guaranteed non-null.
     */
    <R> Try<R> mapSuccessAsNullable(@NonNull ThrowingFunction<T, R> successMapper);
    /**
     * If this {@link Try} holds a non-null {@code value} (and hence is also a {@link Success}),
     * maps this {@link Try} to another,
     * by calling the successMapper with the {@code value} (which is non-null).
     * Otherwise acts as identity operator, that is,
     * either stay an empty {@link Success} or stay a {@link Failure},
     * though implementations may return a new instance.
     * <p>
     * If given successMapper throws an exception, a failed {@link Try} is returned.
     */
    <R> Try<R> mapSuccessWhenPresent(@NonNull ThrowingFunction<T, R> successMapper);
    /**
     * If this is a {@link Failure}, maps this {@link Try} to another.
     * Otherwise if this is a {@link Success} acts as identity operator,
     * though implementations may return a new instance.
     * <p>
     * If given failureMapper throws an exception, a failed {@link Try} is returned
     * (hiding the original failure).
     */
    Try<T> mapFailure(@NonNull ThrowingFunction<Throwable, Throwable> failureMapper);
    /**
     * If this is a {@link Failure}, recovers to a {@link Success}.
     * Otherwise if this is a {@link Success} acts as identity operator,
     * though implementations may return a new instance.
     * <p>
     * If given recoveryMapper throws an exception, a failed {@link Try} is returned.
     */
    Try<T> mapFailureToSuccess(@NonNull ThrowingFunction<Throwable, T> recoveryMapper);
    /**
     * Maps this {@link Try} to {@link Failure} if this is a {@link Success} with an empty {@code value}.
     * Otherwise acts as identity operator,
     * though implementations may return a new instance.
     */
    Try<T> mapEmptyToFailure();
    /**
     * Maps this {@link Try} to {@link Either}
     * using according mapping function {@code successMapper} or {@code failureMapper}.
     * <p>
     * Any exceptions thrown by given failureMapper or successMapper are propagated without catching.
     * @apiNote It is a common functional programming convention, to map the success value <i>right</i>.
     */
    <L, R> Either<L, R> mapToEither(
            final @NonNull ThrowingFunction<Throwable, L> failureMapper,
            final @NonNull ThrowingFunction<Optional<T>, R> successMapper);

    // -- FLAT MAPPING / FUNCTION COMPOSITION

    /**
     * Variant of {@link #mapSuccess(ThrowingFunction)},
     * utilizing a different successMapper, one that returns a {@link Try}.
     * @see #mapSuccess(ThrowingFunction)
     */
    <R> Try<R> flatMapSuccess(@NonNull ThrowingFunction<Optional<T>, Try<R>> successMapper);

    /**
     * Variant of {@link #mapSuccessAsNullable(ThrowingFunction)},
     * utilizing a different successMapper, one that returns a {@link Try}.
     * @see #mapSuccessAsNullable(ThrowingFunction)
     */
    <R> Try<R> flatMapSuccessAsNullable(@NonNull ThrowingFunction<T, Try<R>> successMapper);

    /**
     * Variant of {@link #mapSuccessWhenPresent(ThrowingFunction)},
     * utilizing a different successMapper, one that returns a {@link Try}.
     * @see #mapSuccessWhenPresent(ThrowingFunction)
     */
    <R> Try<R> flatMapSuccessWhenPresent(@NonNull ThrowingFunction<T, Try<R>> successMapper);

    // -- ACCEPT

    /**
     * Either consumes the success or the failure.
     * <p>
     * However, if any of given failureConsumer or successConsumer throws an exception, a failed {@link Try} is returned.
     * @apiNote Order of arguments conforms to {@link #mapToEither(ThrowingFunction, ThrowingFunction)}
     */
    Try<T> accept(
            final @NonNull ThrowingConsumer<Throwable> failureConsumer,
            final @NonNull ThrowingConsumer<Optional<T>> successConsumer);

    // -- FOLDING

    /**
     * Maps the contained {@code value} or {@code failure} to a new value of type {@code R}
     * using according mapping function {@code successMapper} or {@code failureMapper}.
     * <p>
     * Any exceptions thrown by given failureMapper or successMapper are propagated without catching.
     * @apiNote Order of arguments conforms to {@link #mapToEither(ThrowingFunction, ThrowingFunction)}
     */
    <R> R fold(
            final @NonNull ThrowingFunction<Throwable, R> failureMapper,
            final @NonNull ThrowingFunction<Optional<T>, R> successMapper);

    // -- CONCATENATION

    /**
     * If this is a {@link Success}, maps it to a new {@link Try} based on given {@link Callable}.
     * Otherwise if its a {@link Failure}, acts as identity operator.
     */
    <R> Try<R> thenCall(final @NonNull Callable<R> callable);
    /**
     * If this is a {@link Success}, maps it to a new {@link Try} based on given {@link ThrowingRunnable}.
     * Otherwise if this is a {@link Failure}, acts as identity operator.
     */
    Try<Void> thenRun(final @NonNull ThrowingRunnable runnable);
    /**
     * If this is a {@link Success}, maps it to a new {@link Try} based on given {@link Supplier}.
     * Otherwise if this is a {@link Failure}, acts as identity operator.
     */
    <R> Try<R> then(final @NonNull Callable<? extends Try<R>> next);

    /**
     * If this is a {@link Failure}, maps it to a new {@link Try} based on given {@link Callable}.
     * Otherwise if this is a {@link Success}, acts as identity operator.
     */
    Try<T> orCall(final @NonNull Callable<T> fallback);

    // -- SHORTCUTS

    /**
     * If this is a {@link Failure} throws the contained failure,
     * otherwise if this is a {@link Success}, returns the success value as null-able.
     */
    @Nullable
    default T valueAsNullableElseFail() {
        ifFailureFail();
        return getValue().orElse(null);
    }

    /**
     * If this is a {@link Failure} throws the contained failure,
     * otherwise if this is a {@link Success},
     * either returns the success value if it is NOT <code>null</code>
     * or throws a {@link NoSuchElementException}.
     */
    default T valueAsNonNullElseFail() {
        ifFailureFail();
        return getValue().orElseThrow();
    }

    // -- SUCCESS

    record Success<T>(@Nullable T _value) implements Try<T>, Serializable {

        @Override public boolean isSuccess() { return true; }
        @Override public boolean isFailure() { return false; }

        @Override public Optional<T> getValue() { return Optional.ofNullable(_value); }
        @Override public Optional<Throwable> getFailure() { return Optional.empty(); }

        @Override
        public Try<T> ifSuccess(final @NonNull ThrowingConsumer<Optional<T>> valueConsumer) {
            try {
                valueConsumer.accept(getValue());
                return this;
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }
        @Override
        public Try<T> ifSuccessAsNullable(final @NonNull ThrowingConsumer<T> valueConsumer) {
            try {
                valueConsumer.accept(getValue().orElse(null));
                return this;
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }

        @Override
        public Success<T> ifFailure(final @NonNull ThrowingConsumer<Throwable> exceptionConsumer) {
            return this;
        }

        @Override
        public Success<T> ifFailureFail() {
            return this;
        }

        @Override
        public Success<T> ifAbsentFail() {
            if(_value==null) throw _Exceptions.noSuchElement();
            return this;
        }

        @Override
        public <R> Try<R> mapSuccess(final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return Try.call(()->successMapper.apply(getValue()));
        }
        @Override
        public <R> Try<R> mapSuccessAsNullable(final @NonNull ThrowingFunction<T, R> successMapper) {
            return Try.call(()->successMapper.apply(getValue().orElse(null)));
        }
        @Override
        public <R> Try<R> mapSuccessWhenPresent(final @NonNull ThrowingFunction<T, R> successMapper) {
            return getValue()
                    .map(value->Try.call(()->successMapper.apply(value)))
                    .orElseGet(Try::empty);
        }
        @Override
        public <R> Try<R> flatMapSuccess(final @NonNull ThrowingFunction<Optional<T>, Try<R>> successMapper) {
            try {
                return successMapper.apply(getValue());
            } catch (Throwable ex) {
                return Try.failure(ex);
            }
        }
        @Override
        public <R> Try<R> flatMapSuccessAsNullable(final @NonNull ThrowingFunction<T, Try<R>> successMapper) {
            try {
                return successMapper.apply(getValue().orElse(null));
            } catch (Throwable ex) {
                return Try.failure(ex);
            }
        }
        @Override
        public <R> Try<R> flatMapSuccessWhenPresent(final @NonNull ThrowingFunction<T, Try<R>> successMapper) {
            var value = getValue().orElse(null);
            if(value==null) return Try.empty();
            try {
                return successMapper.apply(value);
            } catch (Throwable ex) {
                return Try.failure(ex);
            }
        }

        @Override
        public Success<T> mapFailure(final @NonNull ThrowingFunction<Throwable, Throwable> failureMapper){
            return this;
        }
        @Override
        public Try<T> mapFailureToSuccess(final @NonNull ThrowingFunction<Throwable, T> recoveryMapper) {
            return this;
        }

        @Override
        public Try<T> mapEmptyToFailure() {
            return _value!=null
                    ? this
                    : Try.failure(_Exceptions.noSuchElement());
        }

        @Override
        public <R> Try<R> thenCall(final @NonNull Callable<R> callable) {
            return Try.call(callable);
        }

        @Override
        public Try<Void> thenRun(final @NonNull ThrowingRunnable runnable) {
            return Try.run(runnable);
        }

        @Override
        public <R> Try<R> then(final @NonNull Callable<? extends Try<R>> next) {
            try {
                return next.call();
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }

        @Override
        public Try<T> orCall(final @NonNull Callable<T> fallback) {
            return this;
        }

        @Override
        public Try<T> accept(
                final @NonNull ThrowingConsumer<Throwable> failureConsumer,
                final @NonNull ThrowingConsumer<Optional<T>> successConsumer) {
            try {
                successConsumer.accept(getValue());
                return this;
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }

        @Override
        public <R> R fold(
                final @NonNull ThrowingFunction<Throwable, R> failureMapper,
                final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return successMapper.apply(getValue());
        }

        @Override
        public <L, R> Either<L, R> mapToEither(
                final @NonNull ThrowingFunction<Throwable, L> failureMapper,
                final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return Either.right(successMapper.apply(getValue()));
        }

    }

    // -- FAILURE

    record Failure<T>(@NonNull Throwable throwable) implements Try<T>, Serializable {

        @Override public boolean isSuccess() { return false; }
        @Override public boolean isFailure() { return true; }

        @Override public Optional<T> getValue() { return Optional.empty(); }
        @Override public Optional<Throwable> getFailure() { return Optional.of(throwable); }

        @Override
        public Failure<T> ifSuccess(final @NonNull ThrowingConsumer<Optional<T>> valueConsumer) {
            return this;
        }
        @Override
        public Failure<T> ifSuccessAsNullable(final @NonNull ThrowingConsumer<T> valueConsumer) {
            return this;
        }

        @Override
        public Failure<T> ifFailure(final @NonNull ThrowingConsumer<Throwable> exceptionConsumer) {
            try {
                exceptionConsumer.accept(throwable);
                return this;
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }

        @Override @SneakyThrows
        public Failure<T> ifFailureFail() {
            throw throwable;
        }

        @Override @SneakyThrows
        public Failure<T> ifAbsentFail() {
            throw _Exceptions.noSuchElement();
        }

        @Override
        public <R> Failure<R> mapSuccess(final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }
        @Override
        public <R> Failure<R> mapSuccessAsNullable(final @NonNull ThrowingFunction<T, R> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }
        @Override
        public <R> Failure<R> mapSuccessWhenPresent(final @NonNull ThrowingFunction<T, R> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }
        @Override
        public <R> Failure<R> flatMapSuccess(final @NonNull ThrowingFunction<Optional<T>, Try<R>> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }
        @Override
        public <R> Failure<R> flatMapSuccessAsNullable(final @NonNull ThrowingFunction<T, Try<R>> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }
        @Override
        public <R> Failure<R> flatMapSuccessWhenPresent(final @NonNull ThrowingFunction<T, Try<R>> successMapper) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }

        @Override
        public Failure<T> mapFailure(final @NonNull ThrowingFunction<Throwable, Throwable> failureMapper){
            try {
                return new Failure<>(failureMapper.apply(throwable));
            } catch (Throwable e) {
                return failure(e);
            }
        }
        @Override
        public Try<T> mapFailureToSuccess(final @NonNull ThrowingFunction<Throwable, T> recoveryMapper) {
            return Try.call(()->recoveryMapper.apply(throwable));
        }

        @Override
        public Try<T> mapEmptyToFailure() {
            return this; // identity operation
        }

        @Override
        public <R> Failure<R> thenCall(final @NonNull Callable<R> callable) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }

        @Override
        public Try<Void> thenRun(final @NonNull ThrowingRunnable runnable) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }

        @Override
        public <R> Try<R> then(final @NonNull Callable<? extends Try<R>> next) {
            return new Failure<>(throwable); // railway pattern:  once failed, stays failed
        }

        @Override
        public Try<T> orCall(final @NonNull Callable<T> fallback) {
            return Try.call(fallback);
        }

        @Override
        public Try<T> accept(
                final @NonNull ThrowingConsumer<Throwable> failureConsumer,
                final @NonNull ThrowingConsumer<Optional<T>> successConsumer) {
            try {
                failureConsumer.accept(throwable);
                return this;
            } catch (Throwable e) {
                return Try.failure(e);
            }
        }

        @Override
        public <R> R fold(
                final @NonNull ThrowingFunction<Throwable, R> failureMapper,
                final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return failureMapper.apply(throwable);
        }

        @Override
        public <L, R> Either<L, R> mapToEither(
                final @NonNull ThrowingFunction<Throwable, L> failureMapper,
                final @NonNull ThrowingFunction<Optional<T>, R> successMapper) {
            return Either.left(failureMapper.apply(throwable));
        }

    }

}
