/*
 * #%L
 * gitools-ui-app
 * %%
 * Copyright (C) 2013 Universitat Pompeu Fabra - Biomedical Genomics group
 * %%
 * 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/gpl-3.0.html>.
 * #L%
 */
package org.gitools.ui.app.heatmap.panel;

import org.gitools.api.matrix.view.Direction;
import org.gitools.api.matrix.view.IMatrixViewDimension;
import org.gitools.heatmap.AbstractMatrixViewDimension;
import org.gitools.heatmap.header.HeatmapHeader;
import org.gitools.ui.core.HeatmapPosition;
import org.gitools.ui.platform.os.OSProperties;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;

import static org.gitools.ui.core.interaction.Interaction.*;
import static org.gitools.ui.core.interaction.InteractionStatus.getInteractionStatus;
import static org.gitools.ui.core.interaction.InteractionStatus.setInteractionStatus;

public class HeatmapHeaderMouseController implements MouseListener, MouseMotionListener, MouseWheelListener {

    private final JViewport viewPort;
    private final HeatmapHeaderPanel headerPanel;

    private final HeatmapPanel panel;
    private final boolean horizontal;
    private Point point;
    private HeatmapPosition position;
    private int selLast;
    private Point startPoint;
    private Point startScrollValue;
    private final IMatrixViewDimension dimension;

    private final List<HeatmapMouseListener> listeners = new ArrayList<>(1);
    private int selectionMoveLastIndex;
    private boolean selectionHasMoved = false;

    private int ctrlMask = OSProperties.get().getCtrlMask();
    private int shiftMask = OSProperties.get().getShiftMask();
    private int altMask = OSProperties.get().getAltMask();
    private int metaMask = OSProperties.get().getMetaMask();
    private Timer timer;

    private HeatmapPanelInputProcessor inputProcessor;
    private MouseEvent lastMovingEvent;

    public HeatmapHeaderMouseController(HeatmapPanel panel,
                                        HeatmapPanelInputProcessor inputProcessor,
                                        boolean horizontal) {
        this.viewPort = horizontal ? panel.getColumnViewPort() : panel.getRowViewPort();
        this.headerPanel = horizontal ? panel.getColumnPanel() : panel.getRowPanel();
        this.panel = panel;
        this.horizontal = horizontal;
        this.dimension = horizontal ? panel.getHeatmap().getColumns() : panel.getHeatmap().getRows();

        viewPort.addMouseListener(this);
        viewPort.addMouseMotionListener(this);
        viewPort.addMouseWheelListener(this);

        this.inputProcessor = inputProcessor;
        setInteractionStatus(none);
        this.timer = new Timer(50, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                timer.stop();
                processStoredEvents();
            }
        });
    }

    private void processStoredEvents() {
        if (lastMovingEvent != null) {
            updateSelectionMove(lastMovingEvent, true);
            lastMovingEvent = null;
        }
    }

    public void addHeatmapMouseListener(HeatmapMouseListener listener) {
        listeners.add(listener);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        // Skip right click
        if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
            return;
        }

        panel.requestFocusInWindow();

        int index = convertToIndex(e);
        if (!isValidIndex(index)) {
            return;
        }

        int row = horizontal ? -1 : index;
        int col = horizontal ? index : -1;

        for (HeatmapMouseListener l : listeners)
            l.mouseClicked(row, col, e);
    }

    private int convertToIndex(MouseEvent e) {
        point = e.getPoint();
        Point viewPosition = viewPort.getViewPosition();
        point.translate(viewPosition.x, viewPosition.y);
        position = headerPanel.getDrawer().getPosition(point);

        return horizontal ? position.column : position.row;
    }

    @Override
    public void mousePressed(MouseEvent e) {
        // Skip right click
        if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
            return;
        }

        panel.configureHeaders(e);

        // check if it's a already selected
        int index = convertToIndex(e);

        if (isSelectedIndex(index)) {
            setInteractionStatus(movingSelected);
        } else {
            setInteractionStatus(selectingRowsAndCols);
        }


        switch (getInteractionStatus()) {
            case selectingRowsAndCols:
                updateSelection(e, false);
                break;
            case movingSelected:
                selectionHasMoved = false;
                updateSelectionMove(e, false);
                break;
        }

        panel.requestFocusInWindow();
    }

    private boolean isSelectedIndex(int index) {
        return isValidIndex(index) && dimension.getSelected().contains(dimension.getLabel(index));
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        panel.mouseReleased(e);
        int modifiers = e.getModifiers();
        boolean altDown = ((modifiers & altMask) != 0);

        int index = convertToIndex(e);
        if (getInteractionStatus() == movingSelected) {
            if (!selectionHasMoved) {
                //There was no dragging, user wanted to unselect the selection
                inputProcessor.switchSelection(dimension, index, altDown);
                setLeading(e);
                setInteractionStatus(none);
            } else {
                setInteractionStatus(none);
                dimension.forceUpdate(AbstractMatrixViewDimension.PROPERTY_VISIBLE);
                //updateSelectionMove(e, true);
                selectionHasMoved = false;
            }
        }
    }

    private void setLeading(MouseEvent e) {
        int index = convertToIndex(e);
        if (isValidIndex(index)) {
            dimension.setFocus(dimension.getLabel(index));
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        switch (getInteractionStatus()) {
            case selectingRowsAndCols:
                updateSelection(e, true);
                break;
            case moving:
                updateScroll(e, true);
                break;
            case movingSelected:
                updateSelectionMove(e, true);
                break;
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        int index = convertToIndex(e);
        if (!isValidIndex(index)) {
            return;
        }

        int row = horizontal ? -1 : index;
        int col = horizontal ? index : -1;

        for (HeatmapMouseListener l : listeners)
            l.mouseMoved(row, col, e);
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        int unitsToScroll = e.getUnitsToScroll();

        int modifiers = e.getModifiers();
        boolean ctrlDown = ((modifiers & ctrlMask) != 0);

        setInteractionStatus((ctrlDown) ? zooming : scrolling);

        if (getInteractionStatus() == scrolling) {
            HeatmapPosition pos = panel.getScrollPosition();

            if (horizontal) {
                panel.setScrollColumnPosition(pos.column + unitsToScroll);
            } else {
                panel.setScrollRowPosition(pos.row + unitsToScroll);
            }
        } else {

            if (inputProcessor.isKeyPressed(KeyEvent.VK_C) ||
                    inputProcessor.isKeyPressed(KeyEvent.VK_R)) {
                inputProcessor.zoomHeatmap(unitsToScroll > 0 ? 1 : -1);
            } else {

                point = e.getPoint();
                Point viewPosition = viewPort.getViewPosition();
                point.translate(viewPosition.x, viewPosition.y);

                HeatmapHeader header = headerPanel.getHeaderDrawer().getHeader(point);

                if (inputProcessor.isKeyPressed(KeyEvent.VK_M)) {
                    int margin = header.getMargin() + unitsToScroll * -2;

                    if (margin < 1) {
                        margin = 1;
                    } else {
                        int size = header.getSize() + unitsToScroll * -2;
                        if (size < 10) {
                            size = 10;
                        }
                        header.setSize(size);
                    }
                    header.setMargin(margin);
                } else {
                    int size = header.getSize() + unitsToScroll * -2;
                    if (size < 10) {
                        size = 10;
                    }
                    header.setSize(size);
                }

                header.getHeatmapDimension().updateHeaders();

            }

        }
    }

    private void updateSelectionMove(MouseEvent e, boolean dragging) {

        if (!timer.isRunning()) {
            timer.start();
        } else {
            lastMovingEvent = e;
            return;
        }

        // Scroll heatmap if needed
        Point point = e.getPoint();
        if (horizontal) {
            int left = (int) point.getX() - viewPort.getWidth();
            if (left > 0) {
                panel.setScrollColumnValue(panel.getScrollValue().x + left);
            } else {
                if (point.getX() < 0) {
                    panel.setScrollColumnValue(panel.getScrollValue().x + (int) point.getX());
                }
            }
        } else {
            int down = (int) point.getY() - viewPort.getHeight();
            if (down > 0) {
                panel.setScrollRowValue(panel.getScrollValue().y + down);
            } else {
                if (point.getY() < 0) {
                    panel.setScrollRowValue(panel.getScrollValue().y + (int) point.getY());
                }
            }
        }

        int index = convertToIndex(e);

        if (!isValidIndex(index)) {
            return;
        }

        if (!dragging) {
            selectionMoveLastIndex = index;
        } else {

            int indexDiff = selectionMoveLastIndex - index;
            selectionMoveLastIndex = index;

            if (indexDiff > 0) {
                selectionHasMoved = true;
                for (int i = 0; i < indexDiff; i++) {
                    dimension.move(Direction.LEFT, dimension.getSelected());
                    inputProcessor.shiftSelStart(dimension, -1);
                }
            }

            if (indexDiff < 0) {
                selectionHasMoved = true;
                for (int i = 0; i > indexDiff; i--) {
                    dimension.move(Direction.RIGHT, dimension.getSelected());
                    inputProcessor.shiftSelStart(dimension, 1);
                }
            }
        }
    }

    private void updateSelection(MouseEvent e, boolean dragging) {
        int index = convertToIndex(e);

        if (!isValidIndex(index)) {
            return;
        }

        boolean indexChanged = (selLast != index);
        if (indexChanged) {
            setLeading(e);
        }
        selLast = index;

        int modifiers = e.getModifiers();
        boolean shiftDown = ((modifiers & shiftMask) != 0);

        if (!dragging && !shiftDown) {
            inputProcessor.switchSelection(dimension, index, false);
        } else if (shiftDown) {

            int lastSelected = horizontal ? inputProcessor.getLastSelectedCol() : inputProcessor.getLastSelectedRow();
            inputProcessor.addToSelected(lastSelected, index, dimension);
            inputProcessor.setLastSelected(index, horizontal);

        }
    }

    private int getIndexCount() {
        return dimension.size();
    }

    private boolean isValidIndex(int index) {
        if (index < 0 || index >= getIndexCount()) {
            return false;
        }
        return true;
    }

    private void updateScroll(MouseEvent e, boolean dragging) {
        point = e.getPoint();

        if (!dragging) {
            startPoint = point;
            startScrollValue = panel.getScrollValue();
        } else {
            int widthOffset = point.x - startPoint.x;
            int heightOffset = point.y - startPoint.y;

            if (horizontal) {
                panel.setScrollColumnValue(startScrollValue.x - widthOffset);
            } else {
                panel.setScrollRowValue(startScrollValue.y - heightOffset);
            }
        }
    }

}
