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

import static nz.org.riskscape.engine.function.RiskscapeFunction.*;

import java.util.List;
import java.util.Optional;

import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.locationtech.jts.geom.Geometry;

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.BaseRealizableFunction;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.function.UntypedFunction;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;
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;

/**
 * A function to re-project geometry to a {@link CoordinateReferenceSystem} of the users choice.
 */
public class Reproject extends BaseRealizableFunction {

  public Reproject() {
    super(ArgumentList.create("geom", Types.GEOMETRY, "crs", Types.TEXT), Types.GEOMETRY);
  }

  @Override
  public ResultOrProblems<RiskscapeFunction> realize(RealizationContext context, FunctionCall functionCall,
      List<Type> argumentTypes) {
    return ProblemException.catching(() -> {
      getArguments().validateArity(argumentTypes);

      Type geometryArgType = argumentTypes.get(0);
      if (!context.getProject().getTypeSet().isAssignable(geometryArgType, Types.GEOMETRY)) {
        throw new ProblemException(ArgsProblems.mismatch(arguments.get(0), geometryArgType));
      }
      // we extract the underlying type if it is wrapped in a referenced
      geometryArgType = geometryArgType.findAllowNull(Referenced.class)
          .map(Referenced::getUnderlyingType)
          .orElse(geometryArgType);

      Type reprojectedType = null;
      UntypedFunction reprojectionFunction = null;

      SRIDSet sridSet = context.getProject().getSridSet();

      Type crsArgType = argumentTypes.get(1);

      // We could get the CRS from text or a referenced type
      Optional<Referenced> referencedCrsType = crsArgType.findAllowNull(Referenced.class);

      if (referencedCrsType.isPresent()) {
        // we use the CRS from the referenced type
        CoordinateReferenceSystem targetCrs = referencedCrsType.get().getCrs();
        reprojectedType = Referenced.of(geometryArgType, targetCrs);
        int targetSRID = sridSet.get(targetCrs);
        reprojectionFunction = args -> sridSet.reproject((Geometry) args.get(0), targetSRID);

      } else if (context.getProject().getTypeSet().isAssignable(crsArgType, Types.GEOMETRY)) {
        // the crs arg type is an un-referenced geom. we need to get the target crs dynamically
        reprojectedType = geometryArgType;
        reprojectionFunction = args -> {
          Geometry geom = (Geometry) args.get(0);
          Geometry crs = (Geometry) args.get(1);

          return sridSet.reproject(geom, crs.getSRID());
        };

      } else if (context.getProject().getTypeSet().isAssignable(crsArgType, Types.TEXT)) {
        BindingContext bindingContext = context.getBindingContext();

        // the CRS is from TEXT, we should check if it is constant or not
        ResultOrProblems<String> crsString = arguments.getRequiredArgument(functionCall, "crs")
            .flatMap(a -> a.evaluateConstant(context, String.class, Types.TEXT));

        if (crsString.isPresent()) {
          CoordinateReferenceSystem targetCrs = bindingContext.bind(crsString.get(), CoordinateReferenceSystem.class)
              .getOrThrow(Problems.foundWith(Parameter.class, "crs"));

          reprojectedType = Referenced.of(geometryArgType, targetCrs);
          int targetSRID = sridSet.get(targetCrs);
          reprojectionFunction = args -> sridSet.reproject((Geometry) args.get(0), targetSRID);
        } else {
          // we can't add the referenced wrapping if we don't know the target crs yet
          reprojectedType = geometryArgType;
          reprojectionFunction = args -> {
            Geometry geom = (Geometry)args.get(0);
            String crs = (String)args.get(1);
            CoordinateReferenceSystem targetCrs = bindingContext.bind(crs, CoordinateReferenceSystem.class)
              .orElseThrow(problems -> new RiskscapeException(Problems.foundWith(Parameter.class, "crs", problems)));

            int targetSRID = sridSet.get(targetCrs);
            return sridSet.reproject(geom, targetSRID);
          };
        }
      } else {
        throw new ProblemException(ArgsProblems.mismatch(arguments.get(1), argumentTypes.get(1)));
      }
      return RiskscapeFunction.create(BUILT_IN, argumentTypes, reprojectedType, reprojectionFunction);
    });
  }

}
