/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.problem;

import java.util.ArrayList;
import java.util.List;

import lombok.RequiredArgsConstructor;

/**
 * Thrown from {@link ResultOrProblems#getOrThrow()} as an alternative to using functional style flow control in your
 * code with {@link ResultOrProblems}.  {@link ProblemException} is a checked exception and is explicitly meant to be
 * - it is ised for exceptional flow control: calling code must decide how to deal with the exception;
 * declaring code should be careful that they should be using this instead of returning a ResultOrProblems.
 * Use with {@link #catching(Call)} to 'clean' up exceptional flow control within a method body.
 *
 * This is in contrast to a {@link ResultComputationException} which is a programming error caused by not checking for a
 * result before calling `get()`.  ResultComputationException is an unchecked exception and is not meant to be caught
 * (apart from error reporting code).
 *
 * Example use:
 * ```
 * public ResultOrProblems<Foo> doThing() {
 *   return ProblemException.wrap(() -> {
 *     Struct structType = givenType.find(Struct.class).orElseThrow(new ProblemException(unexpectedTypeProblem()));
 *     RSList listType = otherType.find(RSList.class).orElseThrow(new ProblemException(unexpectedTypeProblem()));
 *     RealizedExpression expr = realizer.realize(structType, someExpr).orElseThrow();
 *
 *     return new Foo(listType, expr);
 *   });
 * }
 * ```
 * A small number of micro benchmarks were done to compare the relative costs of throwing/catching a ProblemException,
 * vs returning a ResultOrProblems, including the cost of using the `wrap` method to clean up code.  Findings were:
 *
 * - throwing exceptions is slower than returning a failed ResultOrProblems, if suppressing stack traces then it becomes
 * almost neglible (say, 20% slower)
 * - throwing exceptions is faster when code mostly succeeds - avoids the overhead of constructing a wrapper object for
 * each method call
 * - using the {@link #catching(Call)} method adds overhead, say, a 10x overhead to succeeding code and a 2x overhead to
 * failing code.   But this was 10ms vs 100ms for 100k iterations around a trivial method.
 *
 * Ultimately this shows that for code that's not in a tight loop or being called a lot, worry about write nice looking
 * code, don't worry about performance.
 *
 */
@RequiredArgsConstructor
public class ProblemException extends Exception {

  public static void throwUnlessEmpty(List<Problem> problems) throws ProblemException {
    if (!problems.isEmpty()) {
      throw new ProblemException(Problems.from(problems));
    }
  }

  /**
   * A function call that will either return an object or throw a {@link ProblemException}.  Use with
   * {@link ProblemException#catching(Call)} to adapt to a non-throwing method signature.
   * @param <T> return type of the method call
   */
  @FunctionalInterface
  public interface Call<T> {
    T call() throws ProblemException;
  }

  /**
   * Alternative to {@link Call} that accepts a list of problems to return in the result
   */
  @FunctionalInterface
  public interface ProblemsCall<T> {
    T call(List<Problem> problems) throws ProblemException;
  }

  /**
   * Wrap some code in a try-catch so that the result of `T` is wrapped in a {@link ResultOrProblems} of type `T`.  If
   * the code throws a {@link ProblemException}, a failed result is returned.
   *
   * @param <T> type the return type of the call
   * @param call the code to call
   * @return Either a successful result of type T or a failed result.
   */
  public static final <T> ResultOrProblems<T> catching(Call<T> call) {
    try {
      return ResultOrProblems.of(call.call());
    } catch (ProblemException e) {
      return e.toResult();
    }
  }

  /**
   * Wrap some code in a try-catch so that the result of `T` is wrapped in a {@link ResultOrProblems} of type `T`.  If
   * the code throws a {@link ProblemException}, a failed result is returned.  Any problems added in to the list will
   * also be returned.  If the problems list has errors, then a failed result will be returned (ignoring any result that
   * might have been returned).
   *
   * @param <T> type the return type of the call
   * @param call the code to call
   * @return Either a successful result of type T or a failed result.
   */
  public static final <T> ResultOrProblems<T> catching(ProblemsCall<T> call) {
    List<Problem> problems = new ArrayList<>();
    try {
      T result = call.call(problems);

      if (Problem.hasErrors(problems)) {
        return ResultOrProblems.failed(problems);
      }

      return ResultOrProblems.of(result, problems);
    } catch (ProblemException e) {
      if (problems.isEmpty()) {
        return e.toResult();
      } else {
        List<Problem> combined = new ArrayList<>();
        combined.addAll(e.getProblems());
        combined.addAll(problems);
        return ResultOrProblems.failed(combined);
      }
    }
  }

  private final Problems problem;

  /**
   * Create a new {@link ProblemException}
   */
  public ProblemException(Problem... problems) {
    this(Problems.from(problems));
  }

  public ProblemException(List<Problem> problems) {
    this(Problems.from(problems));
  }

  /**
   * @return a failed ResultOrProblems that includes the problems attached to this exception
   */
  public <T> ResultOrProblems<T> toResult() {
    return ResultOrProblems.failed(problem.toList());
  }

  @Override
  public synchronized Throwable fillInStackTrace() {
    // suppress stack trace being filled, as we don't want to use these exceptions for debugging (not what problem
    // exception is for) and slows down constructing exceptions considerably
    return this;
  }

  public List<Problem> getProblems() {
    return problem.toList();
  }

  @Override
  public String getMessage() {
    return problem.toProblem().toString();
  }

}
