package com.airlock.iam.base.misc.result;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public final class Result<R, F> {
   private static final Result<?, ? extends Throwable> SUCCESS = result(true);
   private final Optional<R> result;
   private final Optional<F> failure;

   private Result(Optional<R> result, Optional<F> failure) {
      requireEitherResultOrFailure(result, failure);
      this.result = result;
      this.failure = failure;
   }

   public static <R, F> Result<R, F> result(R result) {
      return new Result<>(Optional.of(result), Optional.empty());
   }

   public static <R> Result<R, Throwable> result(Callable<R> callable) {
      try {
         return result(callable.call());
      } catch (Throwable e) {
         return failure(e);
      }
   }

   public static <R, F> Result<R, F> result(Callable<Result<R, F>> callable, Function<Throwable, F> failureMapper) {
      try {
         return callable.call();
      } catch (Throwable e) {
         return failure(failureMapper.apply(e));
      }
   }

   public static Result<?, ? extends Throwable> result(Runnable run) {
      try {
         run.run();
         return SUCCESS;
      } catch (Throwable e) {
         return failure(e);
      }
   }

   public static <R, F> Result<R, F> failure(F failure) {
      return new Result<>(Optional.empty(), Optional.of(failure));
   }

   public boolean isPresent() {
      return this.result.isPresent();
   }

   public boolean isFailure() {
      return this.failure.isPresent();
   }

   public boolean contains(R expectedResult) {
      return this.result.isPresent() && Objects.equals(this.result.get(), expectedResult);
   }

   public boolean containsFailure(F expectedFailure) {
      return this.failure.isPresent() && Objects.equals(this.failure.get(), expectedFailure);
   }

   public <G> Result<R, G> or(Function<F, Result<R, G>> failureMapper) {
      return (Result<R, G>)(this.result.isPresent() ? result(this.result.get()) : failureMapper.apply(this.failure.orElseThrow()));
   }

   public R orMapFailure(Function<F, R> failureMapper) {
      return this.result.orElseGet(() -> failureMapper.apply(this.failure.orElseThrow()));
   }

   public <E extends Throwable> R orElseThrow(Function<F, E> failureMapper) throws E {
      return this.result.orElseThrow(() -> failureMapper.apply(this.failure.orElseThrow()));
   }

   public R get() {
      return this.result.orElseThrow();
   }

   public F getFailure() {
      return this.failure.orElseThrow();
   }

   public Optional<R> peek() {
      return this.result;
   }

   public Optional<F> peekFailure() {
      return this.failure;
   }

   public Result<R, F> filter(Predicate<R> predicate, F newFailure) {
      if (this.result.isPresent()) {
         return predicate.test(this.result.get()) ? this : failure(newFailure);
      } else {
         return this;
      }
   }

   public <S> Result<S, F> mapResult(Function<R, S> mapper) {
      return this.result.isPresent() ? result(mapper.apply(this.result.get())) : failure(this.failure.orElseThrow());
   }

   public void ifResult(Consumer<R> consumer) {
      this.result.ifPresent(consumer);
   }

   public <S> Result<S, F> flatMap(Function<R, Result<S, F>> mapper) {
      return this.result.isPresent() ? mapper.apply(this.result.get()) : failure(this.failure.orElseThrow());
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj) {
         return true;
      } else if (obj != null && this.getClass() == obj.getClass()) {
         Result<?, ?> other = (Result<?, ?>)obj;
         return Objects.equals(this.result, other.result) && Objects.equals(this.failure, other.failure);
      } else {
         return false;
      }
   }

   @Override
   public int hashCode() {
      return Objects.hash(this.result, this.failure);
   }

   @Override
   public String toString() {
      return this.result.isPresent() ? String.format("Result[%s]", this.result.get()) : String.format("Failure[%s]", this.failure.orElseThrow());
   }

   private static void requireEitherResultOrFailure(Optional<?> result, Optional<?> failure) {
      if (result.isPresent() == failure.isPresent()) {
         throw new IllegalArgumentException("Either result or failure must be set");
      }
   }
}
