package com.ibm.nmon.gui.tree;

import java.util.List;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.tree.DefaultTreeModel;

import com.ibm.nmon.data.DataSet;
import com.ibm.nmon.data.ProcessDataSet;

import com.ibm.nmon.data.DataSetListener;
import com.ibm.nmon.data.DataType;
import com.ibm.nmon.data.SubDataType;
import com.ibm.nmon.data.ProcessDataType;

import com.ibm.nmon.data.Process;

import com.ibm.nmon.gui.dnd.TreeTransferHandler;
import com.ibm.nmon.gui.main.NMONVisualizerGui;

import com.ibm.nmon.util.DataHelper;

public final class TreePanel extends JScrollPane implements DataSetListener {
    private static final long serialVersionUID = 8763622839346467286L;

    static final String ROOT_NAME = "All Systems";

    protected final JTree tree;

    public void addTreeSelectionListener(TreeSelectionListener tsl) {
        tree.addTreeSelectionListener(tsl);
    }

    public TreePanel(NMONVisualizerGui gui) {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode(ROOT_NAME);

        tree = new JTree(root);
        tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        tree.setCellRenderer(new TreeCellRenderer());

        setViewportView(tree);

        // make sure panel takes up entire parent, but the actual tree is offset slightly
        setBorder(null);
        tree.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));

        // data changes modify the tree
        gui.addDataSetListener(this);

        tree.addMouseListener(new TreeMouseListener(gui, tree));

        tree.setDragEnabled(true);
        tree.setTransferHandler(new TreeTransferHandler(gui));

        tree.setCellRenderer(new TreeCellRenderer());

        ToolTipManager.sharedInstance().registerComponent(tree);
    }

    @Override
    public final void dataAdded(DataSet data) {
        DefaultMutableTreeNode dataNode = null;
        TreePath selectedPath = null;
        List<TreePath> expandedPaths = null;
        boolean existing = false;
        int insertIdx = 0;

        // find the existing data set node, if any
        DefaultMutableTreeNode root = (DefaultMutableTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();

        for (int i = 0; i < root.getChildCount(); i++) {
            DefaultMutableTreeNode toSearch = (DefaultMutableTreeNode) root.getChildAt(i);
            DataSet currentData = (DataSet) toSearch.getUserObject();

            if (data.toString().compareTo(currentData.toString()) > 0) {
                insertIdx = i + 1;
            }

            if (currentData.equals(data)) {
                dataNode = toSearch;
                existing = true;

                // if the DataSet node has children, remove them since the DataTypes could have
                // changed; save the existing tree state first
                TreePath current = tree.getSelectionPath();

                if ((current != null) && new TreePath(dataNode.getPath()).isDescendant(current)) {
                    selectedPath = current;
                }

                java.util.Enumeration<TreePath> temp = tree
                        .getExpandedDescendants(new TreePath(tree.getModel().getRoot()));

                if (temp != null) {
                    expandedPaths = java.util.Collections.list(temp);
                }
                else {
                    expandedPaths = java.util.Collections.emptyList();
                }

                dataNode.removeAllChildren();

                break;
            }
        }

        // new data set, new tree node
        if (dataNode == null) {
            dataNode = new DefaultMutableTreeNode(data);
        }

        buildDataSetTree(dataNode, data);

        // adding to an existing data set, alert the tree the structure has changed and make
        // sure the same nodes are expanded / selected
        if (existing) {
            ((javax.swing.tree.DefaultTreeModel) tree.getModel()).nodeStructureChanged(dataNode);

            for (TreePath path : expandedPaths) {
                tree.expandPath(rebuildPath(path));
            }

            if (selectedPath != null) {
                tree.setSelectionPath(rebuildPath(selectedPath));
            }
        }
        else {
            if (insertIdx > root.getChildCount()) {
                insertIdx = root.getChildCount() - 1;
            }

            ((DefaultTreeModel) tree.getModel()).insertNodeInto(dataNode, root, insertIdx);

            if (root.getChildCount() == 1) {
                // expand the root node now that there is something in the tree
                tree.expandRow(0);
            }

            selectedPath = tree.getSelectionPath();

            if ((selectedPath == null) || (selectedPath.getLastPathComponent() == root)) {
                // reselect the root node to ensure any TreeSelectionListeners (TreePathParsers)
                // are updated
                tree.setSelectionPath(null);
                tree.setSelectionPath(new TreePath(root));
            }
            // else leave the current selection alone
        }
    }

    @Override
    public final void dataRemoved(DataSet data) {
        DefaultMutableTreeNode root = (DefaultMutableTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();

        for (int i = 0; i < root.getChildCount(); i++) {
            DataSet toSearch = (DataSet) ((DefaultMutableTreeNode) root.getChildAt(i)).getUserObject();

            if (toSearch.equals(data)) {
                Object removed = root.getChildAt(i);

                root.remove(i);

                ((javax.swing.tree.DefaultTreeModel) tree.getModel()).nodesWereRemoved(root, new int[] { i },
                        new Object[] { removed });

                // to remove a node requires it to be selected, so move the selection to the root
                tree.setSelectionPath(new TreePath(root));

                break;
            }
        }
    }

    @Override
    public void dataChanged(DataSet data) {
        dataAdded(data);
    }

    @Override
    public final void dataCleared() {
        DefaultMutableTreeNode root = (DefaultMutableTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();

        // select the root node if any path is selected
        boolean pathSelected = false;

        TreePath path = tree.getSelectionPath();

        if (path != null) {
            pathSelected = true;
        }

        root.removeAllChildren();

        ((javax.swing.tree.DefaultTreeModel) tree.getModel()).reload();

        if (pathSelected) {
            tree.setSelectionPath(new TreePath(root));
        }
    }

    // since saved TreePaths may included nodes that no longer exist, rebuild the path with the
    // new, correct nodes
    private TreePath rebuildPath(TreePath oldPath) {
        Object[] oldPaths = oldPath.getPath();
        DefaultMutableTreeNode[] newPath = new DefaultMutableTreeNode[oldPaths.length];

        DefaultMutableTreeNode parent = ((DefaultMutableTreeNode) tree.getModel().getRoot());

        newPath[0] = parent;
        int pathIdx = 1;

        while ((parent != null) && (pathIdx < newPath.length)) {
            List<javax.swing.tree.TreeNode> children = java.util.Collections
                    .list((java.util.Enumeration<javax.swing.tree.TreeNode>) parent.children());

            for (javax.swing.tree.TreeNode c : children) {
                DefaultMutableTreeNode child = (DefaultMutableTreeNode) c;
                if (((DefaultMutableTreeNode) oldPaths[pathIdx]).getUserObject().equals(child.getUserObject())) {
                    newPath[pathIdx] = child;
                    ++pathIdx;
                    parent = child;
                    break;
                }
            }
        }

        return new TreePath(newPath);
    }

    private void buildDataSetTree(DefaultMutableTreeNode dataNode, DataSet data) {
        boolean buildProcessNode = false;
        List<SubDataType> gcTypes = null;
        Map<String, DefaultMutableTreeNode> subtypes = null;

        // add types and processes
        for (DataType type : data.getTypes()) {
            if (type.getClass().equals(SubDataType.class)) {
                if (type.getId().startsWith("GC")) {
                    if (gcTypes == null) {
                        gcTypes = new java.util.ArrayList<SubDataType>();
                    }

                    gcTypes.add((SubDataType) type);
                }
                else {
                    if (subtypes == null) {
                        subtypes = new java.util.HashMap<String, DefaultMutableTreeNode>();
                    }

                    // create a single TreeNode for the subtype's id and cache it
                    SubDataType subtype = (SubDataType) type;
                    String id = subtype.getPrimaryId();

                    DefaultMutableTreeNode typeNode = subtypes.get(id);

                    if (typeNode == null) {
                        typeNode = new DefaultMutableTreeNode(id);

                        subtypes.put(id, typeNode);
                        dataNode.add(typeNode);
                    }

                    // add a sub-node for each type
                    typeNode.add(new TypeTreeNode(subtype, true));
                }
            }
            else if (type.getClass().equals(ProcessDataType.class)) {
                buildProcessNode = true;
            }
            else {
                dataNode.add(new TypeTreeNode(type));
            }
        }

        if (buildProcessNode) {
            dataNode.add(buildTopTree((ProcessDataSet) data));
        }

        if (gcTypes != null) {
            dataNode.add(buildGCTree(gcTypes));
        }
    }

    private DefaultMutableTreeNode buildTopTree(ProcessDataSet data) {
        // TOP
        // |_ process [single instance] (as ProcessDataType)
        // ...|_ TOP field 1
        // ...|_ TOP field 2
        // ...|_ TOP ...
        // ...|_ TOP field n
        // |_ process name [multiple instances] (String)
        // ...|_ aggregated process (as Process DataType)
        // ......|_ TOP fields
        // ...|_ process 1 (as Process DataType)
        // ......|_ TOP fields
        // ...|_ process 2
        // ......|_ TOP fields
        // ...|_ ...
        // ...|_ process n
        // ......|_ TOP fields

        // format topNode's name like TypeTreeNode
        DefaultMutableTreeNode topNode = new DefaultMutableTreeNode(data.getTypeIdPrefix());

        Map<String, List<Process>> processNameToProcesses = DataHelper.getProcessesByName(data, true);

        for (String processName : processNameToProcesses.keySet()) {
            List<Process> processes = processNameToProcesses.get(processName);

            DefaultMutableTreeNode processNode = null;

            // more than 1 process with the given name => add a subtree
            if (processes.size() > 1) {
                // process node will be an AggregateDataType; sub-tree contains processs
                processNode = new DefaultMutableTreeNode(processes.get(0).getName());

                List<DataType> types = new java.util.ArrayList<DataType>();

                for (Process process : processes) {
                    DefaultMutableTreeNode processInstance = new DefaultMutableTreeNode(process);

                    DataType type = data.getType(process);

                    types.add(type);

                    for (String field : type.getFields()) {
                        processInstance.add(new DefaultMutableTreeNode(field));
                    }

                    processNode.add(processInstance);
                }
            }
            else {
                // process node is the process; no sub-tree
                processNode = new DefaultMutableTreeNode(processes.get(0));

                for (String field : data.getType(processes.get(0)).getFields()) {
                    processNode.add(new DefaultMutableTreeNode(field));
                }
            }

            topNode.add(processNode);
        }

        return topNode;
    }

    private DefaultMutableTreeNode buildGCTree(List<SubDataType> gcTypes) {
        // create a top-level GC node
        DefaultMutableTreeNode gcNode = new DefaultMutableTreeNode("GC");
        Map<String, DefaultMutableTreeNode> jvmNodes = new java.util.HashMap<String, DefaultMutableTreeNode>();

        // under GC, create a node for each JVM to contain all the data types for that JVM
        for (SubDataType type : gcTypes) {
            String jvmName = ((SubDataType) type).getSubId();

            DefaultMutableTreeNode jvmNode = jvmNodes.get(jvmName);

            if (jvmNode == null) {
                jvmNode = new DefaultMutableTreeNode(jvmName);

                gcNode.add(jvmNode);
                jvmNodes.put(jvmName, jvmNode);
            }

            jvmNode.add(new TypeTreeNode(type, false));
        }

        return gcNode;
    }

    private final class TypeTreeNode extends DefaultMutableTreeNode {
        private static final long serialVersionUID = -3704741016840510282L;

        private final String toDisplay;

        TypeTreeNode(DataType type) {
            super(type);

            toDisplay = type.getId();

            for (String field : type.getFields()) {
                add(new DefaultMutableTreeNode(field));
            }
        }

        TypeTreeNode(SubDataType type, boolean showSubId) {
            super(type);

            if (showSubId) {
                toDisplay = type.getSubId();
            }
            else {
                toDisplay = type.getPrimaryId();
            }

            for (String field : type.getFields()) {
                add(new DefaultMutableTreeNode(field));
            }
        }

        @Override
        public String toString() {
            return toDisplay;
        }
    }
}