/**
 * Utility class for manipulating CSS rules
 * @singleton
 */
Ext.define('Ext.util.CSS', function() {
    var CSS,
        rules = null,
        doc = document,
        camelRe = /(-[a-z])/gi,
        camelFn = function(m, a){ return a.charAt(1).toUpperCase(); };

    return {

        singleton: true,

        rules: rules,

        initialized: false,

        constructor: function() {
            // Cache a reference to the singleton
            CSS = this;
        },

        /**
         * Creates a stylesheet from a text blob of rules.
         * These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
         * @param {String} cssText The text containing the css rules
         * @param {String} id An id to add to the stylesheet for later removal
         * @return {CSSStyleSheet}
         */
        createStyleSheet: function (cssText, id) {
            var ss,
                head = doc.getElementsByTagName('head')[0],
                styleEl = doc.createElement('style');

            styleEl.setAttribute('type', 'text/css');

            if (id) {
               styleEl.setAttribute('id', id);
            }

            // Feature detect old IE
            ss = styleEl.styleSheet;
            if (ss) {
                head.appendChild(styleEl);
                ss.cssText = cssText;
            } else {
                styleEl.appendChild(doc.createTextNode(cssText));
                head.appendChild(styleEl);
                ss = styleEl.sheet;
            }

            CSS.cacheStyleSheet(ss);
            return ss;
        },

        /**
         * Removes a style or link tag by id
         * @param {String} id The id of the tag
         */
        removeStyleSheet : function(id) {
            var existing = doc.getElementById(id);
            if (existing) {
                existing.parentNode.removeChild(existing);
            }
        },

        /**
         * Dynamically swaps an existing stylesheet reference for a new one
         * @param {String} id The id of an existing link tag to remove
         * @param {String} url The href of the new stylesheet to include
         */
        swapStyleSheet : function(id, url) {
            var ss;
            CSS.removeStyleSheet(id);
            ss = doc.createElement("link");
            ss.setAttribute("rel", "stylesheet");
            ss.setAttribute("type", "text/css");
            ss.setAttribute("id", id);
            ss.setAttribute("href", url);
            doc.getElementsByTagName("head")[0].appendChild(ss);
        },

        // @private
        cacheStyleSheet : function(ss) {
            if (!rules) {
                rules = CSS.rules = {};
            }
            try {// try catch for cross domain access issue
                var ssRules = ss.cssRules || ss.rules,
                    i = ssRules.length - 1,
                    imports = ss.imports,
                    len = imports ? imports.length : 0,
                    rule, j;
                    
                // Old IE has a different way of handling imports
                for (j = 0; j < len; ++j) {
                    CSS.cacheStyleSheet(imports[j]);
                }

                for (; i >= 0; --i) {
                    rule = ssRules[i];
                    // If it's an @import rule, import its stylesheet
                    if (rule.styleSheet) {
                        CSS.cacheStyleSheet(rule.styleSheet);
                    }
                    CSS.cacheRule(rule, ss);
                }
            } catch(e) {}
        },

        cacheRule: function(cssRule, styleSheet) {
            // If it's an @import rule, import its stylesheet
            if (cssRule.styleSheet) {
                return CSS.cacheStyleSheet(cssRule.styleSheet);
            }

            var selectorText = cssRule.selectorText,
                selectorCount, j;

            if (selectorText) {

                // Split in case there are multiple, comma-delimited selectors
                selectorText = selectorText.split(',');
                selectorCount = selectorText.length;
                for (j = 0; j < selectorCount; j++) {
                    // IE<8 does not keep a reference to parentStyleSheet in the rule, so we
                    // must cache an object like this until IE<8 is deprecated.
                    rules[Ext.String.trim(selectorText[j]).toLowerCase()] = {
                        parentStyleSheet: styleSheet,
                        cssRule: cssRule
                    };
                }
            }
        },

        /**
         * Gets all css rules for the document
         * @param {Boolean} refreshCache true to refresh the internal cache
         * @return {Object} An object (hash) of rules indexed by selector
         */
        getRules : function(refreshCache) {
            var result = {},
                selector;

            if (rules === null || refreshCache) {
                CSS.refreshCache();
            }
            for (selector in rules) {
                result[selector] = rules[selector].cssRule;
            }
            return result;
        },
        
        /**
         * Refresh the rule cache if you have dynamically added stylesheets
         * @return {Object} An object (hash) of rules indexed by selector
         */
        refreshCache: function() {
            var ds = doc.styleSheets,
                i = 0,
                len = ds.length;

            rules = CSS.rules = {};
            for (; i < len; i++) {
                try {
                    if (!ds[i].disabled) {
                        CSS.cacheStyleSheet(ds[i]);
                    }
                } catch(e) {}
            }
        },

        /**
         * Gets an an individual CSS rule by selector(s)
         * @param {String/String[]} selector The CSS selector or an array of selectors to try. The first selector that is found is returned.
         * @param {Boolean} refreshCache true to refresh the internal cache if you have recently updated any rules or added styles dynamically
         * @return {CSSStyleRule} The CSS rule or null if one is not found
         */
        getRule: function(selector, refreshCache, rawCache) {
            var i, result;

            if (!rules || refreshCache) {
                CSS.refreshCache();
            }
            if (!Ext.isArray(selector)) {
                result = rules[selector.toLowerCase()];
                if (result && !rawCache) {
                    result = result.cssRule;
                }
                return result || null;
            }
            for (i = 0; i < selector.length; i++) {
                if (rules[selector[i]]) {
                    return rawCache ? rules[selector[i].toLowerCase()] : rules[selector[i].toLowerCase()].cssRule;
                }
            }
            return null;
        },

        /**
         * Creates a rule.
         * @param {CSSStyleSheet} styleSheet The StyleSheet to create the rule in as returned from {@link #createStyleSheet}.
         * @param {String} selector The selector to target the rule.
         * @param {String} property The cssText specification eg `"color:red;font-weight:bold;text-decoration:underline"`
         * @return {CSSStyleRule} The created rule
         */
        createRule: function(styleSheet, selector, cssText) {
            var result,
                ruleSet = styleSheet.cssRules || styleSheet.rules,
                index = ruleSet.length;

            if (styleSheet.insertRule) {
                styleSheet.insertRule(selector + ' {' + cssText + '}', index);
            } else {
                styleSheet.addRule(selector, cssText||' ');
            }
            CSS.cacheRule(result = ruleSet[index], styleSheet);
            return result;
        },

        /**
         * Updates a rule property
         * @param {String/String[]} selector If it's an array it tries each selector until it finds one. Stops immediately once one is found.
         * @param {String} property The css property or a cssText specification eg `"color:red;font-weight:bold;text-decoration:underline"`
         * @param {String} value The new value for the property
         * @return {Boolean} true If a rule was found and updated
         */
        updateRule : function(selector, property, value) {
            var rule, i, styles;
            if (!Ext.isArray(selector)) {
                rule = CSS.getRule(selector);
                if (rule) {
                    // 2 arg form means cssText sent, so parse it and update each style
                    if (arguments.length === 2) {
                        styles = Ext.Element.parseStyles(property);
                        for (property in styles) {
                            rule.style[property.replace(camelRe, camelFn)] = styles[property];
                        }
                    } else {
                        rule.style[property.replace(camelRe, camelFn)] = value;
                    }
                    return true;
                }
            } else {
                for (i = 0; i < selector.length; i++) {
                    if (CSS.updateRule(selector[i], property, value)) {
                        return true;
                    }
                }
            }
            return false;
        },

        deleteRule: function(selector) {
            var rule = CSS.getRule(selector, false, true),
                styleSheet, index;

            if (rule) {
                styleSheet = rule.parentStyleSheet;
                index = Ext.Array.indexOf(styleSheet.cssRules || styleSheet.rules, rule.cssRule);
                if (styleSheet.deleteRule) {
                    styleSheet.deleteRule(index);
                } else {
                    styleSheet.removeRule(index);
                }
                delete rules[selector];
            }
        }
    };
});
