/*
 * 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.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.function.Function;

import com.codahale.metrics.MetricRegistry;

import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.function.FunctionResolver;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.typeset.TypeSet;
import nz.org.riskscape.problem.ProblemSink;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.Expression;

/**
 * Access to contextual bits and pieces in which realization is going to happen - this metadata will also be
 * relevant for the eventual execution of an {@link Expression} and can be passed through to any
 * {@link RealizedExpression}s and their dependents that result from realization.
 *
 * At the moment this is quite light in contents, but I expect it will eventually be the place where resource
 * management, lifecycle stuff and other supporting parts might go.
 */
public interface RealizationContext {

  /**
   * @return an {@link ExpressionRealizer} for creating {@link RealizedExpression} from a parsed {@link Expression}
   */
  ExpressionRealizer getExpressionRealizer();

  /**
   * Convenience function for going straight from an expression string to a realized expression
   */
  default ResultOrProblems<RealizedExpression> realizeConstant(String constantExpression) {
    return ExpressionParser.INSTANCE.parseOr(constantExpression)
        .flatMap(expr -> getExpressionRealizer().realizeConstant(expr));
  }

  /**
   * @return the {@link Project} that owns the elements that are being Realized
   */
  Project getProject();

  /**
   * @return a {@link FunctionResolver} that can be used for producing {@link RiskscapeFunction}s that work for a
   * particular set of input types.
   */
  FunctionResolver getFunctionResolver();

  /**
   * The {@link BindingContext} connected to this {@link RealizationContext}.  Any binding to things using the
   * {@link BindingContext} returned here that need a {@link RealizationContext} will use this.
   */
  BindingContext getBindingContext();

  /**
   * Returns an equal struct to the given one, such that a subsequent call would return the exact same one.  This
   * method exists to handle situations where a struct is implicitly defined by an {@link Expression} to ensure the
   * subsequent implicitly defined ones use the exact same struct to avoid an {@link IllegalArgumentException} when
   * using {@link Tuple#fetch(nz.org.riskscape.engine.types.Struct.StructMember)}
   *
   * Without this, expressions like `[{foo: 1}, {foo: 2}, {foo: 3}]` would define three equal but different struct
   * objects.
   *
   * This method lives on the context, rather than, say, a project, so that we can rely on the lifecycle of the
   * realization context as a proxy for the life of the set of normalized structs we memoize.  Alternatively, we'd need
   * to have some way of managing the number of structs in this set via something like a {@link WeakReference}.
   *
   * In "the future", a project is likely to be a fairly long lived object whereas a context is going to be used during
   * some short to mid term set
   *
   * @param struct a struct to normalize
   * @return either the exact same object if no struct like this already exists in this {@link RealizationContext}, or
   * a previously seen {@link Struct} that is equal
   */
  Struct normalizeStruct(Struct struct);

  /**
   * @return a place for problems to be output, typically used outside of realization where some user feedback needs to
   * be given that isn't going to cause execution to stop.
   */
  ProblemSink getProblemSink();

  default Engine getEngine() {
    return getProject().getEngine();
  }

  /**
   * Lookup and possibly populate an item in this context's cache, in a thread-safe way, so that there's only ever one
   * of each object in the cache.
   *
   * Note that objects may be removed from the cache at any time. The most likely reason for this would to to free
   * memory. For this reason compute functions may be applied many times.
   *
   * @param cacheKey object whose equals and hashcode methods serve as a unique key for the object.
   * @param expectedType type safety measure to ensure the built and constructed object is of the correct type
   * @param compute a function that constructs the object if it doesn't exist in the context. May be called
   *                more than once if the cache has cleared it's value.
   * @return An object that resulted from compute
   */
  <T> T getOrComputeFromCache(Object cacheKey, Class<T> expectedType, Function<Object, T> compute);

  /**
   * Convenience version of {@link #getOrComputeFromCache(Object, Class, Function)} that namespaces keys with a class
   */
  default <T> T getOrComputeFromCache(Class<?> prefix, Object key, Class<T> expectedType, Function<Object, T> compute) {
    return getOrComputeFromCache(Arrays.asList(prefix, key), expectedType, compute);
  }

  /**
   * Access to the {@link TypeSet} for this context (via the project)
   */
  default TypeSet getTypeSet() {
    return getProject().getTypeSet();
  }

  /**
   * A place to register metrics during realization.
   * TBD - how to we ensure uniqueness of metrics?
   */
  MetricRegistry getMetricRegistry();
}
