angular.module('textAngular.factories', [])
.factory('taBrowserTag', [function(){
    return function(tag){
        /* istanbul ignore next: ie specific test */
        if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
        else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
        else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
    };
}]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
    return function(val){
        var element = angular.element('<div></div>');
        element[0].innerHTML = val;

        angular.forEach(taCustomRenderers, function(renderer){
            var elements = [];
            // get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
            if(renderer.selector && renderer.selector !== '')
                elements = element.find(renderer.selector);
            /* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
            else if(renderer.customAttribute && renderer.customAttribute !== '')
                elements = taDOM.getByAttribute(element, renderer.customAttribute);
            // process elements if any found
            angular.forEach(elements, function(_element){
                _element = angular.element(_element);
                if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
                    if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
                } else renderer.renderLogic(_element);
            });
        });

        return element[0].innerHTML;
    };
}]).factory('taFixChrome', function(){
    // get whaterever rubbish is inserted in chrome
    // should be passed an html string, returns an html string
    var taFixChrome = function(html, keepStyles){
        if(!html || !angular.isString(html) || html.length <= 0) return html;
        // grab all elements with a style attibute
        // a betterSpanMatch matches only a style=... with matching quotes
        // this captures the whole:
        // 'style="background-color: rgb(255, 255, 255);"'
        var betterSpanMatch = /style\s?=\s?(["'])(?:(?=(\\?))\2.)*?\1/ig;
        // where the original spanMatch = /<([^>\/]+?)style=("([^\"]+)"|'([^']+)')([^>]*)>/ig;
        // captures too much and includes the front tag!
        var spanMatch = /<([^>\/]+?)style=("([^\"]+)"|'([^']+)')([^>]*)>/ig;
        var appleConvertedSpaceMatch = /<span class="Apple-converted-space">([^<]+)<\/span>/ig;
        var match, styleVal, appleSpaceVal, newTag, finalHtml = '', lastIndex = 0;
        // remove all the Apple-converted-space spans and replace with the content of the span
        //console.log('before:', html);
        /* istanbul ignore next: apple-contereted-space span match */
        while(match = appleConvertedSpaceMatch.exec(html)){
            appleSpaceVal = match[1];
            appleSpaceVal = appleSpaceVal.replace(/&nbsp;/ig, ' ');
            finalHtml += html.substring(lastIndex, match.index) + appleSpaceVal;
            lastIndex = match.index + match[0].length;
        }
        /* istanbul ignore next: apple-contereted-space span has matched */
        if (lastIndex) {
            // modified....
            finalHtml += html.substring(lastIndex);
            html=finalHtml;
            finalHtml='';
            lastIndex=0;
        }
        /////////////////////////////////////////////////////////////
        //
        // Allow control of this modification
        // taKeepStyles: False - removes these modification
        //
        // taFixChrome removes the following styles:
        //    font-family: inherit;
        //    line-height: <number>
        //    color: inherit;
        //    color: rgb( <rgb-component>#{3} )
        //    background-color: rgb( <rgb-component>#{3} )
        //
        /////////////////////////////////////////////////////////////
        if (!keepStyles) {
            while (match = betterSpanMatch.exec(html)) {
                finalHtml += html.substring(lastIndex, match.index-1);
                styleVal = match[0];
                // test for chrome inserted junk
                match = /font-family: inherit;|line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;|color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);|background-color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);/gi.exec(styleVal);
                if (match) {
                    styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;|( |)color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);|( |)background-color: rgb\(\d{1,3}, \d{1,3}, \d{1,3}\);/ig, '');
                    //console.log(styleVal, styleVal.length);
                    if (styleVal.length > 8) {
                        finalHtml += ' ' + styleVal;
                    }
                } else {
                    finalHtml += ' ' + styleVal;
                }
                lastIndex = betterSpanMatch.lastIndex;
            }
            finalHtml += html.substring(lastIndex);
        }
        //console.log('final:', finalHtml);
        // only replace when something has changed, else we get focus problems on inserting lists
        if(lastIndex > 0){
            // replace all empty strings
            var fe = finalHtml.replace(/<span\s?>(.*?)<\/span>(<br(\/|)>|)/ig, '$1');
            return fe;
        } else return html;
    };
    return taFixChrome;
}).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){

    var convert_infos = [
        {
            property: 'font-weight',
            values: [ 'bold' ],
            tag: 'b'
        },
        {
            property: 'font-style',
            values: [ 'italic' ],
            tag: 'i'
        }
    ];

    var styleMatch = [];
    for(var i = 0; i < convert_infos.length; i++){
        var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
        for(var j = 0; j < convert_infos[i].values.length; j++){
            /* istanbul ignore next: not needed to be tested yet */
            if(j > 0) _partialStyle += '|';
            _partialStyle += convert_infos[i].values[j];
        }
        _partialStyle += ');)';
        styleMatch.push(_partialStyle);
    }
    var styleRegexString = '(' + styleMatch.join('|') + ')';

    function wrapNested(html, wrapTag) {
        var depth = 0;
        var lastIndex = 0;
        var match;
        var tagRegex = /<[^>]*>/ig;
        while(match = tagRegex.exec(html)){
            lastIndex = match.index;
            if(match[0].substr(1, 1) === '/'){
                if(depth === 0) break;
                else depth--;
            }else depth++;
        }
        return wrapTag +
            html.substring(0, lastIndex) +
            // get the start tags reversed - this is safe as we construct the strings with no content except the tags
            angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
            html.substring(lastIndex);
    }

    function transformLegacyStyles(html){
        if(!html || !angular.isString(html) || html.length <= 0) return html;
        var i;
        var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
        var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
        while(match = styleElementMatch.exec(html)){
            // one of the quoted values ' or "
            /* istanbul ignore next: quotations match */
            styleVal = match[3] || match[4];
            var styleRegex = new RegExp(styleRegexString, 'i');
            // test for style values to change
            if(angular.isString(styleVal) && styleRegex.test(styleVal)){
                // remove build tag list
                newTag = '';
                // init regex here for exec
                var styleRegexExec = new RegExp(styleRegexString, 'ig');
                // find relevand tags and build a string of them
                while(subMatch = styleRegexExec.exec(styleVal)){
                    for(i = 0; i < convert_infos.length; i++){
                        if(!!subMatch[(i*2) + 2]){
                            newTag += '<' + convert_infos[i].tag + '>';
                        }
                    }
                }
                // recursively find more legacy styles in html before this tag and after the previous match (if any)
                newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
                // build up html
                if(lastNewTag.length > 0){
                    finalHtml += wrapNested(newHtml, lastNewTag);
                }else finalHtml += newHtml;
                // grab the style val without the transformed values
                styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
                // build the html tag
                finalHtml += '<' + match[1].trim();
                if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
                finalHtml += match[5] + '>';
                // update the start index to after this tag
                lastIndex = match.index + match[0].length;
                lastNewTag = newTag;
            }
        }
        if(lastNewTag.length > 0){
            finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
        }
        else finalHtml += html.substring(lastIndex);
        return finalHtml;
    }

    function transformLegacyAttributes(html){
        if(!html || !angular.isString(html) || html.length <= 0) return html;
        // replace all align='...' tags with text-align attributes
        var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
        var match, finalHtml = '', lastIndex = 0;
        // match all attr tags
        while(match = attrElementMatch.exec(html)){
            // add all html before this tag
            finalHtml += html.substring(lastIndex, match.index);
            // record last index after this tag
            lastIndex = match.index + match[0].length;
            // construct tag without the align attribute
            var newTag = '<' + match[1] + match[5];
            // add the style attribute
            if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
                /* istanbul ignore next: quotations match */
                newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
            }else{
                /* istanbul ignore next: quotations match */
                newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
            }
            newTag += '>';
            // add to html
            finalHtml += newTag;
        }
        // return with remaining html
        return finalHtml + html.substring(lastIndex);
    }

    // use precompiled regexp for speed
    var rsb1 = new RegExp(/<span id="selectionBoundary_\d+_\d+" class="rangySelectionBoundary">[^<>]+?<\/span>/ig);
    var rsb2 = new RegExp(/<span class="rangySelectionBoundary" id="selectionBoundary_\d+_\d+">[^<>]+?<\/span>/ig);
    var rsb3 = new RegExp(/<span id="selectionBoundary_\d+_\d+" class="rangySelectionBoundary">[^<>]+?<\/span>/ig);

    return function taSanitize(unsafe, oldsafe, ignore){
        // unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
        if ( !ignore ) {
            try {
                unsafe = transformLegacyStyles(unsafe);
            } catch (e) {
            }
        }

        // unsafe and oldsafe should be valid HTML strings
        // any exceptions (lets say, color for example) should be made here but with great care
        // setup unsafe element for modification
        unsafe = transformLegacyAttributes(unsafe);

        // we had an issue in the past, where we dumped a whole bunch of <span>'s into the content...
        // so we remove them here
        // IN A FUTURE release this can be removed after all have updated through release 1.5.9
        if (unsafe) {
            try {
                unsafe = unsafe.replace(rsb1, '');
                unsafe = unsafe.replace(rsb2, '');
                unsafe = unsafe.replace(rsb1, '');
                unsafe = unsafe.replace(rsb3, '');
            } catch (e) {
            }
        }

        var safe;
        try {
            safe = $sanitize(unsafe);
            // do this afterwards, then the $sanitizer should still throw for bad markup
            if(ignore) safe = unsafe;
        } catch (e){
            safe = oldsafe || '';
        }

        // Do processing for <pre> tags, removing tabs and return carriages outside of them

        var _preTags = safe.match(/(<pre[^>]*>.*?<\/pre[^>]*>)/ig);
        var processedSafe = safe.replace(/(&#(9|10);)*/ig, '');
        var re = /<pre[^>]*>.*?<\/pre[^>]*>/ig;
        var index = 0;
        var lastIndex = 0;
        var origTag;
        safe = '';
        while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
            safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
            lastIndex = origTag.index + origTag[0].length;
            index++;
        }
        return safe + processedSafe.substring(lastIndex);
    };
}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
    // this must be called on a toolScope or instance
    return function(editor){
        if(editor !== undefined) this.$editor = function(){ return editor; };
        var deferred = $q.defer(),
            promise = deferred.promise,
            _editor = this.$editor();
        // pass into the action the deferred function and also the function to reload the current selection if rangy available
        var result;
        try{
            result = this.action(deferred, _editor.startAction());
            // We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
            promise['finally'](function(){
                _editor.endAction.call(_editor);
            });
        }catch(exc){
            $log.error(exc);
        }
        if(result || result === undefined){
            // if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
            deferred.resolve();
        }
    };
}]);
