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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.bind.ParameterField;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.BaseRealizableFunction;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.problem.ProblemFactory;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.rl.ScopedLambdaExpression;
import nz.org.riskscape.engine.sort.SortBy;
import nz.org.riskscape.engine.types.LambdaType;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.ScopedLambdaType;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.util.FunctionCallOptions;
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.ExpressionParser;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.FunctionCall.Argument;
import nz.org.riskscape.rl.ast.Lambda;

public class ListSort extends BaseRealizableFunction {

  public static class SortOptions {

    @ParameterField
    SortBy.Direction direction = SortBy.Direction.ASC;
  }

  public interface LocalProblems extends ProblemFactory {

    /**
     * Problem indicating that the given list cannot be sorted because the type of the list items
     * are not {@link Comparable}.
     *
     * @param listItemType the Riskscape type of the list items
     * @return problem
     */
    Problem listItemsNotComparable(Type listItemType, Problem... children);

    Problem listItemsNotComparableHint();
  }

  public static final LocalProblems PROBLEMS = Problems.get(LocalProblems.class);

  private static final Lambda DEFAULT_SORT_BY = ExpressionParser.INSTANCE.parse("(i) -> i")
      .isA(Lambda.class)
      .get();

  public static final ArgumentList ARGUMENTS = ArgumentList.fromArray(
      new FunctionArgument("items", RSList.LIST_ANYTHING),
      new FunctionArgument("by", new LambdaType("i"), Optional.of(DEFAULT_SORT_BY)),
      FunctionCallOptions.options(SortOptions.class)
  );

  public ListSort() {
    super(ARGUMENTS, Types.ANYTHING);
  }

  @Override
  @SuppressWarnings("unchecked")
  public ResultOrProblems<RiskscapeFunction> realize(RealizationContext context, FunctionCall functionCall,
      List<Type> givenTypes) {
    return ProblemException.catching(() -> {
      if (givenTypes.size() < 1 || givenTypes.size() > ARGUMENTS.size()) {
        throw new ProblemException(ArgsProblems.get().wrongNumberRange(1, 3, givenTypes.size()));
      }

      RSList listType = givenTypes.get(0)
          .find(RSList.class)
          .orElseThrow(() -> new ProblemException(ArgsProblems.mismatch(ARGUMENTS.get(0), givenTypes.get(0))));

      Optional<Struct> lambdaInputScope;
      Optional<RealizedExpression> lambdaExpression;

      Optional<Argument> lambdaArg =  ARGUMENTS.get("by").getFunctionCallArgument(functionCall);
      if (lambdaArg.isEmpty()) {
        lambdaInputScope = Optional.empty();
        lambdaExpression = Optional.empty();
      } else {
        ScopedLambdaType lambdaType = givenTypes.get(1).find(ScopedLambdaType.class).orElseThrow(() ->
        new ProblemException(TypeProblems.get().mismatch(
            lambdaArg,
            ScopedLambdaType.class,
            givenTypes.get(1).getClass())
        ));

        Lambda lambda = lambdaArg.get().getValue()
            .isA(Lambda.class)
            .orElseThrow();
        if (lambda.getArguments().size() != 1) {
          throw new ProblemException(Problems.foundWith(
              ARGUMENTS.get("by"),
              ExpressionProblems.get().lambdaArityError(lambda, lambda.getArguments().size(), 1)
          ));
        }

        lambdaInputScope = Optional.of(lambdaType.buildCallingScopeType(listType.getMemberType()));
        lambdaExpression = Optional.of(
            context.getExpressionRealizer()
                .realize(lambdaInputScope.get(), lambda.getExpression())
                .getOrThrow()
        );
      }

      // ensure that the thing to compare is a comparable type
      Type typeToCompare = lambdaExpression.map(RealizedExpression::getResultType)
          .orElse(listType.getMemberType());
      if (!Comparable.class.isAssignableFrom(typeToCompare.internalType())) {
          // List does not contain a comparable item
          throw new ProblemException(
              PROBLEMS.listItemsNotComparable(typeToCompare, PROBLEMS.listItemsNotComparableHint())
          );
        }

      SortOptions options = FunctionCallOptions.bindOptions(SortOptions.class, context, arguments, functionCall)
        .getOrThrow();

      return RiskscapeFunction.create(this, givenTypes, listType, (args) -> {
        Function<Object, Comparable> itemMapper = lambdaInputScope.map(inputScope -> {
          // apply input scope
          ScopedLambdaExpression scope = (ScopedLambdaExpression) args.get(1);
          Tuple lambdaInput = scope.buildCallingScope(inputScope);

          Function<Object, Comparable> lambdaMapper = (i) -> {
            lambdaInput.set(scope.getExpression().getArguments().get(0).getValue(), i);
            return (Comparable)lambdaExpression.get().evaluate(lambdaInput);
          };
          return lambdaMapper;
        }).orElseGet(() -> (i) -> (Comparable)i);


        Comparator<? extends Object> c = buildComparator(itemMapper, options);

        List list = new ArrayList((List) args.get(0));
        list.sort(c);
        return list;
      });
    });
  }


  @SuppressWarnings("unchecked")
  private Comparator buildComparator(Function<Object, Comparable> mapToComparable, SortOptions options) {
    Comparator comparator =
        options.direction == SortBy.Direction.ASC ? Comparator.naturalOrder() : Comparator.reverseOrder();
    // we don't want nulls to upset the comparison
    comparator = Comparator.nullsLast(comparator);

    comparator = Comparator.comparing(
        (i) ->  mapToComparable.apply(i),
        comparator
    );
    return comparator;
  }
}
