package daikon.split;

import daikon.tools.jtb.Ast;
import java.util.ArrayList;
import java.util.List;
import jtb.ParseException;
import jtb.syntaxtree.ArgumentList;
import jtb.syntaxtree.Arguments;
import jtb.syntaxtree.Name;
import jtb.syntaxtree.Node;
import jtb.syntaxtree.NodeListOptional;
import jtb.syntaxtree.NodeToken;
import jtb.syntaxtree.PrimaryExpression;
import jtb.syntaxtree.PrimarySuffix;
import jtb.visitor.DepthFirstVisitor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.dataflow.qual.Pure;

/**
 * StatementReplacer is a JTB syntax tree visitor that replaces method calls to one line methods
 * with their bodies. The bodies of the methods have their variable names changed to the correct
 * argument names. Replacer applies it self repeatedly such that if the body of the one liner calls
 * a one liner, it will make both replacements. For example, take the methods
 *
 * <pre>
 *   int someMethod(int x, int y) { return  x + anotherMethod(y + 1); }
 *   int anotherMethod(int z) { return 2*z + 5; }
 * </pre>
 *
 * A call to "makeReplacements(someMethod(a/3, b))" would yield:
 *
 * <pre>
 * (a/3) + 2*((b) + 1) + 5
 * </pre>
 *
 * Once made from ReplaceStatements, a replacer can be used on a series of statements.
 */
class StatementReplacer extends DepthFirstVisitor {

  /** A ReplaceStatementMap for the ReplaceStatements of this. */
  private ReplaceStatementMap statementMap;

  /**
   * True iff a match between the method name in the PrimaryExpression currently being visited and
   * one of the members of methodNames is found.
   */
  private boolean matchFound = false;

  /**
   * Creates a new instance of StatementReplacer that makes the replacements specified by the
   * ReplaceStatements of replaceStatements.
   *
   * @param replaceStatements a list of ReplaceStatements specifying the replacements to be made by
   *     this
   */
  public StatementReplacer(List<ReplaceStatement> replaceStatements) {
    statementMap = new ReplaceStatementMap(replaceStatements);
  }

  static final int MAXREPLACEMENTS = 10;

  /**
   * Makes the replacements in statement that are designated by this. See class description for
   * details.
   *
   * @param expression a segment of valid java code in which the replacements should be made
   * @return statement with the correct replacements made
   */
  public String makeReplacements(String expression) throws ParseException {
    // originalExpression and replacements detect loops.  Gross.
    String originalExpression = expression;
    int replacements = 0;
    String replacedExpression = expression;
    do {
      expression = replacedExpression;
      // expression must be re-parsed with every loop because the
      // syntax tree is made invalid when visited by this.
      // TODO: This removes spaces, turning "a instanceof B" into "ainstanceofB"
      // System.out.println("about to call getJtbTree on: " + expression);
      Node root = Visitors.getJtbTree(expression);
      try {
        root.accept(this);
        replacedExpression = Ast.format(root);
        replacements++;
      } catch (IllegalStateException e) {
        @SuppressWarnings("nullness") // library: ParseException is not yet annotated
        @NonNull String message = e.getMessage();
        // ParseException does not accept optional "cause" argument
        throw new ParseException(message);
      }
    } while ((replacements < MAXREPLACEMENTS) && !replacedExpression.equals(expression));
    if (replacements >= MAXREPLACEMENTS) {
      return originalExpression;
    } else {
      // System.out.println("makeReplacements(" + originalExpression + ") ==> " +
      // replacedExpression);
      return replacedExpression;
    }
  }

  /**
   * This method should not be used directly by users of this class; however, it must be public to
   * fulfill the visitor interface. If n is a method call with a replacement, then the variables of
   * the replacement statement are replaced by the arguments to the method call and then the
   * replacement statement is substituted for the method call. The first token of n is set to the
   * replace statement. All the other tokens are set to the empty string by visit(NodeToken n).
   *
   * @param n the possible method call in which replacement may be made
   */
  @Override
  public void visit(PrimaryExpression n) {
    if (!matchFound) {
      ReplaceStatement replaceStatement;
      NodeToken firstToken;
      List<String> newArgs;
      if (isNonThisMethod(n)) {
        replaceStatement = statementMap.get(getNonThisName(n));
        firstToken = ((Name) n.f0.f0.choice).f0;
        newArgs = getNonThisArgs(n);
      } else if (isThisDotMethod(n)) {
        replaceStatement = statementMap.get(getThisName(n));
        firstToken = (NodeToken) n.f0.f0.choice;
        newArgs = getArgs(n);
      } else {
        replaceStatement = null;
        firstToken = null;
        newArgs = null;
      }
      if (replaceStatement != null && firstToken != null && newArgs != null) {
        List<String> oldArgs = getParameterNames(replaceStatement.getParameters());
        String newReturnStatement;
        try {
          newReturnStatement =
              TokenReplacer.replaceTokens(replaceStatement.getReturnStatement(), oldArgs, newArgs);
          matchFound = true;
          super.visit(n);
          matchFound = false;
          firstToken.tokenImage = newReturnStatement;
          return;
        } catch (ParseException e) {
          // need to throw an unchecked Exception since visit
          // cannot throw a checked Exception.
          throw new IllegalStateException(e.getMessage(), e);
        }
      }
    }
    super.visit(n);
  }

  /**
   * Returns a List of the parameter names (as Strings) of the MethodParameters of params.
   *
   * @param params the MethodParameters' whose names are desired
   */
  private List<String> getParameterNames(ReplaceStatement.MethodParameter[] params) {
    List<String> paramNames = new ArrayList<>();
    for (int i = 0; i < params.length; i++) {
      paramNames.add(params[i].name);
    }
    return paramNames;
  }

  /**
   * This method should not be used directly by users of this class; however, it must be public to
   * fulfill the visitor interface. Sets all tokens except the first in a Primary expression to the
   * empty string. All begin columns and endColumns are set to -1, to ensure that Ast printing
   * exceptions are not thrown.
   */
  @Override
  public void visit(NodeToken n) {
    if (matchFound) {
      n.tokenImage = "";
    }
    n.beginColumn = -1;
    n.endColumn = -1;
    super.visit(n);
  }

  /**
   * Returns the name of the method call represented by n.
   *
   * @param n a "non-this" method call
   */
  private String getNonThisName(PrimaryExpression n) {
    Name nameNode = (Name) n.f0.f0.choice;
    return Ast.format(nameNode);
  }

  /**
   * Returns the name of the method call represented by n including the "this." prefix.
   *
   * @param n a "this" method call
   */
  private String getThisName(PrimaryExpression n) {
    return Ast.format(n.f0) + Ast.format(n.f1.elementAt(0));
  }

  /**
   * Returns true if n represents a "non-this" call to a method. "Non-this" methods calls are not
   * prefixed with "this.". For example "get(5)" and "Collections.sort(new ArrayList())" are
   * "non-this" method calls.
   */
  @Pure
  private boolean isNonThisMethod(PrimaryExpression n) {
    return (n.f0.f0.choice instanceof Name
        && n.f1.size() > 0
        && n.f1.elementAt(0) instanceof PrimarySuffix
        && ((PrimarySuffix) n.f1.elementAt(0)).f0.choice instanceof Arguments);
  }

  /**
   * Returns true if n represents a "this" call to a method. "This" methods calls are prefixed with
   * "this.". For example "this.get(5)" is a "this" method call.
   */
  @Pure
  private boolean isThisDotMethod(PrimaryExpression n) {
    return (n.f0.f0.choice instanceof NodeToken
        && Visitors.isThis((NodeToken) n.f0.f0.choice)
        && n.f1.size() == 2
        && n.f1.elementAt(1) instanceof PrimarySuffix
        && ((PrimarySuffix) n.f1.elementAt(1)).f0.choice instanceof Arguments);
  }

  /**
   * Returns the arguments from the "this" method call n. For example "method(x, y + 1)" would yield
   * "[(x), (y + 1)]"
   *
   * @param n the "this" method call from which the arguments should be extracted
   * @return a list of arguments from the method call n
   */
  private List<String> getArgs(PrimaryExpression n) {
    List<String> args = new ArrayList<>();
    int index = n.f1.size() - 1;
    if (index > 1) {
      index = 1;
    }
    Arguments argumentNode = (Arguments) ((PrimarySuffix) n.f1.elementAt(index)).f0.choice;
    if (argumentNode.f1.present()) {
      ArgumentList argListNode = (ArgumentList) argumentNode.f1.node;
      args.add(addParens(Ast.format(argListNode.f0)));
      if (argListNode.f1.present()) {
        NodeListOptional additionalArgsNode = argListNode.f1;
        for (int i = 0; i < additionalArgsNode.size(); i++) {
          args.add(addParens(Ast.format(additionalArgsNode.elementAt(i))));
        }
      }
    }
    return args;
  }

  /**
   * Returns the arguments from the "non-this" method call n. For example, "method(x, y)" would
   * yield "[x, y]".
   *
   * @param n the "non-this" method call from which the arguments should be extracted
   * @return a list of arguments from the method call n
   */
  private List<String> getNonThisArgs(PrimaryExpression n) {
    List<String> args = new ArrayList<>();
    int index = 0;
    Arguments argumentNode = (Arguments) ((PrimarySuffix) n.f1.elementAt(index)).f0.choice;
    if (argumentNode.f1.present()) {
      ArgumentList argListNode = (ArgumentList) argumentNode.f1.node;
      args.add(Ast.format(argListNode.f0));
      if (argListNode.f1.present()) {
        NodeListOptional additionalArgsNode = argListNode.f1;
        for (int i = 0; i < additionalArgsNode.size(); i++) {
          args.add(Ast.format(additionalArgsNode.elementAt(i)));
        }
      }
    }
    return args;
  }

  /**
   * Returns the argument with parens placed around it unless there are already parens the argument.
   * For example, "x" would yield "(x)", "x + 1" would yield "(x + 1)", and "(x+1)" would yield no
   * change.
   */
  public static String addParens(String arg) {
    arg = arg.trim();
    if (arg.charAt(0) == '(' && arg.charAt(arg.length() - 1) == ')') {
      return arg;
    }
    return "(" + arg + ")";
  }
}
