/** MIT License (c) copyright B Cavalier & J Hann */

/**
 * curl css! plugin
 *
 * Licensed under the MIT License at:
 * 		http://www.opensource.org/licenses/mit-license.php
 *
 */

(function (global) {
"use strict";

/*
 * AMD css! plugin
 * This plugin will load and wait for css files.  This could be handy when
 * loading css files as part of a component or a theme.
 * Some browsers do not support the load event handler of the link element.
 * Therefore, we have to use other means to detect when a css file loads.
 * Some browsers don't support the error event handler, either.
 * The HTML5 spec states that the LINK element should have both load and
 * error events:
 * http://www.w3.org/TR/html5/semantics.html#the-link-element
 *
 * This plugin tries to use the load event and a universal work-around when
 * it is invoked.  If the load event works, it is used on every successive load.
 * Therefore, browsers that support the load event will just work (i.e. no
 * need for hacks!).  FYI, sniffing for the load event is tricky
 * since most browsers still have a non-functional onload property.
 *
 * IE is a special case since it also has a 31-stylesheet limit (finally
 * fixed in IE 10).  To get around this, we can use a set of <style>
 * elements instead of <link> elements and add @import; rules into them.
 * This allows us to add considerably more than 31 stylesheets.  See the
 * comment for the loadImport method for more information.
 *
 * The universal work-around for other browsers watches a stylesheet
 * until its rules are available (not null or undefined).  There are
 * nuances, of course, between the various browsers.  The isLinkReady
 * function accounts for these.
 *
 * Note: it appears that all browsers load @import'ed stylesheets before
 * fully processing the rest of the importing stylesheet. Therefore, we
 * don't need to find and wait for any @import rules explicitly.  They'll
 * be waited for implicitly.
 *
 * Global configuration options:
 *
 * cssNoWait: Boolean. You can instruct this plugin to not wait
 * for any css resources. They'll get loaded asap, but other code won't wait
 * for them.  Note: you cannot use this option and use more than 31
 * stylesheets in IE6-9!
 *
 * cssWatchPeriod: if direct load-detection techniques fail, this option
 * determines the msec to wait between brute-force checks for rules. The
 * default is 50 msec.
 *
 * You may specify an alternate file extension or no extension:
 *      require('css!myproj/component.less') // --> myproj/component.less
 *      require('css!myproj/component') // --> myproj/component.css
 *
 * When using alternative file extensions, be sure to serve the files from
 * the server with the correct mime type (text/css) or some browsers won't
 * parse them, causing an error.
 *
 * usage:
 *      require(['css!myproj/comp.css']); // load and wait for myproj/comp.css
 *      define(['css!some/folder/file'], {}); // wait for some/folder/file.css
 *      require(['css!myWidget']);
 *
 * Tested in:
 *      Firefox 3.6, 4.0, 11-16
 *      Safari 3.0.4, 3.2.1, 5.0
 *      Chrome 19
 *      Opera 11.62, 12.01
 *      IE 6-10
 *  Error handlers work in the following:
 *  	Firefox 12+
 *  	Safari 6+
 *  	Chrome 9+
 *  	IE6-9
 *  Error handlers don't work in:
 *  	Opera 11.62, 12.01
 *  	Firefox 3.6, 4.0
 *  	IE 10
*/

    var
        // compressibility shortcuts
        createElement = 'createElement',
        parentNode = 'parentNode',
        setTimeout = global.setTimeout,
        // doc will be undefined during a build
        doc = global.document,
        // find the head element and set it to it's standard property if nec.
        head,
        // infer IE 6-9
        // IE 10 still doesn't seem to have link.onerror support,
        // but it doesn't choke on >31 stylesheets at least!
        shouldCollectSheets = doc && doc.createStyleSheet && !(doc.documentMode >= 10),
        ieCollectorSheets = [],
        ieCollectorPool = [],
        ieCollectorQueue = [],
        ieMaxCollectorSheets = 12,
        loadSheet,
        msgHttp = 'HTTP or network error.',
        hasEvent = {};

    if (doc) {
        head = doc.head || doc.getElementsByTagName('head')[0];
        if (shouldCollectSheets) {
            loadSheet = loadImport;
        }
        else {
            loadSheet = loadLink;
        }
    }

    /**
     * Once we've absolutely determined if the current browser supports an
     * event handler, we set it with this function.
     * @private
     * @param event
     * @param hasNative
     */
    function setLoadDetection (event, hasNative) {
        hasEvent[event] = hasEvent[event] || hasNative;
    }

    /**
     * Creates a link element.
     * IE6-9 don't use this function.
     * @private
     * @return {Element}
     */
    function createLink () {
        var link;
        link = doc[createElement]('link');
        link.rel = "stylesheet";
        link.type = "text/css";
        return link;
    }

    /***** load functions for compliant browsers *****/

    /**
     * This is the load handler for compliant browsers.
     * Latest Chrome, Safari, Opera, FF, IE10
     * @private
     * @param link
     * @param cb
     */
    function loadHandler (link, cb) {
        link.onload = function () {
            // we know browser is compliant now!
            setLoadDetection('load', true);
            cb();
        };
    }

    /**
     * This is the load handler for compliant browsers.
     * Latest Chrome, Safari (not Opera 12 or IE10)
     * @private
     * @param link
     * @param cb
     */
    function errorHandler (link, cb) {
        link.onerror = function () {
            // we know browser is compliant now!
            setLoadDetection('error', true);
            cb();
        };
    }

    /***** ie load functions *****/

    /**
     * Loads a stylesheet via IE's addImport() method, which is the only
     * way to detect both onload and onerror in IE.  If we create a "parent
     * stylesheet", we can addImport() other sheets into it.  The tricky part
     * is that we have to load one sheet at a time and create a new onload
     * and onerror event for each one.  (IE only fires an onload or onerror
     * function once, but if you replace the onload or onerror functions,
     * it'll fire the new ones if there's another load or error event.
     * Way to be awesome, IE team!)
     *
     * To get around the one-sheet-at-a-time problem, we create many
     * parent stylesheets at once.  If we create 12 parent sheets, we can load
     * up to 12 imported sheets at once.  This has an additional benefit:
     * we can load 372 (12 * 31) stylesheets.  IE 6-9 can dynamically load only
     * 31 stylesheets in any one scope.  By creating multiple parent sheets, we
     * create multiple scopes.
     *
     * The astute reader will have discovered a major flaw with this approach:
     * we've killed the cascade (the "C" in CSS).  Rules in stylesheets override
     * rules in stylesheets that were declared earlier.  This is universal.
     * However, the IE team interpreted the word "earlier" differently than
     * everybody else (including the w3c).  IE interprets it as meaning "earlier
     * in time" (temporal), rather than "earlier in the document" (spacial).
     * Specifically, the temporal order of the insertion of the sheet into the
     * DOM/BOM is what matters in IE.
     *
     * In other words: the bungling of the IE team (both in allowing sheet
     * error handlers to execute multiple times and in forcing us to use
     * temporal order rather than dom order) has allowed us to implement
     * this work-around.  Almost seems like they planned it.
     *
     * Note: CSS debugging tools in IE 6-8 seem to sometimes fail when inserting
     * stylesheets dynamically, no matter which method we use to insert them.
     *
     * IE 6-9 only.
     * @private
     * @param url {String}
     * @param cb {Function}
     * @param eb {Function}
     */
    function loadImport (url, cb, eb) {
        var coll;

        // push stylesheet and callbacks on queue
        ieCollectorQueue.push({
            url:url,
            cb:cb,
            eb: function failure () { eb(new Error(msgHttp)); }
        });

        // find an available collector
        coll = getIeCollector();

        // if we have an available collector, import a stylesheet from the queue
        if (coll) {
            loadNextImport(coll);
        }

    }

    /**
     * Grabs the next sheet/callback item from the queue and imports it into
     * the provided collector sheet.
     * IE 6-9 only.
     * @private
     * @param coll {Stylesheet}
     */
    function loadNextImport (coll) {
        var imp, collSheet;

        imp = ieCollectorQueue.shift();
        collSheet = coll.styleSheet;

        if (imp) {
            coll.onload = function () {
                imp.cb(imp.ss);
                loadNextImport(coll);
            };
            coll.onerror = function () {
                imp.eb();
                loadNextImport(coll);
            };
            imp.ss = collSheet.imports[collSheet.addImport(imp.url)];
        }
        else {
            finalize(coll);
            returnIeCollector(coll);
        }
    }

    /**
     * Returns a collector sheet to the pool.
     * IE 6-9 only.
     * @private
     * @param coll {Stylesheet}
     */
    function returnIeCollector (coll) {
        ieCollectorPool.push(coll);
    }

    /**
     * Gets the next collector sheet in the pool.  If there is no collector
     * in the pool and less than the maximum collector sheets has been created,
     * a new one is created. If the max collectors have been created,
     * undefined is returned.
     * IE 6-9 only.
     * @private
     * @return {HTMLElement} a stylesheet element to act as a collector sheet
     */
    function getIeCollector () {
        var el;

        el = ieCollectorPool.shift();

        if (!el && ieCollectorSheets.length < ieMaxCollectorSheets) {
            el = doc.createElement('style');
            ieCollectorSheets.push(el);
            head.appendChild(el);
        }

        return el;
    }

    /***** load functions for legacy browsers (old Safari and FF, Opera) *****/

    /**
     * Try all sorts of crazy shiz to determine when the stylesheet is loaded.
     * @private
     * @param link
     * @return {Boolean}
     */
    function isLinkReady (link) {
        var ready, sheet, rules;
        // don't bother testing until we've fully initialized the link and doc.
        if (!link.href || !isDocumentComplete()) return false;

        ready = false;
        try {
            sheet = link.sheet;
            if (sheet) {
                // old FF will throw a security exception here when an XD
                // sheet is loaded. webkits (that don't support onload)
                // will return null when an XD sheet is loaded
                rules = sheet.cssRules;
                ready = rules === null;
                if (!ready && rules) {
                    // Safari needs to further test for rule manipulation
                    // on local stylesheets (Opera too?)
                    sheet.insertRule('-curl-css-test {}', 0);
                    sheet.deleteRule(0);
                    ready = true;
                }
            }
        }
        catch (ex) {

            // a "security" or "access denied" error indicates that an XD
            // stylesheet has been successfully loaded in old FF
            // Opera throws before the sheet is loaded (and before onload
            // in some cases, so we have to test for it here)
            ready = (Object.prototype.toString.call( window.opera ) != '[object Opera]' &&
                    /security|denied/i.test(ex.message) == false); // fix == false from quiqqer
        }

        return ready;
    }

    /**
     * Indicate that a link element is loaded or errored.
     * @private
     * @param link
     */
    function finalize (link) {
        // noop serves as a flag that a link event fired
        // note: Opera and IE won't clear handlers if we use a non-function
        link.onload = link.onerror = noop;
    }

    /**
     * Detect is a link is loaded or errored.
     * @private
     * @param link
     * @return {Boolean}
     */
    function isFinalized (link) {
        return link.onload == noop || !link.onload;
    }

    /**
     * This is the function that will be used when a browser doesn't support
     * the standard onload and handler -- or until we've verified that the
     * browser supports it.
     * @private
     * @param link
     * @param wait {Number} msec between checks
     * @param cb
     */
    function loadWatcher (link, wait, cb) {
        // watches a stylesheet for loading signs.
        if (hasEvent['load']) return; // always check on re-entry
        if (isLinkReady(link)) {
            cb(link.sheet);
        }
        else if (!isFinalized(link)) {
            setTimeout(function () { loadWatcher(link, wait, cb); }, wait);
        }
    }

    /**
     * This is the function that wil be used when a browser doens't support
     * the standard onerror handler -- or until we've verifies that the
     * browser supports it.
     * Note: so far we don't have a good way to check or failed stylesheets
     * without attempting to refetch the stylesheet using an IMG, OBJECT,
     * or any other means, so we haven't implemented this.
     * @private
     * @param link
     * @param wait {Number} msec between checks
     * @param eb
     */
    function errorWatcher (link, wait, eb) {
        if (hasEvent['error']) return;
        // TODO: figure out a method to test for stylesheet failure without risk of re-fetching
        // TODO: timeout?
    }

    /**
     * Launch both standards-compliant onload and fallback at the same time.
     * One of these will eventually work.
     * @private
     * @param link
     * @param wait {Number} msec between checks
     * @param cb
     */
    function linkLoaded (link, wait, cb) {
        // most browsers now support link.onload, but many older browsers
        // don't. Browsers that don't will launch the loadWatcher to repeatedly
        // test the link for readiness.
        function load () {
            // only executes once (link.onload is acting as a flag)
            if (isFinalized(link)) return;
            finalize(link);
            waitForDocumentComplete(function () { cb(link.sheet); });
        }
        // always try standard handler
        loadHandler(link, load);
        // also try the fallback
        loadWatcher(link, wait, load);
    }

    /**
     * Launch both standards-compliant onerror and fallback at the same time.
     * One of these will eventually work in most browsers.
     * @private
     * @param link
     * @param wait {Number} msec between checks
     * @param cb
     */
    function linkErrored (link, wait, cb) {
        // very few browsers (Chrome 19+ and FF9+ as of Apr 2012) have a
        // functional onerror handler (and those only detect 40X/50X http
        // errors, not parsing errors as per the w3c spec).
        // IE6-10 call onload when there's an http error. (nice, real nice)
        // this only matters in IE10 since IE6-9 use the addImport method
        // which does call onerror.
        function error () {
            // only executes once (link.onload is acting as a flag)
            if (isFinalized(link)) return;
            finalize(link);
            cb(new Error(msgHttp));
        }
        // always try standard handler
        errorHandler(link, error);
        // if we are not sure if the native error event works, try the fallback
        errorWatcher(link, wait, error);
    }

    /**
     * Kick-start the load and detection process.
     * @private
     * @param url
     * @param cb
     * @param eb
     * @param period {Number} msec between checks
     */
    function loadLink (url, cb, eb, period) {
        var link;
        link = createLink();
        linkLoaded(link, period, cb);
        linkErrored(link, period, eb);
        link.href = url;
        head.appendChild(link);
    }

    /**
     * Keep checking for the document readyState to be "complete" since
     * Chrome doesn't apply the styles to the document until that time.
     * If we return before readyState == 'complete', Chrome may not have
     * applied the styles, yet.
     * Chrome only.
     * @private
     * @param cb
     */
    function waitForDocumentComplete (cb) {
        // this isn't exactly the same as domReady (when dom can be
        // manipulated). it's later (when styles are applied).
        // chrome needs this (and opera?)
        function complete () {
            if (isDocumentComplete()) {
                cb();
            }
            else {
                setTimeout(complete, 10);
            }
        }
        complete();
    }

    /**
     * Returns true if the documents' readyState == 'complete' or the
     * document doesn't implement readyState.
     * Chrome only.
     * @private
     * @return {Boolean}
     */
    function isDocumentComplete () {
        return !doc.readyState || doc.readyState == 'complete';
    }

    function nameWithExt (name, defaultExt) {
        return name.lastIndexOf('.') <= name.lastIndexOf('/') ?
            name + '.' + defaultExt : name;
    }

    function noop () {}

    /***** finally! the actual plugin *****/

    define(/*=='curl/plugin/css',==*/ {

        'normalize': function (resourceId, normalize) {
            var resources, normalized;

            if (!resourceId) return resourceId;

            resources = resourceId.split(",");
            normalized = [];

            for (var i = 0, len = resources.length; i < len; i++) {
                normalized.push(normalize(resources[i]));
            }

            return normalized.join(',');
        },

        'load': function (resourceId, require, callback, config) {
            var sheets, resources, cssWatchPeriod, cssNoWait, loadingCount, i;
            sheets = [];
            resources = (resourceId || '').split(",");
            cssWatchPeriod = config['cssWatchPeriod'] || 50;
            cssNoWait = config['cssNoWait'];
            loadingCount = resources.length;

            // this function must get called just once per stylesheet!
            function loaded (ss) {
                if (resources.length > 1) sheets.push(ss);
                if (--loadingCount == 0) {
                    callback(resources.length == 1 ? ss : sheets);
                }
            }

            function failed (ex) {
                var eb;
                eb = callback.reject || function (ex) {
                    throw ex;
                };
                eb(ex);
            }

            for (i = 0; i < resources.length; i++) {

                resourceId = resources[i];

                var url, link;
                url = nameWithExt(require['toUrl'](resourceId), 'css');

                if (cssNoWait) {
                    link = createLink();
                    link.href = url;
                    head.appendChild(link);
                    loaded(link.sheet || link.styleSheet);
                }
                else {
                    loadSheet(url, loaded, failed, cssWatchPeriod);
                }
            }

        },

        'plugin-builder': 'css',
        'pluginBuilder': 'css'

    });

})(this);
