/**
 * This class manages a set of numeric spans (2-element arrays marking begin and end
 * points). The method of this class coalesce and split spans as necessary to store
 * the fewest possible pairs needed to represent the covered (one-dimensional) area.
 *
 * @private
 * @since 6.5.0
 */
Ext.define('Ext.util.Spans', {
    isSpans: true,
 
    constructor: function() {
        this.spans = this.spans || [];
    },
 
    /**
     * Clears all spans.
     * @return {Ext.util.Spans} This Spans object.
     */
    clear: function() {
        this.spans.length = 0;
 
        return this;
    },
 
    /**
     * Adds a new span to the current set of spans. This will coalesce adjacent spans
     * as necessary to store the minimum number of spans possible.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} `true` if the new span changes this object, `false` if the
     * span was already in the set.
     */
    add: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            }
            else {
                end = begin[1];
                begin = begin[0];
            }
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            spans = me.spans,
            b, e, first, last, span;
 
        first = me.bisect(begin);
 
        if (first) {  // if (there is a previous span)
            span = spans[first - 1];
            b = span[0];
            e = span[1];
 
            if (begin <= e) {
                // This new span touches the previous one, but perhaps this new
                // span is contained inside the previous span...
                if (end <= e) {
                    return false;  // no change
                }
 
                // The new begin touches the previous span, but the new end goes
                // beyond it, so we extend it by replacing it.
                begin = b;
                spans.splice(--first, 1);
            }
        }
 
        // Now there is either no previous span, or if there was one, it does
        // not touch the new one.
        last = me.bisect(end);
 
        if (last > first) {
            // If we are replacing any spans, make sure the new "end" is at
            // least as large as the end of the last span we are replacing.
            span = spans[last - 1];
            end = Math.max(end, span[1]);
        }
 
        if (last < spans.length) {
            span = spans[last];
 
            // The span beyond our new span may be touching the end of the
            // new span, in which case we need to coalesce there as well.
            // Since we are removing it, we need to expand "end" to include
            // this additional span.
            if (end === span[0]) {
                end = span[1];
                ++last;
            }
        }
 
        spans.splice(first, last - first, [begin, end]);
 
        return true;
    },
 
    /**
     * Returns `true` if the given span is fully in the current set of spans.
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} 
     */
    contains: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            }
            else {
                end = begin[1];
                begin = begin[0];
            }
        }
 
        // eslint-disable-next-line vars-on-top
        var spans = this.spans,
            index = this.bisect(begin),
            ret = false,
            e, span;
 
        if (index && begin < (= spans[index - 1][1])) {
            ret = end <= e;
        }
        else if (index < spans.length) {
            span = spans[index];
            ret = span[0] <= begin && end <= span[1];
        }
 
        return ret;
    },
 
    /**
     * Calls the passed function for every integer in every span.
     * @param {Function} fn The function to call. Returning `false` will abort the operation.
     * @param {Mixed} scope The scope (`this` reference) in which the function will execute.
     */
    each: function(fn, scope) {
        var spans = this.spans,
            len = spans.length,
            i, span, j;
 
        for (= 0; i < len; i++) {
            span = spans[i];
 
            for (= span[0]; j < span[1]; j++) {
                if (fn.call(scope || this, i) === false) {
                    return;
                }
            }
        }
    },
 
    /**
     * Returns `true` if the specified span intersects with the current set of spans.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} 
     */
    intersects: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            }
            else {
                end = begin[1];
                begin = begin[0];
            }
        }
 
        // eslint-disable-next-line vars-on-top
        var spans = this.spans,
            index = this.bisect(begin),
            ret = false;
 
        if (index && begin < spans[index - 1][1]) {
            ret = true;
        }
        else if (index < spans.length) {
            ret = spans[index][0] < end;
        }
 
        return ret;
    },
 
    /**
     * Removes a span from the current set of spans. This will coalesce adjacent spans
     * as necessary to store the minimum number of spans possible.
     *
     * @param {Number/Number[]} begin Either the beginning of the span or a 2-element
     * array of `[begin,end]`.
     * @param {Number} [end] If `begin` is just the position, the second argument is
     * the end of the span to add. This value is exclusive of the span, that is it
     * marks the first position beyond the span. This ensures that `end - begin` is
     * the length of the span.
     * @return {Boolean} `true` if removing the span changes this object, `false` if the
     * span was not in the set.
     */
    remove: function(begin, end) {
        if (end === undefined) {
            if (typeof begin === 'number') {
                end = begin + 1;
            }
            else {
                end = begin[1];
                begin = begin[0];
            }
        }
 
        // eslint-disable-next-line vars-on-top
        var me = this,
            spans = me.spans,
            first = me.bisect(begin),
            ret = false,
            last, span, tmp;
 
        if (first) {
            span = spans[first - 1];
            tmp = span[1];
 
            if (begin < tmp) {
                span[1] = begin;
 
                if (end < tmp) {
                    spans.splice(first, 0, [end, tmp]);
 
                    return true;
                }
 
                ret = true;
            }
        }
 
        last = me.bisect(end);
 
        if (first < last) {
            ret = true;
            span = spans[last - 1];
 
            if (end < span[1]) {
                span[0] = end;
                --last;
            }
 
            last -= first;
 
            if (last) {
                spans.splice(first, last);
            }
        }
 
        return ret;
    },
 
    /**
     * Returns an object that holds the current state and can be passed back later
     * to `unstash` to restore that state.
     * @return {Object} 
     */
    stash: function() {
        return this.spans.slice();
    },
 
    /**
     * Takes an object a state object returned by `stash` and makes that the current
     * state.
     * @return {Ext.util.Spans} This Spans object.
     */
    unstash: function(pickle) {
        this.spans = pickle;
 
        return this;
    },
 
    /**
     * @return {Number} the number of integer locations covered by all the spans.
     */
    getCount: function() {
        var spans = this.spans,
            len = spans.length,
            result = 0,
            i, span;
 
        for (= 0; i < len; i++) {
            span = spans[i];
            result += span[1] - span[0];
        }
 
        return result;
    },
 
    privates: {
        bisect: function(value) {
            return Ext.Number.bisectTuples(this.spans, value, 0);
        }
    }
});