/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * 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:
 *     Original authors and others - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.viewport;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

public abstract class ScrollBarHandlerTemplate implements Listener {

    public static final int DEFAULT_OFFSET = 1;
    protected final ViewportLayer viewportLayer;
    protected final IUniqueIndexLayer scrollableLayer;
    protected final IScroller<?> scroller;

    /**
     * Flag to remember if the scroll bar is moved by dragging. Needed because
     * if the scroll bar is moved by dragging, there will be another event that
     * is handled for releasing the drag mode. We only need to handle the
     * dragging once, otherwise if the DialogErrorHandling strategy is used, the
     * dialog would be showed twice.
     */
    private boolean dragging = false;

    /**
     * Flag to remember if the drag operation should be handled or not. The
     * value will be set on trying to commit an possible open editor. If that is
     * not possible, the value will be set to <code>false</code> which will
     * result in not performing a scrolling operation.
     * <p>
     * This is necessary to avoid inconsistent state when having an editor that
     * contains invalid data and you are trying to scroll. If scrolling would be
     * handled, the open editor wouldn't close which results in broken
     * rendering.
     * </p>
     */
    private boolean globalHandle = true;

    /**
     * The table control of this scroll bar handler.
     * <p>
     * Required in order to close the active editor when the user starts to
     * scroll
     * </p>
     */
    private NatTable table;

    public ScrollBarHandlerTemplate(ViewportLayer viewportLayer, IScroller<?> scroller) {
        this.viewportLayer = viewportLayer;
        this.scrollableLayer = viewportLayer.getScrollableLayer();
        this.scroller = scroller;
        this.scroller.addListener(SWT.Selection, this);
    }

    public void dispose() {
        if (this.scroller != null && !this.scroller.isDisposed()) {
            this.scroller.removeListener(SWT.Selection, this);
        }
    }

    @Override
    public void handleEvent(Event event) {
        // ensure that the keep in viewport setting is reset if there is one
        // active
        this.viewportLayer.setKeepInViewportRowPosition(-1);

        if (!this.dragging) {
            // Only try to commit and close an possible open editor once
            // when starting the drag operation. Otherwise the conversion
            // and validation errors would raise multiple times.
            if (this.table != null && !this.table.commitAndCloseActiveCellEditor()) {
                this.globalHandle = false;
            }
        }

        boolean handle = this.globalHandle;

        if (event.detail == SWT.DRAG) {
            this.dragging = true;
        } else {
            // dragging is finished so we reset the global states
            this.dragging = false;
            this.globalHandle = true;
        }

        if (handle && event.widget == this.scroller.getUnderlying()) {
            setViewportOrigin(getViewportMinimumOrigin() + this.scroller.getSelection());
            setScrollIncrement();
            event.doit = false;
        } else {
            adjustScrollBar();
        }
    }

    void adjustScrollBar() {
        if (this.scroller.isDisposed()) {
            return;
        }
        int startPixel = getViewportOrigin() - getViewportMinimumOrigin();

        this.scroller.setSelection(startPixel);
    }

    void recalculateScrollBarSize() {
        if (this.scroller.isDisposed()) {
            return;
        }

        int max = getScrollableLayerSpan() - getViewportMinimumOrigin();
        if (!this.scroller.isDisposed()) {
            this.scroller.setMaximum(max);
        }

        adjustScrollBar();

        int viewportWindowSpan = getViewportWindowSpan();

        if (viewportWindowSpan < max && viewportWindowSpan != 0) {
            this.scroller.setThumb(viewportWindowSpan);
            this.scroller.setEnabled(true);
            this.scroller.setVisible(true);

            setScrollIncrement();

            this.scroller.setPageIncrement(viewportWindowSpan);
        } else {
            this.scroller.setThumb(max);
            this.scroller.setEnabled(false);
            this.scroller.setVisible(viewportWindowSpan == 0 && max > 0);
        }
    }

    void setScrollIncrement() {
        int scrollIncrement = Math.min(getScrollIncrement(), getViewportWindowSpan() / 4);
        this.scroller.setIncrement(scrollIncrement);
    }

    /**
     * Sets the table belonging to this handler.
     * <p>
     * The table is required to close the active editor when scrolling. If the
     * table is NOT set then the active editor will stay open during the
     * scrolling leading to drawing errors.
     * </p>
     *
     * @param table
     *            the table
     */
    void setTable(NatTable table) {
        this.table = table;
    }

    /**
     * Methods to be implemented by the Horizontal/Vertical scroll bar handlers.
     *
     * @return
     */
    abstract int getViewportWindowSpan();

    abstract int getScrollableLayerSpan();

    abstract boolean keepScrolling();

    abstract int getViewportOrigin();

    abstract int getViewportMinimumOrigin();

    abstract void setViewportOrigin(int pixel);

    abstract MoveDirectionEnum scrollDirectionForEventDetail(int eventDetail);

    abstract int getScrollIncrement();

}
