/*
 * Require-CSS RequireJS css! loader plugin
 * Guy Bedford 2013
 * MIT
 */

/*
 *
 * Usage:
 *  require(['css!./mycssFile']);
 *
 * NB leave out the '.css' extension.
 *
 * - Fully supports cross origin CSS loading
 * - Works with builds
 *
 * Tested and working in (up to latest versions as of March 2013):
 * Android
 * iOS 6
 * IE 6 - 10
 * Chome 3 - 26
 * Firefox 3.5 - 19
 * Opera 10 - 12
 * 
 * browserling.com used for virtual testing environment
 *
 * Credit to B Cavalier & J Hann for the elegant IE 6 - 9 hack.
 * 
 * Sources that helped along the way:
 * - https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent
 * - http://www.phpied.com/when-is-a-stylesheet-really-loaded/
 * - https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
 *
 */

define(['./normalize'], function(normalize) {
  function indexOf(a, e) { for (var i=0, l=a.length; i < l; i++) if (a[i] === e) return i; return -1 }

  if (typeof window == 'undefined')
    return { load: function(n, r, load){ load() } };

  // set to true to enable test prompts for device testing
  var testing = false;
  
  var head = document.getElementsByTagName('head')[0];

  var engine = window.navigator.userAgent.match(/Trident\/([^ ;]*)|AppleWebKit\/([^ ;]*)|Opera\/([^ ;]*)|rv\:([^ ;]*)(.*?)Gecko\/([^ ;]*)|MSIE\s([^ ;]*)/);
  var hackLinks = false;

  if (!engine) {}
  else if (engine[1] || engine[7]) {
    hackLinks = parseInt(engine[1]) < 6 || parseInt(engine[7]) <= 9;
    engine = 'trident';
  }
  else if (engine[2]) {
    // unfortunately style querying still doesnt work with onload callback in webkit
    hackLinks = true;
    engine = 'webkit';
  }
  else if (engine[3]) {
    // engine = 'opera';
  }
  else if (engine[4]) {
    hackLinks = parseInt(engine[4]) < 18;
    engine = 'gecko';
  }
  else if (testing)
    alert('Engine detection failed');
  
  //main api object
  var cssAPI = {};

  var absUrlRegEx = /^\/|([^\:\/]*:)/;
  
  cssAPI.pluginBuilder = './css-builder';

  // used by layer builds to register their css buffers
  
  // the current layer buffer items (from addBuffer)
  var curBuffer = [];

  // the callbacks for buffer loads
  var onBufferLoad = {};

  // the full list of resources in the buffer
  var bufferResources = [];

  cssAPI.addBuffer = function(resourceId) {
    // just in case layer scripts are included twice, also check
    // against the previous buffers
    if (indexOf(curBuffer, resourceId) != -1)
      return;
    if (indexOf(bufferResources, resourceId) != -1)
      return;
    curBuffer.push(resourceId);
    bufferResources.push(resourceId);
  }
  cssAPI.setBuffer = function(css, isLess) {
    var pathname = window.location.pathname.split('/');
    pathname.pop();
    pathname = pathname.join('/') + '/';

    var baseParts = require.toUrl('base_url').split('/');
    baseParts.pop();
    var baseUrl = baseParts.join('/') + '/';
    baseUrl = normalize.convertURIBase(baseUrl, pathname, '/');
    if (!baseUrl.match(absUrlRegEx))
      baseUrl = '/' + baseUrl;
    if (baseUrl.substr(baseUrl.length - 1, 1) != '/')
      baseUrl = baseUrl + '/';

    cssAPI.inject(normalize(css, baseUrl, pathname));

    // set up attach callback if registered
    // clear the current buffer for the next layer
    // (just the less or css part as we have two buffers in one effectively)
    for (var i = 0; i < curBuffer.length; i++) {
      // find the resources in the less or css buffer dependening which one this is
      if ((isLess && curBuffer[i].substr(curBuffer[i].length - 5, 5) == '.less') ||
        (!isLess && curBuffer[i].substr(curBuffer[i].length - 4, 4) == '.css')) {
        (function(resourceId) {
          // mark that the onBufferLoad is about to be called (set to true if not already a callback function)
          onBufferLoad[resourceId] = onBufferLoad[resourceId] || true;

          // set a short timeout (as injection isn't instant in Chrome), then call the load
          setTimeout(function() {
            if (typeof onBufferLoad[resourceId] == 'function')
              onBufferLoad[resourceId]();
            // remove from onBufferLoad to indicate loaded
            delete onBufferLoad[resourceId];
          }, 7);
        })(curBuffer[i]);

        // remove the current resource from the buffer
        curBuffer.splice(i--, 1);
      }
    }
  }
  cssAPI.attachBuffer = function(resourceId, load) {
    // attach can happen during buffer collecting, or between injection and callback
    // we assume it is not possible to attach multiple callbacks
    // requirejs plugin load function ensures this by queueing duplicate calls

    // check if the resourceId is in the current buffer
    for (var i = 0; i < curBuffer.length; i++)
      if (curBuffer[i] == resourceId) {
        onBufferLoad[resourceId] = load;
        return true;
      }

    // check if the resourceId is waiting for injection callback
    // (onBufferLoad === true is a shortcut indicator for this)
    if (onBufferLoad[resourceId] === true) {
      onBufferLoad[resourceId] = load;
      return true;
    }

    // if it's in the full buffer list and not either of the above, its loaded already
    if (indexOf(bufferResources, resourceId) != -1) {
      load();
      return true;
    }
  }

  var webkitLoadCheck = function(link, callback) {
    setTimeout(function() {
      for (var i = 0; i < document.styleSheets.length; i++) {
        var sheet = document.styleSheets[i];
        if (sheet.href == link.href)
          return callback();
      }
      webkitLoadCheck(link, callback);
    }, 10);
  }

  var mozillaLoadCheck = function(style, callback) {
    setTimeout(function() {
      try {
        style.sheet.cssRules;
        return callback();
      } catch (e){}
      mozillaLoadCheck(style, callback);
    }, 10);
  }

  // ie link detection, as adapted from https://github.com/cujojs/curl/blob/master/src/curl/plugin/css.js
  if (engine == 'trident' && hackLinks) {
    var ieStyles = [],
      ieQueue = [],
      ieStyleCnt = 0;
    var ieLoad = function(url, callback) {
      var style;
      ieQueue.push({
        url: url,
        cb: callback
      });
      style = ieStyles.shift();
      if (!style && ieStyleCnt++ < 12) {
        style = document.createElement('style');
        head.appendChild(style);
      }
      if (style){
        ieLoadNextImport(style); 
      }
    }
    var ieLoadNextImport = function(style) {
      var curImport = ieQueue.shift();
      if (!curImport) {
        style.onload = noop;
        ieStyles.push(style);
        return;  
      }
      style.onload = function() {
        curImport.cb(curImport.ss);
        ieLoadNextImport(style);
      };
      var curSheet = style.styleSheet;
      curImport.ss = curSheet.imports[curSheet.addImport(curImport.url)];
    }
  }

  // uses the <link> load method
  var createLink = function(url) {
    var link = document.createElement('link');
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.href = url;
    return link;
  }

  var noop = function(){}

  cssAPI.linkLoad = function(url, callback) {
    var timeout = setTimeout(function() {
      if (testing) alert('timeout');
      callback();
    }, waitSeconds * 1000 - 100);
    var _callback = function() {
      clearTimeout(timeout);
      if (link)
        link.onload = noop;
      // for style querying, a short delay still seems necessary
      setTimeout(callback, 7);
    }
    if (!hackLinks) {
      var link = createLink(url);
      link.onload = _callback;
      head.appendChild(link);
    }
    // hacks
    else {
      if (engine == 'webkit') {
        var link = createLink(url);
        webkitLoadCheck(link, _callback);
        head.appendChild(link);
      }
      else if (engine == 'gecko') {
        var style = document.createElement('style');
        style.textContent = '@import "' + url + '"';
        mozillaLoadCheck(style, _callback);
        head.appendChild(style);
      }
      else if (engine == 'trident')
        ieLoad(url, _callback);
    }
  }

  /* injection api */
  var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
  var fileCache = {};
  var get = function(url, callback, errback) {
    if (fileCache[url]) {
      callback(fileCache[url]);
      return;
    }

    var xhr, i, progId;
    if (typeof XMLHttpRequest !== 'undefined')
      xhr = new XMLHttpRequest();
    else if (typeof ActiveXObject !== 'undefined')
      for (i = 0; i < 3; i += 1) {
        progId = progIds[i];
        try {
          xhr = new ActiveXObject(progId);
        }
        catch (e) {}
  
        if (xhr) {
          progIds = [progId];  // so faster next time
          break;
        }
      }
    
    xhr.open('GET', url, requirejs.inlineRequire ? false : true);
  
    xhr.onreadystatechange = function (evt) {
      var status, err;
      //Do not explicitly handle errors, those should be
      //visible via console output in the browser.
      if (xhr.readyState === 4) {
        status = xhr.status;
        if (status > 399 && status < 600) {
          //An http 4xx or 5xx error. Signal an error.
          err = new Error(url + ' HTTP status: ' + status);
          err.xhr = xhr;
          errback(err);
        }
        else {
          fileCache[url] = xhr.responseText;
          callback(xhr.responseText);
        }
      }
    };
    
    xhr.send(null);
  }
  //uses the <style> load method
  var styleCnt = 0;
  var curStyle;
  cssAPI.inject = function(css) {
    if (styleCnt < 31) {
      curStyle = document.createElement('style');
      curStyle.type = 'text/css';
      head.appendChild(curStyle);
      styleCnt++;
    }
    if (curStyle.styleSheet)
      curStyle.styleSheet.cssText += css;
    else
      curStyle.appendChild(document.createTextNode(css));
  }
  
  // NB add @media query support for media imports
  var importRegEx = /@import\s*(url)?\s*(('([^']*)'|"([^"]*)")|\(('([^']*)'|"([^"]*)"|([^\)]*))\))\s*;?/g;

  var pathname = window.location.pathname.split('/');
  pathname.pop();
  pathname = pathname.join('/') + '/';

  var loadCSS = function(fileUrl, callback, errback) {

    //make file url absolute
    if (!fileUrl.match(absUrlRegEx))
      fileUrl = '/' + normalize.convertURIBase(fileUrl, pathname, '/');

    get(fileUrl, function(css) {

      // normalize the css (except import statements)
      css = normalize(css, fileUrl, pathname);

      // detect all import statements in the css and normalize
      var importUrls = [];
      var importIndex = [];
      var importLength = [];
      var match;
      while (match = importRegEx.exec(css)) {
        var importUrl = match[4] || match[5] || match[7] || match[8] || match[9];

        importUrls.push(importUrl);
        importIndex.push(importRegEx.lastIndex - match[0].length);
        importLength.push(match[0].length);
      }

      // load the import stylesheets and substitute into the css
      var completeCnt = 0;
      for (var i = 0; i < importUrls.length; i++)
        (function(i) {
          loadCSS(importUrls[i], function(importCSS) {
            css = css.substr(0, importIndex[i]) + importCSS + css.substr(importIndex[i] + importLength[i]);
            var lenDiff = importCSS.length - importLength[i];
            for (var j = i + 1; j < importUrls.length; j++)
              importIndex[j] += lenDiff;
            completeCnt++;
            if (completeCnt == importUrls.length) {
              callback(css);
            }
          }, errback);
        })(i);

      if (importUrls.length == 0)
        callback(css);
    }, errback);
  }

  
  cssAPI.normalize = function(name, normalize) {
    if (name.substr(name.length - 4, 4) == '.css')
      name = name.substr(0, name.length - 4);
    
    return normalize(name);
  }
  
  var waitSeconds;
  var alerted = false;
  cssAPI.load = function(cssId, req, load, config, parse) {
    
    waitSeconds = waitSeconds || config.waitSeconds || 7;

    var resourceId = cssId + (!parse ? '.css' : '.less');

    // attach the load function to a buffer if there is one in registration
    // if not, we do a full injection load
    if (cssAPI.attachBuffer(resourceId, load))
      return;

    fileUrl = req.toUrl(resourceId);
    
    if (!alerted && testing) {
      alert(hackLinks ? 'hacking links' : 'not hacking');
      alerted = true;
    }

    if (!parse) {
      cssAPI.linkLoad(fileUrl, load);
    }
    else {
      loadCSS(fileUrl, function(css) {
        // run parsing after normalization - since less is a CSS subset this works fine
        if (parse)
          css = parse(css, function(css) {
            cssAPI.inject(css);
            setTimeout(load, 7);
          });
      });
    }
  }

  if (testing)
    cssAPI.inspect = function() {
      if (stylesheet.styleSheet)
        return stylesheet.styleSheet.cssText;
      else if (stylesheet.innerHTML)
        return stylesheet.innerHTML;
    }
  
  return cssAPI;
});
