/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

#include "../Source/Utils/Utils.h"

namespace juce
{

static double getStepSize (const Slider& slider)
{
    const auto interval = slider.getInterval();

    return ! approximatelyEqual (interval, 0.0) ? interval
                                                : slider.getRange().getLength() * 0.01;
}

class Slider::Pimpl   : public AsyncUpdater, // this needs to be public otherwise it will cause an
                                             // error when JUCE_DLL_BUILD=1
                        private Value::Listener
{
public:
    Pimpl (Slider& s, SliderStyle sliderStyle, TextEntryBoxPosition textBoxPosition)
      : owner (s),
        style (sliderStyle),
        textBoxPos (textBoxPosition)
    {
        rotaryParams.startAngleRadians = MathConstants<float>::pi * 1.2f;
        rotaryParams.endAngleRadians   = MathConstants<float>::pi * 2.8f;
        rotaryParams.stopAtEnd = true;
    }

    ~Pimpl() override
    {
        currentValue.removeListener (this);
        valueMin.removeListener (this);
        valueMax.removeListener (this);
        popupDisplay.reset();
    }

    //==============================================================================
    void registerListeners()
    {
        currentValue.addListener (this);
        valueMin.addListener (this);
        valueMax.addListener (this);
    }

    bool isHorizontal() const noexcept
    {
        return style == LinearHorizontal
            || style == LinearBar
            || style == TwoValueHorizontal
            || style == ThreeValueHorizontal;
    }

    bool isVertical() const noexcept
    {
        return style == LinearVertical
            || style == LinearBarVertical
            || style == TwoValueVertical
            || style == ThreeValueVertical;
    }

    bool isRotary() const noexcept
    {
        return style == Rotary
            || style == RotaryHorizontalDrag
            || style == RotaryVerticalDrag
            || style == RotaryHorizontalVerticalDrag;
    }

    bool isBar() const noexcept
    {
        return style == LinearBar
            || style == LinearBarVertical;
    }

    bool isTwoValue() const noexcept
    {
        return style == TwoValueHorizontal
            || style == TwoValueVertical;
    }

    bool isThreeValue() const noexcept
    {
        return style == ThreeValueHorizontal
            || style == ThreeValueVertical;
    }

    bool incDecDragDirectionIsHorizontal() const noexcept
    {
        return incDecButtonMode == incDecButtonsDraggable_Horizontal
                || (incDecButtonMode == incDecButtonsDraggable_AutoDirection && incDecButtonsSideBySide);
    }

    float getPositionOfValue (double value) const
    {
        if (isHorizontal() || isVertical())
            return getLinearSliderPos (value);

        jassertfalse; // not a valid call on a slider that doesn't work linearly!
        return 0.0f;
    }

    void setNumDecimalPlacesToDisplay (int decimalPlacesToDisplay)
    {
        fixedNumDecimalPlaces = jmax (0, decimalPlacesToDisplay);
        numDecimalPlaces = fixedNumDecimalPlaces;
    }

    int getNumDecimalPlacesToDisplay() const
    {
        return fixedNumDecimalPlaces == -1 ? numDecimalPlaces : fixedNumDecimalPlaces;
    }

    void updateRange()
    {
        if (fixedNumDecimalPlaces == -1)
        {
            // figure out the number of DPs needed to display all values at this
            // interval setting.
            numDecimalPlaces = 7;

            if (! approximatelyEqual (normRange.interval, 0.0))
            {
                int v = std::abs (roundToInt (normRange.interval * 10000000));

                while ((v % 10) == 0 && numDecimalPlaces > 0)
                {
                    --numDecimalPlaces;
                    v /= 10;
                }
            }
        }

        // keep the current values inside the new range..
        if (style != TwoValueHorizontal && style != TwoValueVertical)
        {
            setValue (getValue(), dontSendNotification);
        }
        else
        {
            setMinValue (getMinValue(), dontSendNotification, false);
            setMaxValue (getMaxValue(), dontSendNotification, false);
        }

        updateText();
    }

    void setRange (double newMin, double newMax, double newInt)
    {
        normRange = NormalisableRange<double> (newMin, newMax, newInt,
                                               normRange.skew, normRange.symmetricSkew);
        updateRange();
    }

    void setNormalisableRange (NormalisableRange<double> newRange)
    {
        normRange = newRange;
        updateRange();
    }

    double getValue() const
    {
        // for a two-value style slider, you should use the getMinValue() and getMaxValue()
        // methods to get the two values.
        jassert (style != TwoValueHorizontal && style != TwoValueVertical);

        return currentValue.getValue();
    }

    void setValue (double newValue, NotificationType notification)
    {
        // for a two-value style slider, you should use the setMinValue() and setMaxValue()
        // methods to set the two values.
        jassert (style != TwoValueHorizontal && style != TwoValueVertical);

        newValue = constrainedValue (newValue);

        if (style == ThreeValueHorizontal || style == ThreeValueVertical)
        {
            jassert (static_cast<double> (valueMin.getValue()) <= static_cast<double> (valueMax.getValue()));

            newValue = jlimit (static_cast<double> (valueMin.getValue()),
                               static_cast<double> (valueMax.getValue()),
                               newValue);
        }

        if (! approximatelyEqual (newValue, lastCurrentValue))
        {
            if (valueBox != nullptr)
                valueBox->hideEditor (true);

            lastCurrentValue = newValue;

            // Need to do this comparison because the Value will use equalsWithSameType to compare
            // the new and old values, so will generate unwanted change events if the type changes.
            // Cast to double before comparing, to prevent comparing as another type (e.g. String).
            // We also want to avoid sending a notification if both new and old values are NaN.
            const auto asDouble = static_cast<double> (currentValue.getValue());

            if (! (approximatelyEqual (asDouble, newValue) || (std::isnan (asDouble) && std::isnan (newValue))))
                currentValue = newValue;

            updateText();
            owner.repaint();

            triggerChangeMessage (notification);
        }
    }

    void setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
    {
        // The minimum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

        newValue = constrainedValue (newValue);

        if (style == TwoValueHorizontal || style == TwoValueVertical)
        {
            if (allowNudgingOfOtherValues && newValue > static_cast<double> (valueMax.getValue()))
                setMaxValue (newValue, notification, false);

            newValue = jmin (static_cast<double> (valueMax.getValue()), newValue);
        }
        else
        {
            if (allowNudgingOfOtherValues && newValue > lastCurrentValue)
                setValue (newValue, notification);

            newValue = jmin (lastCurrentValue, newValue);
        }

        if (! approximatelyEqual (lastValueMin, newValue))
        {
            lastValueMin = newValue;
            valueMin = newValue;
            owner.repaint();
            updatePopupDisplay();

            triggerChangeMessage (notification);
        }
    }

    void setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
    {
        // The maximum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

        newValue = constrainedValue (newValue);

        if (style == TwoValueHorizontal || style == TwoValueVertical)
        {
            if (allowNudgingOfOtherValues && newValue < static_cast<double> (valueMin.getValue()))
                setMinValue (newValue, notification, false);

            newValue = jmax (static_cast<double> (valueMin.getValue()), newValue);
        }
        else
        {
            if (allowNudgingOfOtherValues && newValue < lastCurrentValue)
                setValue (newValue, notification);

            newValue = jmax (lastCurrentValue, newValue);
        }

        if (! approximatelyEqual (lastValueMax, newValue))
        {
            lastValueMax = newValue;
            valueMax = newValue;
            owner.repaint();
            updatePopupDisplay();

            triggerChangeMessage (notification);
        }
    }

    void setMinAndMaxValues (double newMinValue, double newMaxValue, NotificationType notification)
    {
        // The maximum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

        if (newMaxValue < newMinValue)
            std::swap (newMaxValue, newMinValue);

        newMinValue = constrainedValue (newMinValue);
        newMaxValue = constrainedValue (newMaxValue);

        if (! approximatelyEqual (lastValueMax, newMaxValue) || ! approximatelyEqual (lastValueMin, newMinValue))
        {
            lastValueMax = newMaxValue;
            lastValueMin = newMinValue;
            valueMin = newMinValue;
            valueMax = newMaxValue;
            owner.repaint();

            triggerChangeMessage (notification);
        }
    }

    double getMinValue() const
    {
        // The minimum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

        return valueMin.getValue();
    }

    double getMaxValue() const
    {
        // The maximum value only applies to sliders that are in two- or three-value mode.
        jassert (style == TwoValueHorizontal || style == TwoValueVertical
                  || style == ThreeValueHorizontal || style == ThreeValueVertical);

        return valueMax.getValue();
    }

    void triggerChangeMessage (NotificationType notification)
    {
        if (notification != dontSendNotification)
        {
            owner.valueChanged();

            if (notification == sendNotificationSync)
                handleAsyncUpdate();
            else
                triggerAsyncUpdate();
        }
    }

    void handleAsyncUpdate() override
    {
        cancelPendingUpdate();

        LOGA ("Slider '", owner.getName(), "' set value to ", owner.getValue());

        Component::BailOutChecker checker (&owner);
        listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderValueChanged (&owner); });

        if (checker.shouldBailOut())
            return;

        NullCheckedInvocation::invoke (owner.onValueChange);

        if (checker.shouldBailOut())
            return;

        if (auto* handler = owner.getAccessibilityHandler())
            handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged);
    }

    void sendDragStart()
    {
        owner.startedDragging();

        Component::BailOutChecker checker (&owner);
        listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragStarted (&owner); });

        if (checker.shouldBailOut())
            return;

        NullCheckedInvocation::invoke (owner.onDragStart);
    }

    void sendDragEnd()
    {
        owner.stoppedDragging();
        sliderBeingDragged = -1;

        Component::BailOutChecker checker (&owner);
        listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragEnded (&owner); });

        if (checker.shouldBailOut())
            return;

        NullCheckedInvocation::invoke (owner.onDragEnd);
    }

    void incrementOrDecrement (double delta)
    {
        if (style == IncDecButtons)
        {
            auto newValue = owner.snapValue (getValue() + delta, notDragging);

            if (currentDrag != nullptr)
            {
                setValue (newValue, sendNotificationSync);
            }
            else
            {
                ScopedDragNotification drag (owner);
                setValue (newValue, sendNotificationSync);
            }
        }
    }

    void valueChanged (Value& value) override
    {
        if (value.refersToSameSourceAs (currentValue))
        {
            if (style != TwoValueHorizontal && style != TwoValueVertical)
                setValue (currentValue.getValue(), dontSendNotification);
        }
        else if (value.refersToSameSourceAs (valueMin))
        {
            setMinValue (valueMin.getValue(), dontSendNotification, true);
        }
        else if (value.refersToSameSourceAs (valueMax))
        {
            setMaxValue (valueMax.getValue(), dontSendNotification, true);
        }
    }

    void textChanged()
    {
        auto newValue = owner.snapValue (owner.getValueFromText (valueBox->getText()), notDragging);

        if (! approximatelyEqual (newValue, static_cast<double> (currentValue.getValue())))
        {
            ScopedDragNotification drag (owner);
            setValue (newValue, sendNotificationSync);
        }

        updateText(); // force a clean-up of the text, needed in case setValue() hasn't done this.
    }

    void updateText()
    {
        if (valueBox != nullptr)
        {
            auto newValue = owner.getTextFromValue (currentValue.getValue());

            if (newValue != valueBox->getText())
                valueBox->setText (newValue, dontSendNotification);
        }

        updatePopupDisplay();
    }

    double constrainedValue (double value) const
    {
        return normRange.snapToLegalValue (value);
    }

    float getLinearSliderPos (double value) const
    {
        double pos;

        if (normRange.end <= normRange.start)
            pos = 0.5;
        else if (value < normRange.start)
            pos = 0.0;
        else if (value > normRange.end)
            pos = 1.0;
        else
            pos = owner.valueToProportionOfLength (value);

        if (isVertical() || style == IncDecButtons)
            pos = 1.0 - pos;

        jassert (pos >= 0 && pos <= 1.0);
        return (float) (sliderRegionStart + pos * sliderRegionSize);
    }

    void setSliderStyle (SliderStyle newStyle)
    {
        if (style != newStyle)
        {
            style = newStyle;

            owner.repaint();
            owner.lookAndFeelChanged();
            owner.invalidateAccessibilityHandler();
        }
    }

    void setVelocityModeParameters (double sensitivity, int threshold,
                                    double offset, bool userCanPressKeyToSwapMode,
                                    ModifierKeys::Flags newModifierToSwapModes)
    {
        velocityModeSensitivity = sensitivity;
        velocityModeOffset = offset;
        velocityModeThreshold = threshold;
        userKeyOverridesVelocity = userCanPressKeyToSwapMode;
        modifierToSwapModes = newModifierToSwapModes;
    }

    void setIncDecButtonsMode (IncDecButtonMode mode)
    {
        if (incDecButtonMode != mode)
        {
            incDecButtonMode = mode;
            owner.lookAndFeelChanged();
        }
    }

    void setTextBoxStyle (TextEntryBoxPosition newPosition,
                          bool isReadOnly,
                          int textEntryBoxWidth,
                          int textEntryBoxHeight)
    {
        if (textBoxPos != newPosition
             || editableText != (! isReadOnly)
             || textBoxWidth != textEntryBoxWidth
             || textBoxHeight != textEntryBoxHeight)
        {
            textBoxPos = newPosition;
            editableText = ! isReadOnly;
            textBoxWidth = textEntryBoxWidth;
            textBoxHeight = textEntryBoxHeight;

            owner.repaint();
            owner.lookAndFeelChanged();
        }
    }

    void setTextBoxIsEditable (bool shouldBeEditable)
    {
        editableText = shouldBeEditable;
        updateTextBoxEnablement();
    }

    void showTextBox()
    {
        jassert (editableText); // this should probably be avoided in read-only sliders.

        if (valueBox != nullptr)
            valueBox->showEditor();
    }

    void hideTextBox (bool discardCurrentEditorContents)
    {
        if (valueBox != nullptr)
        {
            valueBox->hideEditor (discardCurrentEditorContents);

            if (discardCurrentEditorContents)
                updateText();
        }
    }

    void setTextValueSuffix (const String& suffix)
    {
        if (textSuffix != suffix)
        {
            textSuffix = suffix;
            updateText();
        }
    }

    void updateTextBoxEnablement()
    {
        if (valueBox != nullptr)
        {
            bool shouldBeEditable = editableText && owner.isEnabled();

            if (valueBox->isEditable() != shouldBeEditable) // (to avoid changing the single/double click flags unless we need to)
                valueBox->setEditable (shouldBeEditable);
        }
    }

    void lookAndFeelChanged (LookAndFeel& lf)
    {
        if (textBoxPos != NoTextBox)
        {
            auto previousTextBoxContent = (valueBox != nullptr ? valueBox->getText()
                                                               : owner.getTextFromValue (currentValue.getValue()));

            valueBox.reset();
            valueBox.reset (lf.createSliderTextBox (owner));
            owner.addAndMakeVisible (valueBox.get());

            valueBox->setWantsKeyboardFocus (false);
            valueBox->setText (previousTextBoxContent, dontSendNotification);
            valueBox->setTooltip (owner.getTooltip());
            updateTextBoxEnablement();
            valueBox->onTextChange = [this] { textChanged(); };

            if (style == LinearBar || style == LinearBarVertical)
            {
                valueBox->addMouseListener (&owner, false);
                valueBox->setMouseCursor (MouseCursor::ParentCursor);
            }
        }
        else
        {
            valueBox.reset();
        }

        if (style == IncDecButtons)
        {
            incButton.reset (lf.createSliderButton (owner, true));
            decButton.reset (lf.createSliderButton (owner, false));

            auto tooltip = owner.getTooltip();

            auto setupButton = [&] (Button& b, bool isIncrement)
            {
                owner.addAndMakeVisible (b);
                b.onClick = [this, isIncrement] { incrementOrDecrement (isIncrement ? normRange.interval : -normRange.interval); };

                if (incDecButtonMode != incDecButtonsNotDraggable)
                    b.addMouseListener (&owner, false);
                else
                    b.setRepeatSpeed (300, 100, 20);

                b.setTooltip (tooltip);
                b.setAccessible (false);
            };

            setupButton (*incButton, true);
            setupButton (*decButton, false);
        }
        else
        {
            incButton.reset();
            decButton.reset();
        }

        owner.setComponentEffect (lf.getSliderEffect (owner));

        owner.resized();
        owner.repaint();
    }

    void showPopupMenu()
    {
        PopupMenu m;
        m.setLookAndFeel (&owner.getLookAndFeel());
        m.addItem (1, TRANS ("Velocity-sensitive mode"), true, isVelocityBased);
        m.addSeparator();

        if (isRotary())
        {
            PopupMenu rotaryMenu;
            rotaryMenu.addItem (2, TRANS ("Use circular dragging"),           true, style == Rotary);
            rotaryMenu.addItem (3, TRANS ("Use left-right dragging"),         true, style == RotaryHorizontalDrag);
            rotaryMenu.addItem (4, TRANS ("Use up-down dragging"),            true, style == RotaryVerticalDrag);
            rotaryMenu.addItem (5, TRANS ("Use left-right/up-down dragging"), true, style == RotaryHorizontalVerticalDrag);

            m.addSubMenu (TRANS ("Rotary mode"), rotaryMenu);
        }

        m.showMenuAsync (PopupMenu::Options(),
                         ModalCallbackFunction::forComponent (sliderMenuCallback, &owner));
    }

    static void sliderMenuCallback (int result, Slider* slider)
    {
        if (slider != nullptr)
        {
            switch (result)
            {
                case 1:   slider->setVelocityBasedMode (! slider->getVelocityBasedMode()); break;
                case 2:   slider->setSliderStyle (Rotary); break;
                case 3:   slider->setSliderStyle (RotaryHorizontalDrag); break;
                case 4:   slider->setSliderStyle (RotaryVerticalDrag); break;
                case 5:   slider->setSliderStyle (RotaryHorizontalVerticalDrag); break;
                default:  break;
            }
        }
    }

    int getThumbIndexAt (const MouseEvent& e)
    {
        if (isTwoValue() || isThreeValue())
        {
            auto mousePos = isVertical() ? e.position.y : e.position.x;

            auto normalPosDistance = std::abs (getLinearSliderPos (currentValue.getValue()) - mousePos);
            auto minPosDistance    = std::abs (getLinearSliderPos (valueMin.getValue()) + (isVertical() ? 0.1f : -0.1f) - mousePos);
            auto maxPosDistance    = std::abs (getLinearSliderPos (valueMax.getValue()) + (isVertical() ? -0.1f : 0.1f) - mousePos);

            if (isTwoValue())
                return maxPosDistance <= minPosDistance ? 2 : 1;

            if (normalPosDistance >= minPosDistance && maxPosDistance >= minPosDistance)
                return 1;

            if (normalPosDistance >= maxPosDistance)
                return 2;
        }

        return 0;
    }

    //==============================================================================
    void handleRotaryDrag (const MouseEvent& e)
    {
        auto dx = e.position.x - (float) sliderRect.getCentreX();
        auto dy = e.position.y - (float) sliderRect.getCentreY();

        if (dx * dx + dy * dy > 25.0f)
        {
            auto angle = std::atan2 ((double) dx, (double) -dy);

            while (angle < 0.0)
                angle += MathConstants<double>::twoPi;

            if (rotaryParams.stopAtEnd && e.mouseWasDraggedSinceMouseDown())
            {
                if (std::abs (angle - lastAngle) > MathConstants<double>::pi)
                {
                    if (angle >= lastAngle)
                        angle -= MathConstants<double>::twoPi;
                    else
                        angle += MathConstants<double>::twoPi;
                }

                if (angle >= lastAngle)
                    angle = jmin (angle, (double) jmax (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians));
                else
                    angle = jmax (angle, (double) jmin (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians));
            }
            else
            {
                while (angle < rotaryParams.startAngleRadians)
                    angle += MathConstants<double>::twoPi;

                if (angle > rotaryParams.endAngleRadians)
                {
                    if (smallestAngleBetween (angle, rotaryParams.startAngleRadians)
                         <= smallestAngleBetween (angle, rotaryParams.endAngleRadians))
                        angle = rotaryParams.startAngleRadians;
                    else
                        angle = rotaryParams.endAngleRadians;
                }
            }

            auto proportion = (angle - rotaryParams.startAngleRadians) / (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians);
            valueWhenLastDragged = owner.proportionOfLengthToValue (jlimit (0.0, 1.0, proportion));
            lastAngle = angle;
        }
    }

    void handleAbsoluteDrag (const MouseEvent& e)
    {
        auto mousePos = (isHorizontal() || style == RotaryHorizontalDrag) ? e.position.x : e.position.y;
        double newPos = 0;

        if (style == RotaryHorizontalDrag
            || style == RotaryVerticalDrag
            || style == IncDecButtons
            || ((style == LinearHorizontal || style == LinearVertical || style == LinearBar || style == LinearBarVertical)
                && ! snapsToMousePos))
        {
            auto mouseDiff = (style == RotaryHorizontalDrag
                                || style == LinearHorizontal
                                || style == LinearBar
                                || (style == IncDecButtons && incDecDragDirectionIsHorizontal()))
                              ? e.position.x - mouseDragStartPos.x
                              : mouseDragStartPos.y - e.position.y;

            newPos = owner.valueToProportionOfLength (valueOnMouseDown)
                       + mouseDiff * (1.0 / pixelsForFullDragExtent);

            if (style == IncDecButtons)
            {
                incButton->setState (mouseDiff < 0 ? Button::buttonNormal : Button::buttonDown);
                decButton->setState (mouseDiff > 0 ? Button::buttonNormal : Button::buttonDown);
            }
        }
        else if (style == RotaryHorizontalVerticalDrag)
        {
            auto mouseDiff = (e.position.x - mouseDragStartPos.x)
                               + (mouseDragStartPos.y - e.position.y);

            newPos = owner.valueToProportionOfLength (valueOnMouseDown)
                       + mouseDiff * (1.0 / pixelsForFullDragExtent);
        }
        else
        {
            newPos = (mousePos - (float) sliderRegionStart) / (double) sliderRegionSize;

            if (isVertical())
                newPos = 1.0 - newPos;
        }

        newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
                                                          : jlimit (0.0, 1.0, newPos);
        valueWhenLastDragged = owner.proportionOfLengthToValue (newPos);
    }

    void handleVelocityDrag (const MouseEvent& e)
    {
        bool hasHorizontalStyle =
            (isHorizontal() ||  style == RotaryHorizontalDrag
                            || (style == IncDecButtons && incDecDragDirectionIsHorizontal()));

        auto mouseDiff = style == RotaryHorizontalVerticalDrag
                            ? (e.position.x - mousePosWhenLastDragged.x) + (mousePosWhenLastDragged.y - e.position.y)
                            : (hasHorizontalStyle ? e.position.x - mousePosWhenLastDragged.x
                                                  : e.position.y - mousePosWhenLastDragged.y);

        auto maxSpeed = jmax (200.0, (double) sliderRegionSize);
        auto speed = jlimit (0.0, maxSpeed, (double) std::abs (mouseDiff));

        if (! approximatelyEqual (speed, 0.0))
        {
            speed = 0.2 * velocityModeSensitivity
                      * (1.0 + std::sin (MathConstants<double>::pi * (1.5 + jmin (0.5, velocityModeOffset
                                                                                         + jmax (0.0, (double) (speed - velocityModeThreshold))
                                                                                            / maxSpeed))));

            if (mouseDiff < 0)
                speed = -speed;

            if (isVertical() || style == RotaryVerticalDrag
                 || (style == IncDecButtons && ! incDecDragDirectionIsHorizontal()))
                speed = -speed;

            auto newPos = owner.valueToProportionOfLength (valueWhenLastDragged) + speed;
            newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
                                                              : jlimit (0.0, 1.0, newPos);
            valueWhenLastDragged = owner.proportionOfLengthToValue (newPos);

            e.source.enableUnboundedMouseMovement (true, false);
        }
    }

    void mouseDown (const MouseEvent& e)
    {
        incDecDragged = false;
        useDragEvents = false;
        mouseDragStartPos = mousePosWhenLastDragged = e.position;
        currentDrag.reset();
        popupDisplay.reset();

        if (owner.isEnabled())
        {
            if (e.mods.isPopupMenu() && menuEnabled)
            {
                showPopupMenu();
            }
            else if (canDoubleClickToValue()
                     && (singleClickModifiers != ModifierKeys() && e.mods.withoutMouseButtons() == singleClickModifiers))
            {
                mouseDoubleClick();
            }
            else if (normRange.end > normRange.start)
            {
                useDragEvents = true;

                if (valueBox != nullptr)
                    valueBox->hideEditor (true);

                sliderBeingDragged = getThumbIndexAt (e);

                minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());

                if (! isTwoValue())
                    lastAngle = rotaryParams.startAngleRadians
                                    + (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians)
                                         * owner.valueToProportionOfLength (currentValue.getValue());

                valueWhenLastDragged = (sliderBeingDragged == 2 ? valueMax
                                                                : (sliderBeingDragged == 1 ? valueMin
                                                                                           : currentValue)).getValue();
                valueOnMouseDown = valueWhenLastDragged;

                if (showPopupOnDrag || showPopupOnHover)
                {
                    showPopupDisplay();

                    if (popupDisplay != nullptr)
                        popupDisplay->stopTimer();
                }

                currentDrag = std::make_unique<ScopedDragNotification> (owner);
                mouseDrag (e);
            }
        }
    }

    void mouseDrag (const MouseEvent& e)
    {
        if (useDragEvents && normRange.end > normRange.start
             && ! ((style == LinearBar || style == LinearBarVertical)
                    && e.mouseWasClicked() && valueBox != nullptr && valueBox->isEditable()))
        {
            DragMode dragMode = notDragging;

            if (style == Rotary)
            {
                handleRotaryDrag (e);
            }
            else
            {
                if (style == IncDecButtons && ! incDecDragged)
                {
                    if (e.getDistanceFromDragStart() < 10 || ! e.mouseWasDraggedSinceMouseDown())
                        return;

                    incDecDragged = true;
                    mouseDragStartPos = e.position;
                }

                if (isAbsoluteDragMode (e.mods) || (normRange.end - normRange.start) / sliderRegionSize < normRange.interval)
                {
                    dragMode = absoluteDrag;
                    handleAbsoluteDrag (e);
                }
                else
                {
                    dragMode = velocityDrag;
                    handleVelocityDrag (e);
                }
            }

            valueWhenLastDragged = jlimit (normRange.start, normRange.end, valueWhenLastDragged);

            if (sliderBeingDragged == 0)
            {
                setValue (owner.snapValue (valueWhenLastDragged, dragMode),
                          sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationSync);
            }
            else if (sliderBeingDragged == 1)
            {
                setMinValue (owner.snapValue (valueWhenLastDragged, dragMode),
                             sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true);

                if (e.mods.isShiftDown())
                    setMaxValue (getMinValue() + minMaxDiff, dontSendNotification, true);
                else
                    minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());
            }
            else if (sliderBeingDragged == 2)
            {
                setMaxValue (owner.snapValue (valueWhenLastDragged, dragMode),
                             sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true);

                if (e.mods.isShiftDown())
                    setMinValue (getMaxValue() - minMaxDiff, dontSendNotification, true);
                else
                    minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());
            }

            mousePosWhenLastDragged = e.position;
        }
    }

    void mouseUp()
    {
        if (owner.isEnabled()
             && useDragEvents
             && (normRange.end > normRange.start)
             && (style != IncDecButtons || incDecDragged))
        {
            restoreMouseIfHidden();

            if (sendChangeOnlyOnRelease && ! approximatelyEqual (valueOnMouseDown, static_cast<double> (currentValue.getValue())))
                triggerChangeMessage (sendNotificationAsync);

            currentDrag.reset();
            popupDisplay.reset();

            if (style == IncDecButtons)
            {
                incButton->setState (Button::buttonNormal);
                decButton->setState (Button::buttonNormal);
            }
        }
        else if (popupDisplay != nullptr)
        {
            popupDisplay->startTimer (200);
        }

        currentDrag.reset();
    }

    void mouseMove()
    {
        // this is a workaround for a bug where the popup display being dismissed triggers
        // a mouse move causing it to never be hidden
        auto shouldShowPopup = showPopupOnHover
                                && (Time::getMillisecondCounterHiRes() - lastPopupDismissal) > 250;

        if (shouldShowPopup
             && ! isTwoValue()
             && ! isThreeValue())
        {
            if (owner.isMouseOver (true))
            {
                if (popupDisplay == nullptr)
                    showPopupDisplay();

                if (popupDisplay != nullptr && popupHoverTimeout != -1)
                    popupDisplay->startTimer (popupHoverTimeout);
            }
        }
    }

    void mouseExit()
    {
        popupDisplay.reset();
    }

    bool keyPressed (const KeyPress& key)
    {
        if (key.getModifiers().isAnyModifierKeyDown())
            return false;

        const auto getInterval = [this]
        {
            if (auto* accessibility = owner.getAccessibilityHandler())
                if (auto* valueInterface = accessibility->getValueInterface())
                    return valueInterface->getRange().getInterval();

            return getStepSize (owner);
        };

        const auto valueChange = [&]
        {
            if (key == KeyPress::rightKey || key == KeyPress::upKey)
                return getInterval();

            if (key == KeyPress::leftKey || key == KeyPress::downKey)
                return -getInterval();

            return 0.0;
        }();

        if (approximatelyEqual (valueChange, 0.0))
            return false;

        setValue (getValue() + valueChange, sendNotificationSync);
        return true;
    }

    void showPopupDisplay()
    {
        if (style == IncDecButtons)
            return;

        if (popupDisplay == nullptr)
        {
            popupDisplay.reset (new PopupDisplayComponent (owner, parentForPopupDisplay == nullptr));

            if (parentForPopupDisplay != nullptr)
                parentForPopupDisplay->addChildComponent (popupDisplay.get());
            else
                popupDisplay->addToDesktop (ComponentPeer::windowIsTemporary
                                            | ComponentPeer::windowIgnoresKeyPresses
                                            | ComponentPeer::windowIgnoresMouseClicks);

            updatePopupDisplay();
            popupDisplay->setVisible (true);
        }
    }

    void updatePopupDisplay()
    {
        if (popupDisplay == nullptr)
            return;

        const auto valueToShow = [this]
        {
            constexpr SliderStyle multiSliderStyles[] { SliderStyle::TwoValueHorizontal,
                                                        SliderStyle::TwoValueVertical,
                                                        SliderStyle::ThreeValueHorizontal,
                                                        SliderStyle::ThreeValueVertical };

            if (std::find (std::begin (multiSliderStyles), std::end (multiSliderStyles), style) == std::end (multiSliderStyles))
                return getValue();

            if (sliderBeingDragged == 2)
                return getMaxValue();

            if (sliderBeingDragged == 1)
                return getMinValue();

            return getValue();
        }();

        popupDisplay->updatePosition (owner.getTextFromValue (valueToShow));
    }

    bool canDoubleClickToValue() const
    {
        return doubleClickToValue
                && style != IncDecButtons
                && normRange.start <= doubleClickReturnValue
                && normRange.end >= doubleClickReturnValue;
    }

    void mouseDoubleClick()
    {
        if (canDoubleClickToValue())
        {
            ScopedDragNotification drag (owner);
            setValue (doubleClickReturnValue, sendNotificationSync);
        }
    }

    double getMouseWheelDelta (double value, double wheelAmount)
    {
        if (style == IncDecButtons)
            return normRange.interval * wheelAmount;

        auto proportionDelta = wheelAmount * 0.15;
        auto currentPos = owner.valueToProportionOfLength (value);
        auto newPos = currentPos + proportionDelta;
        newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
                                                          : jlimit (0.0, 1.0, newPos);
        return owner.proportionOfLengthToValue (newPos) - value;
    }

    bool mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
    {
        if (scrollWheelEnabled
             && style != TwoValueHorizontal
             && style != TwoValueVertical)
        {
            // sometimes duplicate wheel events seem to be sent, so since we're going to
            // bump the value by a minimum of the interval, avoid doing this twice..
            if (e.eventTime != lastMouseWheelTime)
            {
                lastMouseWheelTime = e.eventTime;

                if (normRange.end > normRange.start && ! e.mods.isAnyMouseButtonDown())
                {
                    if (valueBox != nullptr)
                        valueBox->hideEditor (false);

                    auto value = static_cast<double> (currentValue.getValue());
                    auto delta = getMouseWheelDelta (value, (std::abs (wheel.deltaX) > std::abs (wheel.deltaY)
                                                                  ? -wheel.deltaX : wheel.deltaY)
                                                               * (wheel.isReversed ? -1.0f : 1.0f));
                    if (! approximatelyEqual (delta, 0.0))
                    {
                        auto newValue = value + jmax (normRange.interval, std::abs (delta)) * (delta < 0 ? -1.0 : 1.0);

                        ScopedDragNotification drag (owner);
                        setValue (owner.snapValue (newValue, notDragging), sendNotificationSync);
                    }
                }
            }

            return true;
        }

        return false;
    }

    void modifierKeysChanged (const ModifierKeys& modifiers)
    {
        if (style != IncDecButtons && style != Rotary && isAbsoluteDragMode (modifiers))
            restoreMouseIfHidden();
    }

    bool isAbsoluteDragMode (ModifierKeys mods) const
    {
        return isVelocityBased == (userKeyOverridesVelocity && mods.testFlags (modifierToSwapModes));
    }

    void restoreMouseIfHidden()
    {
        for (auto& ms : Desktop::getInstance().getMouseSources())
        {
            if (ms.isUnboundedMouseMovementEnabled())
            {
                ms.enableUnboundedMouseMovement (false);

                auto pos = sliderBeingDragged == 2 ? getMaxValue()
                                                   : (sliderBeingDragged == 1 ? getMinValue()
                                                                              : static_cast<double> (currentValue.getValue()));
                Point<float> mousePos;

                if (isRotary())
                {
                    mousePos = ms.getLastMouseDownPosition();

                    auto delta = (float) (pixelsForFullDragExtent * (owner.valueToProportionOfLength (valueOnMouseDown)
                                                                       - owner.valueToProportionOfLength (pos)));

                    if (style == RotaryHorizontalDrag)      mousePos += Point<float> (-delta, 0.0f);
                    else if (style == RotaryVerticalDrag)   mousePos += Point<float> (0.0f, delta);
                    else                                    mousePos += Point<float> (delta / -2.0f, delta / 2.0f);

                    mousePos = owner.getScreenBounds().reduced (4).toFloat().getConstrainedPoint (mousePos);
                    mouseDragStartPos = mousePosWhenLastDragged = owner.getLocalPoint (nullptr, mousePos);
                    valueOnMouseDown = valueWhenLastDragged;
                }
                else
                {
                    auto pixelPos = (float) getLinearSliderPos (pos);

                    mousePos = owner.localPointToGlobal (Point<float> (isHorizontal() ? pixelPos : ((float) owner.getWidth()  / 2.0f),
                                                                       isVertical()   ? pixelPos : ((float) owner.getHeight() / 2.0f)));
                }

                const_cast <MouseInputSource&> (ms).setScreenPosition (mousePos);
            }
        }
    }

    //==============================================================================
    void paint (Graphics& g, LookAndFeel& lf)
    {
        if (style != IncDecButtons)
        {
            if (isRotary())
            {
                auto sliderPos = (float) owner.valueToProportionOfLength (lastCurrentValue);
                jassert (sliderPos >= 0 && sliderPos <= 1.0f);

                lf.drawRotarySlider (g,
                                     sliderRect.getX(), sliderRect.getY(),
                                     sliderRect.getWidth(), sliderRect.getHeight(),
                                     sliderPos, rotaryParams.startAngleRadians,
                                     rotaryParams.endAngleRadians, owner);
            }
            else
            {
                lf.drawLinearSlider (g,
                                     sliderRect.getX(), sliderRect.getY(),
                                     sliderRect.getWidth(), sliderRect.getHeight(),
                                     getLinearSliderPos (lastCurrentValue),
                                     getLinearSliderPos (lastValueMin),
                                     getLinearSliderPos (lastValueMax),
                                     style, owner);
            }
        }
    }

    //==============================================================================
    void resized (LookAndFeel& lf)
    {
        auto layout = lf.getSliderLayout (owner);
        sliderRect = layout.sliderBounds;

        if (valueBox != nullptr)
            valueBox->setBounds (layout.textBoxBounds);

        if (isHorizontal())
        {
            sliderRegionStart = layout.sliderBounds.getX();
            sliderRegionSize = layout.sliderBounds.getWidth();
        }
        else if (isVertical())
        {
            sliderRegionStart = layout.sliderBounds.getY();
            sliderRegionSize = layout.sliderBounds.getHeight();
        }
        else if (style == IncDecButtons)
        {
            resizeIncDecButtons();
        }
    }

    //==============================================================================
    void resizeIncDecButtons()
    {
        auto buttonRect = sliderRect;

        if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight)
            buttonRect.expand (-2, 0);
        else
            buttonRect.expand (0, -2);

        incDecButtonsSideBySide = buttonRect.getWidth() > buttonRect.getHeight();

        if (incDecButtonsSideBySide)
        {
            decButton->setBounds (buttonRect.removeFromLeft (buttonRect.getWidth() / 2));
            decButton->setConnectedEdges (Button::ConnectedOnRight);
            incButton->setConnectedEdges (Button::ConnectedOnLeft);
        }
        else
        {
            decButton->setBounds (buttonRect.removeFromBottom (buttonRect.getHeight() / 2));
            decButton->setConnectedEdges (Button::ConnectedOnTop);
            incButton->setConnectedEdges (Button::ConnectedOnBottom);
        }

        incButton->setBounds (buttonRect);
    }

    //==============================================================================
    Slider& owner;
    SliderStyle style;

    ListenerList<Slider::Listener> listeners;
    Value currentValue, valueMin, valueMax;
    double lastCurrentValue = 0, lastValueMin = 0, lastValueMax = 0;
    NormalisableRange<double> normRange { 0.0, 10.0 };
    double doubleClickReturnValue = 0;
    double valueWhenLastDragged = 0, valueOnMouseDown = 0, lastAngle = 0;
    double velocityModeSensitivity = 1.0, velocityModeOffset = 0, minMaxDiff = 0;
    int velocityModeThreshold = 1;
    RotaryParameters rotaryParams;
    Point<float> mouseDragStartPos, mousePosWhenLastDragged;
    int sliderRegionStart = 0, sliderRegionSize = 1;
    int sliderBeingDragged = -1;
    int pixelsForFullDragExtent = 250;
    Time lastMouseWheelTime;
    Rectangle<int> sliderRect;
    std::unique_ptr<ScopedDragNotification> currentDrag;

    TextEntryBoxPosition textBoxPos;
    String textSuffix;
    int numDecimalPlaces = 7;
    int fixedNumDecimalPlaces = -1;
    int textBoxWidth = 80, textBoxHeight = 20;
    IncDecButtonMode incDecButtonMode = incDecButtonsNotDraggable;
    ModifierKeys::Flags modifierToSwapModes = ModifierKeys::ctrlAltCommandModifiers;

    bool editableText = true;
    bool doubleClickToValue = false;
    bool isVelocityBased = false;
    bool userKeyOverridesVelocity = true;
    bool incDecButtonsSideBySide = false;
    bool sendChangeOnlyOnRelease = false;
    bool showPopupOnDrag = false;
    bool showPopupOnHover = false;
    bool menuEnabled = false;
    bool useDragEvents = false;
    bool incDecDragged = false;
    bool scrollWheelEnabled = true;
    bool snapsToMousePos = true;

    int popupHoverTimeout = 2000;
    double lastPopupDismissal = 0.0;

    ModifierKeys singleClickModifiers;

    std::unique_ptr<Label> valueBox;
    std::unique_ptr<Button> incButton, decButton;

    //==============================================================================
    struct PopupDisplayComponent final : public BubbleComponent,
                                         public Timer
    {
        PopupDisplayComponent (Slider& s, bool isOnDesktop)
            : owner (s),
              font (s.getLookAndFeel().getSliderPopupFont (s))
        {
            if (isOnDesktop)
                setTransform (AffineTransform::scale (Component::getApproximateScaleFactorForComponent (&s)));

            setAlwaysOnTop (true);
            setAllowedPlacement (owner.getLookAndFeel().getSliderPopupPlacement (s));
            setLookAndFeel (&s.getLookAndFeel());
        }

        ~PopupDisplayComponent() override
        {
            if (owner.pimpl != nullptr)
                owner.pimpl->lastPopupDismissal = Time::getMillisecondCounterHiRes();
        }

        void paintContent (Graphics& g, int w, int h) override
        {
            g.setFont (font);
            g.setColour (owner.findColour (TooltipWindow::textColourId, true));
            g.drawFittedText (text, Rectangle<int> (w, h), Justification::centred, 1);
        }

        void getContentSize (int& w, int& h) override
        {
            w = GlyphArrangement::getStringWidthInt (font, text) + 18;
            h = (int) (font.getHeight() * 1.6f);
        }

        void updatePosition (const String& newText)
        {
            text = newText;
            BubbleComponent::setPosition (&owner);
            repaint();
        }

        void timerCallback() override
        {
            stopTimer();
            owner.pimpl->popupDisplay.reset();
        }

    private:
        //==============================================================================
        Slider& owner;
        Font font;
        String text;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupDisplayComponent)
    };

    std::unique_ptr<PopupDisplayComponent> popupDisplay;
    Component* parentForPopupDisplay = nullptr;

    //==============================================================================
    static double smallestAngleBetween (double a1, double a2) noexcept
    {
        return jmin (std::abs (a1 - a2),
                     std::abs (a1 + MathConstants<double>::twoPi - a2),
                     std::abs (a2 + MathConstants<double>::twoPi - a1));
    }
};

//==============================================================================
Slider::ScopedDragNotification::ScopedDragNotification (Slider& s)
    : sliderBeingDragged (s)
{
    sliderBeingDragged.pimpl->sendDragStart();
}

Slider::ScopedDragNotification::~ScopedDragNotification()
{
    if (sliderBeingDragged.pimpl != nullptr)
        sliderBeingDragged.pimpl->sendDragEnd();
}

//==============================================================================
Slider::Slider()
{
    init (LinearHorizontal, TextBoxLeft);
}

Slider::Slider (const String& name)  : Component (name)
{
    init (LinearHorizontal, TextBoxLeft);
}

Slider::Slider (SliderStyle style, TextEntryBoxPosition textBoxPos)
{
    init (style, textBoxPos);
}

void Slider::init (SliderStyle style, TextEntryBoxPosition textBoxPos)
{
    setWantsKeyboardFocus (false);
    setRepaintsOnMouseActivity (true);

    pimpl.reset (new Pimpl (*this, style, textBoxPos));

    Slider::lookAndFeelChanged();
    updateText();

    pimpl->registerListeners();
}

Slider::~Slider() {}

//==============================================================================
void Slider::addListener (Listener* l)       { pimpl->listeners.add (l); }
void Slider::removeListener (Listener* l)    { pimpl->listeners.remove (l); }

//==============================================================================
Slider::SliderStyle Slider::getSliderStyle() const noexcept     { return pimpl->style; }
void Slider::setSliderStyle (SliderStyle newStyle)              { pimpl->setSliderStyle (newStyle); }

void Slider::setRotaryParameters (RotaryParameters p) noexcept
{
    // make sure the values are sensible..
    jassert (p.startAngleRadians >= 0 && p.endAngleRadians >= 0);
    jassert (p.startAngleRadians < MathConstants<float>::pi * 4.0f
              && p.endAngleRadians < MathConstants<float>::pi * 4.0f);

    pimpl->rotaryParams = p;
}

void Slider::setRotaryParameters (float startAngleRadians, float endAngleRadians, bool stopAtEnd) noexcept
{
    setRotaryParameters ({ startAngleRadians, endAngleRadians, stopAtEnd });
}

Slider::RotaryParameters Slider::getRotaryParameters() const noexcept
{
    return pimpl->rotaryParams;
}

void Slider::setVelocityBasedMode (bool vb)                 { pimpl->isVelocityBased = vb; }
bool Slider::getVelocityBasedMode() const noexcept          { return pimpl->isVelocityBased; }
bool Slider::getVelocityModeIsSwappable() const noexcept    { return pimpl->userKeyOverridesVelocity; }
int Slider::getVelocityThreshold() const noexcept           { return pimpl->velocityModeThreshold; }
double Slider::getVelocitySensitivity() const noexcept      { return pimpl->velocityModeSensitivity; }
double Slider::getVelocityOffset() const noexcept           { return pimpl->velocityModeOffset; }

void Slider::setVelocityModeParameters (double sensitivity, int threshold,
                                        double offset, bool userCanPressKeyToSwapMode,
                                        ModifierKeys::Flags modifierToSwapModes)
{
    jassert (threshold >= 0);
    jassert (sensitivity > 0);
    jassert (offset >= 0);

    pimpl->setVelocityModeParameters (sensitivity, threshold, offset,
                                      userCanPressKeyToSwapMode, modifierToSwapModes);
}

double Slider::getSkewFactor() const noexcept               { return pimpl->normRange.skew; }
bool Slider::isSymmetricSkew() const noexcept               { return pimpl->normRange.symmetricSkew; }

void Slider::setSkewFactor (double factor, bool symmetricSkew)
{
    pimpl->normRange.skew = factor;
    pimpl->normRange.symmetricSkew = symmetricSkew;
}

void Slider::setSkewFactorFromMidPoint (double sliderValueToShowAtMidPoint)
{
    pimpl->normRange.setSkewForCentre (sliderValueToShowAtMidPoint);
}

int Slider::getMouseDragSensitivity() const noexcept        { return pimpl->pixelsForFullDragExtent; }

void Slider::setMouseDragSensitivity (int distanceForFullScaleDrag)
{
    jassert (distanceForFullScaleDrag > 0);

    pimpl->pixelsForFullDragExtent = distanceForFullScaleDrag;
}

void Slider::setIncDecButtonsMode (IncDecButtonMode mode)                   { pimpl->setIncDecButtonsMode (mode); }

Slider::TextEntryBoxPosition Slider::getTextBoxPosition() const noexcept    { return pimpl->textBoxPos; }
int Slider::getTextBoxWidth() const noexcept                                { return pimpl->textBoxWidth; }
int Slider::getTextBoxHeight() const noexcept                               { return pimpl->textBoxHeight; }

void Slider::setTextBoxStyle (TextEntryBoxPosition newPosition, bool isReadOnly, int textEntryBoxWidth, int textEntryBoxHeight)
{
    pimpl->setTextBoxStyle (newPosition, isReadOnly, textEntryBoxWidth, textEntryBoxHeight);
}

bool Slider::isTextBoxEditable() const noexcept                     { return pimpl->editableText; }
void Slider::setTextBoxIsEditable (const bool shouldBeEditable)     { pimpl->setTextBoxIsEditable (shouldBeEditable); }
void Slider::showTextBox()                                          { pimpl->showTextBox(); }
void Slider::hideTextBox (bool discardCurrentEditorContents)        { pimpl->hideTextBox (discardCurrentEditorContents); }

void Slider::setChangeNotificationOnlyOnRelease (bool onlyNotifyOnRelease)
{
    pimpl->sendChangeOnlyOnRelease = onlyNotifyOnRelease;
}

bool Slider::getSliderSnapsToMousePosition() const noexcept           { return pimpl->snapsToMousePos; }
void Slider::setSliderSnapsToMousePosition (bool shouldSnapToMouse)   { pimpl->snapsToMousePos = shouldSnapToMouse; }

void Slider::setPopupDisplayEnabled (bool showOnDrag, bool showOnHover, Component* parent, int hoverTimeout)
{
    pimpl->showPopupOnDrag = showOnDrag;
    pimpl->showPopupOnHover = showOnHover;
    pimpl->parentForPopupDisplay = parent;
    pimpl->popupHoverTimeout = hoverTimeout;
}

Component* Slider::getCurrentPopupDisplay() const noexcept      { return pimpl->popupDisplay.get(); }

//==============================================================================
void Slider::colourChanged()        { lookAndFeelChanged(); }
void Slider::lookAndFeelChanged()   { pimpl->lookAndFeelChanged (getLookAndFeel()); }
void Slider::enablementChanged()    { repaint(); pimpl->updateTextBoxEnablement(); }

//==============================================================================
NormalisableRange<double> Slider::getNormalisableRange() const noexcept { return pimpl->normRange; }
Range<double> Slider::getRange() const noexcept                         { return { pimpl->normRange.start, pimpl->normRange.end }; }
double Slider::getMaximum() const noexcept                              { return pimpl->normRange.end; }
double Slider::getMinimum() const noexcept                              { return pimpl->normRange.start; }
double Slider::getInterval() const noexcept                             { return pimpl->normRange.interval; }

void Slider::setRange (double newMin, double newMax, double newInt)      { pimpl->setRange (newMin, newMax, newInt); }
void Slider::setRange (Range<double> newRange, double newInt)            { pimpl->setRange (newRange.getStart(), newRange.getEnd(), newInt); }
void Slider::setNormalisableRange (NormalisableRange<double> newRange)   { pimpl->setNormalisableRange (newRange); }

double Slider::getValue() const                  { return pimpl->getValue(); }
Value& Slider::getValueObject() noexcept         { return pimpl->currentValue; }
Value& Slider::getMinValueObject() noexcept      { return pimpl->valueMin; }
Value& Slider::getMaxValueObject() noexcept      { return pimpl->valueMax; }

void Slider::setValue (double newValue, NotificationType notification)
{
    pimpl->setValue (newValue, notification);
}

double Slider::getMinValue() const      { return pimpl->getMinValue(); }
double Slider::getMaxValue() const      { return pimpl->getMaxValue(); }

void Slider::setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
{
    pimpl->setMinValue (newValue, notification, allowNudgingOfOtherValues);
}

void Slider::setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
{
    pimpl->setMaxValue (newValue, notification, allowNudgingOfOtherValues);
}

void Slider::setMinAndMaxValues (double newMinValue, double newMaxValue, NotificationType notification)
{
    pimpl->setMinAndMaxValues (newMinValue, newMaxValue, notification);
}

void Slider::setDoubleClickReturnValue (bool isDoubleClickEnabled,  double valueToSetOnDoubleClick, ModifierKeys mods)
{
    pimpl->doubleClickToValue = isDoubleClickEnabled;
    pimpl->doubleClickReturnValue = valueToSetOnDoubleClick;
    pimpl->singleClickModifiers = mods;
}

double Slider::getDoubleClickReturnValue() const noexcept       { return pimpl->doubleClickReturnValue; }
bool Slider::isDoubleClickReturnEnabled() const noexcept        { return pimpl->doubleClickToValue; }

void Slider::updateText()
{
    pimpl->updateText();
}

void Slider::setTextValueSuffix (const String& suffix)
{
    pimpl->setTextValueSuffix (suffix);
}

String Slider::getTextValueSuffix() const
{
    return pimpl->textSuffix;
}

String Slider::getTextFromValue (double v)
{
    auto getText = [this] (double val)
    {
        if (textFromValueFunction != nullptr)
            return textFromValueFunction (val);

        if (getNumDecimalPlacesToDisplay() > 0)
            return String (val, getNumDecimalPlacesToDisplay());

        return String (roundToInt (val));
    };

    return getText (v) + getTextValueSuffix();
}

double Slider::getValueFromText (const String& text)
{
    auto t = text.trimStart();

    if (t.endsWith (getTextValueSuffix()))
        t = t.substring (0, t.length() - getTextValueSuffix().length());

    if (valueFromTextFunction != nullptr)
        return valueFromTextFunction (t);

    while (t.startsWithChar ('+'))
        t = t.substring (1).trimStart();

    return t.initialSectionContainingOnly ("0123456789.,-")
            .getDoubleValue();
}

double Slider::proportionOfLengthToValue (double proportion)
{
    return pimpl->normRange.convertFrom0to1 (proportion);
}

double Slider::valueToProportionOfLength (double value)
{
    return pimpl->normRange.convertTo0to1 (value);
}

double Slider::snapValue (double attemptedValue, DragMode)
{
    return attemptedValue;
}

int Slider::getNumDecimalPlacesToDisplay() const noexcept
{
    return pimpl->getNumDecimalPlacesToDisplay();
}

void Slider::setNumDecimalPlacesToDisplay (int decimalPlacesToDisplay)
{
    pimpl->setNumDecimalPlacesToDisplay (decimalPlacesToDisplay);
    updateText();
}

//==============================================================================
int Slider::getThumbBeingDragged() const noexcept           { return pimpl->sliderBeingDragged; }
void Slider::startedDragging() {}
void Slider::stoppedDragging() {}
void Slider::valueChanged() {}

//==============================================================================
void Slider::setPopupMenuEnabled (bool menuEnabled)         { pimpl->menuEnabled = menuEnabled; }
void Slider::setScrollWheelEnabled (bool enabled)           { pimpl->scrollWheelEnabled = enabled; }

bool Slider::isScrollWheelEnabled() const noexcept          { return pimpl->scrollWheelEnabled; }
bool Slider::isHorizontal() const noexcept                  { return pimpl->isHorizontal(); }
bool Slider::isVertical() const noexcept                    { return pimpl->isVertical(); }
bool Slider::isRotary() const noexcept                      { return pimpl->isRotary(); }
bool Slider::isBar() const noexcept                         { return pimpl->isBar(); }
bool Slider::isTwoValue() const noexcept                    { return pimpl->isTwoValue(); }
bool Slider::isThreeValue() const noexcept                  { return pimpl->isThreeValue(); }

float Slider::getPositionOfValue (double value) const       { return pimpl->getPositionOfValue (value); }

//==============================================================================
void Slider::paint (Graphics& g)        { pimpl->paint (g, getLookAndFeel()); }
void Slider::resized()                  { pimpl->resized (getLookAndFeel()); }

void Slider::focusOfChildComponentChanged (FocusChangeType)     { repaint(); }

void Slider::mouseDown (const MouseEvent& e)    { pimpl->mouseDown (e); }
void Slider::mouseUp   (const MouseEvent&)      { pimpl->mouseUp(); }
void Slider::mouseMove (const MouseEvent&)      { pimpl->mouseMove(); }
void Slider::mouseExit (const MouseEvent&)      { pimpl->mouseExit(); }

// If popup display is enabled and set to show on mouse hover, this makes sure
// it is shown when dragging the mouse over a slider and releasing
void Slider::mouseEnter (const MouseEvent&)     { pimpl->mouseMove(); }

/** @internal */
bool Slider::keyPressed (const KeyPress& k)     { return pimpl->keyPressed (k); }

void Slider::modifierKeysChanged (const ModifierKeys& modifiers)
{
    if (isEnabled())
        pimpl->modifierKeysChanged (modifiers);
}

void Slider::mouseDrag (const MouseEvent& e)
{
    if (isEnabled())
        pimpl->mouseDrag (e);
}

void Slider::mouseDoubleClick (const MouseEvent&)
{
    if (isEnabled())
        pimpl->mouseDoubleClick();
}

void Slider::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
{
    if (! (isEnabled() && pimpl->mouseWheelMove (e, wheel)))
        Component::mouseWheelMove (e, wheel);
}

//==============================================================================
class SliderAccessibilityHandler final : public AccessibilityHandler
{
public:
    explicit SliderAccessibilityHandler (Slider& sliderToWrap)
        : AccessibilityHandler (sliderToWrap,
                                AccessibilityRole::slider,
                                AccessibilityActions{},
                                AccessibilityHandler::Interfaces { std::make_unique<ValueInterface> (sliderToWrap) }),
          slider (sliderToWrap)
    {
    }

    String getHelp() const override   { return slider.getTooltip(); }

private:
    class ValueInterface final : public AccessibilityValueInterface
    {
    public:
        explicit ValueInterface (Slider& sliderToWrap)
            : slider (sliderToWrap),
              useMaxValue (slider.isTwoValue())
        {
        }

        bool isReadOnly() const override  { return false; }

        double getCurrentValue() const override
        {
            return useMaxValue ? slider.getMaximum()
                               : slider.getValue();
        }

        void setValue (double newValue) override
        {
            Slider::ScopedDragNotification drag (slider);

            if (useMaxValue)
                slider.setMaxValue (newValue, sendNotificationSync);
            else
                slider.setValue (newValue, sendNotificationSync);
        }

        String getCurrentValueAsString() const override          { return slider.getTextFromValue (getCurrentValue()); }
        void setValueAsString (const String& newValue) override  { setValue (slider.getValueFromText (newValue)); }

        AccessibleValueRange getRange() const override
        {
            return { { slider.getMinimum(), slider.getMaximum() },
                     getStepSize (slider) };
        }

    private:
        Slider& slider;
        const bool useMaxValue;

        //==============================================================================
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueInterface)
    };

    Slider& slider;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SliderAccessibilityHandler)
};

std::unique_ptr<AccessibilityHandler> Slider::createAccessibilityHandler()
{
    return std::make_unique<SliderAccessibilityHandler> (*this);
}

} // namespace juce
