(function() {
    var Ext = window.Ext4 || window.Ext;

    var UUID_FORMAT = /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|[a-fA-F0-9]{32}/;
    var PERMISSION_FORMAT = /\d+u\d+[pw]\d+/;
    var EXPLICIT_VERSION_FORMAT = /^v{0,1}\d+?\.\d+?$/;

    /**
     * Rally.util.Ref objects can be created to simplify working with reference URIs.
     * A reference URI is a unique identifier that references an object in your Rally subscription.
     * Constructing a Ref object parses the URI into its various components:
     *
     * - Type
     * - OID
     * - WSAPI Version
     * - TypeDefOid [only if a scoped attributedefinition reference]
     * - ScopeType [only if a scoped attributedefinition reference]
     *
     * Parse a WSAPI reference URI, WSAPI object, or {Rally.data.Model} record:
     *
     *     // Create Ref object from relative ref URI string
     *
     *     var ref = Ext.create('Rally.util.Ref', '/workspace/123'); // returns "workspace", 123
     *
     *     // Create Ref object from scoped relative ref URI string
     *
     *     var ref = Ext.create('Rally.util.Ref', '/workspace/123/typedefinition/456/attributedefinition/789'); // returns "attributedefinition", 789, 'workspace', 123, 456
     *
     *     // Create Ref object from ref URI string
     *
     *     ref = Ext.create('Rally.util.Ref', '/webservice/1.31/workspace/123'); // returns "workspace", 123, "1.31"
     *
     *     // Create Ref object from WSAPI object with _ref property
     *
     *     ref = Ext.create('Rally.util.Ref', {_ref: '/workspace/123'}); // returns "workspace", 123
     *
     *     // Create Ref object from UserStory model object
     *
     *     var UserStory = Rally.data.ModelFactory.getModel('UserStory');
     *
     *     var userStory = new UserStory({
     *         _ref: "/hierarchicalrequirement/832687.js"
     *     });
     *     ref = Ext.create('Rally.util.Ref', userStory); // returns "hierarchicalrequirement", 832687
     */
    Ext.define('Rally.util.Ref', {
        requires: [
            'Rally.data.Model',
            'Rally.util.TypeInfo',
            'Rally.util.RegularExpression'
        ],

        statics: {
            getCombinedRegularExpression: function() {
                if (!Rally.environment.narrowJsScriptingValley || (!this.combinedRegEx && Rally.environment.narrowJsScriptingValley)) {
                    this.combinedRegEx = Rally.util.RegularExpression.combineRegularExpressions(Rally.util.RegularExpression.refRegularExpressions);
                }

                return this.combinedRegEx;
            },

            /**
             * Determines if input is a valid reference uri.
             *
             * @param {String} uri the uri to parse
             * @return {Boolean}
             */
            isRefUri: function(uri) {
                return Ext.isString(uri) && Rally.util.Ref.getCombinedRegularExpression().test(uri);
            },

            /**
             * Determines if input is a valid reference uri and it has a valid object id.
             *
             * @param {String} uri the uri to parse
             * @return {Boolean}
             */
            isRefUriWithValidObjectId: function(uri) {
                if (uri && this.isRefUri(uri)) {
                    var oid = this.getRefObject(uri).getOid();

                    if (Ext.isNumber(oid)) {
                        return parseInt(oid, 10) > 0;
                    }

                    // if the oid is not a number, it's probably a uuid
                    // todo: validate uuid format
                    return true;
                }

                return false;
            },

            /**
             * Determines if input is a valid reference object.
             *
             * @param {Object} reference
             * @return {Boolean} True if the passed-in value is an instance of Rally.util.Ref
             */
            isRefObject: function(reference) {
                return (Ext.isObject(reference) &&
                        (!Ext.isEmpty(reference.getOid)) &&
                        (!Ext.isEmpty(reference.getType)));
            },

            getRefUri: function(input) {
                if (this.isRefUri(input)) {
                    return input;
                }

                if (input && Ext.isObject(input)) {
                    if (Ext.isFunction(input.get) && this.isRefUri(input.get('_ref'))) {
                        return input.get('_ref');
                    } else if (this.isRefUri(input._ref)) {
                        return input._ref;
                    }
                }
                return null;
            },

            /**
             * Parses reference data from input of varying types.
             *
             * @param {String/Rally.data.Model/Object} input Either a model object, a valid reference uri string or an object with
             * a _ref property that is a valid reference uri string.
             * @return {Rally.util.Ref} Newly constructed Rally.util.Ref object
             */
            getRefObject: function(input) {
                var refUri = this.getRefUri(input);
                if (!this.isRefUri(refUri)) {
                    throw Error('Reference [' + input + '] must be a valid WSAPI uri or a valid reference object.');
                }
                return Ext.create('Rally.util.Ref', refUri);
            },

            /**
             * Get the reference uri relative to the WSAPI endpoint.
             *
             * @param {Rally.util.Ref/Rally.data.Model/Object/String} reference Either a model object, a valid reference uri string or an object with
             * a _ref property that is a valid reference uri string.
             * @return {String} uri The relative uri of the reference object
             */
            getRelativeUri: function(reference) {
                var refUri = this.getRefUri(reference),
                    relativeUri = null;
                if (refUri) {
                    relativeUri = Ext.create('Rally.util.Ref', refUri).getRelativeUri();
                } else if (this.isRefObject(reference)) {
                    relativeUri = this.getRelativeUriFromUriWithVersion(reference.getRelativeUriWithVersion());
                }

                return relativeUri;
            },

            /**
             * Get an absolute url for a reference
             *
             * @param {Rally.util.Ref/String} reference object with reference parameters
             * @return {String} url The absolute uri of the reference object
             */
            getUrl: function(reference) {
                var uri = this.getUri(reference),
                    origin = window.location.protocol + '//' + window.location.host;

                if (!uri.match(/^http/)) {
                    uri = origin + uri;
                }

                return uri;
            },

            getRelativeUriFromUriWithVersion: function(uri) {
                if (!uri) {
                    return null;
                }
                var results = Rally.util.Ref.getCombinedRegularExpression().exec(uri);
                var scopingPart = results[1];
                var typePart = results[2];
                var oidPart = results[3];
                var collectionPart = results[4];
                if (scopingPart) {
                    uri = "/" + scopingPart + "/" + typePart + "/" + oidPart;
                } else {
                    uri = "/" + typePart + "/" + oidPart;
                }

                if (collectionPart) {
                    uri += collectionPart;
                }
                return uri;
            },

            addProjectParameterIfEditingAtProject: function(reference, url) {
                var refUri = reference.getRelativeUri();
                var urlPartsArray = refUri.split('/');

                if (urlPartsArray[1].toUpperCase() === 'PROJECT') {// || urlPartsArray[1].toUpperCase === 'WORKSPACE'
                    url += '&scopeOid=' + urlPartsArray[2];
                }
                return url;
            },

            /**
             * Get the reference uri.
             *
             * @param {Object} reference object with reference parameters
             * @return {String} uri
             */
            getUri: function(reference) {
                var version = ((Ext.isFunction(reference.getVersion) && reference.getVersion()) || Rally.environment.getServer().getWsapiVersion());
                return Rally.environment.getServer().getWsapiUrl(version) + this.getRelativeUri(reference);
            },


            /**
             * Get the ObjectID from a ref.
             * @param input a ref, like '/hierarchicalrequirement/123'
             * @return {Number|String} the ObjectID, if found.
             */
            getOidFromRef: function(input) {
                var ref = this.getRefUri(input),
                    objectId, oid;
                if (ref) {
                    objectId = Rally.util.Ref.getCombinedRegularExpression().exec(ref)[3];
                    oid = UUID_FORMAT.test(objectId) || PERMISSION_FORMAT.test(objectId) ? objectId : parseInt(objectId, 10);
                    return oid;
                }
                return null;
            },

            getTypeFromRef: function(input) {
                var ref = this.getRefUri(input);
                ref = this.getRelativeUri(ref);

                if (ref) {
                    var groups = Rally.util.Ref.getCombinedRegularExpression().exec(ref);
                    if (groups[1] === 'portfolioitem') {
                        return groups[1] + '/' + groups[2];
                    } else {
                        return groups[2];
                    }
                }
                return null;
            },

            getRefFromTypeAndOid: function(type, oid) {
                if (Ext.isString(type) && !isNaN(oid)) {
                    return "/" + type.toLowerCase() + "/" + oid.toString();
                }
                return null;
            }
        },


        constructor: function(reference) {
            if (Ext.isObject(reference) && reference.oid && reference.type) {
                reference.relativeUriWithVersion = '/' + reference.type + '/' + reference.oid;
                this.reference = reference;
            } else {
                this.reference = this._parseUri(reference);
            }
        },

        /**
         * Get the type of the record.
         *
         * @return {String} type
         */
        getType: function() {
            return this.reference.type || null;
        },

        /**
         * Get the OID of the record.
         *
         * @return {String} oid
         */
        getOid: function() {
            return this.reference.oid || null;
        },

        /*
         * Get the type def oid of the record, if it exists.
         *
         * @return {String} typeDefOid
         */
        getTypeDefOid: function() {
            return this.reference.typeDefOid || null;
        },

        getScopeType: function() {
            return this.reference.scopeType || null;
        },

        getScopeOid: function() {
            return this.reference.scopeOid || null;
        },

        /**
         * Get the version of the API used.
         *
         * @return {String} version
         */
        getVersion: function() {
            return this.reference.version || 'x';
        },

        /**
         * @param {String} version The version of the ref
         */
        setVersion: function (version) {
            this.reference.version = version;
        },

        /**
         * Get the URI string without domain and webservice prefix, but including version.
         * @return {String} The URI string without domain and webservice prefix, but including version.
         */
        getRelativeUriWithVersion: function() {
            return this.reference.relativeUriWithVersion || null;
        },

        /**
         * Get the uri string that was parsed to create this object.
         * @return {String} The uri string that was parsed to create this object.
         */
        getParsedUri: function() {
            return this.reference.parsedUri;
        },
        /**
         * Get the reference uri relative to the WSAPI endpoint.
         *
         * @return {String} The reference uri relative to the WSAPI endpoint.
         */
        getRelativeUri: function() {
            return this.self.getRelativeUri(this);
        },

        /**
         * Get the reference uri.
         *
         * @return {String} The reference uri.
         */
        getUri: function() {
            return this.self.getUri(this);
        },

        getTypeInfo: function() {
            return Rally.util.TypeInfo.getTypeInfoByName(this.getType());
        },

        /**
         * @private
         * Get an object containing the component parts of a reference uri.
         *
         * @param {String} uri the uri to parse
         * @return {Object} reference object with reference details.
         */
        _parseUri: function(inputUri) {
            var uri = inputUri;
            if (!this.self.isRefUri(uri)) {
                throw Error('Reference [' + uri + '] must be a valid WSAPI uri');
            }
            uri = uri.replace(/^https?:\/\/[^\/]+?/, '');
            uri = uri.replace(/^.*?webservice/, '');
               if (!uri.match(/^\//)) {
                uri = '/' + uri;
            }

            var parts = uri.split('/'),
                length = parts.length,
                ref = {
                    oid: this.self.getOidFromRef(uri),
                    type: this._parseTypeFromUri(uri),
                    relativeUriWithVersion: uri,
                    parsedUri: inputUri
                },
                versionCandidate = parts[1],
                server = Rally.environment.getServer();

            if (EXPLICIT_VERSION_FORMAT.test(versionCandidate) || Ext.Array.contains(server.getWsapiVersionAliases(), versionCandidate)) {
                ref.version = versionCandidate;
            } else {
                ref.version = server.getWsapiVersion();
            }

            if (length >= 4 && parts[length - 4].toUpperCase() === "TYPEDEFINITION") {
                ref.typeDefOid = parseInt(parts[length - 3], 10);
            }

            if (length >= 6) {
                var scopeType = parts[length - 6].toUpperCase();
                if (scopeType === "PROJECT") {
                    ref.scopeType = 'project';
                } else if (scopeType === 'WORKSPACE') {
                    ref.scopeType = 'workspace';
                }
                ref.scopeOid = parseInt(parts[length - 5], 10);
            }

            return ref;
        },

        _parseTypeFromUri: function(uri) {
            var relativeUri = this.self.getRelativeUriFromUriWithVersion(uri);
            var relativeUriParts = relativeUri.split("/");

            if(this._isNestedType(relativeUriParts)) {
                return this._buildFullyQualifiedTypeName(relativeUriParts);
            }

            return this._getTypeFromUriParts(relativeUriParts);
        },

        _buildFullyQualifiedTypeName: function(uriParts) {
            return uriParts[1] + "/" + uriParts[2];
        },

        _isNestedType: function(uriParts) {
            return uriParts.length === 4;
        },

        _getTypeFromUriParts: function(uriParts) {
            return uriParts[uriParts.length - 2];
        }
    });
})();