/*
 * 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.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.problem.ProblemSink;
import nz.org.riskscape.problem.ResultOrProblems;

/**
 * Minimal interface for a class that can bind strings to objects according to {@link Parameter}s
 *
 * # A note on deprecation
 *
 *  Various binding methods are being deprecated in favour of ones that don't throw {@link ParameterBindingException}s
 *  for user errors.  Also, the code is moving away from being parameter-first to type-first, and making the parameter
 *  versions added extras, which can add context to any error messages.
 *
 * # Why all the change?
 *
 *  - we never use the whole parameter for the binding, just its type
 *  - there was a bunch of boiler plate code adding the parameter to the problem, but it was up to each implementation
 *  to make sure they did it.  We are going to move that wrapping to the various higher-level parameter set binding
 *  implementations in the future
 *  - code that wanted to use binding without a parameter would have to do it in an awkward way and it took a lot of
 *  care to extricate the parameter name from the error message (and it wasn't always possible)
 *  - ResultOrProblems exists to chain and nest user facing problems, like binding errors, and this has been an
 *  awkward outlier since day dot.  It's also a big bin of untranslated code.
 *  - a lot of non-parameter binding code wasn't catching the exceptions when called
 */
public interface BindingContext {

  /**
   * Checks that the given map of bound parameter objects are valid according to the list of parameters.  This should
   * check both the type and the arity of the bound parameters.
   */
  void validateBoundParameters(List<Parameter> parameters, Map<String, List<?>> boundParameters)
      throws InvalidBoundParametersException;

  /**
   * Returns a single parameter value from the given string input.
   * @throws ParameterBindingException if it couldn't be bound to an object.
   * @deprecated use {@link #bind(Object, Class)} instead
   */
  @Deprecated
  Object bind(Parameter param, String value) throws ParameterBindingException;

  /**
   * Bind a non-null value to a specific type, returning the bound value, or a failed result containing details why it
   * didn't work.
   *
   * This method exists as the new way to bind values, without accepting a parameter and without throwing exceptions
   * because:
   *
   *  - we never use the whole parameter for the parameter, just the type
   *  - there was a bunch of boiler plate code adding the parameter to the problem, but it was up to each implementation
   *  to make sure they did it.  We are going to move that wrapping to the various higher-level parameter set binding
   *  implementations in the future
   *  - code that wanted to use binding without a parameter would have to do it in an awkward way and it took a lot of
   *  care to extricate the parameter name from the error message (and it wasn't always possible)
   *  - ResultOrProblems exists to chain and nest user facing problems, like binding errors, and this has been an
   *  awkward outlier since day dot.  It's also a big bin of untranslated code.
   *  - a lot of non-parameter binding code wasn't catching the exceptions when called
   */
  <T> ResultOrProblems<T> bind(Object value, Class<T> expectedType);

  default <T> List<ResultOrProblems<T>> bindAll(List<?> values, Class<T> expectedType) {
    return values.stream().map(v -> bind(v, expectedType)).collect(Collectors.toList());
  }

  /**
   * Untyped version of bindAll that uses the parameter's type and wraps any problems with parameter context
   */
  default List<ResultOrProblems<?>> bindAll(List<?> values, Parameter parameter) {
    return values.stream().map(v -> bind(v, parameter)).collect(Collectors.toList());
  }

  /**
   * Untyped version of bind that uses the parameter's type and wraps any problems with parameter context
   */
  default ResultOrProblems<?> bind(Object value, Parameter parameter) {
    try {
      return bind(value, parameter.getType())
          .withMoreProblems(bound -> parameter.validate(this, bound))
          .composeProblems((s, ps) -> {
              return ParamProblems.bindingError(parameter, value, ps);
          });
    } catch (NoBindingAvailableException e) {
      e.setParameter(parameter);
      throw e;
    }
  }

  /**
   * Returns a {@link ParameterBinder} that can convert a value of fromType to toType.  Note that most binders will
   * convert from fromType by converting the value to a string, but more specific binders could be present and should
   * be returned in favour of a less specific one.
   */
  Optional<ParameterBinder> getBinder(Class<?> fromType, Class<?> toType);

  /**
   * @return a new {@link BindingContext} that will bind resources relative to a new base URI
   * TODO see if we can factor this out when we do the new resource binding stuff
   */
  BindingContext withNewRelativeTo(URI relativeTo);

  Project getProject();
  Engine getEngine();

  URI getRelativeTo();

  /**
   * @return a temporary directory linked to this BindingContext - can be used for storing temporary files that will
   * stick around while binding.
   */
  Path getTempDirectory();

  /**
   * Adds a new binder to just this context to the top of the list of binders (so that it takes priority)
   */
  void addBinder(ParameterBinder binder);

  /**
   * @return a place for problems to be output where some user feedback needs to
   * be given that isn't going to cause binding to stop.
   */
  ProblemSink getProblemSink();
}
