/*!
 *
 * 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    RangeSlider.qml
 * \author  (last)      Behrouz NematiPour
 * \date    (last)      31-Mar-2023
 * \author  (original)  Behrouz NematiPour
 * \date    (original)  17-Sep-2020
 *
 */

// Qt
import QtQuick 2.12

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

/*!
 * \brief   Denali project RangeSlider
 * \details This Component contains two handler for minimum and maximum
 *          which cannot pass each other.
 *          Can be used to adjust the minimum and maximum of a range.
 *
 */
RangeRect { id: _root
    enum HandlerOption {
        None    ,
        Min     ,
        Max
    }

    property alias  lowerText   : _rangeRect.minText
    property alias  upperText   : _rangeRect.maxText
    property alias  lowerTextHorizontalCenter: _rangeRect.minTextHorizontalCenter
    property alias  upperTextHorizontalCenter: _rangeRect.maxTextHorizontalCenter

    property alias  minValue    : _rangeRect.lowerBound             ///< value of the minimum slider handler
    property alias  maxValue    : _rangeRect.upperBound             ///< value of the maximum slider handler

    property bool   maxVerticalEdgeVisible: true
    property bool   minVerticalEdgeVisible: true

    property int    gapValue    : 0                                 ///< the gap between minValue and maxValue so the minValue shall always be gap value less than maxValue and vise versa.
    property real   minValueLowerBound    : 0
    property real   minValueUpperBound    : 0
    property real   maxValueLowerBound    : 0
    property real   maxValueUpperBound    : 0

    property bool   lowerBoundValid : true
    property bool   upperBoundValid : true

    property int    curHandler  : RangeSlider.HandlerOption.None    ///< current active slider handler
    property int    diameter    : Variables.progressbarHandler      ///< handlers diameter

    property real   step        : 1                                 ///< each step difference
    property bool   stepSnap    : false                             ///< values within steps are not selectable if true

    property bool   ticks       : false                             ///< visible the tick marks

    property alias  color       : _rangeRect.color                  ///< the within range sliding color
    property alias  bgColor     : _root.color                       ///< the out of range sliding color

    property bool   minAdjusted : false                             ///< first time user adjustment happens
    property bool   maxAdjusted : false                             ///< first time user adjustment happens
    property bool   hasAdjust   : false                             ///< if set to true then component is grayed out until both min and max are adjusted

    property bool showTickmarks         : true                      ///< Indicate whether tickMarks should be shown
    property bool isTickMarksRound      : true                      ///< Indicate whether the tickmarks are round
    property bool isTickMarksCentered   : false                     ///< Indicate whether the tickmarks are centered in the slider body
    property int  tickMarkYDisplacement : (-diameter + Variables.progressbarHandlerBorderWidth*2)  ///< The amount of y displacement if the TickMarks are not centered

    property bool   isRoundedEnds       : true
    property bool   hasBorder           : true
    property color disabledBorderColor   : Colors.borderDisableButton
    property color enabledBorderColor    : Colors.borderButton

    onHasAdjustChanged  : {
        setAdjusted()
    }
    onMinAdjustedChanged: {
        setAdjusted()
    }
    onMaxAdjustedChanged: {
        setAdjusted()
    }

    /// root attributes
    clip            :  false
    color: Colors.transparent

    height          : Variables.sliderDefaultBodyHeight
    touchMargin     : 25
    leftRightTouchMargin: _root.diameter / 2

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

    border.width: _root.hasBorder ? Variables.rangeRectBorderWidth : 0
    border.color: (_root.minAdjusted && _root.maxAdjusted) ? enabledBorderColor : disabledBorderColor

    minimum         : 0
    maximum         : 0

    /// The label under the rangeSlider for minimum value
    minText {
        visible          : true
        anchors.topMargin: Variables.sliderTextMargin
        font.pixelSize   : Fonts.fontPixelSliderMarker
        font.bold        : false
    }

    /// The label under the rangeSlider for maximum value
    maxText {
        visible          : true
        anchors.topMargin: Variables.sliderTextMargin
        font.pixelSize   : Fonts.fontPixelSliderMarker
        font.bold        : false
    }

    function incrementMax(vInStepSegments) {
        if ( ! maxAdjusted ) { setMaxValue(_root.maxValueUpperBound )}
        else                 { updateMaxValue(vInStepSegments, true )}
    }
    function decrementMax(vInStepSegments) {
        if ( ! maxAdjusted ) { setMaxValue(_root.maxValueUpperBound )}
        else                 { updateMaxValue(vInStepSegments, false)}
    }

    function incrementMin(vInStepSegments) {
        if ( ! minAdjusted ) { setMinValue(_root.minValueLowerBound )}
        else                 { updateMinValue(vInStepSegments, true )}
    }
    function decrementMin(vInStepSegments) {
        if ( ! minAdjusted ) { setMinValue(_root.minValueLowerBound )}
        else                 { updateMinValue(vInStepSegments, false)}
    }
    ///
    /// \brief   grays out the rangebar and handler if not adjusted and hasAdjust set to true
    ///
    function setAdjusted() {
        function check() {
            _rangeRect   .color = minAdjusted && maxAdjusted ? Colors.sliderHighlightColor : Colors.createTreatmentInactive
            _handlerLeft .color = minAdjusted ? Colors.createTreatmentActive: Colors.createTreatmentInactive
            _handlerRight.color = maxAdjusted ? Colors.createTreatmentActive: Colors.createTreatmentInactive
        }
        function active() {
            _rangeRect   .color = Colors.createTreatmentActive
            _handlerLeft .color = Colors.createTreatmentActive
            _handlerRight.color = Colors.createTreatmentActive
        }
        if ( ! hasAdjust ) {
            active()
        }
        else {
            check()
        }
    }

    ///
    /// \brief   calculate the value by position x
    /// \param   x : x cordinate
    ///
    function getValueOfX(x) {
        return ( x * ( maximum - minimum ) ) / width + minimum
    }

    function getXOfValue(vValue) {
        return ( ( vValue - minimum ) * width ) / ( maximum - minimum )
    }

    ///
    /// \brief   calculate the value by position x of the handler
    /// \details calculates the current value withing the range of minimum and maximum regarding the x cordinate of the active slider.
    /// \param   x : the active slider x cordinate.
    ///
    function setValue(x) {

        let mValue = 0
        if ( x <     0 ) { mValue = minimum;         return mValue; }
        if ( x > width ) { mValue = maximum;         return mValue; }

        mValue = getValueOfX(x)

        if ( step === 1 ) {                         return mValue; }

        let start = 0
        if ( ! stepSnap ) start = minimum
        mValue = Math.round((mValue - start) / step) * step + start

        let decimals = Math.round(-Math.log10(step))
        if (decimals > 0) mValue = mValue.toFixed(decimals)

        if ( mValue < minimum ) { mValue = minimum;   return mValue; }
        if ( mValue > maximum ) { mValue = maximum;   return mValue; }
        return mValue;
    }

    ///
    /// \brief      updates correct lower or upper bound value regarding the x position
    /// \details    regarding the current mouse x position selects the correct handler and updated the bound value.
    /// \param      x : mouse x position.
    function setBound(x) {
        let mValue = setValue(x)

        // console.debug( maxValue - minValue, minValue, value, maxValue )
        if ( maxValue - minValue <= gapValue ) {
            // max correction if the values get too close together less than defined gap
            if ( curHandler === RangeSlider.HandlerOption.Max )  maxValue = minValue + gapValue
            // min correction if the values get too close together less than defined gap
            if ( curHandler === RangeSlider.HandlerOption.Min )  minValue = maxValue - gapValue
            // while value is between min and max do nothing and let the value gets out of the bound and then apply value.
            if ( minValue <= mValue && mValue <= maxValue ) return
        }

        let minDiff = Math.abs(minValue - mValue)
        let maxDiff = Math.abs(maxValue - mValue)

        if ( minDiff === maxDiff ) {
            if ( curHandler === RangeSlider.HandlerOption.Max ) checkLimitsMaxValueBounds(mValue)
                                                           else checkLimitsMinValueBounds(mValue)
        }
        else {
            if ( minDiff < maxDiff ) {
                // if (minDiff > limitGap) return // not sure if it needs but kept it as an idea.
                checkLimitsMinValueBounds(mValue)
            }
            else {
                // if (maxDiff > limitGap) return // not sure if it needs but kept it as an idea.
                checkLimitsMaxValueBounds(mValue)
            }
        }
    }

    function checkLimitsMinValueBounds          ( vValue ) {
        if ( vValue < minValueLowerBound ) { minValue = minValueLowerBound; return }
        if ( vValue > minValueUpperBound ) { minValue = minValueUpperBound; return }
        setMinValue(vValue)
    }

    function checkLimitsMaxValueBounds          ( vValue ) {
        if ( vValue < maxValueLowerBound ) { maxValue = maxValueLowerBound; return }
        if ( vValue > maxValueUpperBound ) { maxValue = maxValueUpperBound; return }
        setMaxValue(vValue)
    }

    function setMinValue(vValue) {
        // added an if block in case for the first time (not adjusted yet) the value is not changed (still on def value), to force the valueChanged emit.
        if ( ! minAdjusted ) {
            minAdjusted = true
            if ( vValue == minValue )
                minValueChanged(vValue)
            else
                minValue = vValue
        }
        else {
            minValue     = vValue
        }
    }

    function setMaxValue(vValue) {
        // added an if block in case for the first time (not adjusted yet) the value is not changed (still on def value), to force the valueChanged emit.
        if ( ! maxAdjusted ) {
            maxAdjusted = true
            if ( vValue == maxValue )
                maxValueChanged(vValue)
            else
                maxValue = vValue
        }
        else {
            maxValue     = vValue
        }
    }

    // This is a helper function that will calculate and return a new value
    // that was either incremented or decremented by 1 or step amount
    function determineNewValue(vOldValue, vInStepSegments, vIsIncrementing) {
        let amountChanged = 1
        if (vInStepSegments) {
            amountChanged = step
        }

        let newValue = Number.NaN
        if(vIsIncrementing) {
            newValue = vOldValue + amountChanged
        } else {
            newValue = vOldValue - amountChanged
        }

        // Capping values based on min/max threshold
        if ( newValue < minimum ) newValue = minimum
        if ( newValue > maximum ) newValue = maximum

        return newValue.toFixed(decimal)
    }

    function updateMaxValue(vInStepSegments, vIsIncrementing) {
        // Set the "focused" handle to be the maxium handle
        curHandler = RangeSlider.HandlerOption.Max

        // In order to use pre-existing rules for handling min/max ranges, need to calculate X of new value
        let newMaxValue = determineNewValue(_root.maxValue, vInStepSegments, vIsIncrementing)
        let newX = ((newMaxValue - _root.minimum)*_root.width) / (_root.maximum - _root.minimum)

        // Update the boundary
        setBound(newX)
    }

    function updateMinValue(vInStepSegments, vIsIncrementing) {
        // Set the "focused" handle to be the minium handle
        curHandler = RangeSlider.HandlerOption.Min

        // In order to use pre-existing rules for handling min/max ranges, need to calculate X of new value
        let newMinValue = determineNewValue(_root.minValue, vInStepSegments, vIsIncrementing)
        let newX = ((newMinValue - _root.minimum)*_root.width) / (_root.maximum - _root.minimum)

        // Update the boundary
        setBound(newX)
    }

    function getColor(vIsActive, vIsValid) {
        let color = Colors.textMain
        if ( ! vIsValid     ) { color = Colors.createTreatmentInvalidParam  ; return color }
        if ( ! vIsActive    ) { color = Colors.textDisableButton            ; return color }
        return color
    }


    /// The main range rectangle bar - This is the "highlighted" area of the slider
    RangeRect { id: _rangeRect
        property alias  lowerBound  : _rangeRect.minimum
        property alias  upperBound  : _rangeRect.maximum
        property real   minmaxDiff  : parent.maximum - parent.minimum
        leftRightTouchMargin: _root.diameter / 2

        x       : minmaxDiff ? ((parent.width * (lowerBound - parent.minimum)) / minmaxDiff) : minmaxDiff
        width   : minmaxDiff ? ((parent.width * (upperBound - lowerBound    )) / minmaxDiff) : minmaxDiff
        height  : parent.height
        radius  : _root.isRoundedEnds ? (height/2) : Variables.rangeRectRadius

        border.width: _root.hasBorder ? Variables.rangeRectBorderWidth : 0
        border.color: (_root.minAdjusted && _root.maxAdjusted) ? Colors.sliderProgressBorderActive : Colors.borderDisableButton

        decimal : _root.decimal

        minText { // The minimum value label above the handle
            visible          : false
            anchors.topMargin: -60
            color            : _root.getColor((_root.minAdjusted && _root.maxAdjusted), lowerBoundValid)
        }
        maxText { // the maximum value label above the handle
            visible          : false
            anchors.topMargin: -60
            color            : _root.getColor((_root.minAdjusted && _root.maxAdjusted), upperBoundValid)
        }

        // propagation is not working on drag !
        // so it has to be implemented here as well
        onDragged: {
            // Add in the x position of the range rect to account for it moving on the
            // main slider line and the displacement of the touch margin increase
            setBound(vMouseEvent.x + _rangeRect.x - _rangeRect.leftRightTouchMargin)
        }

        onReleased: {
            // Add in the x position of the range rect to account for it moving on the
            // main slider line and the displacement of the touch margin increase
            setBound(vMouseEvent.x + _rangeRect.x - _rangeRect.leftRightTouchMargin)
        }
    }

    // used loader for performance since it may not always be required.
    // and can be a heavy Component
    /// Tick mark on the slider regarding the defined step within minimum and maximum in the range
    Loader { id: _ticksLoader
        active          : _root.ticks
        width           : _root.width
        height          : (_rangeRect.height - Variables.sliderDefaultRoundTickMarkDiameter)
        anchors.bottom  : _root.bottom
        sourceComponent : _root.showTickmarks ? _tickMarkFactory : undefine
    }

    Component { id:_tickMarkFactory
        TickMarks {
            decimal     : _root.decimal
            minimum     : _root.minimum
            maximum     : _root.maximum
            step        : _root.step
            stepSnap    : _root.stepSnap
            textColor   : _root.color
            lineTickMarkHeight:  _rangeRect.height
            isTickMarkRound : _root.isTickMarksRound
            yDisplacement   : {
                if ( _root.isTickMarksCentered ) {
                    return (_root.border.width/2 - 1) // had to subtract 1 to center on the _rangeRect
                } else {
                    return _root.tickMarkYDisplacement
                }
            }
            showEndMarks    : true
            color: _root.isActive ? Colors.borderButton : Colors.borderDisableButton
        }
    }

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

    /// Left most maximum range vertical edge
    Rectangle { id : _minVerticalEdge
        visible             : _root.minVerticalEdgeVisible
        color               : bgColor
        anchors.right       : _root.left
        anchors.top         : _root.top
        anchors.topMargin   : - (height/2 - _rangeRect.height/2)
        width               : 2
        height              : 30
    }

    /// Right most minimum range vertical edge
    Rectangle { id : _maxVerticalEdge
        visible             : _root.maxVerticalEdgeVisible
        color               : bgColor
        anchors.left        : _root.right
        anchors.top         : _root.top
        anchors.topMargin   : - (height/2 - _rangeRect.height/2)
        width               : 2
        height              : 30
    }

    /// Left most handler
    Rectangle { id: _handlerLeft
        property real diameter  : _root.diameter

        anchors.verticalCenter  : parent.verticalCenter
        anchors.horizontalCenter: _rangeRect.left

        width   : diameter
        height  : diameter
        radius  : diameter
        color   : Colors.highlightProgressBar
        border  {
            width   : Variables.progressbarHandlerBorderWidth
            color   : Colors.textMain
        }
        MouseArea { id: _leftHandleMouseArea
            propagateComposedEvents: true // propagate the click to the lower layer
            anchors.fill: parent
            onPressed: {
                mouse.accepted = false // indicate that the current component did not handle the mouse action, propagate
                _root.curHandler = RangeSlider.HandlerOption.Min
            }

            onReleased: {
                mouse.accepted = false // indicate that the current component did not handle the mouse action, propagate
                _root.curHandler = RangeSlider.HandlerOption.None
            }
        }
    }

    /// Right most handler
    Rectangle { id: _handlerRight
        property real diameter          : _root.diameter

        anchors.verticalCenter      : parent.verticalCenter
        anchors.horizontalCenter    : _rangeRect.right

        width   : diameter
        height  : diameter
        radius  : diameter
        color   : Colors.highlightProgressBar
        border  {
            width   : Variables.progressbarHandlerBorderWidth
            color   : Colors.textMain
        }
        MouseArea { id: _rightHandlerMouseArea
            propagateComposedEvents: true // propagate the click to the lower layer
            anchors.fill: parent

            onPressed:{
                mouse.accepted = false // indicate that the current component did not handle the mouse action, propagate
                _root.curHandler = RangeSlider.HandlerOption.Max
            }
            onReleased: {
                mouse.accepted = false // indicate that the current component did not handle the mouse action, propagate
                _root.curHandler = RangeSlider.HandlerOption.None
            }
        }
    }
}
