package com.annimon.stream;

import com.annimon.stream.function.Consumer;
import com.annimon.stream.function.Function;
import com.annimon.stream.function.Supplier;
import com.annimon.stream.function.ThrowableFunction;
import com.annimon.stream.function.ThrowableSupplier;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * A container for values which provided by {@code ThrowableSupplier}.
 *
 * Stores value which provided by {@link ThrowableSupplier} or an exception which were thrown.
 *
 * <pre><code>
 *     Exceptional.of(new ThrowableSupplier&lt;String, Throwable&gt;() {
 *          &#64;Override
 *          public String get() throws Throwable {
 *              return IOUtils.read(inputStream);
 *          }
 *      }).ifExceptionIs(IOException.class, new Consumer&lt;IOException&gt;() {
 *          &#64;Override
 *          public void accept(IOException exception) {
 *              logger.log(Level.WARNING, "read file", exception);
 *          }
 *      }).getOrElse("default string");
 *
 *      Exceptional.of(() -&gt; IOUtils.readBytes(inputStream)).getOrElse(new byte[0]);
 * </code></pre>
 *
 * @param <T> the type of the inner value
 */
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
public class Exceptional<T> {

    /**
     * Returns an {@code Exceptional} with value provided by given {@code ThrowableSupplier} function.
     *
     * @param <T> the type of value
     * @param supplier  a supplier function
     * @return an {@code Exceptional}
     * @throws NullPointerException if {@code supplier} is null
     */
    @NotNull
    public static <T> Exceptional<T> of(@NotNull ThrowableSupplier<T, Throwable> supplier) {
        Objects.requireNonNull(supplier);
        try {
            return new Exceptional<T>(supplier.get(), null);
        } catch (Throwable throwable) {
            return of(throwable);
        }
    }

    /**
     * Returns an {@code Exceptional} with throwable already set.
     *
     * @param <T> the type of value
     * @param throwable  throwable instance
     * @return an {@code Exceptional}
     */
    @NotNull
    @Contract("_ -> new")
    public static <T> Exceptional<T> of(@NotNull Throwable throwable) {
        Objects.requireNonNull(throwable);
        return new Exceptional<T>(null, throwable);
    }

    private final T value;
    private final Throwable throwable;

    private Exceptional(@Nullable T value, @Nullable Throwable throwable) {
        this.value = value;
        this.throwable = throwable;
    }

    /**
     * Returns inner value.
     *
     * @return inner value.
     */
    @Nullable
    public T get() {
        return value;
    }

    /**
     * Checks value present (i.e. there were no exceptions).
     *
     * @return {@code true} if a value present, {@code false} otherwise
     */
    public boolean isPresent() {
        return throwable == null;
    }

    /**
     * Returns inner value if there were no exceptions, otherwise returns {@code other}.
     *
     * @param other  the value to be returned if there were any exception
     * @return inner value if there were no exceptions, otherwise {@code other}
     */
    @Nullable
    public T getOrElse(@Nullable T other) {
        return throwable == null ? value : other;
    }

    /**
     * Returns inner value if there were no exceptions, otherwise returns value produced by supplier function.
     *
     * @param other  the supplier function that produces value if there were any exception
     * @return inner value if there were no exceptions, otherwise value produced by supplier function
     * @since 1.1.9
     */
    @Nullable
    public T getOrElse(@NotNull Supplier<? extends T> other) {
        return throwable == null ? value : other.get();
    }

    /**
     * Wraps inner value with {@code Optional} container
     *
     * @return an {@code Optional}
     */
    @NotNull
    public Optional<T> getOptional() {
        return Optional.ofNullable(value);
    }

    /**
     * Returns exception.
     *
     * @return exception
     */
    @Nullable
    public Throwable getException() {
        return throwable;
    }

    /**
     * Returns inner value if there were no exceptions, otherwise throws an exception.
     *
     * @return inner value if there were no exceptions
     * @throws Throwable that was thrown in supplier function
     */
    @Nullable
    public T getOrThrow() throws Throwable {
        if (throwable != null) {
            throw throwable;
        }
        return value;
    }

    /**
     * Returns inner value if there were no exceptions, otherwise throws {@code RuntimeException}.
     *
     * @return inner value if there were no exceptions
     * @throws RuntimeException with wrapped exception which was thrown in supplier function
     */
    @Nullable
    public T getOrThrowRuntimeException() throws RuntimeException {
        if (throwable != null) {
            throw new RuntimeException(throwable);
        }
        return value;
    }

    /**
     * Returns inner value if there were no exceptions, otherwise throws the given {@code exception}.
     *
     * @param <E> the type of exception
     * @param exception  an exception to be thrown
     * @return inner value if there were no exceptions
     * @throws E if there were exceptions in supplier function
     */
    @Nullable
    public <E extends Throwable> T getOrThrow(@NotNull E exception) throws E {
        if (throwable != null) {
            exception.initCause(throwable);
            throw exception;
        }
        return value;
    }

    /**
     * Returns current {@code Exceptional} if there were no exceptions, otherwise
     * returns an {@code Exceptional} produced by supplier function.
     *
     * @param supplier  supplier function that produced an {@code Exceptional} to be returned
     * @return this {@code Exceptional} if there were no exceptions, otherwise
     *         an {@code Exceptional} produced by supplier function
     * @throws NullPointerException if {@code supplier} or its result is null
     */
    @NotNull
    public Exceptional<T> or(@NotNull Supplier<Exceptional<T>> supplier) {
        if (throwable == null) return this;

        Objects.requireNonNull(supplier);
        return Objects.requireNonNull(supplier.get());
    }

    /**
     * Applies custom operator on {@code Exceptional}.
     *
     * @param <R> the type of the result
     * @param function  a transforming function
     * @return a result of the transforming function
     * @throws NullPointerException if {@code function} is null
     * @since 1.1.9
     */
    @Nullable
    public <R> R custom(@NotNull Function<Exceptional<T>, R> function) {
        Objects.requireNonNull(function);
        return function.apply(this);
    }

    /**
     * Invokes mapping function on inner value if there were no exceptions.
     *
     * @param <U> the type of result value
     * @param mapper  mapping function
     * @return an {@code Exceptional} with transformed value if there were no exceptions
     * @throws NullPointerException if {@code mapper} is null
     */
    @NotNull
    public <U> Exceptional<U> map(@NotNull ThrowableFunction<? super T, ? extends U, Throwable> mapper) {
        if (throwable != null) {
            return of(throwable);
        }
        Objects.requireNonNull(mapper);
        try {
            return new Exceptional<U>(mapper.apply(value), null);
        } catch (Throwable t) {
            return of(t);
        }
    }

    /**
     * Invokes consumer function with value if present.
     *
     * @param consumer  a consumer function
     * @return this {@code Exceptional}
     * @since 1.1.2
     */
    @NotNull
    public Exceptional<T> ifPresent(@NotNull Consumer<? super T> consumer) {
        if (throwable == null) {
            consumer.accept(value);
        }
        return this;
    }

    /**
     * Invokes consumer function if there were any exception.
     *
     * @param consumer  a consumer function
     * @return an {@code Exceptional}
     */
    @NotNull
    public Exceptional<T> ifException(@NotNull Consumer<Throwable> consumer) {
        if (throwable != null) {
            consumer.accept(throwable);
        }
        return this;
    }

    /**
     * Invokes consumer function if exception class matches {@code throwableClass}.
     *
     * @param <E> the type of exception
     * @param throwableClass  the class of an exception to be compared
     * @param consumer  a consumer function
     * @return an {@code Exceptional}
     */
    @NotNull
    @SuppressWarnings("unchecked")
    public <E extends Throwable> Exceptional<T> ifExceptionIs(@NotNull Class<E> throwableClass, @NotNull Consumer<? super E> consumer) {
        if ( (throwable != null) &&
                (throwableClass.isAssignableFrom(throwable.getClass())) ) {
            consumer.accept((E) throwable);
        }
        return this;
    }

    /**
     * Returns current {@code Exceptional} if there were no exceptions, otherwise
     * calls {@code function} and wraps produced result with an {@code Exceptional}.
     *
     * @param function  recovering function
     * @return this {@code Exceptional} if there were no exceptions, otherwise
     *         an {@code Exceptional} with wrapped recovering function result
     * @throws NullPointerException if {@code function} is null
     * @since 1.1.2
     */
    @NotNull
    public Exceptional<T> recover(@NotNull final ThrowableFunction<Throwable, ? extends T, Throwable> function) {
        if (throwable == null) return this;

        Objects.requireNonNull(function);
        try {
            return new Exceptional<T>(function.apply(throwable), null);
        } catch (Throwable throwable) {
            return of(throwable);
        }
    }

    /**
     * Returns current {@code Exceptional} if there were no exceptions, otherwise
     * returns an {@code Exceptional} produced by {@code function}.
     *
     * @param function  recovering function
     * @return this {@code Exceptional} if there were no exceptions, otherwise
     *         an {@code Exceptional} produced by recovering function
     * @throws NullPointerException if {@code function} or produced result is null
     * @since 1.1.2
     */
    @NotNull
    public Exceptional<T> recoverWith(@NotNull final Function<Throwable, ? extends Exceptional<T>> function) {
        if (throwable == null) return this;

        Objects.requireNonNull(function);
        return Objects.requireNonNull(function.apply(throwable));
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Exceptional)) {
            return false;
        }

        Exceptional<?> other = (Exceptional<?>) obj;
        return Objects.equals(value, other.value) &&
                Objects.equals(throwable, other.throwable);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value, throwable);
    }

    @NotNull
    @Override
    public String toString() {
        return throwable == null
            ? String.format("Exceptional value %s", value)
            : String.format("Exceptional throwable %s", throwable);
    }
}