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

import nz.org.riskscape.engine.OsUtils;
import nz.org.riskscape.engine.Project;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.rl.EvalException;
import nz.org.riskscape.engine.rl.ExpressionRealizer;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Struct.StructMember;
import nz.org.riskscape.picocli.CommandLine;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.BinaryOperation;
import nz.org.riskscape.rl.ast.BracketedExpression;
import nz.org.riskscape.rl.ast.Constant;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.FunctionCall.Argument;
import nz.org.riskscape.rl.ast.ListDeclaration;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.StructDeclaration;

@CommandLine.Command(name = "expression", aliases = "expr", subcommands = {
    ExpressionCommand.Eval.class,
    ExpressionCommand.Inspect.class
})
public class ExpressionCommand extends StubCommand {

  @CommandLine.Command(name = "eval", aliases = "evaluate")
  public static class Eval extends ApplicationCommand {

    @CommandLine.Parameters(arity = "1", paramLabel = "<expression>", index = "0")
    public String expression;

    @CommandLine.Option(names = {"-t", "--return-type"})
    public boolean showReturnType;

    @Override
    public Object doCommand(Project useProject) {
      ExpressionRealizer realizer = getRealizationContext().getExpressionRealizer();

      ResultOrProblems<RealizedExpression> reOr = realizer.realize(Struct.EMPTY_STRUCT, expression);

      if (reOr.isPresent()) {
        try (RealizedExpression re = reOr.get()) {
          StringBuilder builder = new StringBuilder();
          Object result = re.evaluate(Tuple.of(Struct.EMPTY_STRUCT));
          builder.append(result == null ? "<nothing>" : result.toString());

          if (showReturnType) {
            builder.append(OsUtils.LINE_SEPARATOR).append(re.getResultType());
          }

          return builder.toString();
        } catch (EvalException e) {
          return Problems.caught(e);
        }
      } else {
        return Problem.composite(reOr.getProblems(), "Failed to evaluate expression");
      }
    }
  }

  @CommandLine.Command(name = "inspect")
  public static class Inspect extends ApplicationCommand {

    @CommandLine.Parameters(arity="1", paramLabel = "<expression>")
    String expression;

    @Override
    public Object doCommand(Project useProject) {
      ExpressionRealizer realizer = getRealizationContext().getExpressionRealizer();

      ResultOrProblems<RealizedExpression> re = realizer.realize(Struct.EMPTY_STRUCT, expression);

      if (re.isPresent()) {
        RealizedExpression expr = re.get();
        return prettyPrint(expr);
      } else {
        return Problem.composite(re.getProblems(), "Failed to evaluate expression");
      }
    }

    private String prettyPrint(RealizedExpression expr) {
      StringBuilder str = new StringBuilder();
      prettyPrint("root", expr, "", str);
      return str.toString();
    }

    private void prettyPrint(String name, RealizedExpression rExpr, String indent, StringBuilder str) {
      Expression expr = rExpr.getExpression();
      final String nl = OsUtils.LINE_SEPARATOR;
      str.append(indent).append(name).append(": ").append(expr.getClass().getSimpleName());
      str.append("(").append(rExpr.getResultType()).append(")");

      expr.isA(Constant.class).ifPresent(c -> str.append(" => ").append(c.getToken().rawValue()));

      // TODO what about sorting?
      expr.isA(FunctionCall.class).ifPresent(fc -> {
        str.append(" => ").append(fc.getIdentifier().value).append("(");
        int idx = 0;
        for (Argument fa : fc.getArguments()) {
          if (idx != 0) {
            str.append(", ");
          }
          str.append(fa.getName().orElse("arg" + Integer.toString(idx)));
          idx++;
        }
        str.append(")");


        str.append(nl);
        idx = 0;
        for (Argument fa : fc.getArguments()) {
          String newName = fa.getName().orElse("arg" + Integer.toString(idx));
          prettyPrint(newName, rExpr.getDependencies().get(idx), indent + "  ", str);
          idx++;
        }
      });

      expr.isA(BinaryOperation.class).ifPresent(bop -> {
        str.append(nl);
        String newIndent = indent + "  ";
        prettyPrint("lhs", rExpr.getDependencies().get(0), newIndent, str);
        str.append(newIndent).append(bop.getOperator().value).append(nl);
        prettyPrint("rhs", rExpr.getDependencies().get(1), newIndent, str);
      });

      expr.isA(ListDeclaration.class).ifPresent(ld -> {
        int idx = 0;
        str.append(nl);
        for (RealizedExpression element : rExpr.getDependencies()) {
          prettyPrint(Integer.toString(idx), element, indent + "  ", str);
          idx++;
        }
      });

      expr.isA(StructDeclaration.class).ifPresent(sd -> {
        Struct struct = rExpr.getResultType().find(Struct.class).get();
        int idx = 0;
        str.append(nl);
        for (StructMember sm : struct.getMembers()) {
          prettyPrint(sm.getKey(), rExpr.getDependencies().get(idx), indent + "  ", str);
          idx++;
        }
      });

      expr.isA(PropertyAccess.class).ifPresent(pa -> {
        String ident = pa.getAccessString();
        str.append(" => ").append(ident).append(nl);

        if (pa.getReceiver().isPresent()) {
          prettyPrint("receiver", rExpr.getDependencies().get(0), indent + "  ", str);
        }
      });

      expr.isA(BracketedExpression.class).ifPresent(be -> {
        prettyPrint("bracketed", rExpr.getDependencies().get(0), indent + "  ", str);
      });

      if (!str.subSequence(str.length() - nl.length(), str.length()).equals(nl)) {
        str.append(nl);
      }
    }
  }

}
