////////////////////////////////////////////////////////////////////////////////
// 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.api;

import java.util.BitSet;

import antlr.CommonASTWithHiddenTokens;
import antlr.Token;
import antlr.collections.AST;
import com.puppycrawl.tools.checkstyle.utils.TokenUtils;

/**
 * An extension of the CommonAST that records the line and column number.
 *
 * @author Oliver Burn
 * @author lkuehne
 * @see <a href="http://www.antlr.org/">ANTLR Website</a>
 */
public final class DetailAST extends CommonASTWithHiddenTokens {
    private static final long serialVersionUID = -2580884815577559874L;

    /** Constant to indicate if not calculated the child count. */
    private static final int NOT_INITIALIZED = Integer.MIN_VALUE;

    /** The line number. **/
    private int lineNo = NOT_INITIALIZED;
    /** The column number. **/
    private int columnNo = NOT_INITIALIZED;

    /** Number of children. */
    private int childCount = NOT_INITIALIZED;
    /** The parent token. */
    private DetailAST parent;
    /** Previous sibling. */
    private DetailAST previousSibling;

    /**
     * All token types in this branch.
     * Token 'x' (where x is an int) is in this branch
     * if branchTokenTypes.get(x) is true.
     */
    private BitSet branchTokenTypes;

    @Override
    public void initialize(Token tok) {
        super.initialize(tok);
        lineNo = tok.getLine();

        // expect columns to start @ 0
        columnNo = tok.getColumn() - 1;
    }

    @Override
    public void initialize(AST ast) {
        final DetailAST detailAst = (DetailAST) ast;
        setText(detailAst.getText());
        setType(detailAst.getType());
        lineNo = detailAst.getLineNo();
        columnNo = detailAst.getColumnNo();
        hiddenAfter = detailAst.getHiddenAfter();
        hiddenBefore = detailAst.getHiddenBefore();
    }

    @Override
    public void setFirstChild(AST ast) {
        clearBranchTokenTypes();
        clearChildCountCache(this);
        super.setFirstChild(ast);
        if (ast != null) {
            ((DetailAST) ast).setParent(this);
        }
    }

    @Override
    public void setNextSibling(AST ast) {
        clearBranchTokenTypes();
        clearChildCountCache(parent);
        super.setNextSibling(ast);
        if (ast != null && parent != null) {
            ((DetailAST) ast).setParent(parent);
        }
        if (ast != null) {
            ((DetailAST) ast).previousSibling = this;
        }
    }

    /**
     * Add previous sibling.
     * @param ast
     *        DetailAST object.
     */
    public void addPreviousSibling(DetailAST ast) {
        clearBranchTokenTypes();
        clearChildCountCache(parent);
        if (ast != null) {
            ast.setParent(parent);
            final DetailAST previousSiblingNode = previousSibling;

            if (previousSiblingNode != null) {
                ast.previousSibling = previousSiblingNode;
                previousSiblingNode.setNextSibling(ast);
            }
            else if (parent != null) {
                parent.setFirstChild(ast);
            }

            ast.setNextSibling(this);
            previousSibling = ast;
        }
    }

    /**
     * Add next sibling.
     * @param ast
     *        DetailAST object.
     */
    public void addNextSibling(DetailAST ast) {
        clearBranchTokenTypes();
        clearChildCountCache(parent);
        if (ast != null) {
            ast.setParent(parent);
            final DetailAST nextSibling = getNextSibling();

            if (nextSibling != null) {
                ast.setNextSibling(nextSibling);
                nextSibling.previousSibling = ast;
            }

            ast.previousSibling = this;
            setNextSibling(ast);
        }
    }

    @Override
    public void addChild(AST ast) {
        clearBranchTokenTypes();
        clearChildCountCache(this);
        super.addChild(ast);
        if (ast != null) {
            ((DetailAST) ast).setParent(this);
            getFirstChild().setParent(this);
        }
    }

    /**
     * Returns the number of child nodes one level below this node. That is is
     * does not recurse down the tree.
     * @return the number of child nodes
     */
    public int getChildCount() {
        // lazy init
        if (childCount == NOT_INITIALIZED) {
            childCount = 0;
            AST child = getFirstChild();

            while (child != null) {
                childCount += 1;
                child = child.getNextSibling();
            }
        }
        return childCount;
    }

    /**
     * Returns the number of direct child tokens that have the specified type.
     * @param type the token type to match
     * @return the number of matching token
     */
    public int getChildCount(int type) {
        int count = 0;
        for (AST ast = getFirstChild(); ast != null; ast = ast.getNextSibling()) {
            if (ast.getType() == type) {
                count++;
            }
        }
        return count;
    }

    /**
     * Set the parent token.
     * @param parent the parent token
     */
    private void setParent(DetailAST parent) {
        clearBranchTokenTypes();
        this.parent = parent;
        final DetailAST nextSibling = getNextSibling();
        if (nextSibling != null) {
            nextSibling.setParent(parent);
            nextSibling.previousSibling = this;
        }
    }

    /**
     * Returns the parent token.
     * @return the parent token
     */
    public DetailAST getParent() {
        return parent;
    }

    /**
     * Gets line number.
     * @return the line number
     */
    public int getLineNo() {
        int resultNo = -1;

        if (lineNo == NOT_INITIALIZED) {
            // an inner AST that has been initialized
            // with initialize(String text)
            resultNo = findLineNo(getFirstChild());

            if (resultNo < 0) {
                resultNo = findLineNo(getNextSibling());
            }
        }
        if (resultNo < 0) {
            resultNo = lineNo;
        }
        return resultNo;
    }

    /**
     * Set line number.
     * @param lineNo
     *        line number.
     */
    public void setLineNo(int lineNo) {
        this.lineNo = lineNo;
    }

    /**
     * Gets column number.
     * @return the column number
     */
    public int getColumnNo() {
        int resultNo = -1;

        if (columnNo == NOT_INITIALIZED) {
            // an inner AST that has been initialized
            // with initialize(String text)
            resultNo = findColumnNo(getFirstChild());

            if (resultNo < 0) {
                resultNo = findColumnNo(getNextSibling());
            }
        }
        if (resultNo < 0) {
            resultNo = columnNo;
        }
        return resultNo;
    }

    /**
     * Set column number.
     * @param columnNo
     *        column number.
     */
    public void setColumnNo(int columnNo) {
        this.columnNo = columnNo;
    }

    /**
     * Gets the last child node.
     * @return the last child node
     */
    public DetailAST getLastChild() {
        DetailAST ast = getFirstChild();
        while (ast != null && ast.getNextSibling() != null) {
            ast = ast.getNextSibling();
        }
        return ast;
    }

    /**
     * Finds column number in the first non-comment node.
     *
     * @param ast DetailAST node.
     * @return Column number if non-comment node exists, -1 otherwise.
     */
    private static int findColumnNo(DetailAST ast) {
        int resultNo = -1;
        DetailAST node = ast;
        while (node != null) {
            // comment node can't be start of any java statement/definition
            if (TokenUtils.isCommentType(node.getType())) {
                node = node.getNextSibling();
            }
            else {
                resultNo = node.getColumnNo();
                break;
            }
        }
        return resultNo;
    }

    /**
     * Finds line number in the first non-comment node.
     *
     * @param ast DetailAST node.
     * @return Line number if non-comment node exists, -1 otherwise.
     */
    private static int findLineNo(DetailAST ast) {
        int resultNo = -1;
        DetailAST node = ast;
        while (node != null) {
            // comment node can't be start of any java statement/definition
            if (TokenUtils.isCommentType(node.getType())) {
                node = node.getNextSibling();
            }
            else {
                resultNo = node.getLineNo();
                break;
            }
        }
        return resultNo;
    }

    /**
     * @return the token types that occur in the branch as a sorted set.
     */
    private BitSet getBranchTokenTypes() {
        // lazy init
        if (branchTokenTypes == null) {

            branchTokenTypes = new BitSet();
            branchTokenTypes.set(getType());

            // add union of all children
            DetailAST child = getFirstChild();
            while (child != null) {
                final BitSet childTypes = child.getBranchTokenTypes();
                branchTokenTypes.or(childTypes);

                child = child.getNextSibling();
            }
        }
        return branchTokenTypes;
    }

    /**
     * Checks if this branch of the parse tree contains a token
     * of the provided type.
     * @param type a TokenType
     * @return true if and only if this branch (including this node)
     *     contains a token of type {@code type}.
     */
    public boolean branchContains(int type) {
        return getBranchTokenTypes().get(type);
    }

    /**
     * Returns the previous sibling or null if no such sibling exists.
     * @return the previous sibling or null if no such sibling exists.
     */
    public DetailAST getPreviousSibling() {
        return previousSibling;
    }

    /**
     * Returns the first child token that makes a specified type.
     * @param type the token type to match
     * @return the matching token, or null if no match
     */
    public DetailAST findFirstToken(int type) {
        DetailAST returnValue = null;
        for (DetailAST ast = getFirstChild(); ast != null; ast = ast.getNextSibling()) {
            if (ast.getType() == type) {
                returnValue = ast;
                break;
            }
        }
        return returnValue;
    }

    @Override
    public String toString() {
        return super.toString() + "[" + getLineNo() + "x" + getColumnNo() + "]";
    }

    @Override
    public DetailAST getNextSibling() {
        return (DetailAST) super.getNextSibling();
    }

    @Override
    public DetailAST getFirstChild() {
        return (DetailAST) super.getFirstChild();
    }

    /**
     * Clears the child count for the ast instance.
     * @param ast The ast to clear.
     */
    private static void clearChildCountCache(DetailAST ast) {
        if (ast != null) {
            ast.childCount = NOT_INITIALIZED;
        }
    }

    /**
     * Clears branchTokenTypes cache for all parents of the current DetailAST instance, and the
     * child count for the current DetailAST instance.
     */
    private void clearBranchTokenTypes() {
        DetailAST prevParent = getParent();
        while (prevParent != null) {
            prevParent.branchTokenTypes = null;
            prevParent = prevParent.getParent();
        }
    }
}
