/*
 * Copyright 2016 Google Inc. 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 com.google.turbine.tree;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.turbine.model.TurbineTyKind;
import com.google.turbine.tree.Tree.Anno;
import com.google.turbine.tree.Tree.ClassLiteral;
import com.google.turbine.tree.Tree.Ident;
import com.google.turbine.tree.Tree.ModDecl;
import com.google.turbine.tree.Tree.ModDirective;
import com.google.turbine.tree.Tree.ModExports;
import com.google.turbine.tree.Tree.ModOpens;
import com.google.turbine.tree.Tree.ModProvides;
import com.google.turbine.tree.Tree.ModRequires;
import com.google.turbine.tree.Tree.ModUses;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.nullness.Nullable;

/** A pretty-printer for {@link Tree}s. */
public class Pretty implements Tree.Visitor<@Nullable Void, @Nullable Void> {

  static String pretty(Tree tree) {
    Pretty pretty = new Pretty();
    tree.accept(pretty, null);
    return pretty.sb.toString();
  }

  private final StringBuilder sb = new StringBuilder();
  int indent = 0;
  boolean newLine = false;

  void printLine() {
    append('\n');
    newLine = true;
  }

  void printLine(String line) {
    if (!newLine) {
      append('\n');
    }
    append(line).append('\n');
    newLine = true;
  }

  @CanIgnoreReturnValue
  Pretty append(char c) {
    if (c == '\n') {
      newLine = true;
    } else if (newLine) {
      sb.append(Strings.repeat(" ", indent * 2));
      newLine = false;
    }
    sb.append(c);
    return this;
  }

  @CanIgnoreReturnValue
  Pretty append(String s) {
    if (newLine) {
      sb.append(Strings.repeat(" ", indent * 2));
      newLine = false;
    }
    sb.append(s);
    return this;
  }

  @Override
  public @Nullable Void visitIdent(Ident ident, @Nullable Void input) {
    sb.append(ident.value());
    return null;
  }

  @Override
  public @Nullable Void visitWildTy(Tree.WildTy wildTy, @Nullable Void input) {
    printAnnos(wildTy.annos());
    append('?');
    if (wildTy.lower().isPresent()) {
      append(" super ");
      wildTy.lower().get().accept(this, null);
    }
    if (wildTy.upper().isPresent()) {
      append(" extends ");
      wildTy.upper().get().accept(this, null);
    }
    return null;
  }

  @Override
  public @Nullable Void visitArrTy(Tree.ArrTy arrTy, @Nullable Void input) {
    arrTy.elem().accept(this, null);
    if (!arrTy.annos().isEmpty()) {
      append(' ');
      printAnnos(arrTy.annos());
    }
    append("[]");
    return null;
  }

  @Override
  public @Nullable Void visitPrimTy(Tree.PrimTy primTy, @Nullable Void input) {
    append(primTy.tykind().toString());
    return null;
  }

  @Override
  public @Nullable Void visitVoidTy(Tree.VoidTy voidTy, @Nullable Void input) {
    append("void");
    return null;
  }

  @Override
  public @Nullable Void visitClassTy(Tree.ClassTy classTy, @Nullable Void input) {
    if (classTy.base().isPresent()) {
      classTy.base().get().accept(this, null);
      append('.');
    }
    printAnnos(classTy.annos());
    append(classTy.name().value());
    if (!classTy.tyargs().isEmpty()) {
      append('<');
      boolean first = true;
      for (Tree t : classTy.tyargs()) {
        if (!first) {
          append(", ");
        }
        t.accept(this, null);
        first = false;
      }
      append('>');
    }
    return null;
  }

  @Override
  public @Nullable Void visitLiteral(Tree.Literal literal, @Nullable Void input) {
    append(literal.value().toString());
    return null;
  }

  @Override
  public @Nullable Void visitParen(Tree.Paren paren, @Nullable Void input) {
    paren.expr().accept(this, null);
    return null;
  }

  @Override
  public @Nullable Void visitTypeCast(Tree.TypeCast typeCast, @Nullable Void input) {
    append('(');
    typeCast.ty().accept(this, null);
    append(") ");
    typeCast.expr().accept(this, null);
    return null;
  }

  @Override
  public @Nullable Void visitUnary(Tree.Unary unary, @Nullable Void input) {
    switch (unary.op()) {
      case POST_INCR:
      case POST_DECR:
        unary.expr().accept(this, null);
        append(unary.op().toString());
        break;
      case PRE_INCR:
      case PRE_DECR:
      case UNARY_PLUS:
      case NEG:
      case NOT:
      case BITWISE_COMP:
        append(unary.op().toString());
        unary.expr().accept(this, null);
        break;
      default:
        throw new AssertionError(unary.op().name());
    }
    return null;
  }

  @Override
  public @Nullable Void visitBinary(Tree.Binary binary, @Nullable Void input) {
    append('(');
    boolean first = true;
    for (Tree child : binary.children()) {
      if (!first) {
        append(" ").append(binary.op().toString()).append(" ");
      }
      child.accept(this, null);
      first = false;
    }
    append(')');
    return null;
  }

  @Override
  public @Nullable Void visitConstVarName(Tree.ConstVarName constVarName, @Nullable Void input) {
    append(Joiner.on('.').join(constVarName.name()));
    return null;
  }

  @Override
  public @Nullable Void visitClassLiteral(ClassLiteral classLiteral, @Nullable Void input) {
    classLiteral.type().accept(this, input);
    append(".class");
    return null;
  }

  @Override
  public @Nullable Void visitAssign(Tree.Assign assign, @Nullable Void input) {
    append(assign.name().value()).append(" = ");
    assign.expr().accept(this, null);
    return null;
  }

  @Override
  public @Nullable Void visitConditional(Tree.Conditional conditional, @Nullable Void input) {
    append("(");
    conditional.cond().accept(this, null);
    append(" ? ");
    conditional.iftrue().accept(this, null);
    append(" : ");
    conditional.iffalse().accept(this, null);
    append(")");
    return null;
  }

  @Override
  public @Nullable Void visitArrayInit(Tree.ArrayInit arrayInit, @Nullable Void input) {
    append('{');
    boolean first = true;
    for (Tree.Expression e : arrayInit.exprs()) {
      if (!first) {
        append(", ");
      }
      e.accept(this, null);
      first = false;
    }
    append('}');
    return null;
  }

  @Override
  public @Nullable Void visitCompUnit(Tree.CompUnit compUnit, @Nullable Void input) {
    if (compUnit.pkg().isPresent()) {
      compUnit.pkg().get().accept(this, null);
      printLine();
    }
    for (Tree.ImportDecl i : compUnit.imports()) {
      i.accept(this, null);
    }
    if (compUnit.mod().isPresent()) {
      printLine();
      compUnit.mod().get().accept(this, null);
    }
    for (Tree.TyDecl decl : compUnit.decls()) {
      printLine();
      decl.accept(this, null);
    }
    return null;
  }

  @Override
  public @Nullable Void visitImportDecl(Tree.ImportDecl importDecl, @Nullable Void input) {
    append("import ");
    if (importDecl.stat()) {
      append("static ");
    }
    append(Joiner.on('.').join(importDecl.type()));
    if (importDecl.wild()) {
      append(".*");
    }
    append(";").append('\n');
    return null;
  }

  @Override
  public @Nullable Void visitVarDecl(Tree.VarDecl varDecl, @Nullable Void input) {
    printVarDecl(varDecl);
    append(';');
    return null;
  }

  private void printVarDecl(Tree.VarDecl varDecl) {
    printAnnos(varDecl.annos());
    printModifiers(varDecl.mods());
    varDecl.ty().accept(this, null);
    append(' ').append(varDecl.name().value());
    if (varDecl.init().isPresent()) {
      append(" = ");
      varDecl.init().get().accept(this, null);
    }
  }

  private void printAnnos(ImmutableList<Anno> annos) {
    for (Tree.Anno anno : annos) {
      anno.accept(this, null);
      append(' ');
    }
  }

  @Override
  public @Nullable Void visitMethDecl(Tree.MethDecl methDecl, @Nullable Void input) {
    for (Tree.Anno anno : methDecl.annos()) {
      anno.accept(this, null);
      printLine();
    }
    printModifiers(methDecl.mods());
    if (!methDecl.typarams().isEmpty()) {
      append('<');
      boolean first = true;
      for (Tree.TyParam t : methDecl.typarams()) {
        if (!first) {
          append(", ");
        }
        t.accept(this, null);
        first = false;
      }
      append('>');
      append(' ');
    }
    if (methDecl.ret().isPresent()) {
      methDecl.ret().get().accept(this, null);
      append(' ');
    }
    append(methDecl.name().value());
    append('(');
    boolean first = true;
    for (Tree.VarDecl param : methDecl.params()) {
      if (!first) {
        append(", ");
      }
      printVarDecl(param);
      first = false;
    }
    append(')');
    if (!methDecl.exntys().isEmpty()) {
      append(" throws ");
      first = true;
      for (Tree.Type e : methDecl.exntys()) {
        if (!first) {
          append(", ");
        }
        e.accept(this, null);
        first = false;
      }
    }
    if (methDecl.defaultValue().isPresent()) {
      append(" default ");
      methDecl.defaultValue().get().accept(this, null);
      append(";");
    } else if (methDecl.mods().contains(TurbineModifier.ABSTRACT)
        || methDecl.mods().contains(TurbineModifier.NATIVE)) {
      append(";");
    } else {
      append(" {}");
    }
    return null;
  }

  @Override
  public @Nullable Void visitAnno(Tree.Anno anno, @Nullable Void input) {
    append('@');
    append(Joiner.on('.').join(anno.name()));
    if (!anno.args().isEmpty()) {
      append('(');
      boolean first = true;
      for (Tree.Expression e : anno.args()) {
        if (!first) {
          append(", ");
        }
        e.accept(this, null);
        first = false;
      }
      append(')');
    }
    return null;
  }

  @Override
  public @Nullable Void visitTyDecl(Tree.TyDecl tyDecl, @Nullable Void input) {
    for (Tree.Anno anno : tyDecl.annos()) {
      anno.accept(this, null);
      printLine();
    }
    printModifiers(tyDecl.mods());
    switch (tyDecl.tykind()) {
      case CLASS:
        append("class");
        break;
      case INTERFACE:
        append("interface");
        break;
      case ENUM:
        append("enum");
        break;
      case ANNOTATION:
        append("@interface");
        break;
      case RECORD:
        append("record");
        break;
    }
    append(' ').append(tyDecl.name().value());
    if (!tyDecl.typarams().isEmpty()) {
      append('<');
      boolean first = true;
      for (Tree.TyParam t : tyDecl.typarams()) {
        if (!first) {
          append(", ");
        }
        t.accept(this, null);
        first = false;
      }
      append('>');
    }
    if (tyDecl.tykind().equals(TurbineTyKind.RECORD)) {
      append("(");
      boolean first = true;
      for (Tree.VarDecl c : tyDecl.components()) {
        if (!first) {
          append(", ");
        }
        printVarDecl(c);
        first = false;
      }
      append(")");
    }
    if (tyDecl.xtnds().isPresent()) {
      append(" extends ");
      tyDecl.xtnds().get().accept(this, null);
    }
    if (!tyDecl.impls().isEmpty()) {
      append(" implements ");
      boolean first = true;
      for (Tree.ClassTy t : tyDecl.impls()) {
        if (!first) {
          append(", ");
        }
        t.accept(this, null);
        first = false;
      }
    }
    if (!tyDecl.permits().isEmpty()) {
      append(" permits ");
      boolean first = true;
      for (Tree.ClassTy t : tyDecl.permits()) {
        if (!first) {
          append(", ");
        }
        t.accept(this, null);
        first = false;
      }
    }
    append(" {").append('\n');
    indent++;
    switch (tyDecl.tykind()) {
      case ENUM:
        {
          List<Tree> nonConsts = new ArrayList<>();
          for (Tree t : tyDecl.members()) {
            if (t instanceof Tree.VarDecl) {
              Tree.VarDecl decl = (Tree.VarDecl) t;
              if (decl.mods().contains(TurbineModifier.ACC_ENUM)) {
                append(decl.name().value()).append(',').append('\n');
                continue;
              }
            }
            nonConsts.add(t);
          }
          printLine(";");
          boolean first = true;
          for (Tree t : nonConsts) {
            if (!first) {
              printLine();
            }
            t.accept(this, null);
            first = false;
          }
          break;
        }
      default:
        {
          boolean first = true;
          for (Tree t : tyDecl.members()) {
            if (!first) {
              printLine();
            }
            t.accept(this, null);
            first = false;
          }
          break;
        }
    }
    indent--;
    printLine("}");
    return null;
  }

  private void printModifiers(ImmutableSet<TurbineModifier> mods) {
    List<TurbineModifier> modifiers = new ArrayList<>(mods);
    Collections.sort(modifiers);
    for (TurbineModifier mod : modifiers) {
      switch (mod) {
        case PRIVATE:
        case PROTECTED:
        case PUBLIC:
        case ABSTRACT:
        case FINAL:
        case STATIC:
        case VOLATILE:
        case SYNCHRONIZED:
        case STRICTFP:
        case NATIVE:
        case TRANSIENT:
        case DEFAULT:
        case TRANSITIVE:
        case SEALED:
        case NON_SEALED:
          append(mod.toString()).append(' ');
          break;
        case ACC_SUPER:
        case VARARGS:
        case INTERFACE:
        case ACC_ENUM:
        case ACC_ANNOTATION:
        case ACC_SYNTHETIC:
        case ACC_BRIDGE:
        case COMPACT_CTOR:
          break;
      }
    }
  }

  @Override
  public @Nullable Void visitTyParam(Tree.TyParam tyParam, @Nullable Void input) {
    printAnnos(tyParam.annos());
    append(tyParam.name().value());
    if (!tyParam.bounds().isEmpty()) {
      append(" extends ");
      boolean first = true;
      for (Tree bound : tyParam.bounds()) {
        if (!first) {
          append(" & ");
        }
        bound.accept(this, null);
        first = false;
      }
    }
    return null;
  }

  @Override
  public @Nullable Void visitPkgDecl(Tree.PkgDecl pkgDecl, @Nullable Void input) {
    for (Tree.Anno anno : pkgDecl.annos()) {
      anno.accept(this, null);
      printLine();
    }
    append("package ").append(Joiner.on('.').join(pkgDecl.name())).append(';');
    return null;
  }

  @Override
  public @Nullable Void visitModDecl(ModDecl modDecl, @Nullable Void input) {
    for (Tree.Anno anno : modDecl.annos()) {
      anno.accept(this, null);
      printLine();
    }
    if (modDecl.open()) {
      append("open ");
    }
    append("module ").append(modDecl.moduleName()).append(" {");
    indent++;
    append('\n');
    for (ModDirective directive : modDecl.directives()) {
      directive.accept(this, null);
    }
    indent--;
    append("}\n");
    return null;
  }

  @Override
  public @Nullable Void visitModRequires(ModRequires modRequires, @Nullable Void input) {
    append("requires ");
    printModifiers(modRequires.mods());
    append(modRequires.moduleName());
    append(";");
    append('\n');
    return null;
  }

  @Override
  public @Nullable Void visitModExports(ModExports modExports, @Nullable Void input) {
    append("exports ");
    append(modExports.packageName().replace('/', '.'));
    if (!modExports.moduleNames().isEmpty()) {
      append(" to").append('\n');
      indent += 2;
      boolean first = true;
      for (String moduleName : modExports.moduleNames()) {
        if (!first) {
          append(',').append('\n');
        }
        append(moduleName);
        first = false;
      }
      indent -= 2;
    }
    append(";");
    append('\n');
    return null;
  }

  @Override
  public @Nullable Void visitModOpens(ModOpens modOpens, @Nullable Void input) {
    append("opens ");
    append(modOpens.packageName().replace('/', '.'));
    if (!modOpens.moduleNames().isEmpty()) {
      append(" to").append('\n');
      indent += 2;
      boolean first = true;
      for (String moduleName : modOpens.moduleNames()) {
        if (!first) {
          append(',').append('\n');
        }
        append(moduleName);
        first = false;
      }
      indent -= 2;
    }
    append(";");
    append('\n');
    return null;
  }

  @Override
  public @Nullable Void visitModUses(ModUses modUses, @Nullable Void input) {
    append("uses ");
    append(Joiner.on('.').join(modUses.typeName()));
    append(";");
    append('\n');
    return null;
  }

  @Override
  public @Nullable Void visitModProvides(ModProvides modProvides, @Nullable Void input) {
    append("provides ");
    append(Joiner.on('.').join(modProvides.typeName()));
    if (!modProvides.implNames().isEmpty()) {
      append(" with").append('\n');
      indent += 2;
      boolean first = true;
      for (ImmutableList<Ident> implName : modProvides.implNames()) {
        if (!first) {
          append(',').append('\n');
        }
        append(Joiner.on('.').join(implName));
        first = false;
      }
      indent -= 2;
    }
    append(";");
    append('\n');
    return null;
  }
}
