////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2018 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.utils;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import com.google.common.collect.ImmutableMap;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
import com.puppycrawl.tools.checkstyle.api.TextBlock;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtils;
import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtils;
import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo;

/**
 * Contains utility methods for working with Javadoc.
 * @author Lyle Hanson
 */
public final class JavadocUtils {

    /**
     * The type of Javadoc tag we want returned.
     */
    public enum JavadocTagType {

        /** Block type. */
        BLOCK,
        /** Inline type. */
        INLINE,
        /** All validTags. */
        ALL

    }

    /** Maps from a token name to value. */
    private static final ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE;
    /** Maps from a token value to name. */
    private static final String[] TOKEN_VALUE_TO_NAME;

    /** Exception message for unknown JavaDoc token id. */
    private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
            + " token id. Given id: ";

    /** Newline pattern. */
    private static final Pattern NEWLINE = Pattern.compile("\n");

    /** Return pattern. */
    private static final Pattern RETURN = Pattern.compile("\r");

    /** Tab pattern. */
    private static final Pattern TAB = Pattern.compile("\t");

    // Using reflection gets all token names and values from JavadocTokenTypes class
    // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections.
    static {
        final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();

        final Field[] fields = JavadocTokenTypes.class.getDeclaredFields();

        String[] tempTokenValueToName = CommonUtils.EMPTY_STRING_ARRAY;

        for (final Field field : fields) {
            // Only process public int fields.
            if (!Modifier.isPublic(field.getModifiers())
                    || field.getType() != Integer.TYPE) {
                continue;
            }

            final String name = field.getName();

            final int tokenValue = TokenUtils.getIntFromField(field, name);
            builder.put(name, tokenValue);
            if (tokenValue > tempTokenValueToName.length - 1) {
                final String[] temp = new String[tokenValue + 1];
                System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length);
                tempTokenValueToName = temp;
            }
            if (tokenValue == -1) {
                tempTokenValueToName[0] = name;
            }
            else {
                tempTokenValueToName[tokenValue] = name;
            }
        }

        TOKEN_NAME_TO_VALUE = builder.build();
        TOKEN_VALUE_TO_NAME = tempTokenValueToName;
    }

    /** Prevent instantiation. */
    private JavadocUtils() {
    }

    /**
     * Gets validTags from a given piece of Javadoc.
     * @param textBlock
     *        the Javadoc comment to process.
     * @param tagType
     *        the type of validTags we're interested in
     * @return all standalone validTags from the given javadoc.
     */
    public static JavadocTags getJavadocTags(TextBlock textBlock,
            JavadocTagType tagType) {
        final boolean getBlockTags = tagType == JavadocTagType.ALL
                                         || tagType == JavadocTagType.BLOCK;
        final boolean getInlineTags = tagType == JavadocTagType.ALL
                                          || tagType == JavadocTagType.INLINE;

        final List<TagInfo> tags = new ArrayList<>();

        if (getBlockTags) {
            tags.addAll(BlockTagUtils.extractBlockTags(textBlock.getText()));
        }

        if (getInlineTags) {
            tags.addAll(InlineTagUtils.extractInlineTags(textBlock.getText()));
        }

        final List<JavadocTag> validTags = new ArrayList<>();
        final List<InvalidJavadocTag> invalidTags = new ArrayList<>();

        for (TagInfo tag : tags) {
            final int col = tag.getPosition().getColumn();

            // Add the starting line of the comment to the line number to get the actual line number
            // in the source.
            // Lines are one-indexed, so need a off-by-one correction.
            final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1;

            if (JavadocTagInfo.isValidName(tag.getName())) {
                validTags.add(
                    new JavadocTag(line, col, tag.getName(), tag.getValue()));
            }
            else {
                invalidTags.add(new InvalidJavadocTag(line, col, tag.getName()));
            }
        }

        return new JavadocTags(validTags, invalidTags);
    }

    /**
     * Checks that commentContent starts with '*' javadoc comment identifier.
     * @param commentContent
     *        content of block comment
     * @return true if commentContent starts with '*' javadoc comment
     *         identifier.
     */
    public static boolean isJavadocComment(String commentContent) {
        boolean result = false;

        if (!commentContent.isEmpty()) {
            final char docCommentIdentifier = commentContent.charAt(0);
            result = docCommentIdentifier == '*';
        }

        return result;
    }

    /**
     * Checks block comment content starts with '*' javadoc comment identifier.
     * @param blockCommentBegin
     *        block comment AST
     * @return true if block comment content starts with '*' javadoc comment
     *         identifier.
     */
    public static boolean isJavadocComment(DetailAST blockCommentBegin) {
        final String commentContent = getBlockCommentContent(blockCommentBegin);
        return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
    }

    /**
     * Gets content of block comment.
     * @param blockCommentBegin
     *        block comment AST.
     * @return content of block comment.
     */
    private static String getBlockCommentContent(DetailAST blockCommentBegin) {
        final DetailAST commentContent = blockCommentBegin.getFirstChild();
        return commentContent.getText();
    }

    /**
     * Get content of Javadoc comment.
     * @param javadocCommentBegin
     *        Javadoc comment AST
     * @return content of Javadoc comment.
     */
    public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
        final DetailAST commentContent = javadocCommentBegin.getFirstChild();
        return commentContent.getText().substring(1);
    }

    /**
     * Returns the first child token that has a specified type.
     * @param detailNode
     *        Javadoc AST node
     * @param type
     *        the token type to match
     * @return the matching token, or null if no match
     */
    public static DetailNode findFirstToken(DetailNode detailNode, int type) {
        DetailNode returnValue = null;
        DetailNode node = getFirstChild(detailNode);
        while (node != null) {
            if (node.getType() == type) {
                returnValue = node;
                break;
            }
            node = getNextSibling(node);
        }
        return returnValue;
    }

    /**
     * Gets first child node of specified node.
     *
     * @param node DetailNode
     * @return first child
     */
    public static DetailNode getFirstChild(DetailNode node) {
        DetailNode resultNode = null;

        if (node.getChildren().length > 0) {
            resultNode = node.getChildren()[0];
        }
        return resultNode;
    }

    /**
     * Checks whether node contains any node of specified type among children on any deep level.
     *
     * @param node DetailNode
     * @param type token type
     * @return true if node contains any node of type type among children on any deep level.
     */
    public static boolean containsInBranch(DetailNode node, int type) {
        boolean result = true;
        DetailNode curNode = node;
        while (type != curNode.getType()) {
            DetailNode toVisit = getFirstChild(curNode);
            while (curNode != null && toVisit == null) {
                toVisit = getNextSibling(curNode);
                if (toVisit == null) {
                    curNode = curNode.getParent();
                }
            }

            if (curNode == toVisit) {
                result = false;
                break;
            }

            curNode = toVisit;
        }
        return result;
    }

    /**
     * Gets next sibling of specified node.
     *
     * @param node DetailNode
     * @return next sibling.
     */
    public static DetailNode getNextSibling(DetailNode node) {
        DetailNode nextSibling = null;
        final DetailNode parent = node.getParent();
        if (parent != null) {
            final int nextSiblingIndex = node.getIndex() + 1;
            final DetailNode[] children = parent.getChildren();
            if (nextSiblingIndex <= children.length - 1) {
                nextSibling = children[nextSiblingIndex];
            }
        }
        return nextSibling;
    }

    /**
     * Gets next sibling of specified node with the specified type.
     *
     * @param node DetailNode
     * @param tokenType javadoc token type
     * @return next sibling.
     */
    public static DetailNode getNextSibling(DetailNode node, int tokenType) {
        DetailNode nextSibling = getNextSibling(node);
        while (nextSibling != null && nextSibling.getType() != tokenType) {
            nextSibling = getNextSibling(nextSibling);
        }
        return nextSibling;
    }

    /**
     * Gets previous sibling of specified node.
     * @param node DetailNode
     * @return previous sibling
     */
    public static DetailNode getPreviousSibling(DetailNode node) {
        DetailNode previousSibling = null;
        final int previousSiblingIndex = node.getIndex() - 1;
        if (previousSiblingIndex >= 0) {
            final DetailNode parent = node.getParent();
            final DetailNode[] children = parent.getChildren();
            previousSibling = children[previousSiblingIndex];
        }
        return previousSibling;
    }

    /**
     * Returns the name of a token for a given ID.
     * @param id
     *        the ID of the token name to get
     * @return a token name
     */
    public static String getTokenName(int id) {
        final String name;
        if (id == JavadocTokenTypes.EOF) {
            name = "EOF";
        }
        else if (id > TOKEN_VALUE_TO_NAME.length - 1) {
            throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
        }
        else {
            name = TOKEN_VALUE_TO_NAME[id];
            if (name == null) {
                throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
            }
        }
        return name;
    }

    /**
     * Returns the ID of a token for a given name.
     * @param name
     *        the name of the token ID to get
     * @return a token ID
     */
    public static int getTokenId(String name) {
        final Integer id = TOKEN_NAME_TO_VALUE.get(name);
        if (id == null) {
            throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
        }
        return id;
    }

    /**
     * Gets tag name from javadocTagSection.
     *
     * @param javadocTagSection to get tag name from.
     * @return name, of the javadocTagSection's tag.
     */
    public static String getTagName(DetailNode javadocTagSection) {
        final String javadocTagName;
        if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
            javadocTagName = getNextSibling(
                    getFirstChild(javadocTagSection)).getText();
        }
        else {
            javadocTagName = getFirstChild(javadocTagSection).getText();
        }
        return javadocTagName;
    }

    /**
     * Replace all control chars with escaped symbols.
     * @param text the String to process.
     * @return the processed String with all control chars escaped.
     */
    public static String escapeAllControlChars(String text) {
        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
    }

    /**
     * Checks Javadoc comment it's in right place.
     * <p>From Javadoc util documentation:
     * "Placement of comments - Documentation comments are recognized only when placed
     * immediately before class, interface, constructor, method, field or annotation field
     * declarations -- see the class example, method example, and field example.
     * Documentation comments placed in the body of a method are ignored."</p>
     * <p>If there are many documentation comments per declaration statement,
     * only the last one will be recognized.</p>
     *
     * @param blockComment Block comment AST
     * @return true if Javadoc is in right place
     * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
     *     Javadoc util documentation</a>
     */
    private static boolean isCorrectJavadocPosition(DetailAST blockComment) {
        // We must be sure that after this one there are no other documentation comments.
        DetailAST sibling = blockComment.getNextSibling();
        while (sibling != null) {
            if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
                if (isJavadocComment(getBlockCommentContent(sibling))) {
                    // Found another javadoc comment, so this one should be ignored.
                    break;
                }
                sibling = sibling.getNextSibling();
            }
            else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
                sibling = sibling.getNextSibling();
            }
            else {
                // Annotation, declaration or modifier is here. Do not check further.
                sibling = null;
            }
        }
        return sibling == null
            && (BlockCommentPosition.isOnType(blockComment)
                || BlockCommentPosition.isOnMember(blockComment));
    }

}
