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

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ListDeclaration;

/**
 * Describes the type of value we expect for this parameter, i.e. what the
 * parameter should bind against (Java type, not RiskScape type). This property
 * can be used to bind a generic 'expression' parameter against a more specific
 * type, e.g. it must be a number.
 *
 * The idea is we want a simple, flexible set of types here. RiskScape types can be a
 * little too prescriptive (e.g. there's integer or there's floating - there is no 'number'),
 * and can confuse even expert users (e.g. `relation(struct(foo: floating,...))` vs 'bookmark').
 * However, we don't want to make the types too Java-ry for the user either, e.g. we don't
 * want the user typing `properties = UsgsShakemapBookmarkResolver`.
 *
 * To try to do the 'right thing', RiskScape may infer (guess) properties for a parameter,
 * e.g. "this thing looks like a number". Guessing a prescriptive type (e.g. integer) would be
 * annoying, as the user might also want to enter floating values.
 *
 * Ultimately, these typed parameters need to be converted back into a RiskScape expression,
 * so that the value can be injected into pipeline DSL.
 */
@RequiredArgsConstructor
public enum TypedProperty implements ParameterProperty {

  /**
   * We don't support binding to just 'Number', but Double should work for
   * anything that accepts either a float or integer RiskScape type
   */
  NUMERIC(Double.class),

  /**
   * Used to explicitly state the parameter should only accept whole numbers, e.g. 'year_built'.
   */
  INTEGER(Long.class, NUMERIC),

  /**
   * Used to explicitly state the parameter should be a text string, e.g. 'foo'.
   * In general, you'd be better off either using {@link #BOOKMARK} or specifying
   * the possible values as choices, rather than accepting any old text string,
   * i.e. `param.example.choices = 'foo', 'bar', 'baz'`. One case for using text
   * might be if you wanted to parameterize a column name or output filename
   */
  TEXT(String.class),

  /**
   * We don't really support binding against a Java {@link List}, so just bind lists as expressions.
   * Using {@link InputFieldProperty#MULTISELECT} or {@link InputFieldProperty#CHECKBOX}
   * instead should cover most cases in the UI where the user needs to enter a list of values,
   * but this is here for completeness.
   */
  LIST(ListDeclaration.class),

  /**
   * The parameter should accept a bookmark. Note that you can't always use a
   * bookmark in places that expect a filepath - in those cases use {@link TypedProperty#FILE}
   */
  BOOKMARK(String.class),

  /**
   * The parameter should accept a path to a file. Use this when a bookmark
   * won't work.
   */
  FILE(String.class, TEXT),

  /**
   * A user can explicitly state that a parameter takes an expression to avoid
   * RiskScape inferring the wrong type. E.g. even though the default value is a
   * numeric constant, the user still wants to be able to specify `exposure.road_width`.
   */
  EXPRESSION(Expression.class);

  /**
   * Indicates cases where a TypedProperty needs to take precedence over other,
   * e.g. we want to bind to INTEGER, even if NUMERIC is also present
   */
  private static final List<TypedProperty> PRECEDENCE = Arrays.asList(
      EXPRESSION,
      LIST,
      INTEGER
  );

  /**
   * @return the best TypedProperty to use (i.e. for binding), if any
   */
  public static Optional<TypedProperty> findBest(Collection<ParameterProperty> properties) {
    List<TypedProperty> filtered = ParameterProperty.filter(properties, TypedProperty.class);

    // check for precedence order first, if it's possible there's multiple TypedProperties
    // e.g. integer takes precedence over numeric
    for (TypedProperty prop : PRECEDENCE) {
      if (filtered.contains(prop)) {
        return Optional.of(prop);
      }
    }
    // otherwise return the first matching TypedProperty
    return filtered.stream().findFirst();
  }

  @Getter
  private final Class<?> parameterType;
  @Getter
  private final List<ParameterProperty> implied;

  TypedProperty(Class<?> parameterType, ParameterProperty... implied) {
    this(parameterType, Arrays.asList(implied));
  }

  public boolean isCompatible(ParameterProperty that) {
    boolean isImplied = this.implies(that) || that.implies(this);
    // list can be combined with anything and it's technically valid
    boolean isAList = this.equals(LIST) || that.equals(LIST);
    boolean related = isImplied || isAList || this.equals(that);
    // we only allow a single TypedProperty, unless the properties are related/implied,
    // e.g. integer implies numeric, so we allow both properties
    return related || !(that instanceof TypedProperty);
  }

  @Override
  public String getKeyword() {
    return name().toLowerCase();
  }
}

