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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.google.common.base.CaseFormat;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.bind.JavaParameterSet;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.bind.ParameterField;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructBuilder;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinSet;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ProblemException;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.FunctionCall;

/**
 * Utilities for handling binding a RiskScape struct expression to a java class for handling options-style
 * function arguments.
 *
 * To use functions should specify an options class which has options annotated with
 * {@link ParameterField}. The options should be one of {@link String}, {@link Number} or
 * {@link Enum} types and may be wrapped in an {@link Optional}.
 */
public class FunctionCallOptions {

  public static final String OPTIONS_ARG_NAME = "options";
  public static final CaseFormat OPTONS_NAME_CASE = CaseFormat.LOWER_UNDERSCORE;

  /**
   * Convenience that will return an options {@link FunctionArgument} with a type that is built for the
   * given optionsClass.
   *
   * Only use this method when the options argument should be named the default {@link #OPTIONS_ARG_NAME}.
   */
  public static <T> FunctionArgument options(Class<T> optionsClass) {
    return options(OPTIONS_ARG_NAME, optionsClass);
  }

  /**
   * Same as {@link #options(java.lang.Class) } but allows the options argument to be named optionsArgName.
   */
  public static <T> FunctionArgument options(String optionsArgName, Class<T> optionsClass) {
    return new FunctionArgument(optionsArgName, optionsToStruct(optionsClass));
  }

  /**
   * Build a {@link Struct} for the given optionsClass.
   *
   * optionsClass should have each option annotated with {@link ParameterField}.
   * The options should be one of {@link String}, {@link Number} or {@link Enum} types and may be wrapped
   * in an {@link Optional}.
   */
  public static <T> Struct optionsToStruct(Class<T> optionsClass) {
    StructBuilder builder = Struct.builder();

    JavaParameterSet<T> parameterSet = JavaParameterSet.fromBindingClass(optionsClass, OPTONS_NAME_CASE);

    for (Parameter param: parameterSet.getDeclared()) {
      Class<?> parameterType = param.getType();

      Type type;
      if (parameterType.isEnum()) {
        type = WithinSet.fromEnum(parameterType);
      } else {
        if (parameterType == Object.class) {
          // special case for when the options really do say anything goes. when this is used it's likely
          // that #bindTuple will need to be used to get the actual types for the options.
          type = Types.ANYTHING;
        } else {
          // let's get the riskscape type. this will work fine for simple types which is all we should be using
          // in options. Otherwise the get() will blow up and any testing will find this problem.
          type = Types.fromJavaTypeOptional(parameterType).get();
        }
      }
      if (param.isMany()) {
        // if the param can accept many values we need to wrap it's type in a list
        type = RSList.create(type);
      }

      builder.add(param.getName(), type);
    }

    return builder.build();
  }

  /**
   * Binds a constant options argument from a function call expression to a Tuple if possible.
   *
   * Useful when you need to know the actual types of the option values. Otherwise
   * {@link #bindOptions(Class, RealizationContext, ArgumentList, FunctionCall) } should be used instead.
   *
   * Only use this method when the options argument is using the default name of {@link #OPTIONS_ARG_NAME}.
   */
  public static Optional<ResultOrProblems<Tuple>> bindTuple(RealizationContext context,
      ArgumentList argList, FunctionCall functionCall) {
    return bindTuple(OPTIONS_ARG_NAME, context, argList, functionCall);
  }

  /**
   * Same as {@link #bindTupleRealizationContext, ArgumentList, FunctionCall) } but used when the name of
   * the options argument is not {@link #OPTIONS_ARG_NAME}
   */
  public static Optional<ResultOrProblems<Tuple>> bindTuple(String optionsArgName, RealizationContext context,
      ArgumentList argList, FunctionCall functionCall) {
    if (!argList.hasArgument(optionsArgName)) {
      // you shouldn't bind options if the argList doesn't have options
      throw new RuntimeException("cannot bindOptions when argList has no options");
    }

    return argList.getArgument(functionCall, optionsArgName)
        .map(arg -> arg.evaluateConstant(context, Tuple.class, argList.get(optionsArgName).getType()));
  }

  /**
   * Binds a constant options argument from a function call expression to a java class annotated with ParameterField,
   * handling binding exceptions as realization problems.
   *
   * Only use this method when the options argument is using the default name of {@link #OPTIONS_ARG_NAME}.
   */
  public static <T> ResultOrProblems<T> bindOptions(Class<T> optionsClass, RealizationContext context,
      ArgumentList argList, FunctionCall functionCall) {
    return bindOptions(OPTIONS_ARG_NAME, optionsClass, context, argList, functionCall);
  }

  /**
   * Same as {@link #bindOptions(Class, RealizationContext, ArgumentList, FunctionCall) ) } but used when the name of
   * the options argument is not {@link #OPTIONS_ARG_NAME}
   */
  public static <T> ResultOrProblems<T> bindOptions(String optionsArgName, Class<T> optionsClass,
      RealizationContext context, ArgumentList argList, FunctionCall functionCall) {
    JavaParameterSet<T> parameterSet = JavaParameterSet.fromBindingClass(optionsClass, OPTONS_NAME_CASE);
    Map<String, List<?>> values = new HashMap<>();

    return bindTuple(optionsArgName, context, argList, functionCall)
        .map(t ->
          t.flatMap(tuple -> {
            // now we map the constant options into the values map for binding
            tuple.toMap().entrySet()
                .forEach(entry -> {
                  if (entry.getValue() instanceof List<?> list) {
                    values.put(entry.getKey(), list);
                  } else {
                    values.put(entry.getKey(), List.of(entry.getValue()));
                  }
                });

            // now bind the values to the options class
            return parameterSet.bindToObject(context.getBindingContext(), values)
                .flatMap(bound -> {
                  if (!bound.getExtraneous().isEmpty()) {
                    // XXX go boom
                    List<Problem> problems = new ArrayList<>();
                    List<String> possible = optionsToStruct(optionsClass).getMemberKeys();
                    // XXX use the soundex/did you mean code here
                    bound.getExtraneous().keySet().stream()
                        .forEach(key -> problems.add(
                        GeneralProblems.get().notAnOption(key, "options", possible)
                    ));
                    return ResultOrProblems.failed(problems);
                  }
                  return ResultOrProblems.of(bound.getBoundToObject());
                });
          })
        )
        .orElseGet(() -> {
          // the function call does not have an options argument so we bind our empty values to the options.
          // as there could be a bunch of default values to get.
          return parameterSet.bindToObject(context.getBindingContext(), values)
              .map(bound -> bound.getBoundToObject());
        });

  }

  /**
   * Convenience version of {@link #bindOptions(Class, RealizationContext, ArgumentList, FunctionCall) }
   * that will throw a {@link ProblemException} should any problems exist.
   *
   * Only use this method when the options argument is using the default name of {@link #OPTIONS_ARG_NAME}.
   */
  public static <T> T bindOptionsOrThrow(Class<T> optionsClass, RealizationContext context,
      ArgumentList argList, FunctionCall functionCall) throws ProblemException {
    return bindOptions(optionsClass, context, argList, functionCall)
        .getOrThrow(problems -> Problems.foundWith(OPTIONS_ARG_NAME, problems));
  }
}
