/**
 * Slider component used by Ext.field.Slider.
 */
Ext.define('Ext.slider.Slider', {
    extend: 'Ext.Component',
    xtype: 'slider',

    requires: [
        'Ext.slider.Thumb',
        'Ext.fx.easing.EaseOut'
    ],

    /**
    * @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.EventObject} 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.EventObject} 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.EventObject} e
    */
    config: {
        /**
         * @cfg {Object} thumbDefaults The config object to factory {@link Ext.slider.Thumb} instances
         * @accessor
         * @cmd-auto-dependency { defaultType: "Ext.slider.Thumb", aliasPrefix:'widget.',typeProperty: 'xtype' }
         */
        thumbDefaults: {
            xtype: 'thumb',
            draggable: {
                translatable: {
                    easingX: {
                        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
    },

    /**
     * @cfg {Number/Number[]} values Alias to {@link #value}
     */

    classCls: Ext.baseCSSPrefix + 'slider',

    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) {
        config = config || {};

        if (config.hasOwnProperty('values')) {
            config.value = config.values;
        }

        this.thumbs = [];

        this.callParent([config]);
    },

    /**
     * @private
     */
    initialize: function() {
        var element = this.element,
            thumb;

        this.callParent();

        element.on({
            scope: this,
            tap: 'onTap',
            resize: 'onResize'
        });

        thumb = this.thumbs[0];
        if (thumb) {
            thumb.on('resize', 'onThumbResize', this);
        }
    },

    applyThumbDefaults: function(defaults) {
        defaults.slider = this;

        return defaults;
    },

    /**
     * @private
     */
    factoryThumb: function() {
        return Ext.factory(this.getThumbDefaults(), Ext.slider.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,
            valueRange = me.getMaxValue() - me.getMinValue(),
            trackWidth = me.elementWidth - me.thumbWidth;

        me.offsetValueRatio = valueRange === 0 ? 0 : trackWidth / valueRange;
    },

    onThumbResize: function(){
        var thumb = this.thumbs[0];
        if (thumb) {
            this.thumbWidth = thumb.getElementWidth();
        }
        this.refresh();
    },

    onResize: function(element, info) {
        this.elementWidth = info.width;
        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;

        me.refreshAllThumbConstraints();

        e.claimGesture();

        if (me.getAllowThumbsOverlapping()) {
            me.setActiveThumb(thumb);
        }

        me.dragStartValue = me.getArrayValues()[me.getThumbIndex(thumb)];
        me.fireEvent('dragstart', me, thumb, me.dragStartValue, e);
    },

    onThumbDrag: function(thumb, e, offsetX) {
        var me = this,
            index = me.getThumbIndex(thumb),
            offsetValueRatio = me.offsetValueRatio,
            constrainedValue = me.constrainValue(me.getMinValue() + 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(),
            draggable = thumb.getDraggable();

        draggable.setOffset((value - minValue) * offsetValueRatio, null, animation);

        values[index] = minValue + Math.round((draggable.offset.x / offsetValueRatio) / increment) * increment;

        me.setValue(values);
    },

    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.fireEvent('change', me, thumb, newValue, oldValue);
        }
    },

    getThumbIndex: function(thumb) {
        return this.thumbs.indexOf(thumb);
    },

    refreshThumbConstraints: function(thumb) {
        var me = this,
            allowThumbsOverlapping = me.getAllowThumbsOverlapping(),
            offsetX = thumb.getDraggable().getOffset().x,
            thumbs = me.thumbs,
            index = me.getThumbIndex(thumb),
            previousThumb = thumbs[index - 1],
            nextThumb = thumbs[index + 1],
            thumbWidth = me.thumbWidth;

        if (previousThumb) {
            previousThumb.getDraggable().addExtraConstraint({
                max: {
                    x: offsetX - ((allowThumbsOverlapping) ? 0 : thumbWidth)
                }
            });
        }

        if (nextThumb) {
            nextThumb.getDraggable().addExtraConstraint({
                min: {
                    x: offsetX + ((allowThumbsOverlapping) ? 0 : thumbWidth)
                }
            });
        }
    },

    /**
     * @private
     */
    onTap: function(e) {
        var me = this,
            element = me.element,
            minDistance = Infinity,
            i, absDistance, testValue, closestIndex, oldValue, thumb,
            ln, values, value, offset, elementX, targetElement, touchPointX;

        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;
        elementX = element.getX();
        offset = 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 (i = 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());
        me.refreshThumbConstraints(thumb);

        if (oldValue !== value) {
            me.fireEvent('change', me, 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, result;

        for (i = 0,ln = values.length; i < ln; i++) {
            filteredValue = me.constrainValue(values[i]);

            if (filteredValue < previousFilteredValue) {
                //<debug>
                Ext.Logger.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) {
            if (Ext.Array.equals(values, oldValue)) {
                filteredValues = undefined;
            }
        }

        if (filteredValues) {
            me.values = filteredValues;
        }

        if (valueIsArray) {
            result = filteredValues;
        } else {
            if (value && value.length === 1) {
                result = value[0];
            } else {
                result = value;
            }
        }
        return result;
    },

    /**
     * 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 (i = 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, ln, thumb;

        if (thumbsCount > count) {
            for (i = 0,ln = thumbsCount - count; i < ln; i++) {
                thumb = thumbs[thumbs.length - 1];
                thumb.destroy();
                Ext.Array.remove(thumbs, thumb);
            }
        }
        else if (thumbsCount < count) {
            for (i = thumbsCount, ln = count; i < ln; i++) {
                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 (i = 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(i === 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) {
        this.callParent(arguments);

        var thumbs = this.thumbs,
            ln = thumbs.length,
            i;

        for (i = 0; i < ln; i++) {
            thumbs[i].setDisabled(disabled);
        }
    },

    doDestroy: function() {
        this.thumbs = Ext.destroy(this.thumbs);
        this.callParent();
    },

    privates: {
        refreshAllThumbConstraints: function() {
            var thumbs = this.thumbs,
                len = thumbs.length,
                i;

            for (i = 0; i < len; i++) {
                this.refreshThumbConstraints(thumbs[i]);
            }
        },

        snapThumbPosition: function(thumb, value) {
            var ratio = this.offsetValueRatio,
                draggable = thumb.getDraggable(),
                offset;

            if (isFinite(ratio)) {
                offset = Ext.Number.correctFloat((value - this.getMinValue()) * ratio);
                draggable.refreshContainerSize().setExtraConstraint(null).setOffset(offset);
            }
        },

        syncFill: function(thumb, offset) {
            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),
                thumbOffset, fillElement, i;

            offset = offset + Math.ceil(thumb.element.getWidth() / 2);

            for (i = 0; i < ln; i++) {
                thumb = thumbs[i];
                fillElement = fillElements[i];
                // during animation offset may be different from what the draggable reports
                thumbOffset = (i === thumbIndex) ? offset :
                    thumb.getDraggable().getOffset().x + (thumb.element.getWidth() / 2);

                fillElement.setWidth(thumbOffset - prevOffset);
                fillElement.setLocalX(prevOffset);

                prevOffset = thumbOffset;
            }
        },

        onThumbAnimationStart: function() {
            this.isThumbAnimating++;
        },

        onThumbAnimationEnd: function() {
            this.isThumbAnimating--;
        }
    }
});
