/*
 * 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.util.List;
import java.util.Optional;
import java.util.function.Function;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.rl.RealizableFunction;
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.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.FunctionCall.Argument;

@RequiredArgsConstructor @Data
public class FunctionArgument {

  private final String keyword;
  private final Type type;
  private final Optional<Object> defaultValue;
  private int index = -1;

  public FunctionArgument(int index, Type type) {
    this(null, type, Optional.empty());
    this.index = index;
  }

  public FunctionArgument(String keyword, Type type) {
    this(keyword, type, Optional.empty());
  }

  public void setIndex(int newIndex) {
    /*
     * For simplicities sake, we want to be able to set the index of an argument 'after the fact', but as we often
     * mess about with function arguments, let's make sure we're not mistakenly re-setting the index of an argument,
     * as this might cause chaos later
     */
    if (this.index != -1 && newIndex != this.index) {
      throw new IllegalStateException("index field is effectively immutable - can not be set twice");
    }

    this.index = newIndex;
  }

  public String getKeyword() {
    if (keyword == null) {
      return "arg" + index;
    } else {
      return keyword;
    }
  }

  public boolean hasKeyword() {
    return keyword != null;
  }

  /**
   * Java-bean friendly and negative version of hasKeyword
   */
  public boolean isAnonymous() {
    return keyword == null;
  }

  @Override
  public String toString() {
    if (hasKeyword()) {
      return String.format("%s: %s", getKeyword(), type);
    } else {
      // no user-friendly keyword - just output its type.
      return type.toString();
    }
  }

  /**
   * @return the actual FunctionCall {@link Argument} supplied by the user (if any)
   */
  public Optional<Argument> getFunctionCallArgument(FunctionCall fc) {
    // see if the argument was given in the expression with a keyword
    int givenIndex = fc.indexOfArgNamed(getKeyword());
    if (givenIndex >= 0) {
      // nice and easy - they user specified the argument with a keyword, rather than positionally
      return Optional.of(fc.getArguments().get(givenIndex));
    } else {
      List<Argument> positionalArguments = fc.getPositionalArguments();
      // ok, it wasn't specified with a keyword, see what's at the declared position
      if (index < positionalArguments.size()) {
        return Optional.of(positionalArguments.get(index));
      } else {
        // arity exception?
        return Optional.empty();
      }
    }
  }

  /**
   * Helper method for a {@link RealizableFunction} to extract a constant from the arguments with various error
   * handling along the way.  This method will fail if the expression is not a constant expression (e.g. it depends on
   * values extracted from scope).
   * @param <T> the java return type
   * @param context context in which realization is happening - typically the argument that was passed to
   * {@link RealizableFunction#realize(RealizationContext, FunctionCall, List)}
   * @param functionCall the functionCall expression that contains an argument to be realized constantly
   * @param requiredJavaType the desired java type of the constant - must map to a built-in riskscape type via
   * {@link Types#fromJavaType(Class)}
   * @return the constant value from the function call expression, or a failed result
   */
  public <T> ResultOrProblems<T> evaluateConstant(
      RealizationContext context,
      FunctionCall functionCall,
      Class<T> requiredJavaType
  ) {
    return mapFunctionCall(functionCall, arg -> arg.evaluateConstant(context, requiredJavaType, type));
  }

  /**
   * Convenience method to apply a mapping to actual value that was supplied (i.e.
   * the FunctionCall {@link Argument}), with appropriate boilerplate error
   * handling if the mapping failed. E.g. use like this:
   *
   * result = argumentList.get("my-arg").mapFunctionCall(fc, arg -> {
   *    // do stuff to the supplied FunctionCall.Argument...
   * }).getOrThrow();
   */
  public <T> ResultOrProblems<T> mapFunctionCall(FunctionCall fc, Function<Argument, ResultOrProblems<T>> mapper) {
    return getFunctionCallArgument(fc).map(arg -> mapper.apply(arg)
          .composeProblems(Problems.foundWith(this))
       // TODO could use GeneralProblems.get().required(this) here
      ).orElse(ResultOrProblems.failed(ArgsProblems.get().required(getKeyword())));
  }
}
