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

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import nz.org.riskscape.engine.types.CoercionException;
import nz.org.riskscape.engine.types.SimpleType;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.typeset.LinkedType;
import nz.org.riskscape.engine.typeset.TypeSet;
import nz.org.riskscape.engine.typexp.AST.ComplexType;
import nz.org.riskscape.engine.typexp.AST.Symbol;

/**
 * Interface for constructing riskscape {@link Type} objects from a riskscape type-expression, e.g.
 * ```
 * # returns a simple text type
 * text
 * # returns an integer that can be null
 * nullable(integer)
 * # returns a one-based enum containing foo -1, bar -2, baz -3
 * enum('foo', 'bar', 'baz')
 * # returns a WithinRange type of Integers between 0 and 100
 * range(integer, 0, 100)
 * # return a struct
 * struct(animal: enum('cat', 'dog', 'pig'), weight: range(floating, 0, 1000000))
 * ```
 */
public interface TypeBuilder {

  /**
   * Construct a Riskscape {@link Type} from a type-expression.
   * @throws TypeBuildingException if there was a problem building the type
   */
  Type build(String typeExpression);

  /**
   * Construct a type from a symbol.  These are normally going to be {@link SimpleType}s
   * but this is not enforced
   */
  Type buildSimpleType(Symbol symbol);

  /**
   * Construct a type from a complex type expression - e.g. the type is a composition of other types
   */
  Type buildComplexType(ComplexType ast);

  /**
   * A type set to use for referring to user-defined {@link LinkedType}s
   */
  TypeSet getTypeSet();

  /**
   * Helper method for {@link TypeConstructor}s for expecting an {@link AST} to be either a {@link AST.Symbol} or a
   * {@link AST.ComplexType}.
   * If ast is a type-ish node, then a {@link Type} is built and returned
   * @param orElse exception supplier if ast is not of the correct type
   */
  default Type expectType(AST ast, Function<AST, RuntimeException> orElse) {
    if (ast instanceof AST.Symbol) {
      AST.Symbol symbol = (AST.Symbol) ast;

      return buildSimpleType(symbol);
    } else if (ast instanceof AST.ComplexType) {
      return buildComplexType((ComplexType) ast);
    } else {
      throw orElse.apply(ast);
    }
  }

  /**
   * Helper method for {@link TypeConstructor}s that expects an {@link AST} to be an {@link AST.Constant} and return
   * its java value.
   * @param orElse exception supplier if ast is not an {@link AST.Constant}
   */
  default Object expectConstant(AST ast, Function<AST, RuntimeException> orElse) {
    if (ast instanceof AST.Constant) {
      return ((AST.Constant) ast).toNative();
    } else {
      throw orElse.apply(ast);
    }
  }

  /**
   * Helper method for {@link TypeConstructor}s that expects the tail of an argument list to all be constants that
   * coerce to a particular riskscape type.
   * @param complexType the {@link AST} we are building a {@link Type} from
   * @param subType the {@link Type} that all constants must coerce to
   * @param args the argument list
   * @param argOffset index to start from in the argument list
   * @return the coerced list of arguments
   */
  default List<Object> expectConstantsOfType(
      ComplexType complexType,
      Type subType,
      List<AST> args,
      int argOffset) {

    List<Object> values = new ArrayList<>(args.size());
    for (AST ast : args) {
      int index = values.size() + argOffset;
      Object constant = expectConstant(ast, arg
          -> new TypeArgumentException(complexType, String.format("argument %d expected to be a constant", index)));

      Object coerced;
      try {
        coerced = subType.coerce(constant);
      } catch (CoercionException ex) {
        throw new TypeArgumentException(complexType, String.format(
            "Argument %d could not be coerced to %s - %s",
            index,
            subType,
            ast));
      }

      values.add(coerced);
    }

    return values;
  }

  /**
   * Helper method for {@link TypeConstructor}s for return a cast {@link AST} where it is a particular type of
   * {@link AST} node.
   * @param expectedASTClass the desired class
   * @param ast the node to check
   * @param orElse an exception supplier if the ast is not of the desired type.
   * @return the ast, cast according to expectedASTClass
   */
  default <T extends AST> T expectAST(Class<T> expectedASTClass, AST ast, Function<AST, RuntimeException> orElse) {
    if (expectedASTClass.isInstance(ast)) {
      return expectedASTClass.cast(ast);
    } else {
      throw orElse.apply(ast);
    }
  }

}
