angular.module('umbraco.services').factory('archetypeLabelService', function (archetypeCacheService, $q, $injector, $timeout) {
    //private

    /**
     * This will repeatedly wait for all promises in an array of promises to resolve, and it allows
     * for promises to be added to the array (i.e., if you add more promises, those added promises
     * will also be resolved too).
     * @param promises The array of promises.
     * @returns {*} The promise that will resolve once all promises in the array resolve.
     */
    function repeatedlyWaitForPromises(promises) {

        // Remember the original number of promises being resolved.
        var originalLength = promises.length;
        
        return $q.all(promises).then(function () {

            // If there are new promises, resolve those too.
            if (promises.length > originalLength) {
                promises = promises.slice(originalLength);
                return repeatedlyWaitForPromises(promises);
            }
        });
    }

    /**
     * Processes a value to be used in a fieldset label. Since it might be a promise or a function, the
     * value is repeatedly processed until it becomes a string.
     * @param labelValue The value to be used in the fieldset label.
     * @param promises The collection of promises to add any promises to.
     * @param match The current matched substring object in a label template.
     */
    function processLabelValue(labelValue, promises, match) {

        // Normalize null/undefined values to an empty string.
        if(!labelValue) {
            labelValue = "";
        }

        // Check the type of value (may be a string, promise, function, or other).
        if (isString(labelValue)) {

            // handle collapsing dollar signs in labels (#387)
            if (labelValue.indexOf("$$") >= 0) {
                labelValue = labelValue.replace(/\$\$/g, "$$$$$$$$");
            }

            // Set a new value now that it has been processed.
            //console.log("Processing string..." + labelValue);
            match.value = labelValue;
        } else if (isPromise(labelValue)) {
            // Remember the promise so we can wait for it to be completed before constructing the
            // fieldset label.
            promises.push(labelValue);
            labelValue.then(function (value) {
                //console.log("Processing final value..." + value);
                // The value will probably be a string, but recursively process it in case it's
                // something else.
                processLabelValue(value, promises, match);
            });
        } else if (_.isFunction(labelValue)) {

            // Allow for the function to accept injected parameters, and invoke it.
            labelValue = $injector.invoke(labelValue);

            // Recursively check result (may be a string, promise, or another function (another
            // function would be pretty strange, though I see no reason to disallow it).
            processLabelValue(labelValue, promises, match);
        } else {
            // Some other data type (e.g., number, date, object).
            match.value = labelValue;
        }
    }

    /**
     * Checks if the specified value is of type string.
     * @param value The value to check the type of.
     * @returns {boolean} True, if the value is a string; otherwise, false.
     */
    function isString(value) {
        if (value === null) {
            return false;
        } else if (typeof value === 'string') {
            return true;
        } else if (value instanceof String) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if the specified value is a JavaScript promise.
     * @param value The value that may be a promise.
     * @returns {*} True, if the value appears to be a promise; otherwise, false.
     */
    function isPromise(value) {
        return value && _.isFunction(value.then);
    }

    /**
     * Splits a string value into a collection based on a regular expression.
     * @param rgx The regular expression to use to find matches in the string value.
     * @param value The string value to split.
     * @returns {Array} A collection of objects, each representing a portion of the supplied
     *      string. Each object will contain the substring value, as well as a property
     *      indicating whether or not that substring was matched the regular expression.
     */
    function splitByRegex(rgx, value) {
        // Validate input.
        if (!rgx || !value) {
            return [];
        }

        // Variables.
        var substring,
            splitParts = [],
            nextIndex = 0,
            index;

        // Reset regex so we get all the matches.
        rgx.lastIndex = 0;

        // Loop through each match until there are no more matches.
        var match = rgx.exec(value);
        while (match) {

            // Extract match index.
            index = match.index;

            // Is there text between the prior match and this one?
            if (nextIndex < index) {
                substring = value.substring(nextIndex, index);
                splitParts.push({
                    isMatch: false,
                    value: substring
                });
            }

            // Remember the end of this match for the next loop iteration.
            nextIndex = rgx.lastIndex;

            // Store info about this match.
            substring = value.substring(index, nextIndex);
            splitParts.push({
                isMatch: true,
                value: substring
            });

            // Get next match.
            match = rgx.exec(value);
        }

        // The text after the last match.
        if (nextIndex < value.length) {
            substring = value.substring(nextIndex);
            splitParts.push({
                isMatch: false,
                value: substring
            });
        }

        // Reset regex in case somebody else wants to use it.
        rgx.lastIndex = 0;

        // Return information about the matches.
        return splitParts;
    }

    function executeFunctionByName(functionName, context) {
        var args = Array.prototype.slice.call(arguments).splice(2);

        var namespaces = functionName.split(".");
        var func = namespaces.pop();

        for(var i = 0; i < namespaces.length; i++) {
            context = context[namespaces[i]];
        }

        if(context && context[func]) {
            return context[func].apply(this, args);
        }

        return "";
    }

    function getNativeLabel(datatype, value, scope) {
        switch (datatype.selectedEditor) {
                case "Imulus.UrlPicker":
                    return imulusUrlPicker(value, scope, {});
                case "Umbraco.TinyMCEv3":
                    return coreTinyMce(value, scope, {});
                case "Umbraco.MultiNodeTreePicker":
                    return coreMntp(value, scope, datatype);
                case "Umbraco.MultiNodeTreePicker2":
                    return coreMntpV2(value, scope, datatype);
                case "Umbraco.MultipleMediaPicker":
                case "Umbraco.MediaPicker":
                    return coreMediaPicker(value, scope, datatype);
                case "Umbraco.MediaPicker2":
                    return coreMediaPickerV2(value, scope, datatype);
                case "Umbraco.DropDown":
                    return coreDropdown(value, scope, datatype);
                case "RJP.MultiUrlPicker":
                    return rjpMultiUrlPicker(value, scope, {});
                case "Umbraco.ContentPickerAlias":
                    return coreContentPicker(value, scope, datatype);
                case "Umbraco.ContentPicker2":
                    return coreContentPickerV2(value, scope, datatype);
                default:
                    return null;
        }
    }

    function coreDropdown(value, scope, args) {

        if(!value)
            return "";

        var prevalue = args.preValues[0].value[value];

        if(prevalue) {
            return prevalue.value;
        }

        return "";
    }

    function coreMntp(value, scope, args) {
        var ids = value.split(',');
        var type = "Document";
        var deferred = $q.defer();

        switch(args.preValues[0].value.type) {
            case 'content':
                type = 'Document';
                break;
            case 'media':
                type = 'media';
                break;
            case 'member':
                type = 'member';
                break;
            default:
                break;
        }

        var entityArray = [];
        var promises = [];

        _.each(ids, function(id){
            if(id) {
               promises.push(archetypeCacheService.getEntityById(id, type).then(function(entity){                
                    if(entity) {
                        entityArray.push(entity.name);
                    }
               }).promise);
            }
        });

        $q.all(promises).then(function() {
            deferred.resolve(entityArray.join(', '));
        });
                
        return deferred.promise;
    }

    function coreMntpV2(value, scope, args) {
        var deferred = $q.defer();
        
        var ids = value.split(',');
        
        if (ids.length == 0) {
          return "";
        }
        
        var type = "document";

        switch(args.preValues[0].value.type) {
            case 'content':
                type = 'document';
                break;
            case 'media':
                type = 'media';
                break;
            case 'member':
                type = 'member';
                break;

            default:
                break;
        }
        
        var entityArray = [];
        var promises = [];

        _.each(ids, function (id) {            
            if(id) {
                promises.push(archetypeCacheService.getEntityByUmbracoId(id, type).then(function(entity) {
                    if(entity) {
                        entityArray.push(entity.name);
                    }
                }).promise);
            }
        });
        
        $q.all(promises).then(function() {           
            if(entityArray.length == 0) {
                deferred.resolve("");
            }
            
            var firstEntityName = entityArray[0];
            
            var value = firstEntityName + (ids.length > 1 ? ", ..." : "");
            
            deferred.resolve(value);
        });
                
        return deferred.promise;
    }

    function coreMediaPicker(value, scope, args) {
        var deferred = $q.defer();
        
        if(value) {
            archetypeCacheService.getEntityById(value, "media").then(function(entity) {
                deferred.resolve(entity.name);
            });
        }
        else
        {
            deferred.resolve("");
        }

        return deferred.promise;
    }

    function coreMediaPickerV2(value, scope, args) {
        var deferred = $q.defer();
       
        //console.log("value=");
        //console.log(value);       
       
        if(value) {
            archetypeCacheService.getEntityByUmbracoId(value, "media").then(function(entity) {
                deferred.resolve(entity.name);
            });
        }
        else
        {
            deferred.resolve("");
        }

        return deferred.promise;
    }

    function coreContentPicker(value, scope, args) {
        var deferred = $q.defer();
        
        if(value) {
            archetypeCacheService.getEntityById(value, "document").then(function(entity) {
                deferred.resolve(entity.name);
            });
        }
        else 
        {
            deferred.resolve("");
        }
        
        return deferred.promise;
    }

    function coreContentPickerV2(value, scope, args) {
        var deferred = $q.defer();
      
        if (value) {
            archetypeCacheService.getEntityByUmbracoId(value, "document").then(function(entity) {
                deferred.resolve(entity.name);
            });
        }
        else 
        {
            deferred.resolve("");
        }

        return deferred.promise;
    }

    function imulusUrlPicker(value, scope, args) {
        var deferred = $q.defer();        
        
        if(!value) {
            deferred.resolve("");
            
            return deferred.promise;
        }
        
        if(!args.propertyName) {
            args = {propertyName: "name"}
        }
        
        if(typeof(value) != "object") {
            value = JSON.parse(value);
        }

        //console.log("urlpicker value...");
        //console.log(value);
        
        if(value.length) {
            value = value[0];
        }       
               
        switch (value.type) {
            case "content":
                if(value.typeData.contentId) {
                    archetypeCacheService.getEntityById(value.typeData.contentId, "Document").then(function(entity) {
                        //console.log("Retrived entity from cache!");
                        //console.log("Resolving the entity name with " + entity[args.propertyName]);
                        deferred.resolve(entity[args.propertyName]);
                    });
                }
                break;

            case "media":
                if(value.typeData.mediaId) {
                    archetypeCacheService.getEntityById(value.typeData.mediaId, "Media").then(function(entity) {
                        deferred.resolve(entity[args.propertyName]);
                    });
                }
                break;

            case "url":
                deferred.resolve(value.typeData.url);
                
                return deferred.promise;
                
            default:
                break;
        }
        
        return deferred.promise;
    }

    function coreTinyMce(value, scope, args) {
        if(!args.contentLength) {
            args = {contentLength: 160}
        }

        var suffix = "";
        var strippedText = $("<div/>").html(value).text();

        if(strippedText.length > args.contentLength) {
            suffix = "…";
        }

        return strippedText.substring(0, args.contentLength) + suffix;
    }

    function rjpMultiUrlPicker(values, scope, args) {
        var names = [];

        _.each(values, function (value) {
            if (value.name) {
                names.push(value.name);
            }
        });

        return names.join(", ");
    }
    
    return {
        getFieldsetTitle: function(scope, fieldsetConfigModel, fieldsetIndex) {

            if(!fieldsetConfigModel)
                return $q.when("");

            var fieldset = scope.model.value.fieldsets[fieldsetIndex];
            var fieldsetConfig = scope.getConfigFieldsetByAlias(fieldset.alias);
            var template = fieldsetConfigModel.labelTemplate;
            var promises = [];

            if (template.length < 1)
                return $q.when(fieldsetConfig.label);

            var rgx = /{{.*?}}/g;
            var matches = splitByRegex(rgx, template);

            _.each(matches, function (match) {

                // Skip over substrings that didn't match the regex (they do not require a transformation).
                if (!match.isMatch) {
                    return;
                }

                // split the template in case it consists of multiple property aliases and/or functions
                var templates = match.value.replace("{{", '').replace("}}", '').split("|");
                var templateLabelValue = "";

                for(var i = 0; i < templates.length; i++) {
                    // stop looking for a template label value if a previous template part already yielded a value
                    if(templateLabelValue !== "") {
                        break;
                    }
                    
                    var template = templates[i];
                    
                    //test for function
                    var beginParamsIndexOf = template.indexOf("(");
                    var endParamsIndexOf = template.indexOf(")");

                    //if passed a function
                    if(beginParamsIndexOf !== -1 && endParamsIndexOf !== -1)
                    {
                        var functionName = template.substring(0, beginParamsIndexOf);
                        var propertyAlias = template.substring(beginParamsIndexOf + 1, endParamsIndexOf).split(',')[0];

                        var args = {};

                        var beginArgsIndexOf = template.indexOf(',');

                        if(beginArgsIndexOf !== -1) {

                            var argsString = template.substring(beginArgsIndexOf + 1, endParamsIndexOf).trim();

                            var normalizedJsonString = argsString.replace(/(\w+)\s*:/g, '"$1":');

                            args = JSON.parse(normalizedJsonString);
                        }
                       
                        templateLabelValue = executeFunctionByName(functionName, window, scope.getPropertyValueByAlias(fieldset, propertyAlias), scope, args);
                    }
                    //normal {{foo}} syntax
                    else {
                        propertyAlias = template;
                        
                        var rawValue = scope.getPropertyValueByAlias(fieldset, propertyAlias);

                        templateLabelValue = rawValue;

                        //determine the type of editor
                        var propertyConfig = _.find(fieldsetConfigModel.properties, function(property){
                            return property.alias === propertyAlias;
                        });

                        if(propertyConfig) {
                            var datatype = archetypeCacheService.getDatatypeByGuid(propertyConfig.dataTypeGuid);

                            if(datatype) {
                                //try to get built-in label
                                var label = getNativeLabel(datatype, rawValue, scope);
                                
                                if (label) {
                                    templateLabelValue = label;
                                }
                            }
                        }
                    }
                }

                // Process the value (i.e., reduce any functions or promises down to strings).
                processLabelValue(templateLabelValue, promises, match);
            });

            // Wait for all of the promises to resolve before constructing the full fieldset label.
            return repeatedlyWaitForPromises(promises).then(function () {

                //console.log("done waiting...")
                // Extract string values and combine them into a single string.
                var substrings = _.map(matches, function (value) {
                    return value.value;
                });
                
                var combinedSubstrings = substrings.join('');

                //console.log(combinedSubstrings);
                
                // Return the title.
                return combinedSubstrings;
            });
        }
    }
});