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

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Lists;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.bind.BoundParameters;
import nz.org.riskscape.engine.bind.JavaParameterSet;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionFramework;
import nz.org.riskscape.engine.function.FunctionFrameworkSupport;
import nz.org.riskscape.engine.function.FunctionMetadata;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.MetadataParams;
import nz.org.riskscape.engine.problem.ProblemFactory;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeRegistry;
import nz.org.riskscape.engine.types.TypeVisitor;
import nz.org.riskscape.engine.types.WrappingType;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * Framework for running a python function natively in CPython
 * (as opposed to JythonFunctionFramework in nz.org.riskscape.jython) which executes the user's
 * python code in Jython).
 */
@RequiredArgsConstructor
public class CPythonFunctionFramework implements FunctionFramework, FunctionFrameworkSupport {

  interface LocalProblems extends ProblemFactory {
    // CPython functions need at least one argument
    Problem zeroArgsNotSupported();

    // we don't support serializing the given type to CPython
    Problem typeNotSupported(String typeName, Object context, List<String> supportedTypes);

    // A cpython function is wanted but the plugin didn't set up the cpython function framework
    Problem notEnabled();
  }

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

  /**
   * @return a special cpython function framework which won't claim .py files and will always return an error on build
   */
  public static CPythonFunctionFramework notEnabled() {
    return new CPythonFunctionFramework(null) {
      @Override
      public boolean canBuild(Project project, Map<String, List<?>> unbound) {
        return false;
      }

      @Override
      public ResultOrProblems<IdentifiedFunction> build(String functionName, Project project, BoundParameters bound) {
        return ResultOrProblems.failed(PROBLEMS.notEnabled());
      }
    };
  }

  private final CPythonSpawner spawner;

  @Getter
  private final String id = "cpython";

  @Getter
  private final JavaParameterSet<MetadataParams> parameterSet =
    JavaParameterSet.fromBindingClass(MetadataParams.class);

  @Override
  public ResultOrProblems<IdentifiedFunction> build(String functionName, Project project, BoundParameters bound) {
    // build the function with the given metadata
    MetadataParams params = parameterSet.bindToObject(bound).getBoundToObject();

    return loadFunctionFromParams(project.getEngine(), params)
      .flatMap(resource -> build(project, params.toFunctionMetadata(functionName), resource))
      .composeProblems(Problems.foundWith(IdentifiedFunction.class, functionName));

  }

  // package-scoped for testing
  ResultOrProblems<IdentifiedFunction> build(Project project, FunctionMetadata metadata, Resource pythonScript) {
    List<Problem> problems = checkMetadataValid(project.getTypeSet().getTypeRegistry(), metadata);

    if (!problems.isEmpty()) {
      return ResultOrProblems.failed(problems);
    }

    return ResultOrProblems.of(new CPythonRealizableFunction(metadata, pythonScript, spawner, project).identified());
  }

  @Override
  public boolean canBuild(Project project, Map<String, List<?>> unbound) {
    return MetadataParams.locationEndsWith(unbound, ".py");
  }

  /**
   * Checks the argument-types and return-type are valid before building the user's function.
   */
  private List<Problem> checkMetadataValid(TypeRegistry registry, FunctionMetadata function) {
    List<Problem> problems = new ArrayList<>();

    if (function.getArguments().size() == 0) {
      // a function with no args will actually work OK, but the python code will just sit
      // there adding return-values to the stdout pipe as fast as it can, which is not
      // ideal. It's also unlikely that users would really ever *need* a no-args function
      problems.add(PROBLEMS.zeroArgsNotSupported());
    }

    ArgumentList args = function.getArguments();
    for (int i = 0; i < args.size(); i++) {
      Type argsType = args.getArgumentTypes().get(i);
      for (String unsupported : findUnsupportedTypes(registry, argsType, CPythonFunction.SUPPORTED_ARGUMENT_TYPES)) {
        problems.add(PROBLEMS.typeNotSupported(unsupported, args.get(i), getSupportedArgumentTypeNames(registry)));
      }
    }

    for (String unsupported
        : findUnsupportedTypes(registry, function.getReturnType(), CPythonFunction.SUPPORTED_RETURN_TYPES)) {
      problems.add(PROBLEMS.typeNotSupported(unsupported, "'return-type'", getSupportedReturnTypeNames(registry)));
    }

    return problems;
  }

  /**
   * Returns a list of any types we don't support with CPython.
   */
  public static List<String> findUnsupportedTypes(TypeRegistry registry, Type type,
      List<Class<? extends Type>> allowedTypes) {
    return TypeVisitor.bfs(type, new LinkedList<String>(), (list, childType) -> {
      // for the purposes of serialization, we don't care about wrapping types, only value holding types
      // NB we use instanceof rather than Type.isA here - there's a kludge in linked types that ignores the linked type
      // for isA - I could shave a yak and figure out why on earth that was needed and see if it still is *or* I can
      // do this...
      if (!(childType instanceof WrappingType) && !allowedTypes.contains(childType.getClass())) {
        list.add(getTypeConstructorName(registry, childType.getClass()));
      }
    });
  }

  public static List<String> getSupportedArgumentTypeNames(TypeRegistry registry) {
    return Lists.transform(CPythonFunction.SUPPORTED_ARGUMENT_TYPES, t -> getTypeConstructorName(registry, t));
  }

  List<String> getSupportedReturnTypeNames(TypeRegistry registry) {
    return Lists.transform(CPythonFunction.SUPPORTED_RETURN_TYPES, t -> getTypeConstructorName(registry, t));
  }

  private static String getTypeConstructorName(TypeRegistry registry, Class<? extends Type> someType) {
    return registry.findTypeInformation(someType)
        .map(ti -> ti.getId())
        .orElse(someType.getSimpleName().toLowerCase());

  }

  @Override
  public int getPriority() {
    return 10;
  }

}
