/*
 * Copyright 2014 - 2017 Cognizant Technology Solutions
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.cognizant.cognizantits.ide.main.utils.tree;

import com.cognizant.cognizantits.datalib.or.common.ORObjectInf;
import com.cognizant.cognizantits.ide.main.utils.Utils;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import static javax.swing.Action.ACTION_COMMAND_KEY;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 *
 * 
 */
public class TreeSearch extends JPanel implements ActionListener {

    JToolBar searchBar;
    JTextField searchField;
    JTree tree;

    public static TreeSearch installFor(JTree tree) {
        return new TreeSearch(tree);
    }

    public static TreeSearch installForOR(JTree tree) {
        return new TreeSearch(tree) {
            @Override
            public void selectAndSrollTo(TreeNode node) {
                if (node instanceof ORObjectInf) {
                    TreePath path = ((ORObjectInf) node).getTreePath();
                    tree.setSelectionPath(path);
                    tree.scrollPathToVisible(path);
                } else {
                    super.selectAndSrollTo(node);
                }
            }
        };
    }

    public TreeSearch(JTree tree) {
        this.tree = tree;
        init();
    }

    private void init() {
        setLayout(new BorderLayout());
        createToolBar();
        addSearchListener();
        addTreeListener();
        add(new JScrollPane(tree), BorderLayout.CENTER);
        add(searchBar, BorderLayout.SOUTH);
        searchBar.setVisible(false);
    }

    private void createToolBar() {
        searchBar = new JToolBar();
        searchBar.setFloatable(false);
        searchBar.setLayout(new BoxLayout(searchBar, BoxLayout.X_AXIS));
        searchBar.setBorder(BorderFactory.createEtchedBorder());

        JLabel searchLabel = new JLabel(Utils.getIconByResourceName("/ui/resources/search"));

        searchField = new JTextField();
        searchField.setActionCommand("SearchField");
        searchField.addActionListener(this);

        searchBar.add(searchLabel);
        searchBar.add(new javax.swing.Box.Filler(new java.awt.Dimension(5, 0),
                new java.awt.Dimension(5, 0),
                new java.awt.Dimension(5, 32767)));
        searchBar.add(searchField);

    }

    private void addSearchListener() {

        searchField.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent de) {
                search();
            }

            @Override
            public void removeUpdate(DocumentEvent de) {
                search();
            }

            @Override
            public void changedUpdate(DocumentEvent de) {
                search();
            }

        });

        searchField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("F3"), "Next");
        AbstractAction nextAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                TreeSearch.this.actionPerformed(ae);
            }
        };
        nextAction.putValue(ACTION_COMMAND_KEY, "Next");
        searchField.getActionMap().put("Next", nextAction);

        searchField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("shift F3"), "Previous");
        AbstractAction prevAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                TreeSearch.this.actionPerformed(ae);
            }
        };
        prevAction.putValue(ACTION_COMMAND_KEY, "Previous");
        searchField.getActionMap().put("Previous", prevAction);

        searchField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ESCAPE"), "Hide");
        searchField.getActionMap().put("Hide", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                searchBar.setVisible(false);
                tree.requestFocusInWindow();
            }
        });

        searchField.setToolTipText("<html>"
                + "Press <b>F3</b> to go to next search"
                + "<br/>"
                + "Press <b>Shift+F3</b> to go to previous search"
                + "<br/>"
                + "Press <b>Escape</b> to hide the searchBox"
                + "<br/>"
                //                + "To perfrom regex search add <b>$</b> before the search string"
                //                + "<br/>"
                + "</html>");
    }

    private void addTreeListener() {

        tree.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ctrl F"), "Search");

        tree.getActionMap().put("Search", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent ae) {
                searchBar.setVisible(true);
                searchField.requestFocusInWindow();
                searchField.selectAll();
            }
        });

        tree.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ESCAPE"), "Hide");

        tree.getActionMap().put("Hide", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent ae) {
                searchBar.setVisible(false);
                tree.requestFocusInWindow();
            }
        });

//        tree.addKeyListener(new KeyAdapter() {
//            @Override
//            public void keyTyped(KeyEvent ke) {
//                if (!searchBar.isVisible()) {
//                    searchBar.setVisible(true);
//                    searchField.setText("" + ke.getKeyChar());
//                    searchField.requestFocusInWindow();
//                }
//            }
//        });
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        switch (ae.getActionCommand()) {
            case "SearchField":
                searchBar.setVisible(false);
                break;
            case "Next":
                goToNext();
                break;
            case "Previous":
                goToPrevious();
                break;
        }
    }

    private void search() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                search(searchField.getText());
            }
        });
    }

    private void goToNext() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                goToNext(searchField.getText());
            }
        });
    }

    private void goToPrevious() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                goToPrevious(searchField.getText());
            }
        });
    }

    private TreeNode getSelectedNode() {
        TreePath path = tree.getSelectionPath();
        if (path != null) {
            return (TreeNode) path.getLastPathComponent();
        }
        return null;
    }

    private void search(String text) {
        TreeNode currNode = (TreeNode) tree.getModel().getRoot();
        while (currNode != null) {
            if (currNode.toString().contains(text)) {
                selectAndSrollTo(currNode);
                break;
            }
            currNode = getNextNode(currNode);
        }
    }

    private void goToNext(String text) {
        TreeNode currNode = getSelectedNode();
        if (currNode == null) {
            currNode = (TreeNode) tree.getModel().getRoot();
        } else {
            currNode = getNextNode(currNode);
        }
        while (currNode != null) {
            if (currNode.toString().contains(text)) {
                selectAndSrollTo(currNode);
                break;
            }
            currNode = getNextNode(currNode);
        }
    }

    private void goToPrevious(String text) {
        TreeNode currNode = getSelectedNode();
        if (currNode == null) {
            return;
        } else {
            currNode = getPreviousNode(currNode);
        }
        while (currNode != null) {
            if (currNode.toString().contains(text)) {
                selectAndSrollTo(currNode);
                break;
            }
            currNode = getPreviousNode(currNode);
        }
    }

    public void selectAndSrollTo(TreeNode node) {
        TreePath path = new TreePath(getPath(node));
        tree.setSelectionPath(path);
        tree.scrollPathToVisible(path);
    }

    /**
     * Returns the node that follows this node in a preorder traversal of this
     * node's tree. Returns null if this node is the last node of the traversal.
     * This is an inefficient way to traverse the entire tree; use an
     * enumeration, instead.
     *
     * @param node
     * @see #preorderEnumeration
     * @return the node that follows this node in a preorder traversal, or null
     * if this node is last
     */
    public TreeNode getNextNode(TreeNode node) {
        if (node.getChildCount() == 0) {
            // No children, so look for nextSibling
            TreeNode nextSibling = getNextSibling(node);

            if (nextSibling == null) {
                TreeNode aNode = node.getParent();

                do {
                    if (aNode == null) {
                        return null;
                    }

                    nextSibling = getNextSibling(aNode);
                    if (nextSibling != null) {
                        return nextSibling;
                    }

                    aNode = (TreeNode) aNode.getParent();
                } while (true);
            } else {
                return nextSibling;
            }
        } else {
            return (TreeNode) node.getChildAt(0);
        }
    }

    /**
     * Returns the next sibling of this node in the parent's children array.
     * Returns null if this node has no parent or is the parent's last child.
     * This method performs a linear search that is O(n) where n is the number
     * of children; to traverse the entire array, use the parent's child
     * enumeration instead.
     *
     * @param node
     * @see #children
     * @return the sibling of this node that immediately follows this node
     */
    public TreeNode getNextSibling(TreeNode node) {
        TreeNode retval;

        TreeNode myParent = node.getParent();

        if (myParent == null) {
            retval = null;
        } else {
            retval = getChildAfter(myParent, node);      // linear search
        }

        if (retval != null && !isNodeSibling(node, retval)) {
            //            throw new Error("child of parent is not a sibling");
        }

        return retval;
    }

    /**
     * Returns the child in this node's child array that immediately follows
     * <code>aChild</code>, which must be a child of this node. If
     * <code>aChild</code> is the last child, returns null. This method performs
     * a linear search of this node's children for <code>aChild</code> and is
     * O(n) where n is the number of children; to traverse the entire array of
     * children, use an enumeration instead.
     *
     * @param parent
     * @param aChild
     * @see #children
     * @exception IllegalArgumentException if <code>aChild</code> is null or is
     * not a child of this node
     * @return the child of this node that immediately follows
     * <code>aChild</code>
     */
    public TreeNode getChildAfter(TreeNode parent, TreeNode aChild) {
        if (aChild == null) {
            throw new IllegalArgumentException("argument is null");
        }

        int index = parent.getIndex(aChild);           // linear search

        if (index == -1) {
            throw new IllegalArgumentException("node is not a child");
        }

        if (index < parent.getChildCount() - 1) {
            return parent.getChildAt(index + 1);
        } else {
            return null;
        }
    }

    /**
     * Returns the node that precedes this node in a preorder traversal of this
     * node's tree. Returns <code>null</code> if this node is the first node of
     * the traversal -- the root of the tree. This is an inefficient way to
     * traverse the entire tree; use an enumeration, instead.
     *
     * @param node
     * @see #preorderEnumeration
     * @return the node that precedes this node in a preorder traversal, or null
     * if this node is the first
     */
    public TreeNode getPreviousNode(TreeNode node) {
        TreeNode previousSibling;
        TreeNode myParent = (TreeNode) node.getParent();

        if (myParent == null) {
            return null;
        }

        previousSibling = getPreviousSibling(node);

        if (previousSibling != null) {
            if (previousSibling.getChildCount() == 0) {
                return previousSibling;
            } else {
                return getLastLeaf(previousSibling);
            }
        } else {
            return myParent;
        }
    }

    /**
     * Returns the previous sibling of this node in the parent's children array.
     * Returns null if this node has no parent or is the parent's first child.
     * This method performs a linear search that is O(n) where n is the number
     * of children.
     *
     * @param node
     * @return the sibling of this node that immediately precedes this node
     */
    public TreeNode getPreviousSibling(TreeNode node) {
        TreeNode retval;

        TreeNode myParent = (TreeNode) node.getParent();

        if (myParent == null) {
            retval = null;
        } else {
            retval = getChildBefore(myParent, node);     // linear search
        }

        if (retval != null && !isNodeSibling(node, retval)) {
//            throw new Error("child of parent is not a sibling");
        }

        return retval;
    }

    /**
     * Returns the child in this node's child array that immediately precedes
     * <code>aChild</code>, which must be a child of this node. If
     * <code>aChild</code> is the first child, returns null. This method
     * performs a linear search of this node's children for <code>aChild</code>
     * and is O(n) where n is the number of children.
     *
     * @param parent
     * @param aChild
     * @exception IllegalArgumentException if <code>aChild</code> is null or is
     * not a child of this node
     * @return the child of this node that immediately precedes
     * <code>aChild</code>
     */
    public TreeNode getChildBefore(TreeNode parent, TreeNode aChild) {
        if (aChild == null) {
            throw new IllegalArgumentException("argument is null");
        }

        int index = parent.getIndex(aChild);           // linear search

        if (index == -1) {
            throw new IllegalArgumentException("argument is not a child");
        }

        if (index > 0) {
            return parent.getChildAt(index - 1);
        } else {
            return null;
        }
    }

    /**
     * Finds and returns the last leaf that is a descendant of this node --
     * either this node or its last child's last leaf. Returns this node if it
     * is a leaf.
     *
     * @param node
     * @see #isLeaf
     * @see #isNodeDescendant
     * @return the last leaf in the subtree rooted at this node
     */
    public TreeNode getLastLeaf(TreeNode node) {

        while (!node.isLeaf()) {
            node = getLastChild(node);
        }

        return node;
    }

    /**
     * Returns this node's last child. If this node has no children, throws
     * NoSuchElementException.
     *
     * @param node
     * @return the last child of this node
     */
    public TreeNode getLastChild(TreeNode node) {
        if (node.getChildCount() == 0) {
            throw new Error("node has no children");
        }
        return node.getChildAt(node.getChildCount() - 1);
    }

    /**
     * Returns true if <code>anotherNode</code> is a sibling of (has the same
     * parent as) this node. A node is its own sibling. If
     * <code>anotherNode</code> is null, returns false.
     *
     * @param node
     * @param anotherNode node to test as sibling of this node
     * @return true if <code>anotherNode</code> is a sibling of this node
     */
    public boolean isNodeSibling(TreeNode node, TreeNode anotherNode) {
        boolean retval;

        if (anotherNode == null) {
            retval = false;
        } else if (anotherNode == node) {
            retval = true;
        } else {
            TreeNode myParent = node.getParent();
            retval = (myParent != null && myParent == anotherNode.getParent());

            if (retval && !isNodeChild(node.getParent(), anotherNode)) {
                throw new Error("sibling has different parent");
            }
        }

        return retval;
    }

    /**
     * Returns true if <code>aNode</code> is a child of this node. If
     * <code>aNode</code> is null, this method returns false.
     *
     * @param parent
     * @param aNode
     * @return true if <code>aNode</code> is a child of this node; false if
     * <code>aNode</code> is null
     */
    public boolean isNodeChild(TreeNode parent, TreeNode aNode) {
        boolean retval;

        if (aNode == null) {
            retval = false;
        } else if (parent.getChildCount() == 0) {
            retval = false;
        } else {
            retval = (aNode.getParent() == parent);
        }

        return retval;
    }

    /**
     * Returns the path from the root, to get to this node. The last element in
     * the path is this node.
     *
     * @param node
     * @return an array of TreeNode objects giving the path, where the first
     * element in the path is the root and the last element is this node.
     */
    public TreeNode[] getPath(TreeNode node) {
        return getPathToRoot(node, 0);
    }

    /**
     * Builds the parents of node up to and including the root node, where the
     * original node is the last element in the returned array. The length of
     * the returned array gives the node's depth in the tree.
     *
     * @param aNode the TreeNode to get the path for
     * @param depth an int giving the number of steps already taken towards the
     * root (on recursive calls), used to size the returned array
     * @return an array of TreeNodes giving the path from the root to the
     * specified node
     */
    protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {
        TreeNode[] retNodes;

        /* Check for null, in case someone passed in a null node, or
           they passed in an element that isn't rooted at root. */
        if (aNode == null) {
            if (depth == 0) {
                return null;
            } else {
                retNodes = new TreeNode[depth];
            }
        } else {
            depth++;
            retNodes = getPathToRoot(aNode.getParent(), depth);
            retNodes[retNodes.length - depth] = aNode;
        }
        return retNodes;
    }

}