package com.github.jknack.antlr4ide.runtime;

import java.io.File;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;

import org.antlr.v4.Tool;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.LexerInterpreter;
import org.antlr.v4.runtime.ParserInterpreter;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.misc.Nullable;
import org.antlr.v4.runtime.misc.Utils;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.RuleNode;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.v4.runtime.tree.Tree;
import org.antlr.v4.tool.Grammar;
import org.antlr.v4.tool.LexerGrammar;
import org.antlr.v4.tool.Rule;
import org.antlr.v4.tool.ast.GrammarRootAST;

public class ParseTreeCommand {

  private PrintWriter out;

  public ParseTreeCommand(final PrintWriter out) {
    this.out = out;
  }

  public String run(final String grammarFileName, final String lexerFileName,
      final String outdir, final List<String> imports, final String startRule,
      final String inputText) {
    Tool antlr = new Tool();
    if (imports.size() > 0) {
      antlr.inputDirectory = new File(imports.iterator().next()).getParentFile();
    }
    antlr.libDirectory = outdir;

    // load to examine it
    Grammar g = loadGrammar(antlr, grammarFileName, null);

    final LexerGrammar lg;
    if (g instanceof LexerGrammar) {
      lg = (LexerGrammar) g;
    } else if (lexerFileName != null) {
      lg = (LexerGrammar) antlr.loadGrammar(lexerFileName);
      g = loadGrammar(antlr, grammarFileName, lg);
    } else {
      // combined?
      lg = g.getImplicitLexer();
    }

    final String gname = g.name;
    BaseErrorListener printError = new BaseErrorListener() {
      @Override
      public void syntaxError(final Recognizer<?, ?> recognizer, final Object offendingSymbol,
          final int line, final int position, final String msg,
          final RecognitionException e) {
        out.println(gname + "::" + startRule + ":" + line + ":" + position + ": " + msg);
      }
    };

    ANTLRInputStream input = new ANTLRInputStream(inputText);
    LexerInterpreter lexEngine = (lg == null? g : lg).createLexerInterpreter(input);

    lexEngine.removeErrorListeners();
    lexEngine.addErrorListener(printError);

    CommonTokenStream tokens = new CommonTokenStream(lexEngine);

    ParserInterpreter parser = g.createParserInterpreter(tokens);
    parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);
    parser.removeErrorListeners();
    parser.addErrorListener(printError);
    Rule start = g.getRule(startRule);
    ParseTree tree = parser.parse(start.index);

    // this loop works around a bug in ANTLR 4.2
    // https://github.com/antlr/antlr4/issues/461
    // https://github.com/antlr/intellij-plugin-v4/issues/23
    while (tree.getParent() != null) {
      tree = tree.getParent();
    }
    String sexpression = toStringTree(tree, Arrays.asList(parser.getRuleNames())).trim();

    return sexpression;
  }

  /** Same as loadGrammar(fileName) except import vocab from existing lexer */
  private Grammar loadGrammar(final Tool tool, final String fileName,
      final LexerGrammar lexerGrammar) {
    GrammarRootAST grammarRootAST = tool.parseGrammar(fileName);
    final Grammar g = tool.createGrammar(grammarRootAST);
    g.fileName = fileName;
    if (lexerGrammar != null) {
      g.importVocab(lexerGrammar);
    }
    tool.process(g, false);
    return g;
  }

  private String toStringTree(@NotNull final Tree t, @Nullable final List<String> ruleNames) {
    if (t.getChildCount() == 0) {
      return Utils.escapeWhitespace(getNodeText(t, ruleNames), true);
    }
    StringBuilder buf = new StringBuilder();
    buf.append(" ( ");
    String s = Utils.escapeWhitespace(getNodeText(t, ruleNames), true);
    buf.append(s);
    buf.append(' ');
    for (int i = 0; i < t.getChildCount(); i++) {
      if (i > 0) {
        buf.append(' ');
      }
      buf.append(toStringTree(t.getChild(i), ruleNames));
    }
    buf.append(" ) ");
    return buf.toString();
  }

  private String getNodeText(@NotNull final Tree t, @Nullable final List<String> ruleNames) {
    if (ruleNames != null) {
      if (t instanceof RuleNode) {
        int ruleIndex = ((RuleNode) t).getRuleContext().getRuleIndex();
        String ruleName = ruleNames.get(ruleIndex);
        return ruleName;
      }
      else if (t instanceof ErrorNode) {
        return "<" + t.toString() + ">";
      }
      else if (t instanceof TerminalNode) {
        Token symbol = ((TerminalNode) t).getSymbol();
        if (symbol != null) {
          String s = symbol.getText();
          return "'" + s + "'";
        }
      }
    }
    // no recog for rule names
    Object payload = t.getPayload();
    if (payload instanceof Token) {
      return ((Token) payload).getText();
    }
    return t.getPayload().toString();
  }
}
