/*!
 *
 * Copyright (c) 2020-2024 Diality Inc. - All Rights Reserved.
 * \copyright
 * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN
 * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER.
 *
 * \file    Slider.qml
 * \author  (last)      Behrouz NematiPour
 * \date    (last)      26-Jan-2024
 * \author  (original)  Behrouz NematiPour
 * \date    (original)  18-Mar-2020
 *
 */

// Qt
import QtQuick 2.12

// Project
//  Qml imports
import "qrc:/globals"

/*!
 * \brief   Denali project ProgressBar
 */
RangeRect { id: _root
    property real   value              : _progressRect.value
    property real   minStop            : _root.minimum
    property real   defaultValue       : _root.minimum

    property real   step               : 1
    property bool   stepSnap           : false
    property bool   stepSnapOnRelease  : true

    property bool   ticks              : false

    // TODO  suggest to BN that we should rename color to progressBarBgColor because slider's rangeRect has a color property also
    property alias  color              : _progressRect.color
    property alias  bgColor            : _root.color

    property alias  handler            : _handler
    property alias  handlerColor       : _handler.color
    property alias  handlerVisible     : _handler.visible
    property color  handleBorderColor   : Colors.textMain

    property alias  diameter           : _handler.diameter

    property bool   isActive           : true
    property bool   inActiveZero       : false  // if inActiveZero:true, when is not active (inActive or active:false) sets to zero instead of minimum

    property alias  progressRectMargin : _progressRect.margin

    property int    tickMarksThickness : 2

    property bool   isRoundedEnds       : true
    property bool   hasBorder           : true
    property bool   showMinMaxText      : true
    property color  borderColor                     : Colors.borderDisableButton
    property color  highlightActiveColor            : Colors.sliderHighlightColor
    property color  highlightInactiveColor          : Colors.createTreatmentInactive
    property color  progressBorderActiveColor       : Colors.sliderProgressBorderActive
    property color  progressBorderInactiveColor     : Colors.borderDisableButton
    property color  handlerActiveColor              : Colors.createTreatmentActive
    property color  handlerInactiveColor            : Colors.createTreatmentInactive

    signal activeChanged()
    signal handleSelected()
    signal sliderSelected()

    onDefaultValueChanged: refreshValue()

    // this function shall be used in case that any external value is forced to be set for the slider
    // like the OFF switch wants to externally set the slider value and bypass slider controlls and checks.
    // same is used in the main treatment Blood,dialyzer sliders to be set to the current value when get in to adjustment screen.
    function reset(vValue) {
        _root.value         = vValue
        _progressRect.value = vValue
    }

    function incrementValue (vInStepSegments) {
        updateValue(vInStepSegments, true)
    }

    function decrementValue (vInStepSegments) {
        updateValue(vInStepSegments, false)
    }

    function updateValue(vInStepSegments, vIsIncrement) {
        let amountChanged = 1
        if (vInStepSegments) {
            amountChanged = step
        }

        let newValue = Number.NaN
        if(vIsIncrement) {
            newValue = calculateRoundedValue(_progressRect.value + amountChanged)
        } else {
            newValue = calculateRoundedValue(_progressRect.value - amountChanged)
        }

        // Capping values based on min/max threshold
        let min = calculateMinimum()
        let max = calculateMaximum()
        if ( newValue < min ) newValue = min
        if ( newValue > max ) newValue = max

        // Update the slider's visual value
        _progressRect.previousSliderValue = newValue // for comparison purposes
        _progressRect.value               = newValue // visual value

        // update slider value with rounded new value
        update(newValue)
    }

    function setActiveVisuals(active) {
        if (active) {
            color        = _root.highlightActiveColor
            handlerColor = _root.handlerActiveColor
        } else {
            color        = _root.highlightInactiveColor
            handlerColor = _root.handlerInactiveColor
        }
    }

    /*
      refreshValue() re-evaluates the _root.value and _progressRect.value based on the
      current active state. This function is used when the defaultValue property or
      isActive property changes.
    */
    function refreshValue() {
        // value is assigned a different value based on active-ness of the slider
        // This is to resolve the use of slider with a switch and arrows enabled.
        // It allows correct behavior when using arrow on a first initialize increment of
        // the slider when the slider has inActiveZero to true.
        if(!isActive) {
            _root.value = inActiveZero ? 0 : _root.defaultValue
        } else {
            _root.value = _root.defaultValue
        }

        // need to set the value of progressRect to reflect handle position
        _progressRect.value = _root.defaultValue
    }

    onIsActiveChanged: {
        setActiveVisuals(isActive)
        refreshValue () // re-evaluate values
        activeChanged() // emit
    }

    height              : Variables.sliderDefaultBodyHeight
    touchMargin         : 25
    leftRightTouchMargin: _handler.width/2

    radius  : _root.isRoundedEnds ? (height/2) : Variables.rangeRectRadius

    border.width: _root.hasBorder ? Variables.rangeRectBorderWidth : 0
    border.color: _root.isActive  ? Colors.borderButton : Colors.borderDisableButton

    minimum         : 0
    maximum         : 0

    // real-time bound change should effect the current set value
    onMinimumChanged: {
        if ( !isActive && inActiveZero ) { value = 0; return }
        if (value < minimum )
            value = minimum
    }
    onMaximumChanged: {
        if (value > maximum )
            value = maximum
    }

    minText {
        visible          : _root.showMinMaxText
        anchors.topMargin: Variables.sliderTextMargin
        font.pixelSize   : Fonts.fontPixelSliderMarker
        font.bold        : false
    }
    maxText {
        visible          : _root.showMinMaxText
        anchors.topMargin: Variables.sliderTextMargin
        font.pixelSize   : Fonts.fontPixelSliderMarker
        font.bold        : false
    }

    function calculateRoundedValue(vValue){
        return (Math.round(vValue / step) * step).toFixed(decimal)
    }

    // round the value based on the given precision (not step)
    function calculatePrecisionRoundedValue(value) {
        let multiplier = Math.pow(10, decimal)
        return Math.round(value * multiplier) / multiplier
    }

    function getValueOfX(x) {
        return ( x * ( maximum - minimum ) ) / width + minimum
    }

    function update(vValue) {
        _root.value = vValue
    }

    function calculateValue(x, isSnappingToTicks) {
        let mMinimum = calculateMinimum()
        let mMaximum = calculateMaximum()

        // the center of the handler is aligned on the snap point and half width shall be used to set as min not the entire width.
        // also half of the hadler is out of slider min position when set on min, which proves the same as above.
        if(x < ( _handler.width / 2 ) ) {
            // The outside of the slider, lower bound case
            return mMinimum
        }

        if(x > _root.width ) {
            // The outside of the slider, upper bound case
            return mMaximum
        }

        // Calculate the in-between :
        let mValue = 0
        let start = 0

        if ( ! stepSnap ) start = mMinimum

        // calculate the expected value based on x
        mValue = getValueOfX(x)

        // special handling for the case that the step segments are less than the handle's width
        let stepWidth = width / ((mMaximum - mMinimum) / step)
        if ( stepWidth < _handler.width ) {
            let valueStepCount = parseInt(mValue / step)
            mValue = calculateRoundedValue((valueStepCount * step))
            if ( mValue < mMinimum ) { return mMinimum }
            if ( mValue > mMaximum ) { return mMaximum }
                                       return mValue
        }

        // For sliders with decimal min, max, values, we need to add refinement to
        // the value to achieve slider handle movement close to those of the whole number sliders
        if ( decimal > 0 ) {
            let additionalDecimalResolution = 3
            mValue = mValue.toFixed(decimal + additionalDecimalResolution)
        }

        if ( isSnappingToTicks ) {
            mValue = Math.round((mValue - start) / step) * step + start
            mValue = mValue.toFixed(decimal)
        }

        if ( mValue < mMinimum ) { return mMinimum; }
        if ( mValue > mMaximum ) { return mMaximum; }
                                   return mValue;
    }

    /*!
     * \brief   Calculate the minimum value for the slider based on the current minimum,
     *          minStop, and stepSnap values.
     * \return  If minimum is greater than or equal to minStop, then just return minimum.
     *          If minStop is greater than minimum and stepSnap is false, then return minStop.
     *          If minStop is greater than minimum and stepSnap is true, then return minStep if
     *          it falls on a step, otherwise return the step above minStop.
     */
    function calculateMinimum() {
        let result     = calculateRoundedValue(_root.minimum)
        let rdMinStop  = calculatePrecisionRoundedValue(_root.minStop)
        if (rdMinStop > result) {
            if (stepSnap) {
                // if slider is set to snap and minimum stop is not on a step, then
                // adjust it to one step above
                let rdStep = calculatePrecisionRoundedValue(step)
                result = Math.ceil(rdMinStop/rdStep) * rdStep
            }
            else {
                result = rdMinStop
            }
        }
        return result
    }

    /*!
     * \brief   Calculate the maximum value for the slider based on the current maximum
     *          and stepSnap values.
     * \return  If stepSnap is false, then maximum.
     *          If stepSnap is true and maximum does not fall on a step, then one step below maximum,
     *          otherwise maximum.
     */
    function calculateMaximum() {
        let result = calculatePrecisionRoundedValue(_root.maximum)
        return stepSnap ? Math.min(result, parseInt(result/step) * step)
                        : result
    }

    // used loader for performance since it may not always be required.
    // and can be a heavy Component
    Loader { id: _ticksLoader
        active          : ticks
        anchors.fill    : parent
        sourceComponent : TickMarks {
            decimal     : _root.decimal
            minimum     : _root.minimum
            maximum     : _root.maximum
            step        : _root.step
            stepSnap    : _root.stepSnap
            textColor   : _root.color
            lineTickMarkHeight      : _progressRect.height
            lineTickMarkThickness   : _root.tickMarksThickness
            isTickMarkRound : _root.isRoundedEnds
            yDisplacement   : _root.isRoundedEnds ? (-_handler.height/2 - _handler.border.width/2) : 0
            color           : _root.isActive ? Colors.borderButton : Colors.borderDisableButton
        }
    }

    ProgressRect { id: _progressRect
        property real previousSliderValue: Number.NaN
        value   : minimum
        color   : _root.highlightActiveColor
        decimal : _root.decimal

        minimum : _root.minimum
        maximum : _root.maximum
        leftRightTouchMargin: _handler.width/2

        radius  : _root.isRoundedEnds ? (height/2) : Variables.rangeRectRadius

        border.width: _root.hasBorder ? Variables.rangeRectBorderWidth : 0
        border.color: _root.isActive ? _root.progressBorderActiveColor : _root.progressBorderInactiveColor

        // propagation is not working on drag !
        onDragged: {
            _root.dragged(vMouseEvent)
        }
        onPressed: {
            _root.pressed(vMouseEvent)
        }
        onReleased: {
            _root.released(vMouseEvent)
        }
    }

    function updateHandleValue(vCurrentPositionX)
    {
        // Passing false for snapping to get exact value in respect to x
        let newCurrentValue = calculateValue(vCurrentPositionX, false)

        // Check the cursor's current value to determine if the cursor had moved
        // Do not avoid type coercion, it will produce undesired results
        if(newCurrentValue != _progressRect.previousSliderValue) {

           // The cursor did move
            if(stepSnapOnRelease) {
                // check for snapping state to save an iteration of recalculation if snap is off
                newCurrentValue = calculateValue(vCurrentPositionX, stepSnapOnRelease)
            }

            // Update the slider's value since the cursor did move
            _progressRect.previousSliderValue = newCurrentValue // for comparison purposes
            _progressRect.value          = newCurrentValue // visual value

            // update slider value with rounded new value
            update(calculateRoundedValue(newCurrentValue))

        }
    }

    onDragged: {
        // On position change / dragging, the value is updated based on value calculated
        // Need to account for the extended touch areas
        let adjustedXPosition = vMouseEvent.x - _progressRect.leftRightTouchMargin
        let newCurrentValue = calculateValue(adjustedXPosition, !stepSnapOnRelease)
        _progressRect.value = newCurrentValue // update visual active slider bar

        update(calculateRoundedValue(newCurrentValue)) // displayed/slider value
    }

    onPressed: {
        // Indicate that the slider body was selected to provide handling/activation
        sliderSelected() // emit

        updateHandleValue(vMouseEvent.x - _progressRect.leftRightTouchMargin)
    }
    onReleased: {
        // Need to account for the extended touch areas
        updateHandleValue(vMouseEvent.x - _progressRect.leftRightTouchMargin)
    }

    Rectangle { id: _handler
        property bool handleSelected : false
        property real diameter       : Variables.progressbarHandler

        anchors.verticalCenter  : parent.verticalCenter
        anchors.horizontalCenter: _progressRect.right

        width   : diameter
        height  : diameter
        radius  : diameter
        color   : _root.handlerActiveColor
        border  {
            width: Variables.progressbarHandlerBorderWidth
            color: _root.handleBorderColor
        }
        MouseArea {
            anchors.fill: parent
            propagateComposedEvents: true
            onPressed: {
                mouse.accepted = false // allow propagtion to the lower mouse areas
                handleSelected() // emit
            }
        }
    }
}
