/*
 * Copyright (c) 2010, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.control.skin;

import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.behavior.BehaviorBase;
import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import com.sun.javafx.util.Utils;
import com.sun.javafx.scene.control.behavior.ScrollPaneBehavior;
import static com.sun.javafx.scene.control.skin.Utils.*;
import javafx.geometry.Insets;

import java.util.function.Consumer;

/**
 * Default skin implementation for the {@link ScrollPane} control.
 *
 * @see ScrollPane
 * @since 9
 */
public class ScrollPaneSkin extends SkinBase<ScrollPane> {
    /* *************************************************************************
     *                                                                         *
     * Static fields                                                           *
     *                                                                         *
     **************************************************************************/

    private static final double DEFAULT_PREF_SIZE = 100.0;

    private static final double DEFAULT_MIN_SIZE = 36.0;

    private static final double DEFAULT_SB_BREADTH = 12.0;
    private static final double DEFAULT_EMBEDDED_SB_BREADTH = 8.0;

    private static final double PAN_THRESHOLD = 0.5;



    /* *************************************************************************
     *                                                                         *
     * Private fields                                                          *
     *                                                                         *
     **************************************************************************/

    // state from the control

    private Node scrollNode;
    private final BehaviorBase<ScrollPane> behavior;

    private double nodeWidth;
    private double nodeHeight;
    private boolean nodeSizeInvalid = true;

    private double posX;
    private double posY;

    // working state

    private boolean hsbvis;
    private boolean vsbvis;
    private double hsbHeight;
    private double vsbWidth;

    // substructure

    private StackPane viewRect;
    private StackPane viewContent;
    private double contentWidth;
    private double contentHeight;
    private StackPane corner;
    ScrollBar hsb;
    ScrollBar vsb;

    double pressX;
    double pressY;
    double ohvalue;
    double ovvalue;
    private Cursor saveCursor =  null;
    private boolean dragDetected = false;
    private boolean touchDetected = false;
    private boolean mouseDown = false;

    Rectangle clipRect;

    Timeline sbTouchTimeline;
    KeyFrame sbTouchKF1;
    KeyFrame sbTouchKF2;
    Timeline contentsToViewTimeline;
    KeyFrame contentsToViewKF1;
    KeyFrame contentsToViewKF2;
    KeyFrame contentsToViewKF3;

    private boolean tempVisibility;



    /* *************************************************************************
     *                                                                         *
     * Listeners                                                               *
     *                                                                         *
     **************************************************************************/

    private final InvalidationListener nodeListener = new InvalidationListener() {
        @Override public void invalidated(Observable valueModel) {
            if (!nodeSizeInvalid) {
                final Bounds scrollNodeBounds = scrollNode.getLayoutBounds();
                final double scrollNodeWidth = scrollNodeBounds.getWidth();
                final double scrollNodeHeight = scrollNodeBounds.getHeight();

                /*
                ** if the new size causes scrollbar visibility to change, then need to relayout
                ** we also need to correct the thumb size when the scrollnode's size changes
                */
                if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible() ||
                        (scrollNodeWidth != 0.0  && nodeWidth != scrollNodeWidth) ||
                        (scrollNodeHeight != 0.0 && nodeHeight != scrollNodeHeight)) {
                    getSkinnable().requestLayout();
                } else {
                    /**
                     * we just need to update scrollbars based on new scrollNode size,
                     * but we don't do this while dragging, there's no need,
                     * and it jumps, as dragging updates the scrollbar too.
                     */
                    if (!dragDetected) {
                        updateVerticalSB();
                        updateHorizontalSB();
                    }
                }
            }
        }
    };


    /*
    ** The content of the ScrollPane has just changed bounds, check scrollBar positions.
    */
    private final ChangeListener<Bounds> boundsChangeListener = new ChangeListener<Bounds>() {
        @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds newBounds) {

            /*
            ** For a height change then we want to reduce
            ** viewport vertical jumping as much as possible.
            ** We set a new vsb value to try to keep the same
            ** content position at the top of the viewport
            */
            double oldHeight = oldBounds.getHeight();
            double newHeight = newBounds.getHeight();
            if (oldHeight > 0 && oldHeight != newHeight) {
                double oldPositionY = (snapPositionY(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (oldHeight - contentHeight)));
                double newPositionY = (snapPositionY(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (newHeight - contentHeight)));

                double newValueY = (oldPositionY/newPositionY)*vsb.getValue();
                if (newValueY < 0.0) {
                    vsb.setValue(0.0);
                }
                else if (newValueY < 1.0) {
                    vsb.setValue(newValueY);
                }
                else if (newValueY > 1.0) {
                    vsb.setValue(1.0);
                }
            }

            /*
            ** For a width change then we want to reduce
            ** viewport horizontal jumping as much as possible.
            ** We set a new hsb value to try to keep the same
            ** content position to the left of the viewport
            */
            double oldWidth = oldBounds.getWidth();
            double newWidth = newBounds.getWidth();
            if (oldWidth > 0 && oldWidth != newWidth) {
                double oldPositionX = (snapPositionX(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (oldWidth - contentWidth)));
                double newPositionX = (snapPositionX(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (newWidth - contentWidth)));

                double newValueX = (oldPositionX/newPositionX)*hsb.getValue();
                if (newValueX < 0.0) {
                    hsb.setValue(0.0);
                }
                else if (newValueX < 1.0) {
                    hsb.setValue(newValueX);
                }
                else if (newValueX > 1.0) {
                    hsb.setValue(1.0);
                }
            }
        }
    };



    /* *************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/

    /**
     * Creates a new ScrollPaneSkin instance, installing the necessary child
     * nodes into the Control {@link Control#getChildren() children} list, as
     * well as the necessary input mappings for handling key, mouse, etc events.
     *
     * @param control The control that this skin should be installed onto.
     */
    public ScrollPaneSkin(final ScrollPane control) {
        super(control);

        // install default input map for the ScrollPane control
        behavior = new ScrollPaneBehavior(control);
//        control.setInputMap(behavior.getInputMap());

        initialize();

        // Register listeners
        Consumer<ObservableValue<?>> viewportSizeHintConsumer = e -> {
            // change affects pref size, so requestLayout on control
            getSkinnable().requestLayout();
        };
        registerChangeListener(control.contentProperty(), e -> {
            if (scrollNode != getSkinnable().getContent()) {
                if (scrollNode != null) {
                    scrollNode.layoutBoundsProperty().removeListener(nodeListener);
                    scrollNode.layoutBoundsProperty().removeListener(boundsChangeListener);
                    viewContent.getChildren().remove(scrollNode);
                }
                scrollNode = getSkinnable().getContent();
                if (scrollNode != null) {
                    nodeWidth = snapSizeX(scrollNode.getLayoutBounds().getWidth());
                    nodeHeight = snapSizeY(scrollNode.getLayoutBounds().getHeight());
                    viewContent.getChildren().setAll(scrollNode);
                    scrollNode.layoutBoundsProperty().addListener(nodeListener);
                    scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
                }
            }
            getSkinnable().requestLayout();
        });
        registerChangeListener(control.fitToWidthProperty(), e -> {
            getSkinnable().requestLayout();
            viewRect.requestLayout();
        });
        registerChangeListener(control.fitToHeightProperty(), e -> {
            getSkinnable().requestLayout();
            viewRect.requestLayout();
        });
        registerChangeListener(control.hbarPolicyProperty(), e -> {
            // change might affect pref size, so requestLayout on control
            getSkinnable().requestLayout();
        });
        registerChangeListener(control.vbarPolicyProperty(), e -> {
            // change might affect pref size, so requestLayout on control
            getSkinnable().requestLayout();
        });
        registerChangeListener(control.hvalueProperty(), e -> hsb.setValue(getSkinnable().getHvalue()));
        registerChangeListener(control.hmaxProperty(), e -> hsb.setMax(getSkinnable().getHmax()));
        registerChangeListener(control.hminProperty(), e -> hsb.setMin(getSkinnable().getHmin()));
        registerChangeListener(control.vvalueProperty(), e -> vsb.setValue(getSkinnable().getVvalue()));
        registerChangeListener(control.vmaxProperty(), e -> vsb.setMax(getSkinnable().getVmax()));
        registerChangeListener(control.vminProperty(), e -> vsb.setMin(getSkinnable().getVmin()));
        registerChangeListener(control.prefViewportWidthProperty(), viewportSizeHintConsumer);
        registerChangeListener(control.prefViewportHeightProperty(), viewportSizeHintConsumer);
        registerChangeListener(control.minViewportWidthProperty(), viewportSizeHintConsumer);
        registerChangeListener(control.minViewportHeightProperty(), viewportSizeHintConsumer);
    }



    /* *************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/

    private DoubleProperty contentPosX;
    private final void setContentPosX(double value) { contentPosXProperty().set(value); }
    private final double getContentPosX() { return contentPosX == null ? 0.0 : contentPosX.get(); }
    private final DoubleProperty contentPosXProperty() {
        if (contentPosX == null) {
            contentPosX = new DoublePropertyBase() {
                @Override protected void invalidated() {
                    hsb.setValue(getContentPosX());
                    getSkinnable().requestLayout();
                }

                @Override
                public Object getBean() {
                    return ScrollPaneSkin.this;
                }

                @Override
                public String getName() {
                    return "contentPosX";
                }
            };
        }
        return contentPosX;
    }

    private DoubleProperty contentPosY;
    private final void setContentPosY(double value) { contentPosYProperty().set(value); }
    private final double getContentPosY() { return contentPosY == null ? 0.0 : contentPosY.get(); }
    private final DoubleProperty contentPosYProperty() {
        if (contentPosY == null) {
            contentPosY = new DoublePropertyBase() {
                @Override protected void invalidated() {
                    vsb.setValue(getContentPosY());
                    getSkinnable().requestLayout();
                }

                @Override
                public Object getBean() {
                    return ScrollPaneSkin.this;
                }

                @Override
                public String getName() {
                    return "contentPosY";
                }
            };
        }
        return contentPosY;
    }



    /* *************************************************************************
     *                                                                         *
     * Public API                                                              *
     *                                                                         *
     **************************************************************************/

    /** {@inheritDoc} */
    @Override public void dispose() {
        super.dispose();

        if (behavior != null) {
            behavior.dispose();
        }
    }

    /**
     * Returns the horizontal {@link ScrollBar} used in this ScrollPaneSkin
     * instance.
     * @return the horizontal ScrollBar used in this ScrollPaneSkin instance
     */
    public final ScrollBar getHorizontalScrollBar() {
        return hsb;
    }

    /**
     * Returns the vertical {@link ScrollBar} used in this ScrollPaneSkin
     * instance.
     * @return the vertical ScrollBar used in this ScrollPaneSkin instance
     */
    public final ScrollBar getVerticalScrollBar() {
        return vsb;
    }

    /** {@inheritDoc} */
    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        final ScrollPane sp = getSkinnable();

        double vsbWidth = computeVsbSizeHint(sp);
        double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();

        if (sp.getPrefViewportWidth() > 0) {
            return (sp.getPrefViewportWidth() + minWidth);
        }
        else if (sp.getContent() != null) {
            return (sp.getContent().prefWidth(height) + minWidth);
        }
        else {
            return Math.max(minWidth, DEFAULT_PREF_SIZE);
        }
    }

    /** {@inheritDoc} */
    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        final ScrollPane sp = getSkinnable();

        double hsbHeight = computeHsbSizeHint(sp);
        double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();

        if (sp.getPrefViewportHeight() > 0) {
            return (sp.getPrefViewportHeight() + minHeight);
        }
        else if (sp.getContent() != null) {
            return (sp.getContent().prefHeight(width) + minHeight);
        }
        else {
            return Math.max(minHeight, DEFAULT_PREF_SIZE);
        }
    }

    /** {@inheritDoc} */
    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        final ScrollPane sp = getSkinnable();

        double vsbWidth = computeVsbSizeHint(sp);
        double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();

        if (sp.getMinViewportWidth() > 0) {
            return (sp.getMinViewportWidth() + minWidth);
        } else {
            double w = corner.minWidth(-1);
            return (w > 0) ? (3 * w) : (DEFAULT_MIN_SIZE);
        }

    }

    /** {@inheritDoc} */
    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        final ScrollPane sp = getSkinnable();

        double hsbHeight = computeHsbSizeHint(sp);
        double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();

        if (sp.getMinViewportHeight() > 0) {
            return (sp.getMinViewportHeight() + minHeight);
        } else {
            double h = corner.minHeight(-1);
            return (h > 0) ? (3 * h) : (DEFAULT_MIN_SIZE);
        }
    }

    @Override protected void layoutChildren(final double x, final double y,
                                            final double w, final double h) {
        final ScrollPane control = getSkinnable();
        final Insets padding = control.getPadding();
        final double rightPadding = snapSizeX(padding.getRight());
        final double leftPadding = snapSizeX(padding.getLeft());
        final double topPadding = snapSizeY(padding.getTop());
        final double bottomPadding = snapSizeY(padding.getBottom());

        vsb.setMin(control.getVmin());
        vsb.setMax(control.getVmax());

        //should only do this on css setup
        hsb.setMin(control.getHmin());
        hsb.setMax(control.getHmax());

        contentWidth = w;
        contentHeight = h;

        /*
        ** we want the scrollbars to go right to the border
        */
        double hsbWidth = 0;
        double vsbHeight = 0;

        computeScrollNodeSize(contentWidth, contentHeight);
        computeScrollBarSize();

        for (int i = 0; i < 2; ++i) {
            vsbvis = determineVerticalSBVisible();
            hsbvis = determineHorizontalSBVisible();

            if (vsbvis && !Properties.IS_TOUCH_SUPPORTED) {
                contentWidth = w - vsbWidth;
            }
            hsbWidth = w + leftPadding + rightPadding - (vsbvis ? vsbWidth : 0);
            if (hsbvis && !Properties.IS_TOUCH_SUPPORTED) {
                contentHeight = h - hsbHeight;
            }
            vsbHeight = h + topPadding + bottomPadding - (hsbvis ? hsbHeight : 0);
        }


        if (scrollNode != null && scrollNode.isResizable()) {
            // maybe adjust size now that scrollbars may take up space
            if (vsbvis && hsbvis) {
                // adjust just once to accommodate
                computeScrollNodeSize(contentWidth, contentHeight);

            } else if (hsbvis && !vsbvis) {
                computeScrollNodeSize(contentWidth, contentHeight);
                vsbvis = determineVerticalSBVisible();
                if (vsbvis) {
                    // now both are visible
                    contentWidth -= vsbWidth;
                    hsbWidth -= vsbWidth;
                    computeScrollNodeSize(contentWidth, contentHeight);
                }
            } else if (vsbvis && !hsbvis) {
                computeScrollNodeSize(contentWidth, contentHeight);
                hsbvis = determineHorizontalSBVisible();
                if (hsbvis) {
                    // now both are visible
                    contentHeight -= hsbHeight;
                    vsbHeight -= hsbHeight;
                    computeScrollNodeSize(contentWidth, contentHeight);
                }
            }
        }

        // figure out the content area that is to be filled
        double cx = snappedLeftInset() - leftPadding;
        double cy = snappedTopInset() - topPadding;

        vsb.setVisible(vsbvis);
        if (vsbvis) {
            /*
            ** round up position of ScrollBar, round down it's size.
            **
            ** Positioning the ScrollBar
            **  The Padding should go between the content and the edge,
            **  otherwise changes in padding move the ScrollBar, and could
            **  in extreme cases size the ScrollBar to become unusable.
            **  The -1, +1 plus one bit :
            **   If padding in => 1 then we allow one pixel to appear as the
            **   outside border of the Scrollbar, and the rest on the inside.
            **   If padding is < 1 then we just stick to the edge.
            */
            vsb.resizeRelocate(snappedLeftInset() + w - vsbWidth + (rightPadding < 1 ? 0 : rightPadding - 1) ,
                    cy, vsbWidth, vsbHeight);
        }
        updateVerticalSB();

        hsb.setVisible(hsbvis);
        if (hsbvis) {
            /*
            ** round up position of ScrollBar, round down it's size.
            **
            ** Positioning the ScrollBar
            **  The Padding should go between the content and the edge,
            **  otherwise changes in padding move the ScrollBar, and could
            **  in extreme cases size the ScrollBar to become unusable.
            **  The -1, +1 plus one bit :
            **   If padding in => 1 then we allow one pixel to appear as the
            **   outside border of the Scrollbar, and the rest on the inside.
            **   If padding is < 1 then we just stick to the edge.
            */
            hsb.resizeRelocate(cx, snappedTopInset() + h - hsbHeight + (bottomPadding < 1 ? 0 : bottomPadding - 1),
                    hsbWidth, hsbHeight);
        }
        updateHorizontalSB();

        viewRect.resizeRelocate(snappedLeftInset(), snappedTopInset(), snapSizeX(contentWidth), snapSizeY(contentHeight));
        resetClip();

        if (vsbvis && hsbvis) {
            corner.setVisible(true);
            double cornerWidth = vsbWidth;
            double cornerHeight = hsbHeight;
            corner.resizeRelocate(snapPositionX(vsb.getLayoutX()), snapPositionY(hsb.getLayoutY()), snapSizeX(cornerWidth), snapSizeY(cornerHeight));
        } else {
            corner.setVisible(false);
        }
        control.setViewportBounds(new BoundingBox(snapPositionX(viewContent.getLayoutX()), snapPositionY(viewContent.getLayoutY()), snapSizeX(contentWidth), snapSizeY(contentHeight)));
    }

    /** {@inheritDoc} */
    @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
        switch (attribute) {
            case VERTICAL_SCROLLBAR: return vsb;
            case HORIZONTAL_SCROLLBAR: return hsb;
            default: return super.queryAccessibleAttribute(attribute, parameters);
        }
    }



    /* *************************************************************************
     *                                                                         *
     * Private implementation                                                  *
     *                                                                         *
     **************************************************************************/

    private void initialize() {
        // requestLayout calls below should not trigger requestLayout above ScrollPane
//        setManaged(false);

        ScrollPane control = getSkinnable();
        scrollNode = control.getContent();

        ParentTraversalEngine traversalEngine = new ParentTraversalEngine(getSkinnable());
        traversalEngine.addTraverseListener((node, bounds) -> {
            // auto-scroll so node is within (0,0),(contentWidth,contentHeight)
            scrollBoundsIntoView(bounds);
        });
        ParentHelper.setTraversalEngine(getSkinnable(), traversalEngine);

        if (scrollNode != null) {
            scrollNode.layoutBoundsProperty().addListener(nodeListener);
            scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
        }

        viewRect = new StackPane() {
            @Override protected void layoutChildren() {
                viewContent.resize(getWidth(), getHeight());
            }
        };
        // prevent requestLayout requests from within scrollNode from percolating up
        viewRect.setManaged(false);
        viewRect.setCache(true);
        viewRect.getStyleClass().add("viewport");

        clipRect = new Rectangle();
        viewRect.setClip(clipRect);

        hsb = new ScrollBar();

        vsb = new ScrollBar();
        vsb.setOrientation(Orientation.VERTICAL);

        EventHandler<MouseEvent> barHandler = ev -> {
            if (getSkinnable().isFocusTraversable()) {
                getSkinnable().requestFocus();
            }
        };

        hsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
        vsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);

        corner = new StackPane();
        corner.getStyleClass().setAll("corner");

        viewContent = new StackPane() {
            @Override public void requestLayout() {
                // if scrollNode requested layout, will want to recompute
                nodeSizeInvalid = true;

                super.requestLayout(); // add as layout root for next layout pass

                // Need to layout the ScrollPane as well in case scrollbars
                // appeared or disappeared.
                ScrollPaneSkin.this.getSkinnable().requestLayout();
            }
            @Override protected void layoutChildren() {
                if (nodeSizeInvalid) {
                    computeScrollNodeSize(getWidth(),getHeight());
                }
                if (scrollNode != null && scrollNode.isResizable()) {
                    scrollNode.resize(snapSizeX(nodeWidth), snapSizeY(nodeHeight));
                    if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible()) {
                        getSkinnable().requestLayout();
                    }
                }
                if (scrollNode != null) {
                    scrollNode.relocate(0,0);
                }
            }
        };
        viewRect.getChildren().add(viewContent);

        if (scrollNode != null) {
            viewContent.getChildren().add(scrollNode);
            viewRect.nodeOrientationProperty().bind(scrollNode.nodeOrientationProperty());
        }

        getChildren().clear();
        getChildren().addAll(viewRect, vsb, hsb, corner);

        /*
        ** listeners, and assorted housekeeping
        */
        InvalidationListener vsbListener = valueModel -> {
            if (!Properties.IS_TOUCH_SUPPORTED) {
                posY = Utils.clamp(getSkinnable().getVmin(), vsb.getValue(), getSkinnable().getVmax());
            }
            else {
                posY = vsb.getValue();
            }
            updatePosY();
        };
        vsb.valueProperty().addListener(vsbListener);

        InvalidationListener hsbListener = valueModel -> {
            if (!Properties.IS_TOUCH_SUPPORTED) {
                posX = Utils.clamp(getSkinnable().getHmin(), hsb.getValue(), getSkinnable().getHmax());
            }
            else {
                posX = hsb.getValue();
            }
            updatePosX();
        };
        hsb.valueProperty().addListener(hsbListener);

        viewRect.setOnMousePressed(e -> {
            mouseDown = true;
            if (Properties.IS_TOUCH_SUPPORTED) {
                startSBReleasedAnimation();
            }
            pressX = e.getX();
            pressY = e.getY();
            ohvalue = hsb.getValue();
            ovvalue = vsb.getValue();
        });


        viewRect.setOnDragDetected(e -> {
             if (Properties.IS_TOUCH_SUPPORTED) {
                 startSBReleasedAnimation();
             }
            if (getSkinnable().isPannable()) {
              dragDetected = true;
              if (saveCursor == null) {
                  saveCursor = getSkinnable().getCursor();
                  if (saveCursor == null) {
                      saveCursor = Cursor.DEFAULT;
                  }
                  getSkinnable().setCursor(Cursor.MOVE);
                  getSkinnable().requestLayout();
              }
            }
        });

        viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
             mouseDown = false;
             if (dragDetected == true) {
                 if (saveCursor != null) {
                     getSkinnable().setCursor(saveCursor);
                     saveCursor = null;
                     getSkinnable().requestLayout();
                 }
                 dragDetected = false;
             }

             /*
             ** if the contents need repositioning, and there's is no
             ** touch event in progress, then start the repositioning.
             */
             if ((posY > getSkinnable().getVmax() || posY < getSkinnable().getVmin() ||
                 posX > getSkinnable().getHmax() || posX < getSkinnable().getHmin()) && !touchDetected) {
                 startContentsToViewport();
             }
        });
        viewRect.setOnMouseDragged(e -> {
             if (Properties.IS_TOUCH_SUPPORTED) {
                 startSBReleasedAnimation();
             }
            /*
            ** for mobile-touch we allow drag, even if not pannagle
            */
            if (getSkinnable().isPannable() || Properties.IS_TOUCH_SUPPORTED) {
                double deltaX = pressX - e.getX();
                double deltaY = pressY - e.getY();
                /*
                ** we only drag if not all of the content is visible.
                */
                if (hsb.getVisibleAmount() > 0.0 && hsb.getVisibleAmount() < hsb.getMax()) {
                    if (Math.abs(deltaX) > PAN_THRESHOLD) {
                        if (isReverseNodeOrientation()) {
                            deltaX = -deltaX;
                        }
                        double newHVal = (ohvalue + deltaX / (nodeWidth - viewRect.getWidth()) * (hsb.getMax() - hsb.getMin()));
                        if (!Properties.IS_TOUCH_SUPPORTED) {
                            if (newHVal > hsb.getMax()) {
                                newHVal = hsb.getMax();
                            }
                            else if (newHVal < hsb.getMin()) {
                                newHVal = hsb.getMin();
                            }
                            hsb.setValue(newHVal);
                        }
                        else {
                            hsb.setValue(newHVal);
                        }
                    }
                }
                /*
                ** we only drag if not all of the content is visible.
                */
                if (vsb.getVisibleAmount() > 0.0 && vsb.getVisibleAmount() < vsb.getMax()) {
                    if (Math.abs(deltaY) > PAN_THRESHOLD) {
                        double newVVal = (ovvalue + deltaY / (nodeHeight - viewRect.getHeight()) * (vsb.getMax() - vsb.getMin()));
                        if (!Properties.IS_TOUCH_SUPPORTED) {
                            if (newVVal > vsb.getMax()) {
                                newVVal = vsb.getMax();
                            }
                            else if (newVVal < vsb.getMin()) {
                                newVVal = vsb.getMin();
                            }
                            vsb.setValue(newVVal);
                        }
                        else {
                            vsb.setValue(newVVal);
                        }
                    }
                }
            }
            /*
            ** we need to consume drag events, as we don't want
            ** the scrollpane itself to be dragged on every mouse click
            */
            e.consume();
        });


        /*
        ** don't allow the ScrollBar to handle the ScrollEvent,
        ** In a ScrollPane a vertical scroll should scroll on the vertical only,
        ** whereas in a horizontal ScrollBar it can scroll horizontally.
        */
        // block the event from being passed down to children
        final EventDispatcher blockEventDispatcher = (event, tail) -> event;
        // block ScrollEvent from being passed down to scrollbar's skin
        final EventDispatcher oldHsbEventDispatcher = hsb.getEventDispatcher();
        hsb.setEventDispatcher((event, tail) -> {
            if (event.getEventType() == ScrollEvent.SCROLL &&
                    !((ScrollEvent)event).isDirect()) {
                tail = tail.prepend(blockEventDispatcher);
                tail = tail.prepend(oldHsbEventDispatcher);
                return tail.dispatchEvent(event);
            }
            return oldHsbEventDispatcher.dispatchEvent(event, tail);
        });
        // block ScrollEvent from being passed down to scrollbar's skin
        final EventDispatcher oldVsbEventDispatcher = vsb.getEventDispatcher();
        vsb.setEventDispatcher((event, tail) -> {
            if (event.getEventType() == ScrollEvent.SCROLL &&
                    !((ScrollEvent)event).isDirect()) {
                tail = tail.prepend(blockEventDispatcher);
                tail = tail.prepend(oldVsbEventDispatcher);
                return tail.dispatchEvent(event);
            }
            return oldVsbEventDispatcher.dispatchEvent(event, tail);
        });

        /*
         * listen for ScrollEvents over the whole of the ScrollPane
         * area, the above dispatcher having removed the ScrollBars
         * scroll event handling.
         *
         * Note that we use viewRect here, rather than setting the eventHandler
         * on the ScrollPane itself. This is for RT-31582, and effectively
         * allows for us to prioritise handling (and consuming) the event
         * internally, before it is made available to users listening to events
         * on the control. This is consistent with the VirtualFlow-based controls.
         */
        viewRect.addEventHandler(ScrollEvent.SCROLL, event -> {
            if (Properties.IS_TOUCH_SUPPORTED) {
                startSBReleasedAnimation();
            }
            /*
            ** if we're completely visible then do nothing....
            ** we only consume an event that we've used.
            */
            if (vsb.getVisibleAmount() < vsb.getMax()) {
                double vRange = getSkinnable().getVmax()-getSkinnable().getVmin();
                double vPixelValue;
                if (nodeHeight > 0.0) {
                    vPixelValue = vRange / nodeHeight;
                }
                else {
                    vPixelValue = 0.0;
                }
                double newValue = vsb.getValue()+(-event.getDeltaY())*vPixelValue;
                if (!Properties.IS_TOUCH_SUPPORTED) {
                    if ((event.getDeltaY() > 0.0 && vsb.getValue() > vsb.getMin()) ||
                        (event.getDeltaY() < 0.0 && vsb.getValue() < vsb.getMax())) {
                        vsb.setValue(newValue);
                        event.consume();
                    }
                }
                else {
                    /*
                    ** if there is a repositioning in progress then we only
                    ** set the value for 'real' events
                    */
                    if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
                        vsb.setValue(newValue);
                        if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected)) {
                            startContentsToViewport();
                        }
                        event.consume();
                    }
                }
            }

            if (hsb.getVisibleAmount() < hsb.getMax()) {
                double hRange = getSkinnable().getHmax()-getSkinnable().getHmin();
                double hPixelValue;
                if (nodeWidth > 0.0) {
                    hPixelValue = hRange / nodeWidth;
                }
                else {
                    hPixelValue = 0.0;
                }

                double newValue = hsb.getValue()+(-event.getDeltaX())*hPixelValue;
                if (!Properties.IS_TOUCH_SUPPORTED) {
                    if ((event.getDeltaX() > 0.0 && hsb.getValue() > hsb.getMin()) ||
                        (event.getDeltaX() < 0.0 && hsb.getValue() < hsb.getMax())) {
                        hsb.setValue(newValue);
                        event.consume();
                    }
                }
                else {
                    /*
                    ** if there is a repositioning in progress then we only
                    ** set the value for 'real' events
                    */
                    if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
                        hsb.setValue(newValue);

                        if ((newValue > hsb.getMax() || newValue < hsb.getMin()) && (!mouseDown && !touchDetected)) {
                            startContentsToViewport();
                        }
                        event.consume();
                    }
                }
            }
        });

        /*
        ** there are certain animations that need to know if the touch is
        ** happening.....
        */
        getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {
            touchDetected = true;
            startSBReleasedAnimation();
            e.consume();
        });

        getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> {
            touchDetected = false;
            e.consume();
        });

        // ScrollPanes do not block all MouseEvents by default, unlike most other UI Controls.
        consumeMouseEvents(false);

        // update skin initial state to match control (see RT-35554)
        hsb.setValue(control.getHvalue());
        vsb.setValue(control.getVvalue());
    }

    void scrollBoundsIntoView(Bounds b) {
        double dx = 0.0;
        double dy = 0.0;
        if (b.getMaxX() > contentWidth) {
            dx = b.getMinX() - snappedLeftInset();
        }
        if (b.getMinX() < snappedLeftInset()) {
            dx = b.getMaxX() - contentWidth - snappedLeftInset();
        }
        if (b.getMaxY() > snappedTopInset() + contentHeight) {
            dy = b.getMinY() - snappedTopInset();
        }
        if (b.getMinY() < snappedTopInset()) {
            dy = b.getMaxY() - contentHeight - snappedTopInset();
        }
        // We want to move contentPanel's layoutX,Y by (dx,dy).
        // But to do this we have to set the scrollbars' values appropriately.

        if (dx != 0) {
            double sdx = dx * (hsb.getMax() - hsb.getMin()) / (nodeWidth - contentWidth);
            // Adjust back for some amount so that the Node border is not too close to view border
            sdx += -1 * Math.signum(sdx) * hsb.getUnitIncrement() / 5; // This accounts to 2% of view width
            hsb.setValue(hsb.getValue() + sdx);
            getSkinnable().requestLayout();
        }
        if (dy != 0) {
            double sdy = dy * (vsb.getMax() - vsb.getMin()) / (nodeHeight - contentHeight);
            // Adjust back for some amount so that the Node border is not too close to view border
            sdy += -1 * Math.signum(sdy) * vsb.getUnitIncrement() / 5; // This accounts to 2% of view height
            vsb.setValue(vsb.getValue() + sdy);
            getSkinnable().requestLayout();
        }

    }

    /**
     * Computes the size that should be reserved for horizontal scrollbar in size hints (min/pref height)
     */
    private double computeHsbSizeHint(ScrollPane sp) {
        return ((sp.getHbarPolicy() == ScrollBarPolicy.ALWAYS) ||
                (sp.getHbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportHeight() > 0 || sp.getMinViewportHeight() > 0)))
                ? hsb.prefHeight(ScrollBar.USE_COMPUTED_SIZE)
                : 0;
    }

    /**
     * Computes the size that should be reserved for vertical scrollbar in size hints (min/pref width)
     */
    private double computeVsbSizeHint(ScrollPane sp) {
        return ((sp.getVbarPolicy() == ScrollBarPolicy.ALWAYS) ||
                (sp.getVbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportWidth() > 0
                        || sp.getMinViewportWidth() > 0)))
                ? vsb.prefWidth(ScrollBar.USE_COMPUTED_SIZE)
                : 0;
    }

    private void computeScrollNodeSize(double contentWidth, double contentHeight) {
        if (scrollNode != null) {
            if (scrollNode.isResizable()) {
                ScrollPane control = getSkinnable();
                Orientation bias = scrollNode.getContentBias();
                if (bias == null) {
                    nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
                                                         scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
                    nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
                                                          scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));

                } else if (bias == Orientation.HORIZONTAL) {
                    nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
                                                         scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
                    nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(nodeWidth),
                                                          scrollNode.minHeight(nodeWidth),scrollNode.maxHeight(nodeWidth)));

                } else { // bias == VERTICAL
                    nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
                                                          scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
                    nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(nodeHeight),
                                                         scrollNode.minWidth(nodeHeight),scrollNode.maxWidth(nodeHeight)));
                }

            } else {
                nodeWidth = snapSizeX(scrollNode.getLayoutBounds().getWidth());
                nodeHeight = snapSizeY(scrollNode.getLayoutBounds().getHeight());
            }
            nodeSizeInvalid = false;
        }
    }

    private boolean isReverseNodeOrientation() {
        return (scrollNode != null &&
                getSkinnable().getEffectiveNodeOrientation() !=
                            scrollNode.getEffectiveNodeOrientation());
    }

    private boolean determineHorizontalSBVisible() {
        final ScrollPane sp = getSkinnable();

        if (Properties.IS_TOUCH_SUPPORTED) {
            return (tempVisibility && (nodeWidth > contentWidth));
        }
        else {
            // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
            ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
            return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
                   ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
                   ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
                   (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
        }
    }

    private boolean determineVerticalSBVisible() {
        final ScrollPane sp = getSkinnable();

        if (Properties.IS_TOUCH_SUPPORTED) {
            return (tempVisibility && (nodeHeight > contentHeight));
        }
        else {
            // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
            ScrollBarPolicy vbarPolicy = sp.getVbarPolicy();
            return (ScrollBarPolicy.NEVER == vbarPolicy) ? false :
                   ((ScrollBarPolicy.ALWAYS == vbarPolicy) ? true :
                   ((sp.isFitToHeight() && scrollNode != null ? scrollNode.isResizable() : false) ?
                   (nodeHeight > contentHeight && scrollNode.minHeight(-1) > contentHeight) : (nodeHeight > contentHeight)));
        }
    }

    private void computeScrollBarSize() {
        vsbWidth = snapSizeX(vsb.prefWidth(-1));
        if (vsbWidth == 0) {
            //            println("*** WARNING ScrollPaneSkin: can't get scroll bar width, using {DEFAULT_SB_BREADTH}");
            if (Properties.IS_TOUCH_SUPPORTED) {
                vsbWidth = DEFAULT_EMBEDDED_SB_BREADTH;
            }
            else {
                vsbWidth = DEFAULT_SB_BREADTH;
            }
        }
        hsbHeight = snapSizeY(hsb.prefHeight(-1));
        if (hsbHeight == 0) {
            //            println("*** WARNING ScrollPaneSkin: can't get scroll bar height, using {DEFAULT_SB_BREADTH}");
            if (Properties.IS_TOUCH_SUPPORTED) {
                hsbHeight = DEFAULT_EMBEDDED_SB_BREADTH;
            }
            else {
                hsbHeight = DEFAULT_SB_BREADTH;
            }
        }
    }

    private void updateHorizontalSB() {
        double contentRatio = nodeWidth * (hsb.getMax() - hsb.getMin());
        if (contentRatio > 0.0) {
            hsb.setVisibleAmount(contentWidth / contentRatio);
            hsb.setBlockIncrement(0.9 * hsb.getVisibleAmount());
            hsb.setUnitIncrement(0.1 * hsb.getVisibleAmount());
        }
        else {
            hsb.setVisibleAmount(0.0);
            hsb.setBlockIncrement(0.0);
            hsb.setUnitIncrement(0.0);
        }

        if (hsb.isVisible()) {
            updatePosX();
        } else {
            if (nodeWidth > contentWidth) {
                updatePosX();
            } else {
                viewContent.setLayoutX(0);
            }
        }
    }

    private void updateVerticalSB() {
        double contentRatio = nodeHeight * (vsb.getMax() - vsb.getMin());
        if (contentRatio > 0.0) {
            vsb.setVisibleAmount(contentHeight / contentRatio);
            vsb.setBlockIncrement(0.9 * vsb.getVisibleAmount());
            vsb.setUnitIncrement(0.1 * vsb.getVisibleAmount());
        }
        else {
            vsb.setVisibleAmount(0.0);
            vsb.setBlockIncrement(0.0);
            vsb.setUnitIncrement(0.0);
        }

        if (vsb.isVisible()) {
            updatePosY();
        } else {
            if (nodeHeight > contentHeight) {
                updatePosY();
            } else {
                viewContent.setLayoutY(0);
            }
        }
    }

    private double updatePosX() {
        final ScrollPane sp = getSkinnable();
        double x = isReverseNodeOrientation() ? (hsb.getMax() - (posX - hsb.getMin())) : posX;
        double minX = Math.min((- x / (hsb.getMax() - hsb.getMin()) * (nodeWidth - contentWidth)), 0);
        viewContent.setLayoutX(snapPositionX(minX));
        if (!sp.hvalueProperty().isBound()) sp.setHvalue(Utils.clamp(sp.getHmin(), posX, sp.getHmax()));
        return posX;
    }

    private double updatePosY() {
        final ScrollPane sp = getSkinnable();
        double minY = Math.min((- posY / (vsb.getMax() - vsb.getMin()) * (nodeHeight - contentHeight)), 0);
        viewContent.setLayoutY(snapPositionY(minY));
        if (!sp.vvalueProperty().isBound()) sp.setVvalue(Utils.clamp(sp.getVmin(), posY, sp.getVmax()));
        return posY;
    }

    private void resetClip() {
        clipRect.setWidth(snapSizeX(contentWidth));
        clipRect.setHeight(snapSizeY(contentHeight));
    }

    private void startSBReleasedAnimation() {
        if (sbTouchTimeline == null) {
            /*
            ** timeline to leave the scrollbars visible for a short
            ** while after a scroll/drag
            */
            sbTouchTimeline = new Timeline();
            sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
                tempVisibility = true;
                if (touchDetected == true || mouseDown == true) {
                    sbTouchTimeline.playFromStart();
                }
            });

            sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
                tempVisibility = false;
                getSkinnable().requestLayout();
            });
            sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
        }
        sbTouchTimeline.playFromStart();
    }

    private void startContentsToViewport() {
        double newPosX = posX;
        double newPosY = posY;

        setContentPosX(posX);
        setContentPosY(posY);

        if (posY > getSkinnable().getVmax()) {
            newPosY = getSkinnable().getVmax();
        }
        else if (posY < getSkinnable().getVmin()) {
            newPosY = getSkinnable().getVmin();
        }


        if (posX > getSkinnable().getHmax()) {
            newPosX = getSkinnable().getHmax();
        }
        else if (posX < getSkinnable().getHmin()) {
            newPosX = getSkinnable().getHmin();
        }

        if (!Properties.IS_TOUCH_SUPPORTED) {
            startSBReleasedAnimation();
        }

        /*
        ** timeline to return the contents of the scrollpane to the viewport
        */
        if (contentsToViewTimeline != null) {
            contentsToViewTimeline.stop();
        }
        contentsToViewTimeline = new Timeline();
        /*
        ** short pause before animation starts
        */
        contentsToViewKF1 = new KeyFrame(Duration.millis(50));
        /*
        ** reposition
        */
        contentsToViewKF2 = new KeyFrame(Duration.millis(150), event -> {
            getSkinnable().requestLayout();
        },
                new KeyValue(contentPosX, newPosX),
                new KeyValue(contentPosY, newPosY)
        );
        /*
        ** block out 'aftershocks', but real events will
        ** still reactivate
        */
        contentsToViewKF3 = new KeyFrame(Duration.millis(1500));
        contentsToViewTimeline.getKeyFrames().addAll(contentsToViewKF1, contentsToViewKF2, contentsToViewKF3);
        contentsToViewTimeline.playFromStart();
    }
}
