// @tag foundation,core
// @require ../Ext.js
// @define Ext.Version

/**
 * @class Ext.Version
 *
 * A utility class that wraps around a version number string and provides convenient methods
 * to perform comparisons. A version number is expressed in the following general format:
 *
 *     major[.minor[.patch[.build[release]]]]
 * 
 * The `Version` instance holds various readonly properties that contain the digested form
 * of the version string. The numeric componnets of `major`, `minor`, `patch` and `build`
 * as well as the textual suffix called `release`.
 * 
 * Not depicted in the above syntax are three possible prefixes used to control partial
 * matching. These are '^' (the default), '>' and '~'. These are discussed below.
 *
 * Examples:
 *
 *     var version = new Ext.Version('1.0.2beta'); // or maybe "1.0" or "1.2.3.4RC"
 *     console.log("Version is " + version); // Version is 1.0.2beta
 *
 *     console.log(version.getMajor()); // 1
 *     console.log(version.getMinor()); // 0
 *     console.log(version.getPatch()); // 2
 *     console.log(version.getBuild()); // 0
 *     console.log(version.getRelease()); // beta
 * 
 * The understood values of `release` are assigned numberic equivalents for the sake of
 * comparsion. The order of these from smallest to largest is as follows:
 * 
 *  * `"dev"`
 *  * `"alpha"` or `"a"`
 *  * `"beta"` or `"b"`
 *  * `"RC"` or `"rc"`
 *  * `"#"`
 *  * `"pl"` or `"p"`
 *
 * Any other (unrecognized) suffix is consider greater than any of these.
 * 
 * ## Comparisons
 * There are two forms of comparison that are commonly needed: full and partial. Full
 * comparison is simpler and is also the default.
 * 
 * Example:
 *
 *     var version = new Ext.Version('1.0.2beta');
 *
 *     console.log(version.isGreaterThan('1.0.1')); // True
 *     console.log(version.isGreaterThan('1.0.2alpha')); // True
 *     console.log(version.isGreaterThan('1.0.2RC')); // False
 *     console.log(version.isGreaterThan('1.0.2')); // False
 *     console.log(version.isLessThan('1.0.2')); // True
 *
 *     console.log(version.match(1.0)); // True (using a Number)
 *     console.log(version.match('1.0.2')); // True (using a String)
 * 
 * These comparisons are ultimately implemented by {@link Ext.Version#compareTo compareTo}
 * which returns -1, 0 or 1 depending on whether the `Version' instance is less than, equal
 * to, or greater than the given "other" version.
 * 
 * For example:
 * 
 *      var n = version.compareTo('1.0.1');  // == 1  (because 1.0.2beta > 1.0.1)
 *      
 *      n = version.compareTo('1.1');  // == -1
 *      n = version.compareTo(version); // == 0
 * 
 * ### Partial Comparisons
 * By default, unspecified version number fields are filled with 0. In other words, the
 * version number fields are 0-padded on the right or a "lower bound". This produces the
 * most commonly used forms of comparsion:
 * 
 *      var ver = new Version('4.2');
 *
 *      n = ver.compareTo('4.2.1'); // == -1  (4.2 promotes to 4.2.0 and is less than 4.2.1)
 * 
 * There are two other ways to interpret comparisons of versions of different length. The
 * first of these is to change the padding on the right to be a large number (scuh as
 * Infinity) instead of 0. This has the effect of making the version an upper bound. For
 * example:
 * 
 *      var ver = new Version('^4.2'); // NOTE: the '^' prefix used
 *
 *      n = ver.compreTo('4.3'); // == -1  (less than 4.3)
 *      
 *      n = ver.compareTo('4.2'); // == 1   (greater than all 4.2's)
 *      n = ver.compareTo('4.2.1'); // == 1
 *      n = ver.compareTo('4.2.9'); // == 1
 * 
 * The second way to interpret this comparison is to ignore the extra digits, making the
 * match a prefix match. For example:
 * 
 *      var ver = new Version('~4.2'); // NOTE: the '~' prefix used
 *
 *      n = ver.compreTo('4.3'); // == -1
 *      
 *      n = ver.compareTo('4.2'); // == 0
 *      n = ver.compareTo('4.2.1'); // == 0
 * 
 * This final form can be useful when version numbers contain more components than are
 * important for certain comparisons. For example, the full version of Ext JS 4.2.1 is
 * "4.2.1.883" where 883 is the `build` number.
 * 
 * This is how to create a "partial" `Version` and compare versions to it:
 * 
 *      var version421ish = new Version('~4.2.1');
 *      
 *      n = version421ish.compareTo('4.2.1.883'); // == 0
 *      n = version421ish.compareTo('4.2.1.2'); // == 0
 *      n = version421ish.compareTo('4.2.1'); // == 0
 *
 *      n = version421ish.compareTo('4.2'); // == 1
 *
 * In the above example, '4.2.1.2' compares as equal to '4.2.1' because digits beyond the
 * given "4.2.1" are ignored. However, '4.2' is less than the '4.2.1' prefix; its missing
 * digit is filled with 0.
 */
(function() {

// Current core version
// also fix Ext-more.js
var version = '4.2.0',
    // used by checkVersion to avoid temp arrays:
    checkVerTemp = [''],
    endOfVersionRe = /([^\d\.])/,
    notDigitsRe = /[^\d]/g,
    plusMinusRe = /[\-+]/g,
    stripRe = /\s/g,
    underscoreRe = /_/g,
    Version;

    Ext.Version = Version = Ext.extend(Object, {
        isVersion: true,

        padModes: {
            '~': NaN,
            '^': Infinity
        },

        /**
         * @property {String} [release=""]
         * The release level. The following values are understood:
         * 
         *  * `"dev"`
         *  * `"alpha"` or `"a"`
         *  * `"beta"` or `"b"`
         *  * `"RC"` or `"rc"`
         *  * `"#"`
         *  * `"pl"` or `"p"`
         * @readonly
         */
        release: '',

        /**
         * @param {String/Number} version The version number.
         * @param {String} [defaultMode="^"] The padding mode (e.g., '^', '>' or '~').
         * This is ignored if the `version` contains an explicit mode prefix.
         * @return {Ext.Version} this
         */
        constructor: function (version, defaultMode) {
            var me = this,
                padModes = me.padModes,
                ch, i, pad, parts, release, releaseStartIndex, ver;

            if (version.isVersion) {
                return version;
            }

            me.version = ver = String(version).toLowerCase().
                                    replace(underscoreRe, '.').replace(plusMinusRe, '');

            ch = ver.charAt(0);
            if (ch in padModes) {
                ver = ver.substring(1);
                pad = padModes[ch];
            } else {
                pad = defaultMode ? padModes[defaultMode] : 0; // careful - NaN is falsey!
            }
            me.pad = pad;

            releaseStartIndex = ver.search(endOfVersionRe);
            me.shortVersion = ver;

            if (releaseStartIndex !== -1) {
                me.release = release = ver.substr(releaseStartIndex, version.length);
                me.shortVersion = ver.substr(0, releaseStartIndex);
                release = Version.releaseValueMap[release] || release;
            }

            me.releaseValue = release || pad;
            me.shortVersion = me.shortVersion.replace(notDigitsRe, '');

            /**
             * @property {Number[]} parts
             * The split array of version number components found in the version string.
             * For example, for "1.2.3", this would be `[1, 2, 3]`.
             * @readonly
             * @private
             */
            me.parts = parts = ver.split('.');
            for (i = parts.length; i--; ) {
                parts[i] = parseInt(parts[i], 10);
            }
            if (pad === Infinity) {
                // have to add this to the end to create an upper bound:
                parts.push(pad);
            }

            /**
             * @property {Number} major
             * The first numeric part of the version number string.
             * @readonly
             */
            me.major = parts[0] || pad;

            /**
             * @property {Number} [minor]
             * The second numeric part of the version number string.
             * @readonly
             */
            me.minor = parts[1] || pad;

            /**
             * @property {Number} [patch]
             * The third numeric part of the version number string.
             * @readonly
             */
            me.patch = parts[2] || pad;

            /**
             * @property {Number} [build]
             * The fourth numeric part of the version number string.
             * @readonly
             */
            me.build = parts[3] || pad;

            return me;
        },

        /**
         * Compares this version instance to the specified `other` version.
         *
         * @param {String/Number/Ext.Version} other The other version to which to compare.
         * @return {Number} -1 if this version is less than the target version, 1 if this
         * version is greater, and 0 if they are equal.
         */
        compareTo: function (other) {
             // "lhs" == "left-hand-side"
             // "rhs" == "right-hand-side"
            var me = this,
                lhsPad = me.pad,
                lhsParts = me.parts,
                lhsLength = lhsParts.length,
                rhsVersion = other.isVersion ? other : new Version(other),
                rhsPad = rhsVersion.pad,
                rhsParts = rhsVersion.parts,
                rhsLength = rhsParts.length,
                length = Math.max(lhsLength, rhsLength),
                i, lhs, rhs;

            for (i = 0; i < length; i++) {
                lhs = (i < lhsLength) ? lhsParts[i] : lhsPad;
                rhs = (i < rhsLength) ? rhsParts[i] : rhsPad;

                // When one or both of the values are NaN these tests produce false
                // and we end up treating NaN as equal to anything.
                if (lhs < rhs) {
                    return -1;
                }
                if (lhs > rhs) {
                    return 1;
                }
            }

            // same comments about NaN apply here...
            lhs = me.releaseValue;
            rhs = rhsVersion.releaseValue;
            if (lhs < rhs) {
                return -1;
            }
            if (lhs > rhs) {
                return 1;
            }

            return 0;
        },

        /**
         * Override the native toString method
         * @private
         * @return {String} version
         */
        toString: function() {
            return this.version;
        },

        /**
         * Override the native valueOf method
         * @private
         * @return {String} version
         */
        valueOf: function() {
            return this.version;
        },

        /**
         * Returns the major component value.
         * @return {Number}
         */
        getMajor: function() {
            return this.major;
        },

        /**
         * Returns the minor component value.
         * @return {Number}
         */
        getMinor: function() {
            return this.minor;
        },

        /**
         * Returns the patch component value.
         * @return {Number}
         */
        getPatch: function() {
            return this.patch;
        },

        /**
         * Returns the build component value.
         * @return {Number}
         */
        getBuild: function() {
            return this.build;
        },

        /**
         * Returns the release component text (e.g., "beta").
         * @return {String}
         */
        getRelease: function() {
            return this.release;
        },

        /**
         * Returns the release component value for comparison purposes.
         * @return {Number/String}
         */
        getReleaseValue: function() {
            return this.releaseValue;
        },

        /**
         * Returns whether this version if greater than the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version if greater than the target, false otherwise
         */
        isGreaterThan: function(target) {
            return this.compareTo(target) > 0;
        },

        /**
         * Returns whether this version if greater than or equal to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version if greater than or equal to the target, false otherwise
         */
        isGreaterThanOrEqual: function(target) {
            return this.compareTo(target) >= 0;
        },

        /**
         * Returns whether this version if smaller than the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version if smaller than the target, false otherwise
         */
        isLessThan: function(target) {
            return this.compareTo(target) < 0;
        },

        /**
         * Returns whether this version if less than or equal to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version if less than or equal to the target, false otherwise
         */
        isLessThanOrEqual: function(target) {
            return this.compareTo(target) <= 0;
        },

        /**
         * Returns whether this version equals to the supplied argument
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version equals to the target, false otherwise
         */
        equals: function(target) {
            return this.compareTo(target) === 0;
        },

        /**
         * Returns whether this version matches the supplied argument. Example:
         *
         *     var version = new Ext.Version('1.0.2beta');
         *     console.log(version.match(1)); // True
         *     console.log(version.match(1.0)); // True
         *     console.log(version.match('1.0.2')); // True
         *     console.log(version.match('1.0.2RC')); // False
         *
         * @param {String/Number} target The version to compare with
         * @return {Boolean} True if this version matches the target, false otherwise
         */
        match: function(target) {
            target = String(target);
            return this.version.substr(0, target.length) === target;
        },

        /**
         * Returns this format: [major, minor, patch, build, release]. Useful for comparison.
         * @return {Number[]}
         */
        toArray: function() {
            var me = this;
            return [me.getMajor(), me.getMinor(), me.getPatch(), me.getBuild(), me.getRelease()];
        },

        /**
         * Returns shortVersion version without dots and release
         * @return {String}
         */
        getShortVersion: function() {
            return this.shortVersion;
        },

        /**
         * Convenient alias to {@link Ext.Version#isGreaterThan isGreaterThan}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        gt: function (target) {
            return this.compareTo(target) > 0;
        },

        /**
         * Convenient alias to {@link Ext.Version#isLessThan isLessThan}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        lt: function (target) {
            return this.compareTo(target) < 0;
        },

        /**
         * Convenient alias to {@link Ext.Version#isGreaterThanOrEqual isGreaterThanOrEqual}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        gtEq: function (target) {
            return this.compareTo(target) >= 0;
        },

        /**
         * Convenient alias to {@link Ext.Version#isLessThanOrEqual isLessThanOrEqual}
         * @param {String/Number/Ext.Version} target
         * @return {Boolean}
         */
        ltEq: function (target) {
            return this.compareTo(target) <= 0;
        }
    });

    Ext.apply(Version, {
        // @private
        releaseValueMap: {
            dev:   -6,
            alpha: -5,
            a:     -5,
            beta:  -4,
            b:     -4,
            rc:    -3,
            '#':   -2,
            p:     -1,
            pl:    -1
        },

        /**
         * Converts a version component to a comparable value
         *
         * @static
         * @param {Object} value The value to convert
         * @return {Object}
         */
        getComponentValue: function(value) {
            return !value ? 0 : (isNaN(value) ? this.releaseValueMap[value] || value : parseInt(value, 10));
        },

        /**
         * Compare 2 specified versions by ensuring the first parameter is a `Version`
         * instance and then calling the `compareTo` method.
         *
         * @static
         * @param {String} current The current version to compare to
         * @param {String} target The target version to compare to
         * @return {Number} Returns -1 if the current version is smaller than the target version, 1 if greater, and 0 if they're equivalent
         */
        compare: function (current, target) {
            var ver = current.isVersion ? current : new Version(current);
            return ver.compareTo(target);
        }
    });

    /**
     * @class Ext
     */
    Ext.apply(Ext, {
        /**
         * @private
         */
        versions: {},

        /**
         * @private
         */
        lastRegisteredVersion: null,

        /**
         * Set version number for the given package name.
         *
         * @param {String} packageName The package name, e.g. 'core', 'touch', 'ext'.
         * @param {String/Ext.Version} version The version, e.g. '1.2.3alpha', '2.4.0-dev'.
         * @return {Ext}
         */
        setVersion: function(packageName, version) {
            Ext.lastRegisteredVersion = Ext.versions[packageName] = new Version(version);
            return this;
        },

        /**
         * Get the version number of the supplied package name; will return the last
         * registered version (last `Ext.setVersion` call) if there's no package name given.
         *
         * @param {String} [packageName] The package name, e.g., 'core', 'touch', 'ext'.
         * @return {Ext.Version} The version.
         */
        getVersion: function(packageName) {
            if (packageName === undefined) {
                return Ext.lastRegisteredVersion;
            }

            return Ext.versions[packageName];
        },

        /**
         * This method checks the registered package versions against the provided version
         * `specs`. A `spec` is either a string or an object indicating a boolean operator.
         * This method accepts either form or an array of these as the first argument. The
         * second argument applies only when the first is an array and indicates whether
         * all `specs` must match or just one.
         * 
         * ## Package Version Specifications
         * The string form of a `spec` is used to indicate a version or range of versions
         * for a particular package. This form of `spec` consists of three (3) parts:
         * 
         *  * Package name followed by "@". If not provided, the framework is assumed.
         *  * Minimum version.
         *  * Maximum version.
         * 
         * At least one version number must be provided. If both minimum and maximum are
         * provided, these must be separated by a "-".
         * 
         * Some examples of package version specifications:
         * 
         *      4.2.2           (exactly version 4.2.2 of the framework)
         *      4.2.2+          (version 4.2.2 or higher of the framework)
         *      4.2.2-          (version 4.2.2 or higher of the framework)
         *      4.2.1 - 4.2.3   (versions from 4.2.1 up to 4.2.3 of the framework)
         *      - 4.2.2         (any version up to version 4.2.1 of the framework)
         *      
         *      foo@1.0         (exactly version 1.0 of package "foo")
         *      foo@1.0-1.3     (versions 1.0 up to 1.3 of package "foo")
         * 
         * **NOTE:** This syntax is the same as that used in Sencha Cmd's package
         * requirements declarations.
         * 
         * ## Boolean Operator Specifications
         * Instead of a string, an object can be used to describe a boolean operation to
         * perform on one or more `specs`. The operator is either **`and`** or **`or`**
         * and can contain an optional **`not`**.
         * 
         * For example:
         * 
         *      {
         *          not: true,  // negates boolean result
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ]
         *      }
         * 
         * Each element of the array can in turn be a string or object spec. In other
         * words, the value is passed to this method (recursively) as the first argument
         * so these two calls are equivalent:
         * 
         *      Ext.checkVersion({
         *          not: true,  // negates boolean result
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ]
         *      });
         *
         *      !Ext.checkVersion([
         *              '4.2.2',
         *              'foo@1.0.1 - 2.0.1'
         *          ], true);
         * 
         * ## Examples
         * 
         *      // A specific framework version
         *      Ext.checkVersion('4.2.2');
         * 
         *      // A range of framework versions:
         *      Ext.checkVersion('4.2.1-4.2.3');
         * 
         *      // A specific version of a package:
         *      Ext.checkVersion('foo@1.0.1');
         * 
         *      // A single spec that requires both a framework version and package
         *      // version range to match:
         *      Ext.checkVersion({
         *          and: [
         *              '4.2.2',
         *              'foo@1.0.1-1.0.2'
         *          ]
         *      });
         * 
         *      // These checks can be nested:
         *      Ext.checkVersion({
         *          and: [
         *              '4.2.2',  // exactly version 4.2.2 of the framework *AND*
         *              {
         *                  // either (or both) of these package specs:
         *                  or: [
         *                      'foo@1.0.1-1.0.2',
         *                      'bar@3.0+'
         *                  ]
         *              }
         *          ]
         *      });
         * 
         * ## Version Comparisons
         * Version comparsions are assumed to be "prefix" based. That is to say, `"foo@1.2"`
         * matches any version of "foo" that has a major version 1 and a minor version of 2.
         * 
         * This also applies to ranges. For example `"foo@1.2-2.2"` matches all versions
         * of "foo" from 1.2 up to 2.2 regardless of the specific patch and build.
         * 
         * ## Use in Overrides
         * This methods primary use is in support of conditional overrides on an
         * `Ext.define` declaration.
         * 
         * @param {String/Array/Object} specs A version specification string, an object
         * containing `or` or `and` with a value that is equivalent to `specs` or an array
         * of either of these.
         * @param {Boolean} [matchAll=false] Pass `true` to require all specs to match.
         * @return {Boolean} True if `specs` matches the registered package versions.
         */
        checkVersion: function (specs, matchAll) {
            var isArray = Ext.isArray(specs),
                compat = isArray ? specs : checkVerTemp,
                length = compat.length,
                versions = Ext.versions,
                frameworkVer = versions.ext || versions.touch,
                i, index, matches, minVer, maxVer, spec, range, ver;

            if (!isArray) {
                checkVerTemp[0] = specs;
            }

            for (i = 0; i < length; ++i) {
                if (!Ext.isString(spec = compat[i])) {
                    matches = Ext.checkVersion(spec.and || spec.or, !spec.or);
                    if (spec.not) {
                        matches = !matches;
                    }
                } else {
                    if (spec.indexOf(' ') >= 0) {
                        spec = spec.replace(stripRe, '');
                    }

                    // For "name@..." syntax, we need to find the package by the given name
                    // as a registered package.
                    index = spec.indexOf('@');
                    if (index < 0) {
                        range = spec;
                        ver = frameworkVer;
                    } else {
                        if (!(ver = versions[spec.substring(0, index)])) {
                            // The package is not registered, so if we must matchAll then
                            // we are done - FAIL:
                            if (matchAll) {
                                return false;
                            }
                            // Otherwise this spec is not a match so we can move on to the
                            // next...
                            continue;
                        }
                        range = spec.substring(index+1);
                    }

                    // Now look for a version, version range or partial range:
                    index = range.indexOf('-');
                    if (index < 0) {
                        // just a version or "1.0+"
                        if (range.charAt(index = range.length - 1) === '+') {
                            minVer = range.substring(0, index);
                            maxVer = null;
                        } else {
                            minVer = maxVer = range;
                        }
                    } else if (index > 0) {
                        // a range like "1.0-1.5" or "1.0-"
                        minVer = range.substring(0, index);
                        maxVer = range.substring(index+1); // may be empty
                    } else {
                        // an upper limit like "-1.5"
                        minVer = null;
                        maxVer = range.substring(index+1);
                    }

                    matches = true;
                    if (minVer) {
                        minVer = new Version(minVer, '~'); // prefix matching
                        matches = minVer.ltEq(ver);
                    }
                    if (matches && maxVer) {
                        maxVer = new Version(maxVer, '~'); // prefix matching
                        matches = maxVer.gtEq(ver);
                    }
                } // string spec

                if (matches) {
                    // spec matched and we are looking for any match, so we are GO!
                    if (!matchAll) {
                        return true;
                    }
                } else if (matchAll) {
                    // spec does not match the registered package version
                    return false;
                }
            }

            // In the loop above, for matchAll we return FALSE on mismatch, so getting
            // here with matchAll means we had no mismatches. On the other hand, if we
            // are !matchAll, we return TRUE on match and so we get here only if we found
            // no matches.
            return !!matchAll;
        },

        /**
         * Create a closure for deprecated code.
         *
         *     // This means Ext.oldMethod is only supported in 4.0.0beta and older.
         *     // If Ext.getVersion('extjs') returns a version that is later than '4.0.0beta', for example '4.0.0RC',
         *     // the closure will not be invoked
         *     Ext.deprecate('extjs', '4.0.0beta', function() {
         *         Ext.oldMethod = Ext.newMethod;
         *
         *         ...
         *     });
         *
         * @param {String} packageName The package name
         * @param {String} since The last version before it's deprecated
         * @param {Function} closure The callback function to be executed with the specified version is less than the current version
         * @param {Object} scope The execution scope (`this`) if the closure
         */
        deprecate: function(packageName, since, closure, scope) {
            if (Version.compare(Ext.getVersion(packageName), since) < 1) {
                closure.call(scope);
            }
        }
    }); // End Versioning

    Ext.setVersion('core', version);

}());