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

import java.util.ArrayList;
import java.util.List;

import nz.org.riskscape.dsl.Lexer;
import nz.org.riskscape.dsl.LexerException;
import nz.org.riskscape.dsl.ParseException;
import nz.org.riskscape.dsl.Token;
import nz.org.riskscape.engine.bind.BaseBinder;
import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.typeset.IdentifiedType;
import nz.org.riskscape.engine.typeset.MissingTypeException;
import nz.org.riskscape.engine.typexp.AST;
import nz.org.riskscape.engine.typexp.AST.ComplexType;
import nz.org.riskscape.problem.ProblemException;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.engine.typexp.Parser;
import nz.org.riskscape.engine.typexp.TypeBuildingException;
import nz.org.riskscape.rl.TokenTypes;
import nz.org.riskscape.rl.ast.ExpressionProblems;

public class ArgumentListBinder extends BaseBinder {

  private final TypeBinder typeBinder = new TypeBinder();

  @Override
  public <T> ResultOrProblems<T> bindValue(BindingContext context, Object value, Class<T> destinationType) {
    return ProblemException.catching(() -> {
      return destinationType.cast(parseArgumentList(context, value.toString()));
    });
  }

  private ArgumentList parseArgumentList(BindingContext context, String source) throws ProblemException {
    // We want to parse everything between the [ ] tokens.
    // We need to use a Lexer because the type definitions could get complicated, e.g.
    //   [struct(foo: text, bar: integer), kaiju_attack]
    Lexer<TokenTypes> lexer = new Lexer<>(TokenTypes.tokens(), source);
    List<FunctionArgument> args = new ArrayList<>();
    String exprToParse = source;
    try {
      lexer.expect(TokenTypes.LBRACK);

      // before we start let's stash the remaining expr text in case of an error
      exprToParse = remainingText(lexer);

    // parse the arguments, one at a time
      while (lexer.peekType() != TokenTypes.RBRACK) {

        String keyword = consumeKeyword(lexer);
        Type type = parseType(context, lexer);

        if (keyword == null) {
          args.add(new FunctionArgument(-1, type));
        } else {
          args.add(new FunctionArgument(keyword, type));
        }

        if (lexer.peekType() != TokenTypes.RBRACK) {
          // if we aren't done, then we expect a comma before the next type
          lexer.expect(TokenTypes.COMMA);
        }

        // update exprToParse to remaining, we know we are good up to now
        exprToParse = remainingText(lexer);
      }
    } catch (ParseException | LexerException e) {
      // there's a lot of ways lexing goes wrong. This catch if for exceptions from the lexer.peek()
      // in the while (goes boom if the next token is now allowed like a '+') or if an expected comma
      // is missing.
      throw new ProblemException(
          ExpressionProblems.get().notAValid(Type.class, exprToParse)
              .withChildren(Problems.caught(e))
      );
    }

    return new ArgumentList(args);
  }

  /**
   * @return the keyword identifier for the next argument, if present. Else null.
   */
  private String consumeKeyword(Lexer<TokenTypes> lexer) {
    Token possibleKeyword = lexer.consumeIf(TokenTypes.KEY_IDENTIFIER).orElse(null);
    if (possibleKeyword != null) {
      // it's a keyword. Consume the trailing colon and return the keyword
      lexer.next();
      return possibleKeyword.getValue();
    }
    return null;
  }

  /**
   * Consumes the next type expression and returns it as a {@link Type}. This
   * doesn't read past the end of a valid type expression, so any subsequent
   * argument-types remain in the Lexer.
   */
  private Type parseType(BindingContext context, Lexer<TokenTypes> lexer) throws ProblemException {
    // lex the next type expression and turn it into typexp AST
    Parser parser = new Parser(lexer);
    String exprToParse = remainingText(lexer);
    AST parsed = null;
    try {
      parsed = parser.parseType();
    } catch (ParseException | LexerException e) {
      throw new ProblemException(
          ExpressionProblems.get().notAValid(Type.class, exprToParse)
              .withChildren(Problems.caught(e))
        );
    }

    // turn the AST into a Type
    if (parsed instanceof AST.ComplexType) {
      // it's a complex struct definition, so just turn the AST into a type directly
      try {
        Type built = context.getProject().getTypeBuilder().buildComplexType((ComplexType) parsed);
        if (built instanceof IdentifiedType it) {
          // check to make sure there is actually something backing the identified type because
          // user defined types can fail to build in which case the missing type exception will be thrown
          it.getUnderlyingType();
        }
        return built;
      } catch (TypeBuildingException | MissingTypeException ex) {
        throw new ProblemException(
            ExpressionProblems.get().notAValid(Type.class, exprToParse)
              .withChildren(Problems.caught(ex))
          );
      }
    } else {
      // it's a simple type expression. Use the TypeBinder, which also handles
      // resolving user-defined type names, e.g. we accept just "building" rather
      // than requiring "lookup('building')"
      return typeBinder.bindValue(context, parsed.value.getValue(), Type.class).getOrThrow();
    }
  }

  /**
   * Useful for reporting expression errors back to the user.
   * @return the remaining type expression(s) we still need to parse.
   */
  private String remainingText(Lexer<TokenTypes> lexer) {
    // strip off the unlex'd ']' as that will likely just confuse the user
    return lexer.remaining().replaceAll("\\]$", "").trim();
  }

  @Override
  public boolean canBind(Class<?> sourceType, Class<?> destinationType) {
    return ArgumentList.class.isAssignableFrom(destinationType);
  }

}
