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


import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import nz.org.riskscape.problem.Problem;

/**
 * Parameters are the inputs and toggles that ultimately control how Riskscape works.  They are used by the data
 * and modelling constructs to allow users to customize and inspect how riskscape works.
 * <p>
 * Care has been taken to make this API usable from jvm languages other than Java, and so does not rely on language
 * features such as annotations for their definition (although those are available as an add on convenience for java
 * programmers)
 */
@Data
@EqualsAndHashCode(exclude = { "defaultFunction" })
public class Parameter {

  public static final String NO_DEFAULT = "No default supplied";

  public static final BiFunction<BindingContext, Object, List<Problem>> NO_VALIDATION =
      (c, v) -> Collections.emptyList();

  public static <T> Parameter required(String name, Class<T> type) {
    return range(name, type, 1, 1);
  }

  public static <T> Parameter required(String name, Class<T> type, T constantDefault) {
    return required(name, type).withNewDefaults((a, b) -> Collections.singletonList(constantDefault));
  }

  /**
   * Constructs a {@link Parameter} with a specific min and max number of parameters and no defaults
   */
  public static <T> Parameter range(String name, Class<T> type, int min, int max) {
    return new Parameter(name, type, Optional.empty(), min, max);
  }

  public static Parameter optional(String name, Class<?> type) {
    return range(name, type, 0, 1);
  }

  public static Parameter optional(String name, Class<?> type, int max) {
    return range(name, type, 0, max);
  }

  @NonNull @Getter
  private final String name;
  @NonNull
  private final Class<?> type;
  @NonNull
  private final Optional<BiFunction<BindingContext, Parameter, List<?>>> defaultFunction;

  private final int minRequired;

  private final int maxRequired;

  @NonNull
  private final BiFunction<BindingContext, Object, List<Problem>> validation;

  /**
   * @param name The name that identifies this parameter among others on the model
   * @param type The java type of this parameter.
   * @param function a function that can return a default value for this parameter
   */
  public Parameter(
      @NonNull String name,
      @NonNull Class<?> type,
      @NonNull Optional<BiFunction<BindingContext, Parameter, List<?>>> function,
      int min, int max) {

    this.name = name;
    this.type = type;
    this.defaultFunction = function;
    this.minRequired = min;
    this.maxRequired = max;
    this.validation = NO_VALIDATION;

    if (min < 0) {
      throw new IllegalArgumentException("minRequired can not be less than zero");
    }

    if (max < 1) {
      throw new IllegalArgumentException("maxRequired must be greater than zero");
    }

    if (min > max) {
      throw new IllegalArgumentException("minRequired can not be greater than maxRequired");
    }
  }

  // mutable class of question fields used for cloning
  public static final class MutableFields {

    public MutableFields(Parameter source) {
      this.name = source.name;
      this.type = source.type;
      this.defaultFunction = source.defaultFunction;
      this.minRequired = source.minRequired;
      this.maxRequired = source.maxRequired;
      this.validation = source.validation;
    }

    public String name;
    public Class<?> type;
    public Optional<BiFunction<BindingContext, Parameter, List<?>>> defaultFunction;
    public int minRequired;
    public int maxRequired;
    BiFunction<BindingContext, Object, List<Problem>> validation;
  }

  private Parameter(MutableFields fields) {
    this.name = fields.name;
    this.type = fields.type;
    this.defaultFunction = fields.defaultFunction;
    this.minRequired = fields.minRequired;
    this.maxRequired = fields.maxRequired;
    this.validation = fields.validation;
  }

  protected Parameter clone(Consumer<MutableFields> mutator) {
    MutableFields fields = new MutableFields(this);
    mutator.accept(fields);
    return new Parameter(fields);
  }

  public List<?> getDefaultValues(BindingContext modelContext) throws ParameterBindingException {
    if (defaultFunction.isPresent()) {
      return defaultFunction.get().apply(modelContext, this);
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * @return true if a value is not required for this parameter, e.g. the model should be able to run without it
   */
  public boolean isOptional() {
    return this.minRequired == 0;
  }

  /**
   * @return true if this parameter supports more than one value, e.g. a list or a set.
   */
  public boolean isMany() {
    return maxRequired > 1;
  }

  /**
   * @return true if this parameter has a default value set, e.g. getDefaultValues() should return at least one object
   */
  public boolean hasDefaultValue() {
    return defaultFunction.isPresent();
  }

  /**
   * Builds a new Parameter based on this one, but with new default value(s)
   * @param newDefaultFunction a function that yields new default values
   * @return a new parameter.
   */
  public Parameter withNewDefaults(BiFunction<BindingContext, Parameter, List<?>> newDefaultFunction) {
    return clone(fields -> fields.defaultFunction = Optional.of(newDefaultFunction));
  }

  public Parameter withNewName(String newName) {
    return clone(fields -> fields.name = newName);
  }

  public String getTypeName() {
    return type.getSimpleName();
  }

  public String getArity() {
    if (minRequired == maxRequired) {
      return Integer.toString(minRequired);
    } else {
      if (maxRequired == Integer.MAX_VALUE) {
        return String.format("%d+", minRequired);
      } else {
        return String.format("%d..%d", minRequired, maxRequired);
      }
    }
  }

  public boolean hasCorrectArity(int numValues) {
    return numValues >= minRequired && numValues <= maxRequired;
  }

  @Override
  public String toString() {
    return String.format("%s(type=%s)", name, type.getSimpleName());
  }

  /**
   * @return a clone of this parameter with the type set to `newType`
   */
  public Parameter withNewType(Class<?> newType) {
    return clone(fields -> fields.type = newType);
  }

  /**
   * @return a clone of this parameter with additional validation of the value supplied for the parameter
   */
  public Parameter withValidation(BiFunction<BindingContext, Object, List<Problem>> validationFunction) {
    return clone(fields -> fields.validation = validationFunction);
  }

  /**
   * Performs optional additional validation on a value bound to this parameter.
   * E.g. the parameter might expect not only an integer value, but a value between 1 and 100.
   *
   * @return a list of problems, if the bound value is invalid
   */
  public List<Problem> validate(BindingContext context, Object boundValue) {
    return validation.apply(context, boundValue);
  }
}
