/*-------------------------------------------------------------------------------
Copyright (c)2000 TogetherSoft Corporation. Patents pending. All rights reserved.
-------------------------------------------------------------------------------*/
package com.togethersoft.modules.showcalltree;

import com.togethersoft.openapi.ide.IdeScript;
import com.togethersoft.openapi.ide.IdeStartup;
import com.togethersoft.openapi.ide.IdeContext;
import com.togethersoft.openapi.ide.IdeAccess;
import com.togethersoft.openapi.ide.message.IdeMessageManagerAccess;
import com.togethersoft.openapi.ide.message.IdeMessageType;
import com.togethersoft.openapi.ide.message.IdeMessagePage;
import com.togethersoft.openapi.ide.command.IdeCommandItem;
import com.togethersoft.openapi.ide.command.IdeCommandManager;
import com.togethersoft.openapi.ide.command.IdeCommandManagerAccess;
import com.togethersoft.openapi.ide.command.IdeCommandListener;
import com.togethersoft.openapi.ide.command.IdeCommandConstraints;
import com.togethersoft.openapi.ide.command.IdeCommandEvent;
import com.togethersoft.openapi.ide.resource.IdeResourceIconType;
import com.togethersoft.openapi.ide.project.IdeProjectManagerAccess;
import com.togethersoft.openapi.ide.editor.IdeEditorManager;
import com.togethersoft.openapi.ide.editor.IdeEditor;
import com.togethersoft.openapi.ide.editor.IdeEditorPositionConverter;
import com.togethersoft.openapi.ide.editor.IdeEditorSelectionType;
import com.togethersoft.openapi.ide.editor.IdeEditorVisibleArea;
import com.togethersoft.openapi.ide.editor.IdeEditorSelection;
import com.togethersoft.openapi.rwi.RwiElement;
import com.togethersoft.openapi.rwi.RwiModelAccess;
import com.togethersoft.openapi.sci.SciFunction;
import com.togethersoft.openapi.sci.SciOperation;
import com.togethersoft.openapi.sci.SciModelAccess;
import com.togethersoft.openapi.sci.SciClass;
import com.togethersoft.openapi.sci.SciCodeBlock;
import com.togethersoft.openapi.sci.SciFunctionCallExpression;
import com.togethersoft.openapi.sci.SciExpression;
import com.togethersoft.openapi.sci.SciMemberAccessExpression;
import com.togethersoft.openapi.sci.SciReferenceExpression;
import com.togethersoft.openapi.sci.SciElement;
import com.togethersoft.openapi.sci.SciVariable;
import com.togethersoft.openapi.sci.SciProperty;
import com.togethersoft.openapi.sci.visitor.SciExpressionVisitorAdapter;
import com.togethersoft.openapi.util.text.TextPosition;
import java.util.Vector;
import java.util.Hashtable;
import java.awt.Component;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.event.TreeExpansionEvent;

/**
 * This sample module shows the call tree of the selected operation.
 * The list of method calls done within selected operation is displayed as a
 * treeview in the separate tab of the bottom pane. Each call to a method is
 * represented with a node. Each call to a method with available source file
 * is displayed as an expandable node, if there are calls made from this method.
 * Selecting a line with such method also positions text editor to it.
 * The treeview is implemented as JTree with redefined cell renderer component.
 *
 * @author TogetherSoft
 */
public class ShowCallTree implements IdeScript, IdeStartup
{
   /**
    * Runs this module.
    *
    * @param context the IdeContext instance containing
    * the selection information at the moment the module was called.
    */
    public void run(IdeContext context)
    {
        // checking if project is open
        if (IdeProjectManagerAccess.getProjectManager().getActiveProject() == null)
        {
            // show modal window with error description
            IdeMessageManagerAccess.printMessage
                (IdeMessageType.ERROR_MODAL, "No open project.");
            return;
        }

        // the module can only be executed with one or more selected elements
        // getting array of selected elements
        RwiElement[] selectedRwiElements = context.getRwiElements();

        // if there are not selected elements
        if (selectedRwiElements == null || selectedRwiElements.length == 0)
        {
            IdeMessageManagerAccess.printMessage
                (IdeMessageType.ERROR_MODAL, "No selection was made.");
            return;
        }

        // processing each selected element
        for (int i = 0; i < selectedRwiElements.length; i++)
        {
            // getting code from element
            Object selectedElement = selectedRwiElements[i].getCodeElement();

            // if element does not contain code
            if (selectedElement == null)
            {
                IdeMessageManagerAccess.printMessage
                    (IdeMessageType.ERROR, "Operation not found.");
                continue;
            }

            // if element is not operation
            if (!(selectedElement instanceof SciOperation))
            {
                IdeMessageManagerAccess.printMessage
                    (IdeMessageType.ERROR, "Selected object isn't an operation");
                continue;
            }

            // if operation is abstract
            if (((SciOperation)selectedElement).hasProperty(SciProperty.ABSTRACT))
            {
                IdeMessageManagerAccess.printMessage
                    (IdeMessageType.ERROR, "Selected operation is abstract");
                continue;
            }

            new OperationPage((SciOperation)selectedElement);
        }
    }

    /**
     * This method is called at start up time and menu item initialization is put
     * here, so that the module would also be available from elements' context menu.
     */
    public void autorun()
    {
        // getting command manager
        IdeCommandManager commandManager = IdeCommandManagerAccess.getCommandManager();

        // creation a command listener with instance of anonymous inner class
        // to easily implement event processing
        myItem = commandManager.createItem(
            "CallTreeItem",
            new IdeCommandConstraints
            ("context = element, shapeType = Operation, location = popupMenu, parent = Tools"),
            new IdeCommandListener()
            {
                public void checkStatus(IdeCommandEvent event)
                {
                    // make the menu item enabled or disabled depending on context
                    IdeContext context = event.getElementContext();
                    if (context != null)
                    { // the command can only be command executed on selected element
                        // gettimng array of selected elements
                        RwiElement[] selectedRwiElements = context.getRwiElements();
                        // if there are seleted elements
                        if (selectedRwiElements != null)
                        {
                            // processig each elemnt
                            for (int i = 0; i < selectedRwiElements.length; i++)
                            {
                                SciElement selectedElement =
                                    (SciElement)selectedRwiElements[i].getCodeElement();

                                // make the command enabled only for non-abstract methods
                                if (selectedElement != null &&
                                    selectedElement instanceof SciOperation &&
                                    !((SciOperation)selectedElement).hasProperty(SciProperty.ABSTRACT))
                                {
                                        myItem.setEnabled(true);
                                        return;
                                }
                            }
                        }
                    }
                    myItem.setEnabled(false);
                }
                public void actionPerformed(IdeCommandEvent event)
                { // actually process the command
                    (new ShowCallTree()).run(event.getElementContext());
                }
            });
        // set text to be displayed as menu command
        myItem.setText("Show Call Tree");
    }

    private IdeCommandItem myItem; // command item

    /**
     * Cell renderer component to be used in the JTree treeview.
     * Displays name of operation in the cell with appropriate icon
     * if the callee's class is known. Otherwise displays default gray icon.
     * @see javax.swing.tree.DefaultTreeCellRenderer
     */
    private class OperationTR extends DefaultTreeCellRenderer
    {
        public OperationTR(Hashtable hashtable)
        {
            myNodeOperationHashtable = hashtable;
        }

        public Component getTreeCellRendererComponent
        (JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
        {
            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
            SciOperation operation = (SciOperation)myNodeOperationHashtable.get(value);
            RwiElement element = null;
            if (operation != null && !operation.isDeleted())
            {
                element = RwiModelAccess.getModel().findElement(operation.getUniqueName());
            }
            if (element != null)
            {
                setToolTipText(SciModelAccess.getModel().getLanguageHelper(operation.getLanguage()).convertMemberQualifiedNameToPresentableForm(operation.getQualifiedName()));
                Icon icon =
                IdeAccess.getResourceManager().getIcon(element, IdeResourceIconType.SMALL);
                setIcon(icon);
            }
            else
            {
                setToolTipText(null);
                java.net.URL iconResource = SciModelAccess.getModel().getClass().getResource("/Treeviews/tv-unknown_operation.gif");
                if (iconResource != null)
                {
                    setToolTipText(iconResource.toString());
                    Icon icon = new ImageIcon(iconResource);
                    setIcon(icon);
                }
            }
            return this;
        }

        private Hashtable myNodeOperationHashtable;
    }

    /**
     * The class to represent a tab in the message window pane.
     */
    private class OperationPage
    {
        /*
         * Creates class instance with specified operation.
         */
        public OperationPage(SciFunction operation)
        {
            // root node is the operation where the script was called
            final DefaultMutableTreeNode root = new
                DefaultMutableTreeNode(((SciClass)operation.getContainingScope()).getName() +
                "." + operation.getName() + "()");
            root.setAllowsChildren(true);
            myNodeOperationHashtable.put(root, operation);
            root.add(new DefaultMutableTreeNode("loading..."));

            // create default treeview model to be filled with data later in
            // concurrent thread
            myTreeModel = new DefaultTreeModel(root);
            final JTree tree = new JTree();
            tree.setModel(myTreeModel);
            tree.putClientProperty("JTree.lineStyle", "Angled");
            tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
            tree.setShowsRootHandles(true);
            tree.collapsePath(new TreePath(new Object[] { root }));
            DefaultTreeCellRenderer renderer = new OperationTR(myNodeOperationHashtable);
            tree.setCellRenderer(renderer);

            // all operations with tree are done in concurrent manner
            tree.addTreeWillExpandListener(new TreeWillExpandListener()
            {
                public void treeWillExpand(TreeExpansionEvent e)
                {
                    final DefaultMutableTreeNode node =
                    (DefaultMutableTreeNode)e.getPath().getLastPathComponent();

                    if (node != null)
                    {
                        IdeAccess.getIdeManager().addCommandToQueue(new Runnable()
                        {
                            public void run()
                            {
                                openNode(node);
                            }
                        });
                    }
                }

                public void treeWillCollapse(TreeExpansionEvent e)
                {
                    final DefaultMutableTreeNode node =
                    (DefaultMutableTreeNode)e.getPath().getLastPathComponent();

                    if (node != null)
                    {
                        IdeAccess.getIdeManager().addCommandToQueue(new Runnable()
                        {
                            public void run()
                            {
                                closeNode(node);
                            }
                        });
                    }
                }
            });

            tree.addMouseListener(new MouseListener()
            {
                public void mouseClicked(MouseEvent e) { }
                public void mouseEntered(MouseEvent e) { }
                public void mouseExited(MouseEvent e) { }

                public void mousePressed(MouseEvent e)
                {
                    final DefaultMutableTreeNode node =
                    (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();

                    if (node != null)
                    {
                        IdeAccess.getIdeManager().addCommandToQueue(new Runnable()
                        {
                            public void run()
                            {
                                selectOperation(node);
                            }
                        });
                    }
                }
                public void mouseReleased(MouseEvent e) { }
            });

            ToolTipManager.sharedInstance().registerComponent(tree);
            String messagePageName = operation.getName();
            int i = 0;
            while (IdeAccess.getMessageManager().findPage(messagePageName) != null)
            {
                i++;
                messagePageName = operation.getName() + "(" + i + ")";
            }

            // put the pane with treeview to message window pane and
            // make it active and visible
            IdeMessagePage messagePage =
                IdeAccess.getMessageManager().openPage(messagePageName, new JScrollPane(tree), null);
            messagePage.setTitle(messagePageName);
            IdeAccess.getMessageManager().setActivePage(messagePage);
            IdeAccess.getMessageManager().setPaneVisible(true);
        }

        /*
         * Opens node in tree.
         */
        private void openNode(DefaultMutableTreeNode parent)
        {
            final Vector operations = new Vector();

            // first, remove all existing children
            while (parent.getChildCount() > 1)
            {
                DefaultMutableTreeNode child = (DefaultMutableTreeNode)parent.getFirstChild();
                myNodeOperationHashtable.remove(child);
                closeNode(child);
                myTreeModel.removeNodeFromParent(child);
            }

            DefaultMutableTreeNode emptyChild = (DefaultMutableTreeNode)parent.getFirstChild();

            SciOperation operation = (SciOperation)myNodeOperationHashtable.get(parent);

            if (operation != null && !operation.isDeleted())
            {
                final SciClass nodeClass = operation.getContainingClass();
                final DefaultMutableTreeNode localParent = parent;
                SciCodeBlock body = operation.getBody();
                if (body != null)
                {
                    // facilitate going through expressions with help of visitor pattern;
                    // real visitors functionality defined in anonymous inner class
                    body.visitExpressions(new SciExpressionVisitorAdapter()
                    {
                        public Object visitFunctionCallExpression
                        (SciFunctionCallExpression functionCallExpression)
                        {
                            SciExpression functionExpression =
                            functionCallExpression.getFunction();

                            SciExpression objectExpression = null;

                            if (functionExpression instanceof SciMemberAccessExpression)
                            {
                                objectExpression =
                                ((SciMemberAccessExpression)functionExpression).getObjectExpression();

                                functionExpression =
                                ((SciMemberAccessExpression)functionExpression).getMemberExpression();
                            }

                            SciOperation element =
                            (SciOperation)((SciReferenceExpression)functionExpression).getReferencedElement();

                            DefaultMutableTreeNode child = null;
                            if (element != null)
                            {
                                if (operations.indexOf(element) != -1)
                                {
                                    return null;
                                }
                                operations.addElement(element);
                                String childName = element.getName() + "()";
                                if (!element.hasProperty(SciProperty.CONSTRUCTOR))
                                {
                                    SciClass childClass = element.getContainingClass();
                                    if (!childClass.equals(nodeClass))
                                    {
                                        childName = childClass.getName() + "." + childName;
                                    }
                                }

                                child = new DefaultMutableTreeNode(childName);
                                myNodeOperationHashtable.put(child, element);
                                myTreeModel.insertNodeInto
                                (child, localParent, localParent.getChildCount());

                                if (!element.hasProperty(SciProperty.ABSTRACT))
                                {
                                    myTreeModel.insertNodeInto(
                                        new DefaultMutableTreeNode(), child,
                                        child.getChildCount());
                                }
                            }
                            else
                            {
                                String nodeClassName = null;
                                if (objectExpression != null)
                                {
                                    if (objectExpression instanceof SciFunctionCallExpression)
                                    {
                                        objectExpression =
                                        ((SciFunctionCallExpression)objectExpression).getFunction();

                                        if (objectExpression instanceof SciMemberAccessExpression)
                                        {
                                            objectExpression =
                                            ((SciMemberAccessExpression)objectExpression).getMemberExpression();
                                        }

                                        if (objectExpression instanceof SciReferenceExpression)
                                        {
                                            SciFunction function =
                                            (SciFunction)((SciReferenceExpression)objectExpression).getReferencedElement();

                                            if (function != null)
                                            {
                                                nodeClassName = function.getReturnType().getText();
                                            }
                                        }
                                    }
                                    else
                                    {
                                        if (objectExpression instanceof SciMemberAccessExpression)
                                        {
                                            objectExpression =
                                            ((SciMemberAccessExpression)objectExpression).getObjectExpression();
                                        }

                                        if (objectExpression instanceof SciReferenceExpression)
                                        {
                                            SciElement objectElement =
                                            (SciElement)((SciReferenceExpression)objectExpression).getReferencedElement();

                                            if (objectElement != null && objectElement instanceof SciVariable)
                                            {
                                                nodeClassName = ((SciVariable)objectElement).getType().getText();
                                            }
                                        }
                                    }
                                }

                                String nodeName = functionExpression.getText() + "()";

                                if (nodeClassName != null)
                                {
                                    nodeName = nodeClassName + "." + nodeName;
                                }

                                child = new DefaultMutableTreeNode(nodeName);

                                if (operations.indexOf(nodeName) != -1)
                                {
                                    return null;
                                }

                                operations.addElement(nodeName);
                                myTreeModel.insertNodeInto(child, localParent, localParent.getChildCount());
                            }
                            return null;
                        }
                    });
                }
            }
            myNodeOperationHashtable.remove(emptyChild);
            closeNode(emptyChild);
            myTreeModel.removeNodeFromParent(emptyChild);
        }

        /*
         * Closes node in tree.
         */
        private void closeNode(DefaultMutableTreeNode parent)
        {
            DefaultMutableTreeNode emptyNode = new DefaultMutableTreeNode("loading...");
            myTreeModel.insertNodeInto(emptyNode, parent, parent.getChildCount());

            while (parent.getChildCount() > 1)
            {
                DefaultMutableTreeNode child = (DefaultMutableTreeNode)parent.getFirstChild();
                myNodeOperationHashtable.remove(child);
                closeNode(child);
                myTreeModel.removeNodeFromParent(child);
            }
        }

        /**
         * Opens method's body in text editor window, if sourcecode is available.
         */
        private void selectOperation(DefaultMutableTreeNode node)
        {
            SciOperation operation = (SciOperation)myNodeOperationHashtable.get(node);

            // if empty operation or it was deleted
            if (operation == null || operation.isDeleted())
            {
                return;
            }

            // getting file name
            String fileName = operation.getContainingFile().getQualifiedName();

            // if it's not java file
            if (!fileName.endsWith(".java"))
            {
                return;
            }

            // getting editor manager
            IdeEditorManager editorManager = IdeAccess.getEditorManager();

            // finding file in editor
            IdeEditor editor = editorManager.findEditor(fileName);

            // if this file is not loaded in text editor
            if (editor == null)
            {
                editor = editorManager.openEditor();
                editor.setFile(fileName);
            }

            // makes editor active, sets selection in editor
            // sets cursor position
            editorManager.setActiveEditor(editor);
            IdeEditorPositionConverter converter = editor.getPositionConverter();
            TextPosition position = converter.offsetToCell(operation.getPositions().getStartOffset());
            IdeEditorSelection selection = editor.getSelection(IdeEditorSelectionType.USER);
            selection.setSelectionStart(new TextPosition(position.getLine(), 1));
            selection.setSelectionEnd(new TextPosition(position.getLine() + 1, 1));
            IdeEditorVisibleArea visibleArea = editor.getVisibleArea();
            if (visibleArea == null || visibleArea.getFirstLine() == position.getLine())
            {
                return;
            }
            visibleArea.setFirstLine(position.getLine());
        }

        private Hashtable myNodeOperationHashtable = new Hashtable();
        private DefaultTreeModel myTreeModel;
    }
}


