/**
 * 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,
 
        /**
         * @private
         */
        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/CSSStyleSheet} stylesheet The id of the style tag, or the CSSStyleSheet
         * reference to remove
         */
        removeStyleSheet: function(stylesheet) {
            var styleEl = (typeof stylesheet === 'string')
                ? doc.getElementById(stylesheet)
                : stylesheet.ownerNode;
 
            if (styleEl) {
                styleEl.parentNode.removeChild(styleEl);
            }
        },
 
        /**
         * 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
                // eslint-disable-next-line vars-on-top
                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) {
                // ignore
            }
        },
 
        cacheRule: function(cssRule, styleSheet) {
            var selectorText, selectorCount, j;
            
            // If it's an @import rule, import its stylesheet
            if (cssRule.styleSheet) {
                return CSS.cacheStyleSheet(cssRule.styleSheet);
            }
 
            selectorText = cssRule.selectorText;
 
            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) {
                    // ignore
                }
            }
        },
 
        /**
         * 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
         * @param rawCache 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} cssText The cssText specification e.g.
         * `"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 e.g.
         * `"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];
            }
        }
    };
});