/*
 * ViewerJTree.java Copyright (C) 2020. Daniel H. Huson
 *
 *  (Some files contain contributions from other authors, who are then mentioned separately.)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package megan.viewer.gui;

import jloda.graph.Edge;
import jloda.graph.Node;
import jloda.phylo.PhyloTree;
import jloda.swing.util.PopupMenu;
import jloda.swing.window.IPopupMenuModifier;
import megan.viewer.ClassificationViewer;
import megan.viewer.GUIConfiguration;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreePath;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;

/**
 * tree viewer for classification
 * Created by huson on 2/3/16.
 */
public class ViewerJTree extends JTree {
    private final ClassificationViewer classificationViewer;
    private final Map<Integer, MyJTreeNode> id2node = new HashMap<>();
    private IPopupMenuModifier popupMenuModifier;

    private final PhyloTree inducedTree; // need use own copy of induced tree that has no collapsed nodes
    private final Map<Integer, Set<Node>> id2NodesInInducedTree;

    private final JPopupMenu popupMenu;

    boolean inSelection = false;  // use this to prevent bouncing when selecting from viewer

    /**
     * constructor
     *
     * @param classificationViewer
     */
    public ViewerJTree(ClassificationViewer classificationViewer) {
        this.classificationViewer = classificationViewer;

        inducedTree = new PhyloTree();
        id2NodesInInducedTree = new HashMap<>();

        setCellRenderer(new MyJTreeCellRender(classificationViewer, id2NodesInInducedTree));

        addTreeSelectionListener(new MyJTreeSelectionListener(this, classificationViewer));

        final MyJTreeListener treeListener = new MyJTreeListener(this, classificationViewer, id2node);
        addTreeWillExpandListener(treeListener);
        addTreeExpansionListener(treeListener);
        addMouseListener(new MyMouseListener());

        popupMenu = new PopupMenu(this, GUIConfiguration.getJTreePopupConfiguration(), classificationViewer.getCommandManager());
    }

    /**
     * rescan the jtree
     */
    public void update() {
        if (classificationViewer.getTree().getNumberOfNodes() > 1) {
            removeAll();
            id2node.clear();
            inducedTree.clear();
            id2NodesInInducedTree.clear();
            if (classificationViewer.getDocument().getNumberOfReads() > 0)
                classificationViewer.computeInduceTreeWithNoCollapsedNodes(inducedTree, id2NodesInInducedTree);

            final Node root = classificationViewer.getClassification().getFullTree().getRoot();
            final int id = (Integer) root.getInfo();
            final MyJTreeNode node = new MyJTreeNode(root);
            final DefaultTreeModel model = (DefaultTreeModel) getModel();
            model.setRoot(node);
            id2node.put(id, node);
            setRootVisible(true);
            setShowsRootHandles(true);
            addChildren(node);
        }
    }

    /**
     * add all children of a given node
     *
     * @param node
     */
    public void addChildren(MyJTreeNode node) {
        final Node v = node.getV();
        final DefaultTreeModel model = (DefaultTreeModel) getModel();

        if (v.getOutDegree() > 0 && node.getChildCount() == 0) {
            for (Edge e = v.getFirstOutEdge(); e != null; e = v.getNextOutEdge(e)) {
                final Node w = e.getTarget();
                final MyJTreeNode wNode = new MyJTreeNode(w);
                node.add(wNode);
                id2node.put((Integer) w.getInfo(), wNode);
                model.nodeStructureChanged(wNode);
            }
        }
        model.nodeStructureChanged(node);
    }

    public boolean isInSelection() {
        return inSelection;
    }

    /**
     * select a node by id
     *
     * @param id
     */
    public void setSelected(int id, boolean select) {
        if (!inSelection) {
            inSelection = true;
            DefaultMutableTreeNode n = id2node.get(id);
            if (n != null) {
                TreePath path = new TreePath(n);
                if (select)
                    setSelectionPath(path);
                else
                    removeSelectionPath(path);
                makeVisible(path);
                repaint();
            }
            inSelection = false;
        }
    }

    /**
     * select nodes by their ids
     *
     * @param ids
     */
    public void setSelected(Collection<Integer> ids, boolean select) {
        if (!inSelection) {
            inSelection = true;
            LinkedList<TreePath> paths = new LinkedList<>();

            for (Integer id : ids) {
                DefaultMutableTreeNode n = id2node.get(id);
                if (n != null) {
                    TreePath path = new TreePath(n);
                    paths.add(path);
                    if (paths.size() == 1)
                        makeVisible(path);
                }
            }
            if (select)
                setSelectionPaths(paths.toArray(new TreePath[0]));
            else
                removeSelectionPaths(paths.toArray(new TreePath[0]));
            repaint();
            inSelection = false;
        }
    }

    public void setPopupMenuModifier(IPopupMenuModifier popupMenuModifier) {
        this.popupMenuModifier = popupMenuModifier;
    }

    class MyMouseListener extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
            if (e.isPopupTrigger()) showPopupMenu(e);
        }

        public void mouseReleased(MouseEvent e) {
            if (e.isPopupTrigger()) showPopupMenu(e);
        }
    }

    private void showPopupMenu(MouseEvent e) {
        if (popupMenuModifier != null) {
            popupMenuModifier.apply(popupMenu, classificationViewer.getCommandManager());
            popupMenuModifier = null;
        }
        popupMenu.show(ViewerJTree.this, e.getX(), e.getY());
    }

    /**
     * tree node
     */
    public static class MyJTreeNode extends DefaultMutableTreeNode {
        private final Node v;

        MyJTreeNode(Node v) {
            this.v = v;
        }

        public Node getV() {
            return v;
        }

        public String toString() {
            return "[" + v.getInfo() + "]";
        }

        @Override
        public boolean isLeaf() {
            return v.getOutDegree() == 0;
        }
    }
}

class MyJTreeListener implements TreeWillExpandListener, TreeExpansionListener {
    private final ViewerJTree jTree;
    private final ClassificationViewer classificationViewer;

    /**
     * constructor
     *
     * @param classificationViewer
     */
    MyJTreeListener(ViewerJTree jTree, ClassificationViewer classificationViewer, Map<Integer, ViewerJTree.MyJTreeNode> id2node) {
        this.jTree = jTree;
        this.classificationViewer = classificationViewer;
    }

    /**
     * Invoked whenever a node in the tree is about to be collapsed.
     */
    public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
    }

    /**
     * Called whenever an item in the tree has been collapsed.
     */
    public void treeCollapsed(TreeExpansionEvent event) {
    }

    /**
     * Called whenever an item in the tree has been expanded.
     */
    public void treeExpanded(TreeExpansionEvent event) {
    }

    /**
     * Invoked whenever a node in the tree is about to be expanded.
     */
    public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
        if (classificationViewer.isLocked()) {
            throw new ExpandVetoException(event);
        }
        jTree.addChildren((ViewerJTree.MyJTreeNode) event.getPath().getLastPathComponent());
    }
}

class MyJTreeSelectionListener implements TreeSelectionListener {
    private final ClassificationViewer ClassificationViewer;
    private final ViewerJTree jtree;

    public MyJTreeSelectionListener(ViewerJTree jtree, ClassificationViewer ClassificationViewer) {
        this.jtree = jtree;
        this.ClassificationViewer = ClassificationViewer;
    }

    /**
     * Called whenever the value of the selection changes.
     *
     * @param e the event that characterizes the replace.
     */
    public void valueChanged(TreeSelectionEvent e) {
        if (!jtree.inSelection) {
            jtree.inSelection = true;
            Set<Integer> ids2Select = new HashSet<>();
            Set<Integer> ids2Deselect = new HashSet<>();

            for (TreePath path : e.getPaths()) {
                final ViewerJTree.MyJTreeNode node = (ViewerJTree.MyJTreeNode) path.getLastPathComponent();
                if (e.isAddedPath(path))
                    ids2Select.add((Integer) node.getV().getInfo());
                else
                    ids2Deselect.add((Integer) node.getV().getInfo());
            }
            if (ids2Select.size() > 0 || ids2Deselect.size() > 0) {
                if (ids2Deselect.size() > 0)
                    ClassificationViewer.setSelectedIds(ids2Deselect, false);
                if (ids2Select.size() > 0) {
                    ClassificationViewer.setSelectedIds(ids2Select, true);
                    Node v = ClassificationViewer.getANode(ids2Select.iterator().next());
                    if (v != null)
                        ClassificationViewer.scrollToNode(v);
                }
                ClassificationViewer.repaint();
            }
            jtree.inSelection = false;
        }
    }
}