/*******************************************************************************
 * Copyright (c) 2017, 2020 Dirk Fauth.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.viewport.action;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.ui.action.IDragMode;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.nebula.widgets.nattable.viewport.command.ViewportDragCommand;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;

/**
 * Abstract {@link IDragMode} implementation to support auto-scrolling on
 * dragging.
 *
 * @since 1.5
 */
public abstract class AutoScrollDragMode implements IDragMode {

    private final boolean horizontal;
    private final boolean vertical;

    private AutoScrollRunnable runnable;

    protected int horizontalBorderOffset = GUIHelper.convertHorizontalPixelToDpi(25);
    /**
     * @since 2.0
     */
    protected int verticalBorderOffset = GUIHelper.convertVerticalPixelToDpi(25);

    /**
     *
     * @param horizontal
     *            <code>true</code> to support horizontal auto-scrolling.
     * @param vertical
     *            <code>true</code> to support vertical auto-scrolling.
     */
    public AutoScrollDragMode(boolean horizontal, boolean vertical) {
        this.horizontal = horizontal;
        this.vertical = vertical;
    }

    @Override
    public void mouseMove(NatTable natTable, MouseEvent event) {
        Rectangle clientArea = natTable.getClientAreaProvider().getClientArea();

        int x = event.x;
        int y = event.y;

        int horizontalDiff = 0;
        MoveDirectionEnum horizontal = MoveDirectionEnum.NONE;
        if (this.horizontal) {
            if (event.x < this.horizontalBorderOffset) {
                horizontal = MoveDirectionEnum.LEFT;
                x = Math.max(0, event.x);
                horizontalDiff = -event.x;
            } else if (event.x > (clientArea.width - this.horizontalBorderOffset)) {
                horizontal = MoveDirectionEnum.RIGHT;
                x = clientArea.width - 1;
                horizontalDiff = event.x - clientArea.width;
            }
        }

        int verticalDiff = 0;
        MoveDirectionEnum vertical = MoveDirectionEnum.NONE;
        if (this.vertical) {
            if (event.y < this.verticalBorderOffset) {
                vertical = MoveDirectionEnum.UP;
                y = 0;
                verticalDiff = -event.y;
            } else if (event.y > clientArea.height - this.verticalBorderOffset) {
                vertical = MoveDirectionEnum.DOWN;
                y = clientArea.height - 1;
                verticalDiff = event.y - clientArea.height;
            }
        }

        if ((!MoveDirectionEnum.NONE.equals(horizontal)
                || !MoveDirectionEnum.NONE.equals(vertical)) && this.runnable == null) {
            this.runnable = new AutoScrollRunnable(natTable, x, y, horizontal, vertical);
            this.runnable.schedule();
        } else if (MoveDirectionEnum.NONE.equals(horizontal)
                && MoveDirectionEnum.NONE.equals(vertical)
                && this.runnable != null) {
            this.runnable.cancel();
            this.runnable = null;
        } else if (this.runnable != null) {
            this.runnable.calculateRepeatDelay(horizontalDiff, verticalDiff);
        }

        performDragAction(natTable, x, y, horizontal, vertical);
    }

    @Override
    public void mouseUp(NatTable natTable, MouseEvent event) {
        if (this.runnable != null) {
            this.runnable.cancel();
            this.runnable = null;
        }
    }

    /**
     *
     * @param natTable
     *            The NatTable instance the drag operation is currently
     *            performed on.
     * @param x
     *            The x coordinate of the mouse pointer on mouse move. Corrected
     *            to be inside the NatTable client area.
     * @param y
     *            The y coordinate of the mouse pointer on mouse move. Corrected
     *            to be inside the NatTable client area.
     * @param horizontal
     *            The horizontal direction where the auto-scroll should be
     *            performed to.
     * @param vertical
     *            The vertical direction where the auto-scroll should be
     *            performed to.
     */
    protected void performDragAction(
            NatTable natTable,
            int x, int y,
            MoveDirectionEnum horizontal, MoveDirectionEnum vertical) {
        // do nothing by default
    }

    /**
     * Runnable that continuously scrolls the viewport.
     */
    protected class AutoScrollRunnable implements Runnable {

        private final NatTable natTable;
        private final Display display;

        private final MoveDirectionEnum horizontal;
        private final MoveDirectionEnum vertical;

        private final int x, y;

        private int repeatDelay = 500;

        private boolean active = true;

        public AutoScrollRunnable(NatTable natTable,
                int x, int y,
                MoveDirectionEnum horizontal, MoveDirectionEnum vertical) {
            this.natTable = natTable;
            this.display = natTable.getDisplay();
            this.horizontal = horizontal;
            this.vertical = vertical;
            this.x = x;
            this.y = y;
        }

        /**
         * Schedule this runnable to start with a delay of 500 ms.
         */
        public void schedule() {
            if (this.display != null) {
                this.display.timerExec(500, this);
            }
        }

        /**
         * Calculates the delay of the repeated auto-scroll execution based on
         * the difference between the NatTable borders and the mouse cursor and
         * the move direction that is currently active.
         *
         * @param horizontalDiff
         *            The horizontal difference between the NatTable border and
         *            the mouse cursor.
         * @param verticalDiff
         *            The vertical difference between the NatTable border and
         *            the mouse cursor.
         */
        public void calculateRepeatDelay(int horizontalDiff, int verticalDiff) {
            if (!MoveDirectionEnum.NONE.equals(this.horizontal)) {
                int factor = horizontalDiff / 5;
                factor = Math.min(factor, 10);
                this.repeatDelay = 500 - (factor * 49);
            }

            if (!MoveDirectionEnum.NONE.equals(this.vertical)) {
                int factor = verticalDiff / 5;
                factor = Math.min(factor, 10);
                this.repeatDelay = 500 - (factor * 49);
            }
        }

        /**
         * Cancels the repeated execution.
         */
        public void cancel() {
            this.active = false;
        }

        @Override
        public void run() {
            if (this.active) {
                if (!MoveDirectionEnum.NONE.equals(this.horizontal)
                        || !MoveDirectionEnum.NONE.equals(this.vertical)) {
                    if (this.natTable.doCommand(new ViewportDragCommand(this.horizontal, this.vertical))) {
                        performDragAction(this.natTable, this.x, this.y, this.horizontal, this.vertical);
                    }
                }

                this.display.timerExec(this.repeatDelay, this);
            }
        }
    }
}
