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

import java.util.HashMap;
import java.util.Map;

import antlr.ASTFactory;
import antlr.collections.AST;
import com.puppycrawl.tools.checkstyle.DetailAstImpl;
import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.gui.MainFrameModel.ParseMode;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;

/**
 * The model that backs the parse tree in the GUI.
 *
 */
public class ParseTreeTablePresentation {

    /** Exception message. */
    private static final String UNKNOWN_COLUMN_MSG = "Unknown column";

    /** Column names. */
    private static final String[] COLUMN_NAMES = {
        "Tree",
        "Type",
        "Line",
        "Column",
        "Text",
    };

    /**
     * The root node of the tree table model.
     */
    private final Object root;

    /** Cache to store already parsed Javadoc comments. Used for optimisation purposes. */
    private final Map<DetailAST, DetailNode> blockCommentToJavadocTree = new HashMap<>();

    /** Parsing mode. */
    private ParseMode parseMode;

    /**
     * Constructor initialise root node.
     * @param parseTree DetailAST parse tree.
     */
    public ParseTreeTablePresentation(DetailAST parseTree) {
        root = createArtificialTreeRoot();
        setParseTree(parseTree);
    }

    /**
     * Set parse tree.
     * @param parseTree DetailAST parse tree.
     */
    protected final void setParseTree(DetailAST parseTree) {
        ((AST) root).setFirstChild((AST) parseTree);
    }

    /**
     * Set parse mode.
     * @param mode ParseMode enum
     */
    protected void setParseMode(ParseMode mode) {
        parseMode = mode;
    }

    /**
     * Returns number of available columns.
     * @return the number of available columns.
     */
    public int getColumnCount() {
        return COLUMN_NAMES.length;
    }

    /**
     * Returns name for specified column number.
     * @param column the column number
     * @return the name for column number {@code column}.
     */
    public String getColumnName(int column) {
        return COLUMN_NAMES[column];
    }

    /**
     * Returns type of specified column number.
     * @param column the column number
     * @return the type for column number {@code column}.
     */
    // -@cs[ForbidWildcardAsReturnType] We need to satisfy javax.swing.table.AbstractTableModel
    // public Class<?> getColumnClass(int columnIndex) {...}
    public Class<?> getColumnClass(int column) {
        final Class<?> columnClass;

        switch (column) {
            case 0:
                columnClass = ParseTreeTableModel.class;
                break;
            case 1:
            case 4:
                columnClass = String.class;
                break;
            case 2:
            case 3:
                columnClass = Integer.class;
                break;
            default:
                throw new IllegalStateException(UNKNOWN_COLUMN_MSG);
        }
        return columnClass;
    }

    /**
     * Returns the value to be displayed for node at column number.
     * @param node the node
     * @param column the column number
     * @return the value to be displayed for node {@code node}, at column number {@code column}.
     */
    public Object getValueAt(Object node, int column) {
        final Object result;

        if (node instanceof DetailNode) {
            result = getValueAtDetailNode((DetailNode) node, column);
        }
        else {
            result = getValueAtDetailAST((DetailAST) node, column);
        }

        return result;
    }

    /**
     * Returns the child of parent at index.
     * @param parent the node to get a child from.
     * @param index the index of a child.
     * @return the child of parent at index.
     */
    public Object getChild(Object parent, int index) {
        final Object result;

        if (parent instanceof DetailNode) {
            result = ((DetailNode) parent).getChildren()[index];
        }
        else {
            result = getChildAtDetailAst((DetailAST) parent, index);
        }

        return result;
    }

    /**
     * Returns the number of children of parent.
     * @param parent the node to count children for.
     * @return the number of children of the node parent.
     */
    public int getChildCount(Object parent) {
        final int result;

        if (parent instanceof DetailNode) {
            result = ((DetailNode) parent).getChildren().length;
        }
        else {
            if (parseMode == ParseMode.JAVA_WITH_JAVADOC_AND_COMMENTS
                    && ((AST) parent).getType() == TokenTypes.COMMENT_CONTENT
                    && JavadocUtil.isJavadocComment(((DetailAST) parent).getParent())) {
                //getChildCount return 0 on COMMENT_CONTENT,
                //but we need to attach javadoc tree, that is separate tree
                result = 1;
            }
            else {
                result = ((DetailAST) parent).getChildCount();
            }
        }

        return result;
    }

    /**
     * Returns value of root.
     * @return the root.
     */
    public Object getRoot() {
        return root;
    }

    /**
     * Whether the node is a leaf.
     * @param node the node to check.
     * @return true if the node is a leaf.
     */
    public boolean isLeaf(Object node) {
        return getChildCount(node) == 0;
    }

    /**
     * Return the index of child in parent.  If either {@code parent}
     * or {@code child} is {@code null}, returns -1.
     * If either {@code parent} or {@code child} don't
     * belong to this tree model, returns -1.
     *
     * @param parent a node in the tree, obtained from this data source.
     * @param child the node we are interested in.
     * @return the index of the child in the parent, or -1 if either
     *     {@code child} or {@code parent} are {@code null}
     *     or don't belong to this tree model.
     */
    public int getIndexOfChild(Object parent, Object child) {
        int index = -1;
        for (int i = 0; i < getChildCount(parent); i++) {
            if (getChild(parent, i).equals(child)) {
                index = i;
                break;
            }
        }
        return index;
    }

    /**
     * Indicates whether the the value for node {@code node}, at column number {@code column} is
     * editable.
     * @param column the column number
     * @return true if editable
     */
    public boolean isCellEditable(int column) {
        return false;
    }

    /**
     * Creates artificial tree root.
     * @return artificial tree root.
     */
    private static DetailAST createArtificialTreeRoot() {
        final ASTFactory factory = new ASTFactory();
        factory.setASTNodeClass(DetailAstImpl.class.getName());
        return (DetailAST) factory.create(TokenTypes.EOF, "ROOT");
    }

    /**
     * Gets child of DetailAST node at specified index.
     * @param parent DetailAST node
     * @param index child index
     * @return child DetailsAST or DetailNode if child is Javadoc node
     *         and parseMode is JAVA_WITH_JAVADOC_AND_COMMENTS.
     */
    private Object getChildAtDetailAst(DetailAST parent, int index) {
        final Object result;
        if (parseMode == ParseMode.JAVA_WITH_JAVADOC_AND_COMMENTS
                && parent.getType() == TokenTypes.COMMENT_CONTENT
                && JavadocUtil.isJavadocComment(parent.getParent())) {
            result = getJavadocTree(parent.getParent());
        }
        else {
            int currentIndex = 0;
            DetailAST child = parent.getFirstChild();
            while (currentIndex < index) {
                child = child.getNextSibling();
                currentIndex++;
            }
            result = child;
        }

        return result;
    }

    /**
     * Gets a value for DetailNode object.
     * @param node DetailNode(Javadoc) node.
     * @param column column index.
     * @return value at specified column.
     */
    private static Object getValueAtDetailNode(DetailNode node, int column) {
        final Object value;

        switch (column) {
            case 0:
                // first column is tree model. no value needed
                value = null;
                break;
            case 1:
                value = JavadocUtil.getTokenName(node.getType());
                break;
            case 2:
                value = node.getLineNumber();
                break;
            case 3:
                value = node.getColumnNumber();
                break;
            case 4:
                value = node.getText();
                break;
            default:
                throw new IllegalStateException(UNKNOWN_COLUMN_MSG);
        }
        return value;
    }

    /**
     * Gets a value for DetailAST object.
     * @param ast DetailAST node.
     * @param column column index.
     * @return value at specified column.
     */
    private static Object getValueAtDetailAST(DetailAST ast, int column) {
        final Object value;

        switch (column) {
            case 0:
                // first column is tree model. no value needed
                value = null;
                break;
            case 1:
                value = TokenUtil.getTokenName(ast.getType());
                break;
            case 2:
                value = ast.getLineNo();
                break;
            case 3:
                value = ast.getColumnNo();
                break;
            case 4:
                value = ast.getText();
                break;
            default:
                throw new IllegalStateException(UNKNOWN_COLUMN_MSG);
        }
        return value;
    }

    /**
     * Gets Javadoc (DetailNode) tree of specified block comments.
     * @param blockComment Javadoc comment as a block comment
     * @return root of DetailNode tree
     */
    private DetailNode getJavadocTree(DetailAST blockComment) {
        return blockCommentToJavadocTree.computeIfAbsent(blockComment,
                ParseTreeTablePresentation::parseJavadocTree);
    }

    /**
     * Parses Javadoc (DetailNode) tree of specified block comments.
     * @param blockComment Javadoc comment as a block comment
     * @return root of DetailNode tree
     */
    private static DetailNode parseJavadocTree(DetailAST blockComment) {
        return new JavadocDetailNodeParser().parseJavadocAsDetailNode(blockComment).getTree();
    }

}
