/**
 * The slider component utilized by `Ext.field.Slider`.
 *
 * The slider is a way to allow the user to select a value from a given numerical range.
 * You might use it for choosing a percentage, combine two of them to get min and max
 * values, or use three of them to specify the hex values for a color.
 *
 * Each slider contains a single 'thumb' that can be dragged along the slider's length to
 * change the value.
 *
 * ## Simple Slider
 *
 * ```javascript
 * @example({ framework: 'extjs' })
 * Ext.create({
 *     xtype: 'container',
 *     fullscreen: true,
 *     padding: 20,
 *
 *     items: [{
 *         xtype: 'slider',
 *         value: 42
 *     }]
 * });
 * ```
 *
 * ## Slider with Bound Value
 *
 * This slider does not incorporate two-way binding by default.  Please utilize `publishes` or
 * `twoWayBindable` in order to publish bound values.  You can also use Ext.field.Slider
 * directly for a more feature-rich component.
 *
 * ```javascript
 * @example({ framework: 'extjs' })
 * Ext.create({
 *     xtype: 'container',
 *     fullscreen: true,
 *     padding: 20,
 *
 *     layout: {
 *         type: 'hbox',
 *         pack: 'center'
 *     },
 *     
 *     viewModel: {
 *         data: {
 *             value: 42
 *         }
 *     },
 *
 *     items: [{
 *         xtype: 'slider',
 *         value: '{value}'
 *     }, {
 *         xtype: 'label',
 *         bind: '{value}'
 *     }]
 * })
 * ```
 *
 */
Ext.define('Ext.slider.Slider', {
    extend: 'Ext.Component',
    xtype: 'slider',
 
    requires: [
        'Ext.slider.Thumb',
        'Ext.fx.easing.EaseOut'
    ],
 
    cachedConfig: {
        /**
         * @cfg {Boolean} vertical
         * Orients the slider vertically rather than horizontally.
         */
        vertical: false
    },
 
    /**
    * @event change
    * Fires when the value changes
    * @param {Ext.slider.Slider} this 
    * @param {Ext.slider.Thumb} thumb The thumb being changed
    * @param {Number} newValue The new value
    * @param {Number} oldValue The old value
    */
 
    /**
    * @event dragstart
    * Fires when the slider thumb starts a drag
    * @param {Ext.slider.Slider} this 
    * @param {Ext.slider.Thumb} thumb The thumb being dragged
    * @param {Array} value The start value
    * @param {Ext.event.Event} e 
    */
 
    /**
    * @event drag
    * Fires when the slider thumb starts a drag
    * @param {Ext.slider.Slider} this 
    * @param {Ext.slider.Thumb} thumb The thumb being dragged
    * @param {Ext.event.Event} e 
    */
 
    /**
    * @event dragend
    * Fires when the slider thumb starts a drag
    * @param {Ext.slider.Slider} this 
    * @param {Ext.slider.Thumb} thumb The thumb being dragged
    * @param {Array} value The end value
    * @param {Ext.event.Event} e 
    */
    config: {
        // eslint-disable-next-line max-len
        // @cmd-auto-dependency { defaultType: "Ext.slider.Thumb", aliasPrefix:'widget.',typeProperty: 'xtype' }
        /**
         * @cfg {Object} thumbDefaults The config object to factory
         * {@link Ext.slider.Thumb} instances
         * @accessor
         */
        thumbDefaults: {
            xtype: 'thumb',
            inheritUi: true,
            translatable: {
                easingX: {
                    duration: 300,
                    type: 'ease-out'
                },
                easingY: {
                    duration: 300,
                    type: 'ease-out'
                }
            }
        },
 
        /**
         * @cfg {Number} increment The increment by which to snap each thumb when its value
         * changes. Any thumb movement will be snapped to the nearest value that is a multiple
         * of the increment (e.g. if increment is 10 and the user tries to move the thumb to 67,
         * it will be snapped to 70 instead)
         * @accessor
         */
        increment: 1,
 
        /**
         * @cfg {Number/Number[]} value The value(s) of this slider's thumbs. If you pass
         * a number, it will assume you have just 1 thumb.
         * @accessor
         */
        value: 0,
 
        /**
         * @cfg {Boolean} [valueIsArray=false]
         * If `false` the {@link #value} will be a number when a single value is given
         * (even if it's an array containing that single value), and an array,
         * when an array of values was given.
         * If `true`, the {@link #value} will always be converted to an array.
         */
        valueIsArray: false,
 
        /**
         * @cfg {Number} minValue The lowest value any thumb on this slider can be set to.
         * @accessor
         */
        minValue: 0,
 
        /**
         * @cfg {Number} maxValue The highest value any thumb on this slider can be set to.
         * @accessor
         */
        maxValue: 100,
 
        /**
         * @cfg {Boolean} allowThumbsOverlapping Whether or not to allow multiple thumbs to
         * overlap each other. Setting this to true guarantees the ability to select every
         * possible value in between {@link #minValue} and {@link #maxValue} that
         * satisfies {@link #increment}
         * @accessor
         */
        allowThumbsOverlapping: false,
 
        /**
         * @cfg {Boolean/Object} animation
         * The animation to use when moving the slider. Possible properties are:
         *
         * - duration
         * - easingX
         * - easingY
         *
         * @accessor
         */
        animation: true,
 
        /**
         * Will make this field read only, meaning it cannot be changed from the user interface.
         * @cfg {Boolean} readOnly
         * @accessor
         */
        readOnly: false
    },
 
    defaultBindProperty: 'value',
    twoWayBindable: {
        value: 1
    },
 
    /**
     * @cfg {Number/Number[]} values Alias to {@link #value}
     */
 
    classCls: Ext.baseCSSPrefix + 'slider',
    verticalCls: Ext.baseCSSPrefix + 'slider-vertical',
 
    elementWidth: 0,
 
    offsetValueRatio: 0,
 
    activeThumb: null,
 
    isThumbAnimating: 0,
 
    template: [{
        reference: 'trackElement',
        cls: Ext.baseCSSPrefix + 'track-el'
    }, {
        reference: 'thumbWrapElement',
        cls: Ext.baseCSSPrefix + 'thumb-wrap-el'
    }],
 
    fillSelector: '.' + Ext.baseCSSPrefix + 'fill-el',
 
    constructor: function(config) {
        if (config && config.hasOwnProperty('values')) {
            config = Ext.apply({
                value: config.values
            }, config);
 
            delete config.values;
        }
 
        this.thumbs = [];
 
        this.callParent([config]);
    },
 
    /**
     * @private
     */
    initialize: function() {
        this.callParent();
        this.element.on('tap', 'onTap', this);
    },
 
    onRender: function() {
        this.callParent();
        this.whenVisible('refreshSizes');
    },
 
    applyThumbDefaults: function(defaults) {
        return Ext.apply({
            slider: this,
            ownerCmp: this
        }, defaults);
    },
 
    /**
     * @private
     */
    factoryThumb: function() {
        var thumb = Ext.create(this.getThumbDefaults());
 
        thumb.doInheritUi();
 
        return thumb;
    },
 
    /**
     * Returns the Thumb instances bound to this Slider
     * @return {Ext.slider.Thumb[]} The thumb instances
     */
    getThumbs: function() {
        return this.thumbs;
    },
 
    /**
     * Returns the Thumb instance bound to this Slider
     * @param {Number} [index=0] The index of Thumb to return.
     * @return {Ext.slider.Thumb} The thumb instance
     */
    getThumb: function(index) {
        if (typeof index !== 'number') {
            index = 0;
        }
 
        return this.thumbs[index];
    },
 
    refreshOffsetValueRatio: function() {
        var me = this,
            vertical = me.getVertical(),
            valueRange = me.getMaxValue() - me.getMinValue(),
            trackWidth = me.elementWidth - me.thumbWidth,
            trackHeight = me.elementHeight - me.thumbHeight;
 
        me.offsetValueRatio =
            valueRange === 0 ? 0 : (vertical ? trackHeight : trackWidth) / valueRange;
    },
 
    onThumbResize: function(thumb, thumbWidth, thumbHeight) {
        this.thumbWidth = thumbWidth;
        this.thumbHeight = thumbHeight;
 
        this.refresh();
    },
 
    onResize: function(width, height) {
        this.elementWidth = width;
        this.elementHeight = height;
 
        this.refresh();
    },
 
    refresh: function() {
        this.refreshing = true;
        this.refreshValue();
        this.refreshing = false;
    },
 
    setActiveThumb: function(thumb) {
        var oldActiveThumb = this.activeThumb;
 
        if (oldActiveThumb && oldActiveThumb !== thumb) {
            oldActiveThumb.setZIndex(null);
        }
 
        this.activeThumb = thumb;
        thumb.setZIndex(2);
 
        return this;
    },
 
    onThumbBeforeDragStart: function(thumb, e) {
        if (this.offsetValueRatio === 0 || e.absDeltaX <= e.absDeltaY || this.getReadOnly()) {
            return false;
        }
    },
 
    onThumbDragStart: function(thumb, e) {
        var me = this;
 
        if (me.getAllowThumbsOverlapping()) {
            me.setActiveThumb(thumb);
        }
 
        me.dragStartValue = me.getArrayValues()[me.getThumbIndex(thumb)];
        me.fireEvent('dragstart', me, thumb, me.dragStartValue, e);
    },
 
    onThumbDragMove: function(thumb, e, offsetX, offsetY) {
        var me = this,
            index = me.getThumbIndex(thumb),
            vertical = me.getVertical(),
            offsetValueRatio = me.offsetValueRatio,
            constrainedValue = me.constrainValue(
                me.getMinValue() + (vertical ? offsetY : offsetX) / offsetValueRatio
            );
 
        e.stopPropagation();
 
        me.setIndexValue(index, constrainedValue);
 
        me.fireEvent('drag', me, thumb, me.getArrayValues(), e);
 
        return false;
    },
 
    setIndexValue: function(index, value, animation) {
        var me = this,
            thumb = me.thumbs[index],
            values = me.getArrayValues(),
            minValue = me.getMinValue(),
            offsetValueRatio = me.offsetValueRatio,
            increment = me.getIncrement(),
            pos = (value - minValue) * offsetValueRatio;
 
        thumb.setXY(pos, null, animation);
 
        values[index] = minValue + Math.round((pos / offsetValueRatio) / increment) * increment;
 
        me.setValue(values);
        me.refreshAdjacentThumbConstraints(thumb);
    },
 
    onChange: function(thumb, newValue, oldValue) {
        var me = this;
 
        if (me.hasListeners.change) {
            me.fireEvent('change', me, thumb, newValue, oldValue);
        }
    },
 
    onThumbDragEnd: function(thumb, e) {
        var me = this,
            index = me.getThumbIndex(thumb),
            newValue = me.getArrayValues()[index],
            oldValue = me.dragStartValue;
 
        me.snapThumbPosition(thumb, newValue);
        me.fireEvent('dragend', me, thumb, me.getArrayValues(), e);
 
        if (oldValue !== newValue) {
            me.onChange(thumb, newValue, oldValue);
        }
    },
 
    getThumbIndex: function(thumb) {
        return this.thumbs.indexOf(thumb);
    },
 
    refreshAdjacentThumbConstraints: function(thumb) {
        var me = this,
            vertical = me.getVertical(),
            offsetX = thumb.getLeft(),
            offsetY = thumb.getTop(),
            thumbs = me.thumbs,
            index = me.getThumbIndex(thumb),
            previousThumb = thumbs[index - 1],
            nextThumb = thumbs[index + 1],
            thumbWidth = me.getAllowThumbsOverlapping() ? 0 : me.thumbWidth,
            thumbHeight = me.getAllowThumbsOverlapping() ? 0 : me.thumbHeight;
 
        if (previousThumb) {
            previousThumb.setDragMax(vertical ? offsetY - thumbHeight : offsetX - thumbWidth);
        }
 
        if (nextThumb) {
            nextThumb.setDragMin(vertical ? offsetY + thumbHeight : offsetX + thumbWidth);
        }
    },
 
    /**
     * @private
     */
    onTap: function(e) {
        var me = this,
            vertical = me.getVertical(),
            element = me.element,
            minDistance = Infinity,
            i, absDistance, testValue, closestIndex, oldValue, thumb,
            ln, values, value, offset, elementX, targetElement, touchPointX, touchPointY, elementY;
 
        if (me.offsetValueRatio === 0 || me.isDisabled() || me.getReadOnly()) {
            return;
        }
 
        targetElement = Ext.get(e.target);
 
        if (
            !targetElement ||
            (Ext.browser.engineName === 'WebKit' && targetElement.hasCls('x-thumb'))
        ) {
            return;
        }
 
        touchPointX = e.touch.point.x;
        touchPointY = e.touch.point.y;
 
        elementX = element.getX();
        elementY = element.getY();
 
        offset = vertical
            ? touchPointY - elementY - me.thumbHeight / 2
            : touchPointX - elementX - me.thumbWidth / 2;
        value = me.constrainValue(me.getMinValue() + offset / me.offsetValueRatio);
        values = me.getArrayValues();
        ln = values.length;
 
        if (ln === 1) {
            closestIndex = 0;
        }
        else {
            for (= 0; i < ln; i++) {
                testValue = values[i];
                absDistance = Math.abs(testValue - value);
 
                if (absDistance < minDistance) {
                    minDistance = absDistance;
                    closestIndex = i;
                }
            }
        }
 
        oldValue = values[closestIndex];
        thumb = me.thumbs[closestIndex];
 
        me.setIndexValue(closestIndex, value, me.getAnimation());
 
        if (oldValue !== value) {
            me.onChange(thumb, value, oldValue);
        }
    },
 
    /**
     * @private
     */
    updateThumbs: function(newThumbs) {
        this.add(newThumbs);
    },
 
    applyValue: function(value, oldValue) {
        var me = this,
            values = Ext.Array.from(value || 0),
            valueIsArray = me.getValueIsArray(),
            filteredValues = [],
            previousFilteredValue = me.getMinValue(),
            filteredValue, i, ln;
 
        for (= 0, ln = values.length; i < ln; i++) {
            filteredValue = me.constrainValue(values[i]);
 
            if (filteredValue < previousFilteredValue) {
                //<debug>
                Ext.log.warn("Invalid values of '" + Ext.encode(values) +
                    "', values at smaller indexes must " +
                    "be smaller than or equal to values at greater indexes");
                //</debug>
                filteredValue = previousFilteredValue;
            }
 
            filteredValues.push(filteredValue);
            previousFilteredValue = filteredValue;
        }
 
        if (!me.refreshing && oldValue && Ext.Array.equals(values, oldValue)) {
            filteredValues = undefined;
        }
        else {
            me.values = filteredValues;
 
            if (!valueIsArray && filteredValues.length === 1) {
                filteredValues = filteredValues[0];
            }
        }
 
        return filteredValues;
    },
 
    /**
     * Updates the sliders thumbs with their new value(s)
     */
    updateValue: function() {
        var me = this,
            thumbs = me.thumbs,
            values = me.getArrayValues(),
            len = values.length,
            i;
 
        me.setThumbsCount(len);
 
        if (!this.isThumbAnimating) {
            for (= 0; i < len; i++) {
                me.snapThumbPosition(thumbs[i], values[i]);
            }
        }
    },
 
    /**
     * @private
     */
    refreshValue: function() {
        this.refreshOffsetValueRatio();
 
        this.updateValue();
    },
 
    /**
     * @private
     * Takes a desired value of a thumb and returns the nearest snap value. 
     * e.g if minValue = 0, maxValue = 100, increment = 10 and we pass a value of 67 here, 
     * the returned value will be 70. The returned number is constrained 
     * within {@link #minValue} and {@link #maxValue}, so in the above example 68 would 
     * be returned if {@link #maxValue} was set to 68.
     * @param {Number} value The value to snap
     * @return {Number} The snapped value
     */
    constrainValue: function(value) {
        var me = this,
            minValue = me.getMinValue(),
            maxValue = me.getMaxValue(),
            increment = me.getIncrement(),
            remainder;
 
        value = parseFloat(value);
 
        if (isNaN(value)) {
            value = minValue;
        }
 
        remainder = (value - minValue) % increment;
        value -= remainder;
 
        if (Math.abs(remainder) >= (increment / 2)) {
            value += (remainder > 0) ? increment : -increment;
        }
 
        value = Math.max(minValue, value);
        value = Math.min(maxValue, value);
 
        return value;
    },
 
    setThumbsCount: function(count) {
        var me = this,
            thumbs = me.thumbs,
            thumbsCount = thumbs.length,
            i, thumb;
 
        while (count < thumbs.length) {
            thumb = thumbs.pop();
            thumb.destroy();
        }
 
        while (count > thumbs.length) {
            thumb = me.factoryThumb();
            thumbs.push(thumb);
 
            me.trackElement.appendChild(thumb.fillElement);
            me.thumbWrapElement.appendChild(thumb.element);
            me.element.appendChild(thumb.sizerElement);
        }
 
        if (thumbsCount !== count) {
            for (= 0; i < count; i++) {
                // Default fill behavior is as follows:
                // - if only one thumb, fill is on
                // - if 2 thumbs, fill is off for first thumb, on for 2nd thumb
                // - 3 or more thumbs - no fill
                // TODO: allow the user to configure thumbs in initial slider config
                thumb = thumbs[i];
 
                if (count > 2) {
                    thumb.setFillTrack(false);
                }
                else if (count === 2) {
                    thumb.setFillTrack(=== 1);
                }
                else {
                    thumb.setFillTrack(true);
                }
            }
        }
 
        return this;
    },
 
    /**
     * Convenience method. Calls {@link #setValue}.
     */
    setValues: function(value) {
        this.setValue(value);
    },
 
    /**
     * Convenience method. Calls {@link #getValue}.
     * @return {Object} 
     */
    getValues: function() {
        return this.getValue();
    },
 
    /**
     * @private
     */
    getArrayValues: function() {
        return this.values;
    },
 
    /**
     * Sets the {@link #increment} configuration.
     * @param {Number} increment 
     * @return {Number} 
     */
    applyIncrement: function(increment) {
        if (increment === 0) {
            increment = 1;
        }
 
        return Math.abs(increment);
    },
 
    /**
     * @private
     */
    updateAllowThumbsOverlapping: function(newValue, oldValue) {
        if (typeof oldValue !== 'undefined') {
            this.refreshValue();
        }
    },
 
    /**
     * @private
     */
    updateMinValue: function(newValue, oldValue) {
        if (typeof oldValue !== 'undefined') {
            this.refreshValue();
        }
    },
 
    /**
     * @private
     */
    updateMaxValue: function(newValue, oldValue) {
        if (typeof oldValue !== 'undefined') {
            this.refreshValue();
        }
    },
 
    /**
     * @private
     */
    updateIncrement: function(newValue, oldValue) {
        if (typeof oldValue !== 'undefined') {
            this.refreshValue();
        }
    },
 
    updateDisabled: function(disabled) {
        var thumbs, ln, i;
 
        this.callParent(arguments);
 
        thumbs = this.thumbs;
        ln = thumbs.length;
 
        for (= 0; i < ln; i++) {
            thumbs[i].setDisabled(disabled);
        }
    },
 
    updateVertical: function(vertical) {
        var me = this,
            thumbs = me.thumbs,
            i = 0,
            thumb;
 
        me.toggleCls(me.verticalCls, vertical);
 
        if (me.initialized) {
            for (; i < thumbs.length; i++) {
                thumb = thumbs[i];
 
                // Here we clear the drag limits, these will be recalculated on the next press.
                // We do this as it is likely the dragMin will remain 0 between vert/horz changes
                // This means the updater for dragMin will not be called on the thumb as the 
                // previous and current value would both be 0.
                // The updated is responsible for swapping X and Y constraints so we need to force
                // the values to undefined values so the next press properly resets them.
                thumb.setDragMin(null);
                thumb.setDragMax(null);
                thumb.updateVertical(vertical);
            }
 
            me.refresh();
            me.fireEvent('directionChange', me, vertical);
        }
 
    },
 
    doDestroy: function() {
        this.thumbs = Ext.destroy(this.thumbs);
        this.callParent();
    },
 
    getRefItems: function(deep) {
        return this.thumbs;
    },
 
    privates: {
        /**
         * This method is called by the `thumb` before a drag gets going. We are still
         * allowed to adjust the constraints at this point so we fix them all up.
         * @private
         */
        refreshAllThumbConstraints: function() {
            var me = this,
                vertical = me.getVertical(),
                thumbs = me.thumbs,
                len = thumbs.length,
                thumbWidth = me.getAllowThumbsOverlapping() ? 0 : me.thumbWidth,
                thumbHeight = me.getAllowThumbsOverlapping() ? 0 : me.thumbHeight,
                i;
 
            for (= 0; i < len; i++) {
                me.refreshAdjacentThumbConstraints(thumbs[i]);
            }
 
            thumbs[0].setDragMin(0);
            thumbs[len - 1].setDragMax(
                vertical ? me.elementHeight - thumbHeight : me.elementWidth - thumbWidth
            );
        },
 
        refreshSizes: function() {
            var me = this,
                thumb = me.thumbs[0];
 
            me.elementWidth = me.element.measure('w');
            me.elementHeight = me.element.measure('h');
 
            if (thumb) {
                me.thumbWidth = thumb.element.measure('w');
                me.thumbHeight = thumb.element.measure('h');
            }
 
            me.refresh();
        },
 
        snapThumbPosition: function(thumb, value) {
            var me = this,
                vertical = me.getVertical(),
                ratio = me.offsetValueRatio,
                offset;
 
            if (isFinite(ratio)) {
                offset = Ext.Number.correctFloat((value - me.getMinValue()) * ratio);
 
                if (vertical) {
                    thumb.setXY(0, offset);
                }
                else {
                    thumb.setXY(offset, 0);
                }
            }
        },
 
        syncFill: function(thumb, xOffset, yOffset) {
            var me = this,
                thumbs = me.thumbs,
                values = me.getArrayValues(),
                ln = values.length,
                prevOffset = 0,
                fillElements = me.trackElement.query(me.fillSelector, false),
                thumbIndex = thumbs.indexOf(thumb),
                vertical = me.getVertical(),
                thumbOffset, fillElement, i;
 
            if (vertical) {
                yOffset += Math.ceil(thumb.element.getHeight() / 2);
            }
            else {
                xOffset += Math.ceil(thumb.element.getWidth() / 2);
            }
 
            for (= 0; i < ln; i++) {
                thumb = thumbs[i];
                fillElement = fillElements[i];
 
                if (vertical) {
                    thumbOffset =
                      (=== thumbIndex)
                          ? yOffset
                          : thumb.getTop() + (thumb.element.getHeight() / 2);
                    fillElement.setHeight(thumbOffset - prevOffset);
                    fillElement.setLocalY(prevOffset);
                    fillElement.setLocalX(0);
                    fillElement.setWidth(null);
                }
                else {
                    thumbOffset =
                      (=== thumbIndex)
                          ? xOffset
                          : thumb.getLeft() + (thumb.element.getWidth() / 2);
                    fillElement.setWidth(thumbOffset - prevOffset);
                    fillElement.setLocalX(prevOffset);
                    fillElement.setLocalY(0);
                    fillElement.setHeight(null);
                }
 
                prevOffset = thumbOffset;
            }
        },
 
        onThumbAnimationStart: function() {
            this.isThumbAnimating++;
        },
 
        onThumbAnimationEnd: function() {
            this.isThumbAnimating--;
        }
    }
});