// Copyright 2019 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package net.starlark.java.syntax;

import java.util.List;

/** A pretty-printer for Starlark syntax trees. */
final class NodePrinter {

  private final StringBuilder buf;
  private int indent;

  NodePrinter(StringBuilder buf) {
    this.buf = buf;
  }

  // Constructor exposed to legacy tests.
  // TODO(adonovan): rewrite the tests not to care about the indent parameter.
  NodePrinter(StringBuilder buf, int indent) {
    this.buf = buf;
    this.indent = indent;
  }

  // Main entry point for an arbitrary node.
  // Called by Node.prettyPrint.
  void printNode(Node n) {
    if (n instanceof Expression) {
      printExpr((Expression) n);

    } else if (n instanceof Statement) {
      printStmt((Statement) n);

    } else if (n instanceof StarlarkFile) {
      StarlarkFile file = (StarlarkFile) n;
      // Only statements are printed, not comments.
      for (Statement stmt : file.getStatements()) {
        printStmt(stmt);
      }

    } else if (n instanceof Comment) {
      Comment comment = (Comment) n;
      // We can't really print comments in the right place anyway,
      // due to how their relative order is lost in the representation
      // of StarlarkFile. So don't bother word-wrapping and just print
      // it on a single line.
      printIndent();
      buf.append(comment.getText());

    } else if (n instanceof Argument) {
      printArgument((Argument) n);

    } else if (n instanceof Parameter) {
      printParameter((Parameter) n);

    } else if (n instanceof DictExpression.Entry) {
      printDictEntry((DictExpression.Entry) n);

    } else {
      throw new IllegalArgumentException("unexpected: " + n.getClass());
    }
  }

  private void printSuite(List<Statement> statements) {
    // A suite is non-empty; pass statements are explicit.
    indent++;
    for (Statement stmt : statements) {
      printStmt(stmt);
    }
    indent--;
  }

  private void printIndent() {
    for (int i = 0; i < indent; i++) {
      buf.append("  ");
    }
  }

  private void printArgument(Argument arg) {
    if (arg instanceof Argument.Positional) {
      // nop
    } else if (arg instanceof Argument.Keyword) {
      buf.append(((Argument.Keyword) arg).getIdentifier().getName());
      buf.append(" = ");
    } else if (arg instanceof Argument.Star) {
      buf.append('*');
    } else if (arg instanceof Argument.StarStar) {
      buf.append("**");
    }
    printExpr(arg.getValue());
  }

  private void printParameter(Parameter param) {
    if (param instanceof Parameter.Mandatory) {
      buf.append(param.getName());
    } else if (param instanceof Parameter.Optional) {
      buf.append(param.getName());
      buf.append('=');
      printExpr(param.getDefaultValue());
    } else if (param instanceof Parameter.Star) {
      buf.append('*');
      if (param.getName() != null) {
        buf.append(param.getName());
      }
    } else if (param instanceof Parameter.StarStar) {
      buf.append("**");
      buf.append(param.getName());
    }
  }

  private void printDictEntry(DictExpression.Entry e) {
    printExpr(e.getKey());
    buf.append(": ");
    printExpr(e.getValue());
  }

  // Appends "def f(a, ..., z):" to the buf.
  // Also used by DefStatement.toString.
  void printDefSignature(DefStatement def) {
    buf.append("def ");
    printExpr(def.getIdentifier());
    buf.append('(');
    String sep = "";
    for (Parameter param : def.getParameters()) {
      buf.append(sep);
      printParameter(param);
      sep = ", ";
    }
    buf.append("):");
  }

  private void printStmt(Statement s) {
    printIndent();

    switch (s.kind()) {
      case ASSIGNMENT:
        {
          AssignmentStatement stmt = (AssignmentStatement) s;
          printExpr(stmt.getLHS());
          buf.append(' ');
          if (stmt.isAugmented()) {
            buf.append(stmt.getOperator());
          }
          buf.append("= ");
          printExpr(stmt.getRHS());
          buf.append('\n');
          break;
        }

      case EXPRESSION:
        {
          ExpressionStatement stmt = (ExpressionStatement) s;
          printExpr(stmt.getExpression());
          buf.append('\n');
          break;
        }

      case FLOW:
        {
          FlowStatement stmt = (FlowStatement) s;
          buf.append(stmt.getFlowKind()).append('\n');
          break;
        }

      case FOR:
        {
          ForStatement stmt = (ForStatement) s;
          buf.append("for ");
          printExpr(stmt.getVars());
          buf.append(" in ");
          printExpr(stmt.getCollection());
          buf.append(":\n");
          printSuite(stmt.getBody());
          break;
        }

      case DEF:
        {
          DefStatement stmt = (DefStatement) s;
          printDefSignature(stmt);
          buf.append('\n');
          printSuite(stmt.getBody());
          break;
        }

      case IF:
        {
          IfStatement stmt = (IfStatement) s;
          buf.append(stmt.isElif() ? "elif " : "if ");
          printExpr(stmt.getCondition());
          buf.append(":\n");
          printSuite(stmt.getThenBlock());
          List<Statement> elseBlock = stmt.getElseBlock();
          if (elseBlock != null) {
            if (elseBlock.size() == 1
                && elseBlock.get(0) instanceof IfStatement
                && ((IfStatement) elseBlock.get(0)).isElif()) {
              printStmt(elseBlock.get(0));
            } else {
              printIndent();
              buf.append("else:\n");
              printSuite(elseBlock);
            }
          }
          break;
        }

      case LOAD:
        {
          LoadStatement stmt = (LoadStatement) s;
          buf.append("load(");
          printExpr(stmt.getImport());
          for (LoadStatement.Binding binding : stmt.getBindings()) {
            buf.append(", ");
            Identifier local = binding.getLocalName();
            String origName = binding.getOriginalName().getName();
            if (origName.equals(local.getName())) {
              buf.append('"');
              printExpr(local);
              buf.append('"');
            } else {
              printExpr(local);
              buf.append("=\"");
              buf.append(origName);
              buf.append('"');
            }
          }
          buf.append(")\n");
          break;
        }

      case RETURN:
        {
          ReturnStatement stmt = (ReturnStatement) s;
          buf.append("return");
          if (stmt.getResult() != null) {
            buf.append(' ');
            printExpr(stmt.getResult());
          }
          buf.append('\n');
          break;
        }
    }
  }

  private void printExpr(Expression expr) {
    switch (expr.kind()) {
      case BINARY_OPERATOR:
        {
          BinaryOperatorExpression binop = (BinaryOperatorExpression) expr;
          // TODO(bazel-team): retain parentheses in the syntax tree so we needn't
          // conservatively emit them here.
          buf.append('(');
          printExpr(binop.getX());
          buf.append(' ');
          buf.append(binop.getOperator());
          buf.append(' ');
          printExpr(binop.getY());
          buf.append(')');
          break;
        }

      case COMPREHENSION:
        {
          Comprehension comp = (Comprehension) expr;
          buf.append(comp.isDict() ? '{' : '[');
          printNode(comp.getBody()); // Expression or DictExpression.Entry
          for (Comprehension.Clause clause : comp.getClauses()) {
            buf.append(' ');
            if (clause instanceof Comprehension.For) {
              Comprehension.For forClause = (Comprehension.For) clause;
              buf.append("for ");
              printExpr(forClause.getVars());
              buf.append(" in ");
              printExpr(forClause.getIterable());
            } else {
              Comprehension.If ifClause = (Comprehension.If) clause;
              buf.append("if ");
              printExpr(ifClause.getCondition());
            }
          }
          buf.append(comp.isDict() ? '}' : ']');
          break;
        }

      case CONDITIONAL:
        {
          ConditionalExpression cond = (ConditionalExpression) expr;
          printExpr(cond.getThenCase());
          buf.append(" if ");
          printExpr(cond.getCondition());
          buf.append(" else ");
          printExpr(cond.getElseCase());
          break;
        }

      case DICT_EXPR:
        {
          DictExpression dictexpr = (DictExpression) expr;
          buf.append("{");
          String sep = "";
          for (DictExpression.Entry entry : dictexpr.getEntries()) {
            buf.append(sep);
            printDictEntry(entry);
            sep = ", ";
          }
          buf.append("}");
          break;
        }

      case DOT:
        {
          DotExpression dot = (DotExpression) expr;
          printExpr(dot.getObject());
          buf.append('.');
          printExpr(dot.getField());
          break;
        }

      case CALL:
        {
          CallExpression call = (CallExpression) expr;
          printExpr(call.getFunction());
          buf.append('(');
          String sep = "";
          for (Argument arg : call.getArguments()) {
            buf.append(sep);
            printArgument(arg);
            sep = ", ";
          }
          buf.append(')');
          break;
        }

      case IDENTIFIER:
        buf.append(((Identifier) expr).getName());
        break;

      case INDEX:
        {
          IndexExpression index = (IndexExpression) expr;
          printExpr(index.getObject());
          buf.append('[');
          printExpr(index.getKey());
          buf.append(']');
          break;
        }

      case INT_LITERAL:
        {
          buf.append(((IntLiteral) expr).getValue());
          break;
        }

      case FLOAT_LITERAL:
        {
          buf.append(((FloatLiteral) expr).getValue());
          break;
        }

      case LAMBDA:
        {
          LambdaExpression lambda = (LambdaExpression) expr;
          buf.append("lambda");
          String sep = " ";
          for (Parameter param : lambda.getParameters()) {
            buf.append(sep);
            sep = ", ";
            printParameter(param);
          }
          buf.append(": ");
          printExpr(lambda.getBody());
          break;
        }

      case LIST_EXPR:
        {
          ListExpression list = (ListExpression) expr;
          buf.append(list.isTuple() ? '(' : '[');
          String sep = "";
          for (Expression e : list.getElements()) {
            buf.append(sep);
            printExpr(e);
            sep = ", ";
          }
          if (list.isTuple() && list.getElements().size() == 1) {
            buf.append(',');
          }
          buf.append(list.isTuple() ? ')' : ']');
          break;
        }

      case SLICE:
        {
          SliceExpression slice = (SliceExpression) expr;
          printExpr(slice.getObject());
          buf.append('[');
          // The first separator colon is unconditional.
          // The second separator appears only if step is printed.
          if (slice.getStart() != null) {
            printExpr(slice.getStart());
          }
          buf.append(':');
          if (slice.getStop() != null) {
            printExpr(slice.getStop());
          }
          if (slice.getStep() != null) {
            buf.append(':');
            printExpr(slice.getStep());
          }
          buf.append(']');
          break;
        }

      case STRING_LITERAL:
        {
          StringLiteral literal = (StringLiteral) expr;
          String value = literal.getValue();

          // TODO(adonovan): record the raw text of string (and integer) literals
          // so that we can use the syntax tree for source modification tools.
          // However, that may come with a memory cost until we start compiling
          // (at which point the cost is only transient).
          // For now, just simulate the behavior of repr(str).
          buf.append('"');
          for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            switch (c) {
              case '"':
                buf.append("\\\"");
                break;
              case '\\':
                buf.append("\\\\");
                break;
              case '\r':
                buf.append("\\r");
                break;
              case '\n':
                buf.append("\\n");
                break;
              case '\t':
                buf.append("\\t");
                break;
              default:
                // The Starlark spec (and lexer) are far from complete here,
                // and it's hard to come up with a clean semantics for
                // string escapes that serves Java (UTF-16) and Go (UTF-8).
                // Clearly string literals should not contain non-printable
                // characters. For now we'll continue to pretend that all
                // non-printables are < 32, but this obviously false.
                if (c < 32) {
                  buf.append(String.format("\\x%02x", (int) c));
                } else {
                  buf.append(c);
                }
            }
          }
          buf.append('"');
          break;
        }

      case UNARY_OPERATOR:
        {
          UnaryOperatorExpression unop = (UnaryOperatorExpression) expr;
          // TODO(bazel-team): retain parentheses in the syntax tree so we needn't
          // conservatively emit them here.
          buf.append(unop.getOperator() == TokenKind.NOT ? "not " : unop.getOperator().toString());
          buf.append('(');
          printExpr(unop.getX());
          buf.append(')');
          break;
        }
    }
  }
}
