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

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.UnknownFunctionException;
import nz.org.riskscape.engine.expr.RiskscapeFunctionExpression;
import nz.org.riskscape.engine.expr.ConstantExpression;
import org.geotools.filter.FilterFactoryImpl;
import org.geotools.api.feature.type.Name;
import org.geotools.api.filter.expression.PropertyName;

import nz.org.riskscape.engine.expr.StructAccessExpression;
import static nz.org.riskscape.engine.filter.FilterFactory.RiskscapeFunctions.CUMULATIVE_LOG_NORMAL_DISTRIBUTION;
import static nz.org.riskscape.engine.filter.FilterFactory.RiskscapeFunctions.CUMULATIVE_NORMAL_DISTRIBUTION;
import static nz.org.riskscape.engine.filter.FilterFactory.RiskscapeFunctions.POLYNOMIAL;
import static nz.org.riskscape.engine.filter.FilterFactory.RiskscapeFunctions.POWER;
import static nz.org.riskscape.engine.filter.FilterFactory.RiskscapeFunctions.RSFUNC;
import nz.org.riskscape.engine.function.Maths;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import org.geotools.filter.visitor.IsStaticExpressionVisitor;
import org.geotools.api.filter.expression.Expression;
import org.geotools.api.filter.expression.Function;
import org.geotools.api.filter.expression.Literal;

// TODO consider wrapping FilterFactoryImpl instead of extending it, so that we only support things we test and know
// work
public class FilterFactory extends FilterFactoryImpl {

  public enum RiskscapeFunctions {
    POWER(2, 2),
    POLYNOMIAL(2, Integer.MAX_VALUE),
    CUMULATIVE_NORMAL_DISTRIBUTION(3, 3),
    CUMULATIVE_LOG_NORMAL_DISTRIBUTION(3, 3),
    RSFUNC(2, Integer.MAX_VALUE);

    final int minArgs;
    final int maxArgs;

    RiskscapeFunctions(int minArgs, int maxArgs) {
      this.minArgs = minArgs;
      this.maxArgs = maxArgs;
    }

    public String getFunctionName() {
      return name().toLowerCase();
    }

    public static RiskscapeFunctions get(String name) {
      try {
      return RiskscapeFunctions.valueOf(name.toUpperCase());
      } catch (IllegalArgumentException e) {
        //Not recognised.
        return null;
      }
    }
  }

  public static final String ATTRIBUTE_SEPARATORS = "[/\\.]";
  private final java.util.function.Function<String, Optional<IdentifiedFunction>> functionFinder;

  public FilterFactory() {
    this.functionFinder = null;
  }

  /**
   * @param riskscapeFunctionFinder function that will return an Optional of {@link IdentifiedFunction}
   * with matching id.
   */
  public FilterFactory(java.util.function.Function<String, Optional<IdentifiedFunction>> riskscapeFunctionFinder) {
    this.functionFinder = riskscapeFunctionFinder;
  }

  @Override
  public PropertyName property(Name name) {
    return property(name.getLocalPart());
  }

  public PropertyName path(String... parts) {
    return new StructAccessExpression(parts);
  }

  public PropertyName property(String name) {
    // XXX the ecql compiler seems to convert the dots to slashes - lets hope we don't get slashes in property names,
    // or we'll fall over ...
    String[] parts = name.split(ATTRIBUTE_SEPARATORS);
    return new StructAccessExpression(parts);
  }

  @Override
  public Literal literal(double d) {
    return new ConstantExpression(d);
  }

  @Override
  public Literal literal(float f) {
    return new ConstantExpression(f);
  }

  @Override
  public Literal literal(long l) {
    return new ConstantExpression(l);
  }

  @Override
  public Literal literal(int i) {
    return new ConstantExpression(i);
  }

  @Override
  public Literal literal(Object obj) {
    return new ConstantExpression(obj);
  }

  @Override
  public Function function(String name, Expression... args) {
    RiskscapeFunctions fn = RiskscapeFunctions.get(name);
    if (fn != null) {
      List<Expression> argList = Arrays.asList(args);
      if (fn.minArgs == fn.maxArgs && argList.size() != fn.minArgs) {
        throw new RiskscapeException(String.format("%s requires %d arguments but only %d were supplied",
            fn.getFunctionName(), fn.maxArgs, argList.size()));
      } else if (argList.size() < fn.minArgs) {
        throw new RiskscapeException(String.format("%s requires a minumun of %d arguments but %d were supplied",
            fn.getFunctionName(), fn.maxArgs, argList.size()));
      } else if (argList.size() > fn.maxArgs) {
        throw new RiskscapeException(String.format("%s require a maximum of %d arguments but %d were supplied",
            fn.getFunctionName(), fn.maxArgs, argList.size()));
      }
      switch (fn) {
        case RSFUNC:
          return getRiskscapeFunction(argList);
        case POWER:
          return getPowerFunction(argList);
        case POLYNOMIAL:
          return getPolynomialFunction(argList);
        case CUMULATIVE_NORMAL_DISTRIBUTION:
          return getCumulativeNormalDistributionFunction(argList);
        case CUMULATIVE_LOG_NORMAL_DISTRIBUTION:
          return getCumulativeLogNormalDistributionFunction(argList);
        default:
      }
    }
    return super.function(name, args);
  }

  RiskscapeFunctionExpression getRiskscapeFunction(List<Expression> args) {
    Expression functionNameExpression = args.get(0);
    if (! isStatic(functionNameExpression)) {
      throw new RiskscapeException(RSFUNC.getFunctionName() + " requires the first argument to be static");
    }
    String functionName = functionNameExpression.evaluate(null, String.class);
    List<Expression> functionParameters = args.subList(1, args.size());
    return new RiskscapeFunctionExpression(functionName, () -> {
      return functionFinder.apply(functionName)
          .orElseThrow(() -> new UnknownFunctionException(functionName));
    }, functionParameters);
  }

  private RiskscapeFunctionExpression getPowerFunction(List<Expression> args) {
    return new RiskscapeFunctionExpression(POWER.getFunctionName(), () -> {
      return Maths.newPower();
    }, args);
  }

  private RiskscapeFunctionExpression getPolynomialFunction(List<Expression> args) {
    return new RiskscapeFunctionExpression(POLYNOMIAL.getFunctionName(), () -> {
      return Maths.newPolynomial(args.size() - 1);
    }, args);
  }

  private RiskscapeFunctionExpression getCumulativeNormalDistributionFunction(List<Expression> args) {
    return new RiskscapeFunctionExpression(CUMULATIVE_NORMAL_DISTRIBUTION.getFunctionName(), () -> {
      return Maths.newCumulativeNormalDistribution();
    }, args);
  }

  private RiskscapeFunctionExpression getCumulativeLogNormalDistributionFunction(List<Expression> args) {
    return new RiskscapeFunctionExpression(CUMULATIVE_LOG_NORMAL_DISTRIBUTION.getFunctionName(), () -> {
      return Maths.newCumulativeLogNormalDistribution();
    }, args);
  }

  /**
   * Indicates if the expression is static. That is it will return the same result any time it is evaluated.
   * @param expression to test
   * @return true if the expression is static
   */
  public static boolean isStatic(Expression expression) {
    return (boolean)expression.accept(IsStaticExpressionVisitor.VISITOR, null);
  }

}
