////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import java.io.File;
import java.io.IOException;

import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;

/**
 * Parses file as javadoc DetailNode tree and prints to system output stream.
 * @author bizmailov
 */
public final class DetailNodeTreeStringPrinter {

    /** OS specific line separator. */
    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /** Symbols with which javadoc starts. */
    private static final String JAVADOC_START = "/**";

    /** Prevent instances. */
    private DetailNodeTreeStringPrinter() {
        // no code
    }

    /**
     * Parse a file and print the parse tree.
     * @param file the file to print.
     * @return parse tree as a string
     * @throws IOException if the file could not be read.
     */
    public static String printFileAst(File file) throws IOException {
        return printTree(parseFile(file), "", "");
    }

    /**
     * Parse block comment DetailAST as Javadoc DetailNode tree.
     * @param blockComment DetailAST
     * @return DetailNode tree
     */
    public static DetailNode parseJavadocAsDetailNode(DetailAST blockComment) {
        final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
        final ParseStatus status = parser.parseJavadocAsDetailNode(blockComment);
        if (status.getParseErrorMessage() != null) {
            throw new IllegalArgumentException(getParseErrorMessage(status.getParseErrorMessage()));
        }
        return status.getTree();
    }

    /**
     * Parse javadoc comment to DetailNode tree.
     * @param javadocComment javadoc comment content
     * @return tree
     */
    private static DetailNode parseJavadocAsDetailNode(String javadocComment) {
        final DetailAST blockComment = createFakeBlockComment(javadocComment);
        return parseJavadocAsDetailNode(blockComment);
    }

    /**
     * Builds error message base on ParseErrorMessage's message key, its arguments, etc.
     * @param parseErrorMessage ParseErrorMessage
     * @return error message
     */
    private static String getParseErrorMessage(ParseErrorMessage parseErrorMessage) {
        final LocalizedMessage lmessage = new LocalizedMessage(
                parseErrorMessage.getLineNumber(),
                "com.puppycrawl.tools.checkstyle.checks.javadoc.messages",
                parseErrorMessage.getMessageKey(),
                parseErrorMessage.getMessageArguments(),
                "",
                DetailNodeTreeStringPrinter.class,
                null);
        return "[ERROR:" + parseErrorMessage.getLineNumber() + "] " + lmessage.getMessage();
    }

    /**
     * Print AST.
     * @param ast the root AST node.
     * @param rootPrefix prefix for the root node
     * @param prefix prefix for other nodes
     * @return string AST.
     */
    public static String printTree(DetailNode ast, String rootPrefix, String prefix) {
        final StringBuilder messageBuilder = new StringBuilder();
        DetailNode node = ast;
        while (node != null) {
            if (node.getType() == JavadocTokenTypes.JAVADOC) {
                messageBuilder.append(rootPrefix);
            }
            else {
                messageBuilder.append(prefix);
            }
            messageBuilder.append(getIndentation(node))
                    .append(JavadocUtils.getTokenName(node.getType())).append(" -> ")
                    .append(JavadocUtils.excapeAllControlChars(node.getText())).append(" [")
                    .append(node.getLineNumber()).append(':').append(node.getColumnNumber())
                    .append(']').append(LINE_SEPARATOR)
                    .append(printTree(JavadocUtils.getFirstChild(node), rootPrefix, prefix));
            node = JavadocUtils.getNextSibling(node);
        }
        return messageBuilder.toString();
    }

    /**
     * Get indentation for a node.
     * @param node the DetailNode to get the indentation for.
     * @return the indentation in String format.
     */
    private static String getIndentation(DetailNode node) {
        final boolean isLastChild = JavadocUtils.getNextSibling(node) == null;
        DetailNode currentNode = node;
        final StringBuilder indentation = new StringBuilder();
        while (currentNode.getParent() != null) {
            currentNode = currentNode.getParent();
            if (currentNode.getParent() == null) {
                if (isLastChild) {
                    // only ASCII symbols must be used due to
                    // problems with running tests on Windows
                    indentation.append("`--");
                }
                else {
                    indentation.append("|--");
                }
            }
            else {
                if (JavadocUtils.getNextSibling(currentNode) == null) {
                    indentation.insert(0, "    ");
                }
                else {
                    indentation.insert(0, "|   ");
                }
            }
        }
        return indentation.toString();
    }

    /**
     * Parse a file and return the parse tree.
     * @param file the file to parse.
     * @return the root node of the parse tree.
     * @throws IOException if the file could not be read.
     */
    private static DetailNode parseFile(File file) throws IOException {
        // Details: https://github.com/checkstyle/checkstyle/issues/3034
        //noinspection MismatchedQueryAndUpdateOfCollection
        final FileText text = new FileText(file.getAbsoluteFile(),
            System.getProperty("file.encoding", "UTF-8"));
        return parseJavadocAsDetailNode(text.getFullText().toString());
    }

    /**
     * Creates DetailAST block comment to pass it to the Javadoc parser.
     * @param content comment content.
     * @return DetailAST block comment
     */
    private static DetailAST createFakeBlockComment(String content) {
        final DetailAST blockCommentBegin = new DetailAST();
        blockCommentBegin.setType(TokenTypes.BLOCK_COMMENT_BEGIN);
        blockCommentBegin.setText("/*");
        blockCommentBegin.setLineNo(0);
        blockCommentBegin.setColumnNo(-JAVADOC_START.length());

        final DetailAST commentContent = new DetailAST();
        commentContent.setType(TokenTypes.COMMENT_CONTENT);
        commentContent.setText("*" + content);
        commentContent.setLineNo(0);
        // javadoc should starts at 0 column, so COMMENT_CONTENT node
        // that contains javadoc identificator has -1 column
        commentContent.setColumnNo(-1);

        final DetailAST blockCommentEnd = new DetailAST();
        blockCommentEnd.setType(TokenTypes.BLOCK_COMMENT_END);
        blockCommentEnd.setText("*/");

        blockCommentBegin.setFirstChild(commentContent);
        commentContent.setNextSibling(blockCommentEnd);
        return blockCommentBegin;
    }

}
