/*
 * TreePaneSelector.java
 *
 * Copyright (C) 2006-2014 Andrew Rambaut
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package figtree.treeviewer;

import jebl.evolution.graphs.Node;
import jam.mac.Utils;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Set;

/**
 * @author Andrew Rambaut
 * @version $Id$
 *
 * $HeadURL$
 *
 * $LastChangedBy$
 * $LastChangedDate$
 * $LastChangedRevision$
 */
public class TreePaneSelector implements MouseListener, MouseMotionListener, KeyListener {
    public enum SelectionMode {
        NODE,
        CLADE,
        TAXA
    };

    public enum DragMode {
        SELECT,
        SCROLL
    };

    public enum ToolMode {
        SELECT,
        ROOTING,
        CARTOONING,
        COLLAPSING,
        ROTATING,
        ANNOTATING,
        COLOURING
    };

    public TreePaneSelector(TreePane treePane) {
        this.treePane = treePane;
        treePane.addMouseListener(this);
        treePane.addMouseMotionListener(this);
        treePane.addKeyListener(this);
    }

    public SelectionMode getSelectionMode() {
        return selectionMode;
    }

    public DragMode getDragMode() {
        return dragMode;
    }

    public void setSelectionMode(SelectionMode selectionMode) {
        defaultSelectionMode = selectionMode;
        this.selectionMode = selectionMode;
    }

    public void setDragMode(DragMode dragMode) {
        this.dragMode = dragMode;
    }

    public void setToolMode(ToolMode toolMode) {
        this.toolMode = toolMode;
        setupCursor();
    }

    public boolean isCrossHairCursor() {
        return crossHairCursor;
    }

    public void setCrossHairCursor(boolean crossHairCursor) {
        this.crossHairCursor = crossHairCursor;
    }

    private void setupCursor() {
        if (toolMode != ToolMode.SELECT) {
            treePane.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.CROSSHAIR_CURSOR));
            treePane.setCrosshairShown(crossHairCursor);
        } else if (dragMode == DragMode.SELECT) {
            if (crossHairCursor) {
                treePane.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.CROSSHAIR_CURSOR));
            } else {
                treePane.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.DEFAULT_CURSOR));
            }
            treePane.setCrosshairShown(crossHairCursor);
        } else {
            treePane.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR));
            treePane.setCrosshairShown(false);

        }
        treePane.repaint();
    }

    public void mouseClicked(MouseEvent mouseEvent) {
        if (treePane.getTree() == null) {
            return;
        }

        if (toolMode == ToolMode.ROOTING) {
            Node node = treePane.getNodeAt((Graphics2D) treePane.getGraphics(), mouseEvent.getPoint());
            if (node != null) {
                treePane.setRootLocation(node, 0.5);
            }
        } else if (toolMode == ToolMode.ROTATING) {
            Node node = treePane.getNodeAt((Graphics2D) treePane.getGraphics(), mouseEvent.getPoint());
            treePane.rotateNode(node);
        } else if (dragMode == DragMode.SELECT) {
            boolean isCrossHairShown = treePane.isCrosshairShown();

            treePane.setCrosshairShown(false);

            Node selectedNode = treePane.getNodeAt((Graphics2D) treePane.getGraphics(), mouseEvent.getPoint());

            boolean extendSelection = mouseEvent.isShiftDown();
            boolean invertSelection = isCommandKeyDown(mouseEvent);

            if (!extendSelection && !invertSelection) {
                treePane.clearSelection();
            }

            SelectionMode mode = selectionMode;
            if (mouseEvent.isAltDown()) {
                if (mode == SelectionMode.NODE) {
                    mode = SelectionMode.CLADE;
                } else if (mode == SelectionMode.CLADE) {
                    mode = SelectionMode.NODE;
                }
            }

            switch (mode) {
                case NODE:
                    treePane.addSelectedNode(selectedNode, invertSelection, extendSelection);
                    break;
                case CLADE:
                    treePane.addSelectedClade(selectedNode, invertSelection, extendSelection);
                    break;
                case TAXA:
                    treePane.addSelectedTip(selectedNode, invertSelection, extendSelection);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown SelectionMode: " + selectionMode.name());
            }

            treePane.setCrosshairShown(isCrossHairShown);
        }
    }

    public void mousePressed(MouseEvent mouseEvent) {
        treePane.requestFocus();

        // This is used for dragging in combination with mouseDragged
        // in the MouseMotionListener, below.
        dragPoint = new Point2D.Double(mouseEvent.getPoint().getX(), mouseEvent.getPoint().getY());
    }

    public void mouseReleased(MouseEvent mouseEvent) {
        if (treePane.getTree() == null) {
            return;
        }

        if (dragMode == DragMode.SELECT) {
            if (treePane.getDragRectangle() != null) {
                Set<Node> selectedNodes = treePane.getNodesAt((Graphics2D) treePane.getGraphics(), treePane.getDragRectangle().getBounds());

                boolean extendSelection = mouseEvent.isShiftDown();
                boolean invertSelection = isCommandKeyDown(mouseEvent);

                if (!extendSelection && !invertSelection) {
                    treePane.clearSelection();
                }

                SelectionMode mode = selectionMode;
                if (mouseEvent.isAltDown()) {
                    if (mode == SelectionMode.NODE) {
                        mode = SelectionMode.CLADE;
                    } else if (mode == SelectionMode.CLADE) {
                        mode = SelectionMode.NODE;
                    }
                }

                for (Node selectedNode : selectedNodes) {
                    switch (mode) {
                        case NODE:
                            treePane.addSelectedNode(selectedNode, invertSelection, extendSelection);
                            break;
                        case CLADE:
                            treePane.addSelectedClade(selectedNode, invertSelection, extendSelection);
                            break;
                        case TAXA:
                            treePane.addSelectedTip(selectedNode, invertSelection, extendSelection);
                            break;
                        default:
                            throw new IllegalArgumentException("Unknown SelectionMode: " + selectionMode.name());
                    }
                }
            }
        }
        treePane.setDragRectangle(null);
    }

    public void mouseEntered(MouseEvent mouseEvent) {
//        treePane.requestFocusInWindow();
        if (isCommandKeyDown(mouseEvent)) {
            treePane.setCursorPosition(mouseEvent.getPoint());
        }
    }

    public void mouseExited(MouseEvent mouseEvent) {
        if (isCommandKeyDown(mouseEvent)) {
            treePane.setCursorPosition(mouseEvent.getPoint());
        }
    }

    public void mouseMoved(MouseEvent mouseEvent) {
        if (isCommandKeyDown(mouseEvent)) {
            treePane.setCursorPosition(mouseEvent.getPoint());
        }
    }

    /**
     * On Mac, check for the 'Command' key, otherwise use the 'Control' key
     * @param event
     * @return is it pressed
     */
    private boolean isCommandKeyDown(InputEvent event) {
        return Utils.isMacOSX() ? event.isMetaDown() : event.isControlDown();
    }

    /**
     * On Mac, check for the 'Option' key, otherwise use the 'Alt' key
     * @param event
     * @return is it pressed
     */
    private boolean isOptionKeyDown(InputEvent event) {
        return event.isAltDown();
    }

    public void mouseDragged(MouseEvent mouseEvent) {

        if (toolMode != ToolMode.SELECT || dragPoint == null) {
            return;
        }

        if (dragMode == DragMode.SCROLL) {
            // Calculate how far the mouse has been dragged from the point clicked in
            // mousePressed, above.
            int deltaX = (int) (mouseEvent.getX() - dragPoint.getX());
            int deltaY = (int) (mouseEvent.getY() - dragPoint.getY());

            // Get the currently visible window
            Rectangle visRect = treePane.getVisibleRect();

            // Calculate how much we need to scroll
            if (deltaX > 0) {
                deltaX = visRect.x - deltaX;
            } else {
                deltaX = visRect.x + visRect.width - deltaX;
            }

            if (deltaY > 0) {
                deltaY = visRect.y - deltaY;
            } else {
                deltaY = visRect.y + visRect.height - deltaY;
            }

            // Scroll the visible region
            Rectangle r = new Rectangle(deltaX, deltaY, 1, 1);
            treePane.scrollRectToVisible(r);
        } else {
            double x1 = Math.min(dragPoint.getX(), mouseEvent.getPoint().getX());
            double y1 = Math.min(dragPoint.getY(), mouseEvent.getPoint().getY());
            double x2 = Math.max(dragPoint.getX(), mouseEvent.getPoint().getX());
            double y2 = Math.max(dragPoint.getY(), mouseEvent.getPoint().getY());
            treePane.setDragRectangle(new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1));
            treePane.scrollPointToVisible(mouseEvent.getPoint());
        }
    }

    public void keyTyped(KeyEvent event) {
    }

    public void keyPressed(KeyEvent event) {
        if (event.getKeyCode() == KeyEvent.VK_SPACE) {
            dragMode = DragMode.SCROLL;
        }
        if (isOptionKeyDown(event)) {
            switch (defaultSelectionMode) {
                case NODE:
                case CLADE:
                    selectionMode = SelectionMode.TAXA;
                    break;
                case TAXA:
                    selectionMode = SelectionMode.NODE               ;
                    break;
            }
        } else {
            selectionMode = defaultSelectionMode;
        }
        crossHairCursor = isCommandKeyDown(event);
        setupCursor();
    }

    public void keyReleased(KeyEvent event) {
        if (event.getKeyCode() == KeyEvent.VK_SPACE) {
            dragMode = DragMode.SELECT;
        }
        crossHairCursor = isCommandKeyDown(event);
        setupCursor();
    }

    private TreePane treePane;

    private SelectionMode defaultSelectionMode = SelectionMode.NODE;
    private SelectionMode selectionMode = SelectionMode.NODE;

    private ToolMode toolMode = ToolMode.SELECT;

    private DragMode dragMode = DragMode.SELECT;
    private Point2D dragPoint = null;

    private boolean crossHairCursor = false;
}
