/*
 * 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.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import lombok.Data;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionMetadata;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.function.UntypedFunction;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.FunctionCall;

/**
 * An interface for functions to indicate that they can be realized for a particular set of input arguments in a
 * riskscape language expression.  Useful for functions that want to allow more flexible type computation than the
 * default behaviour provided to all functions
 */
@FunctionalInterface
public interface RealizableFunction {

  /**
   * Returns a  {@link RiskscapeFunction} that is {@link RealizableFunction}, where realization is needed just so the
   * implementation can have access to the {@link RealizationContext}
   * @param args the argument list to advertise and return with realizing
   * @param returnType the return type to advertise and return with realizing
   * @param constructor a function that builds an {@link UntypedFunction} to use for the implementation, supplying the
   * realization context for use during execution.
   * @return a function that returns a wrapped up version of the given function when
   * {@link RiskscapeFunction#getRealizable()} is called
   */
  static RiskscapeFunction contextOnly(
      ArgumentList args,
      Type returnType,
      Function<RealizationContext, UntypedFunction> constructor
  ) {
    // fair bit of wrapping going on here - we are declaring a realizable function as a lambda, which we're wrapping up
    // in a riskscape function via asFunction.  The RealizableFunction we're declaring just calls the given constructor
    // with just the context as an arg - the thing the constructor creates is what ultimately gets called. Clear as mud.
    return asFunction((context, fc, givenTypes) -> {
      return ResultOrProblems.of(
        RiskscapeFunction.create(
          RiskscapeFunction.BUILT_IN, args.getArgumentTypes(), returnType, constructor.apply(context)
        )
      );
    }, args, returnType);
  }

  /**
   * Wrap a {@link RealizableFunction} in a {@link RiskscapeFunction}.  Saves you from the boiler plate of having to
   * make {@link RealizableFunction}'s implement RiskscapeFunction as well.  Instead, just add an `asFunction` method
   * to your RealizableFunction implementation that calls this `asFunction` method (See BaseRealizableFunction in
   * engine)
   * @param function the function to wrap up in a RiskscapeFunction
   * @param args the argument types to advertise - can be different to the ones the RealizableFunction returns on
   * realization
   * @param returnType the return type to advertise - can be default to the one the RealizableFunction returns on
   * realization
   */
  static RiskscapeFunction asFunction(RealizableFunction function, ArgumentList args, Type returnType) {
    return new RiskscapeFunction() {

      @Override
      public Type getReturnType() {
        return returnType;
      }

      @Override
      public ArgumentList getArguments() {
        return args;
      }

      @Override
      public List<Type> getArgumentTypes() {
        return args.getArgumentTypes();
      }

      @Override
      public Object call(List<Object> args) {
        throw new UnsupportedOperationException();
      }

      @Override
      public Optional<RealizableFunction> getRealizable() {
        return Optional.of(function);
      }

      @Override
      public String toString() {
        return "Wrapped(" + function.toString() + ")";
      }
    };
  }

  /**
   * Build an IdentifiedFunction from a RealizableFunction and some FunctionMetadata.  Serves as an alternative to
   * extending BaseRealizableFunction (in engine), which isn't always going to be appropriate.
   */
  static IdentifiedFunction identified(RealizableFunction function, FunctionMetadata metadata) {
    return new MetadataIdentifiedRealizableFunction(function, metadata);
  }

  // not using an anonymous class so I can't use lombok - I want equality!
  // XXX: Use MetadataBasedFunction?
  @Data
  class MetadataIdentifiedRealizableFunction implements IdentifiedFunction {

    private final RealizableFunction function;
    private final FunctionMetadata metadata;

    @Override
    public List<Type> getArgumentTypes() {
      return metadata.getArguments().getArgumentTypes();
    }

    @Override
    public ArgumentList getArguments() {
      return metadata.getArguments();
    }

    @Override
    public Type getReturnType() {
      return metadata.getReturnType();
    }

    @Override
    public Object call(List<Object> args) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String getId() {
      return metadata.getId();
    }

    @Override
    public String getDescription() {
      return metadata.getDescription();
    }

    @Override
    public URI getSourceURI() {
      return metadata.getSource();
    }

    @Override
    public Category getCategory() {
      return metadata.getCategory();
    }

    @Override
    public Optional<RealizableFunction> getRealizable() {
      return Optional.of(function);
    }
  }

  /**
   * Override me and return false if this {@link RealizableFunction} does not want the expression realization to attempt
   * to coerce equivalent argument types before realizing the function.  Any function that advertises generic types,
   * e.g. list(struct(foo: anything)) is going to want to disable this (e.g. override and return false)
   */
  default boolean isDoTypeAdaptation() {
    return true;
  }

  /**
   * Attempt to adapt a function call to the given types.  This can also be used to do constant optimizations etc or
   * anything else.
   * @param functionCall the part of the abstract syntax tree for the function call
   * @param argumentTypes the types of the arguments that have been supplied to the function
   * @return the adapted function, or some problems if it couldn't be adapted.
   */
  ResultOrProblems<RiskscapeFunction> realize(
      RealizationContext context,
      FunctionCall functionCall,
      List<Type> argumentTypes);


}
