/*
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import com.google.common.collect.Lists;
import com.google.common.collect.Range;

import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Value;
import nz.org.riskscape.engine.typeset.IdentifiedType;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.Constant;
import nz.org.riskscape.rl.ast.ListDeclaration;
import nz.org.riskscape.util.ListUtils;

/**
 * Helper class for dealing with {@link ParameterProperty}s.
 */
@RequiredArgsConstructor
public class ParameterProperties {

  public static final KeyValueProperty<Double> MIN_VALUE = KeyValueProperty.numeric("min", Double.MIN_VALUE)
      .withValidation((prop, bound) -> {
        if (((Number) bound).doubleValue() < prop.getValue()) {
          return Arrays.asList(GeneralProblems.get().badValue(bound, prop, ">= " + prop.getValue()));
          }
          return Collections.emptyList();
        });

  public static final KeyValueProperty<Double> MAX_VALUE = KeyValueProperty.numeric("max", Double.MAX_VALUE)
      .withValidation((prop, bound) -> {
        if (((Number) bound).doubleValue() > prop.getValue()) {
          return Arrays.asList(GeneralProblems.get().badValue(bound, prop, "<= " + prop.getValue()));
        }
        return Collections.emptyList();
      });

  /**
   * Used for the 'slider' widget where you can specify the increment you want the
   * slider to go up in, e.g. $10000 increments instead of $1 increments.
   */
  public static final KeyValueProperty<Double> STEP_INCREMENT = KeyValueProperty.numeric("step", 1D);

  /**
   * Specifies a base/template bookmark that should be incorporated into the
   * bookmark that the user supplies. E.g. if the parameter specifies HDF5 data,
   * then the template bookmark may specify the datasets that should be read
   */
  public static final KeyValueProperty<String> BOOKMARK_TEMPLATE = new KeyValueProperty<>("bookmark-template", "")
        .withImplied(TypedProperty.BOOKMARK)
        .withCompatibility(prop -> TypedProperty.BOOKMARK.isCompatible(prop)
          && checkCompatible(TypedProperty.BOOKMARK, prop.getImplied()).isEmpty())
        // the value binding looks up a bookmark to make sure it's valid, but then just returns the id -
        // PipelineModelParameter takes care of converting it in to a workable expression
        .withValueBinding((context, val) ->
          context.getProject().getBookmarks().getOr(val.toString()).map(Bookmark::getId));

  public static final KeyValueProperty<IdentifiedType> TYPE = new KeyValueProperty<>("type", IdentifiedType.class)
      .withImplied(TypedProperty.BOOKMARK)
      .withCompatibility(otherProp -> TypedProperty.BOOKMARK.isCompatible(otherProp)
          && checkCompatible(TypedProperty.BOOKMARK, otherProp.getImplied()).isEmpty());

  public static final List<KeyValueProperty<?>> KEY_VALUE_PROPERTIES = Arrays.asList(
      MIN_VALUE,
      MAX_VALUE,
      STEP_INCREMENT,
      BOOKMARK_TEMPLATE,
      TYPE
  );

  public static final List<ParameterProperty> ALL_PROPERTIES = ListUtils.concat(
      ListUtils.concat(
          Arrays.asList(TypedProperty.values()),
          Arrays.asList(InputFieldProperty.values())),
      KEY_VALUE_PROPERTIES
  );

  public static final List<String> AVAILABLE_KEYWORDS = Lists.transform(ALL_PROPERTIES, p -> p.getKeyword());

  /**
   * @return the ParameterProperty that matches the keyword given
   */
  public static Optional<ParameterProperty> find(String keyword) {
    return ALL_PROPERTIES.stream().filter(p -> p.getKeyword().equals(keyword)).findFirst();
  }

  /**
   * @return any compatibility problems found in the list of properties, if any
   */
  public static List<Problem> checkCompatible(ParameterProperty newProperty,
      Collection<ParameterProperty> existingProperties) {

    List<Problem> problems = new ArrayList<>();
    for (ParameterProperty existing : existingProperties) {
      if (existing.equals(newProperty)) {
        continue;
      } else if (!newProperty.isCompatible(existing) || !existing.isCompatible(newProperty)) {
        problems.add(ParamProblems.get().mutuallyExclusive(newProperty.getKeyword(), existing.getKeyword()));
      }
    }
    return problems;
  }

  /**
   * Returns a property based on the given keyword and optional value
   */
  public static ResultOrProblems<ParameterProperty> build(BindingContext context, String keyword,
      Optional<?> rawValue) {
    ParameterProperty property = find(keyword).orElse(null);
    if (property == null) {
      return ResultOrProblems.failed(
          GeneralProblems.get().notAnOption(keyword, ParameterProperty.class, AVAILABLE_KEYWORDS));
    }

    // properties may or may not be a key-value pair, e.g. `min: 0` vs `numeric`
    if (property.hasKeyValuePair() != rawValue.isPresent()) {
      Range<Integer> expected = property.hasKeyValuePair() ? Range.singleton(1) : Range.singleton(0);
      return ResultOrProblems.failed(
          GeneralProblems.get().badArity(Value.class, property, expected, rawValue.isPresent() ? 1 : 0));
    }

    // if the property accepts a key-value pair, load the specified value
    if (property.hasKeyValuePair()) {
      return property.withValue(context, rawValue.get());
    }
    return ResultOrProblems.of(property);
  }

  /**
   * Infers properties for a parameter based on the parameter's default value
   */
  public static Set<ParameterProperty> infer(Object defaultValue) {
    if (defaultValue instanceof RealizedExpression) {
      return infer((RealizedExpression) defaultValue);
    }
    return Collections.emptySet();
  }


  /**
   * Infers properties for a parameter based on its default value (a realized expression)
   */
  public static Set<ParameterProperty> infer(RealizedExpression defaultValue) {
    Set<ParameterProperty> properties = new HashSet<>();

    // only infer type for simple/constant expressions
    // From a UI point of view, we're mostly interested in the simple case where a
    // constant numeric value is used, e.g. 0, 3.14, 10000. The UI can't do much to help
    // users enter more complicated expressions, e.g. ones that involve function calls
    if (defaultValue.getExpression() instanceof Constant || defaultValue.getExpression() instanceof ListDeclaration) {

      Type result = defaultValue.getResultType();

      if (result.isA(RSList.class)) {
        // we assume a list is always going to be a multiselect of some kind
        // Note that MULTISELECT also implies LIST
        properties.add(InputFieldProperty.MULTISELECT);
        result = result.findAllowNull(RSList.class).map(list -> list.getMemberType()).orElse(result);
      }

      if (result.isNumeric()) {
        properties.add(TypedProperty.NUMERIC);
      }
    }

    return properties;
  }

  /**
   * Infers properties for a parameter based on its expected type. This is more useful
   * for wizard models where we are dealing with Java types rather than RiskScape expressions.
   */
  public static Set<ParameterProperty> infer(Class<?> expectedClass) {
    Set<ParameterProperty> properties = new HashSet<>();
    if (expectedClass == ResolvedBookmark.class) {
      properties.add(TypedProperty.BOOKMARK);
    }

    return properties;
  }

  /**
   * @return the class to use for binding a parameter, if any, based on the given properties
   */
  public static Optional<Class<?>> getBindingClass(Collection<ParameterProperty> properties) {
    return TypedProperty.findBest(properties).map(p -> p.getParameterType());
  }
}

