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

import java.util.Collection;
import java.util.Collections;

import nz.org.riskscape.dsl.LexerException;
import nz.org.riskscape.dsl.ParseException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.rl.agg.RealizedAggregateExpression;
import nz.org.riskscape.engine.types.Nullable;
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.TypeInformation;
import nz.org.riskscape.problem.ProblemCode;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.PropertyAccess;

/**
 * Produces realized (and evaluation-ready) expressions from an AST and an input type as scope.  This interface will
 * eventually be used by pipelines when realizing their parameters against input data.
 */
public interface ExpressionRealizer {

  /**
   * @return a succinct and descriptive name for the given type for use with getImplicitName
   */
  static String getImplicitTypeName(RealizationContext context, Type type) {
    // shed any wrapping types
    type = Nullable.strip(type).getUnwrappedType();
    TypeInformation metaClass = context.getProject().getTypeSet().getTypeRegistry()
        .findTypeInformation(type).orElse(null);

    // special case - because of the way lambda types are ... massaged ... for realization, we see a scoped lambda type
    // for a lambda, rather than a LambdaType (which has the correct type constructor).  So, to avoid boiling the ocean
    // (at least for now) just put a special case in to return something nice and succinct for an anonymous lambda
    // member (even though it's super unlikely anyone is going to ever have an anonymous lambda member)
    if (type instanceof ScopedLambdaType) {
      return "lambda";
    }

    String typeName;
    if (metaClass == null) {
      // XXX should we log a warning here?  or maybe just info?  Nothing a user can do about it, but us devs will want
      // to know
      typeName = type.getClass().getSimpleName();
    } else {
      typeName = metaClass.getId();
    }

    // for lists, we append the type name, e.g a list of integers is list_integer
    return type.find(RSList.class)
        .map(listType -> typeName + "_" + getImplicitTypeName(context, listType.getMemberType()))
        .orElse(typeName);

  }

  /**
   * Return a name we can use to represent the given realized expression in a struct where no member name has been
   * given.
   *
   * ## Examples:
   *
   *  {
   *    # literals
   *    1, # as integer,
   *    2.0, # as floating,
   *    'three', # as text,
   *
   *    # member access
   *    foo, # as foo,
   *    foo.bar, # as bar
   *
   *    # function calls
   *    str(foo),  # as str_foo
   *    if_then_else(a, b, c), # as if_then_else
   *    lookup(foo, bookmark('bar')).event_id # as event_id
   *  }
   *  ```
   *
   * @param used the names that have already been assigned.  If the implicit name would happen to collide with one of
   * these then a counter style suffix is appended to the name to make it distinct, e.g. `{foo, foo, foo}` is equivalent
   * to `{foo: foo, foo_2: foo, foo_3: foo}`
   *
   */
  static String getImplicitName(RealizationContext context, RealizedExpression realized, Collection<String> used) {
    Expression expr = realized.getExpression();
    Type type = realized.getResultType();
    final String converted;

    if (expr instanceof PropertyAccess) {
      // foo.bar.baz becomes baz
      converted = ((PropertyAccess) expr).getLastIdentifier().getValue();
    } else if (expr instanceof FunctionCall) {
      FunctionCall fc = (FunctionCall) expr;
      String prefix = fc.getIdentifier().getValue();
      if (fc.getArguments().size() == 1 && realized.getDependencies().size() == 1) {
        // unary functions have their first arg's implicit name appended.  NB we check both the AST's arg count and
        // the actual realized dependency count here to be defensive in case of bugs like GL575  - tbh we could probably
        // go further and look through the dependencies for one with an expression that matches the function call's
        // first argument - there aren't really any guarantees in the API about what the dependencies relate too - but
        // that might be overkill
        converted = prefix + "_" + getImplicitName(context, realized.getDependencies().get(0), Collections.emptyList());
      } else {
        // otherwise just use the function's name
        converted = prefix;
      }
    } else {
      // for everything else, use the implicit type's name
     converted = getImplicitTypeName(context, type);
    }

    return makeUnique(converted, used);
  }

  /**
   * @return a new identifier based on the given one that is unique among `used` - will append a `_2`, `_3` to the given
   * identifier until it finds one that is not in used.
   */
  static String makeUnique(String identifier, Collection<String> used) {
    int counter = 2;
    String suffixed = identifier;
    while (used.contains(suffixed)) {
      suffixed = identifier + "_" + counter++;
    }

    return suffixed;
  }

  /**
   * Problem code for when a member is missing from the scope of an expression - used by realizeConstant
   */
  ProblemCode NO_SUCH_MEMBER =
      ExpressionProblems.get().noSuchStructMember("", Collections.emptyList()).getCode();


  /**
   * Standard set of problem codes for describing problems during expression realization
   */
  enum ProblemCodes implements ProblemCode {
    /**
     * Keyword supplied for an argument that doesn't exist with this keyword
     */
    KEYWORD_DOES_NOT_EXIST,
    /**
     * Non-keyword arg supplied after keyword args, e.g. (foo, bar=1, 'baz')
     */
    KEYWORD_REQUIRED,
    /**
     * Happens when an argument is supplied multiple times, i.e. positionally and
     * with a keyword
     */
    KEYWORD_OUT_OF_ORDER,

    /**
     * Positional/Keyword arguments are missing, e.g. argument #3 was specified with
     * a keyword but args 1 and 2 were omitted
     */
    MISSING_KEYWORD_ARGUMENTS,
  }


  /**
   * Realize the given expression against an empty struct, returning a meaningful problem if it
   * looks like it failed to realize if it wasn't constant.
   * @return a {@link RealizedExpression} that is constant, i.e should succeed when called like
   * `expr.evaluate(Tuple.EMPTY_TUPLE)` or `expr.evaluateConstant()` and should always return the same result.
   */
  ResultOrProblems<RealizedExpression> realizeConstant(Expression expression);

  ResultOrProblems<RealizedExpression> realize(Type inputType, Expression expression);
  ResultOrProblems<RealizedExpression> realize(Type inputType, String unparsedExpression);

  /**
   * @return a {@link RealizedAggregateExpression} that can aggregate rows of the given input type.
   */
  ResultOrProblems<RealizedAggregateExpression> realizeAggregate(Type inputType, Expression expression);

  /**
   * Convenience method for parsing an rl expression in to an {@link Expression} ast.
   * @param unparsedExpression the string of rl to parse
   * @return the parsed {@link Expression}
   * @throws ParseException, {@link LexerException} if the string couldn't be parsed
   */
  Expression parse(String unparsedExpression);

  /**
   * Convert a realized expression to one that always returns a {@link Tuple} and has return type that
   * is a {@link Struct}.  Note that this is a simpler routine than type coercion, and is offered to simplify user input
   * cases where a pipeline wants a struct, but the user doesn't need to know or care about this, e.g. `select(foo)` is
   * simpler than `select({foo})` but they do exactly the same thing.
   */
  RealizedExpression asStruct(RealizationContext context, RealizedExpression expression);

}
