/*
 * 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.engine.function;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.typeset.IdentifiedType;
import nz.org.riskscape.engine.typeset.MissingTypeException;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * Immutable prototype-builder implementation of {@link IdentifiedFunction}
 * TODO given the use of this an all sorts of other implementations, the name of this needs to change to be more like
 * 'BaseFunction' or 'ImmutableFunction'
 */
@AllArgsConstructor
public class JavaFunction implements IdentifiedFunction {

  public interface Delegate extends Function<List<Object>, Object> {

    /**
     * Apply the arguments to produce the result.
     *
     * The result does not need to match the {@link JavaFunction} returnType exactly as it will be coerced by
     * {@link JavaFunction#call(java.util.List) } before being returned.
     *
     * @param arguments the arguments as passed to {@link JavaFunction#call(java.util.List) }
     * @return the result of processing the arguments
     */
    @Override
    Object apply(List<Object> arguments);

    default List<Problem> validate() {
      return Collections.emptyList();
    }

    /**
     * Called by {@link JavaFunction#getArguments() } to allow the delegate the option to provide an
     * {@link ArgumentList} which enables keyword arguments to be used when the function is called.
     *
     * If Delegates do not wish to provide the argument list the default implementation should not be
     * overridden.
     *
     * @return argument list or null
     */
    default ArgumentList getArguments() {
      return null;
    }
  }

  /**
   * REGEX pattern for valid ID's.
   *
   * ID's may only contain alpha numeric characters, ':', '_' and '-',
   */
  public static final Pattern ID_PATTERN = Pattern.compile("[\\w:_-]*");

  @Getter
  private final String id;

  // this one is mutable to make it easier to set the source during setup via the jar function's url
  @Getter @Setter
  private URI sourceURI;

  @Getter
  private final String description;
  @Getter
  private final List<Type> argumentTypes;
  @Getter
  private final Type returnType;
  @Getter
  private final Delegate delegate;
  @Getter
  private final Category category;

  public static JavaFunction asBuiltin(String id, Category category) {
    return new JavaFunction(id, RiskscapeFunction.BUILT_IN, id, Collections.emptyList(), Types.ANYTHING, (x) -> x,
        category);
  }

  public static JavaFunction withId(String id) {
    return new JavaFunction(id, Resource.UNKNOWN_URI, id, Collections.emptyList(), Types.ANYTHING,
        (x) -> x, Category.UNASSIGNED);
  }

  public JavaFunction(FunctionMetadata metadata, Delegate delegate) {
    this(metadata.getId(),
        metadata.getSource(),
        metadata.getDescription(),
        metadata.getArguments().getArgumentTypes(),
        metadata.getReturnType(),
        delegate,
        metadata.getCategory());
  }

  @Deprecated
  public static JavaFunction withIdAndSource(String id, String source) {
    return new JavaFunction(id, URI.create(source), id, Collections.emptyList(), Types.ANYTHING, (x) -> x,
        Category.UNASSIGNED);
  }

  /**
   * @param id of the function
   * @param source identifies where the function came from. Used when reporting {@link Problem}s
   * @return the function
   */
  public static JavaFunction withIdAndSource(String id, URI source) {
    return new JavaFunction(id, source, id, Collections.emptyList(), Types.ANYTHING, (x) -> x,
        Category.UNASSIGNED);
  }

  @Override
  public Object call(List<Object> args) {
    // NB this code used to coerce the delegate's result to the declared return type using Type::coerce, but
    // this has been removed as we move away from implicit runtime casting of data, and have a more explicit type
    // coercing and casting mechanism (i.e. using an EquivalenceCoercer)
    return delegate.apply(args);

  }

  @Override
  public ResultOrProblems<Boolean> validate(RealizationContext context) {
    List<Problem> problems = validateFunctionAttributes();
    if (problems.isEmpty()) {
      //Only validate the delegate if the function args are good.
      problems = delegate.validate();
    }
    if (!problems.isEmpty()) {
      return ResultOrProblems.failed(problems);
    }
    return ResultOrProblems.of(true);
  }

  /**
   * Validates that the input and return types are valid.
   *
   * To be valid any {@link IdentifiedType}s must have an underlying type.
   *
   * @return any validation problems found with the input and return types.
   */
  private List<Problem> validateFunctionAttributes() {
    List<Problem> problems = new ArrayList<>();
    if (! ID_PATTERN.matcher(id).matches()) {
      problems.add(Problem.error(
          "ID '%s' is not allowed. ID's may only contain letters, digits, ':', '-' and '_' characters", id));
    }
    validateTypeExists(getReturnType(),
        "Return type '%s' does not exist. Refer to 'riskscape type list' for available types", p -> problems.add(p));
    for (Type argumentType: getArgumentTypes()) {
      validateTypeExists(argumentType,
          "Input type '%s' does not exist. Refer to 'riskscape type list' for available types", p -> problems.add(p));
    }
    return problems;
  }

  private void validateTypeExists(Type type, String messageFormat, Consumer<Problem> problems) {
    if (type instanceof IdentifiedType) {
      IdentifiedType identified = (IdentifiedType) type;
      try {
        identified.getUnderlyingType();
      } catch (MissingTypeException e) {
        problems.accept(Problem.error(messageFormat, identified.getId()));
      }
    }
  }

  /**
   * Returns the argument list from {@link Delegate#getArguments() } if this is not null else defers
   * to {@link IdentifiedFunction#getArguments()}.
   *
   * @return argument list
   */
  @Override
  public ArgumentList getArguments() {
    ArgumentList fromDelegate = delegate.getArguments();
    if (fromDelegate != null) {
      return fromDelegate;
    }
    return IdentifiedFunction.super.getArguments();
  }

  public JavaFunction withArgumentTypes(Type... newArgumentTypes) {
    return withArgumentTypes(Arrays.asList(newArgumentTypes));
  }

  public JavaFunction withArgumentTypes(List<Type> newArgumentTypes) {
    return new JavaFunction(id, sourceURI, description, newArgumentTypes, returnType, delegate, category);
  }

  public JavaFunction withReturnType(Type newReturnType) {
    return new JavaFunction(id, sourceURI, description, argumentTypes, newReturnType, delegate, category);
  }

  public JavaFunction calling(Delegate newDelegate) {
    return new JavaFunction(id, sourceURI, description, argumentTypes, returnType, newDelegate, category);
  }

  @Override
  public String toString() {
    return "JavaFunction:" + id;
  }

}
