import RegisterLoader from 'es-module-loader/core/register-loader.js';
import { InternalModuleNamespace as ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js';

import { baseURI, global, isBrowser } from 'es-module-loader/core/common.js';
import { resolveIfNotPlain } from 'es-module-loader/core/resolve.js';

var loader;

// <script type="module"> support
var anonSources = {};
if (typeof document != 'undefined' && document.getElementsByTagName) {
  var handleError = function(err) {
    // dispatch an error event so that we can display in errors in browsers
    // that don't yet support unhandledrejection
    if (window.onunhandledrejection === undefined) {
      try {
        var evt = new Event('error');
      } catch (_eventError) {
        var evt = document.createEvent('Event');
        evt.initEvent('error', true, true);
      }
      evt.message = err.message;
      if (err.fileName) {
        evt.filename = err.fileName;
        evt.lineno = err.lineNumber;
        evt.colno = err.columnNumber;
      } else if (err.sourceURL) {
        evt.filename = err.sourceURL;
        evt.lineno = err.line;
        evt.colno = err.column;
      }
      evt.error = err;
      window.dispatchEvent(evt);
    }

    // throw so it still shows up in the console
    throw err;
  }

  var ready = function() {
    document.removeEventListener('DOMContentLoaded', ready, false );

    var anonCnt = 0;

    var scripts = document.getElementsByTagName('script');
    for (var i = 0; i < scripts.length; i++) {
      var script = scripts[i];
      if (script.type == 'module' && !script.loaded) {
        script.loaded = true;
        if (script.src) {
          loader.import(script.src).catch(handleError);
        }
        // anonymous modules supported via a custom naming scheme and registry
        else {
          var uri = './<anon' + ++anonCnt + '>.js';
          if (script.id !== ""){
            uri = "./" + script.id;
          }

          var anonName = resolveIfNotPlain(uri, baseURI);
          anonSources[anonName] = script.innerHTML;
          loader.import(anonName).catch(handleError);
        }
      }
    }
  }

  // simple DOM ready
  if (document.readyState === 'complete')
    setTimeout(ready);
  else
    document.addEventListener('DOMContentLoaded', ready, false);
}

function BrowserESModuleLoader(baseKey) {
  if (baseKey)
    this.baseKey = resolveIfNotPlain(baseKey, baseURI) || resolveIfNotPlain('./' + baseKey, baseURI);

  RegisterLoader.call(this);

  var loader = this;

  // ensure System.register is available
  global.System = global.System || {};
  if (typeof global.System.register == 'function')
    var prevRegister = global.System.register;
  global.System.register = function() {
    loader.register.apply(loader, arguments);
    if (prevRegister)
      prevRegister.apply(this, arguments);
  };
}
BrowserESModuleLoader.prototype = Object.create(RegisterLoader.prototype);

// normalize is never given a relative name like "./x", that part is already handled
BrowserESModuleLoader.prototype[RegisterLoader.resolve] = function(key, parent) {
  var resolved = RegisterLoader.prototype[RegisterLoader.resolve].call(this, key, parent || this.baseKey) || key;
  if (!resolved)
    throw new RangeError('ES module loader does not resolve plain module names, resolving "' + key + '" to ' + parent);

  return resolved;
};

function xhrFetch(url, resolve, reject) {
  var xhr = new XMLHttpRequest();
  var load = function(source) {
    resolve(xhr.responseText);
  }
  var error = function() {
    reject(new Error('XHR error' + (xhr.status ? ' (' + xhr.status + (xhr.statusText ? ' ' + xhr.statusText  : '') + ')' : '') + ' loading ' + url));
  }

  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      // in Chrome on file:/// URLs, status is 0
      if (xhr.status == 0) {
        if (xhr.responseText) {
          load();
        }
        else {
          // when responseText is empty, wait for load or error event
          // to inform if it is a 404 or empty file
          xhr.addEventListener('error', error);
          xhr.addEventListener('load', load);
        }
      }
      else if (xhr.status === 200) {
        load();
      }
      else {
        error();
      }
    }
  };
  xhr.open("GET", url, true);
  xhr.send(null);
}

var WorkerPool = function (script, size) {
  var current = document.currentScript;
  // IE doesn't support currentScript
  if (!current) {
    // We should be the last loaded script
    var scripts = document.getElementsByTagName('script');
    current = scripts[scripts.length - 1];
  }
  script = current.src.substr(0, current.src.lastIndexOf("/")) + "/" + script;
  this._workers = new Array(size);
  this._ind = 0;
  this._size = size;
  this._jobs = 0;
  this.onmessage = undefined;
  this._stopTimeout = undefined;
  for (var i = 0; i < size; i++) {
    var wrkr = new Worker(script);
    wrkr._count = 0;
    wrkr._ind = i;
    wrkr.onmessage = this._onmessage.bind(this, wrkr);
    wrkr.onerror = this._onerror.bind(this);
    this._workers[i] = wrkr;
  }

  this._checkJobs();
};
WorkerPool.prototype = {
  postMessage: function (msg) {
    if (this._stopTimeout !== undefined) {
      clearTimeout(this._stopTimeout);
      this._stopTimeout = undefined;
    }
    var wrkr = this._workers[this._ind % this._size];
    wrkr._count++;
    this._jobs++;
    wrkr.postMessage(msg);
    this._ind++;
  },

  _onmessage: function (wrkr, evt) {
    wrkr._count--;
    this._jobs--;
    this.onmessage(evt, wrkr);
    this._checkJobs();
  },

  _onerror: function(err) {
    try {
        var evt = new Event('error');
    } catch (_eventError) {
        var evt = document.createEvent('Event');
        evt.initEvent('error', true, true);
    }
    evt.message = err.message;
    evt.filename = err.filename;
    evt.lineno = err.lineno;
    evt.colno = err.colno;
    evt.error = err.error;
    window.dispatchEvent(evt);
  },

  _checkJobs: function () {
    if (this._jobs === 0 && this._stopTimeout === undefined) {
      // wait for 2s of inactivity before stopping (that should be enough for local loading)
      this._stopTimeout = setTimeout(this._stop.bind(this), 2000);
    }
  },

  _stop: function () {
    this._workers.forEach(function(wrkr) {
      wrkr.terminate();
    });
  }
};

var promiseMap = new Map();
var babelWorker = new WorkerPool('babel-worker.js', 3);
babelWorker.onmessage = function (evt) {
    var promFuncs = promiseMap.get(evt.data.key);
    promFuncs.resolve(evt.data);
    promiseMap.delete(evt.data.key);
};

// instantiate just needs to run System.register
// so we fetch the source, convert into the Babel System module format, then evaluate it
BrowserESModuleLoader.prototype[RegisterLoader.instantiate] = function(key, processAnonRegister) {
  var loader = this;

  // load as ES with Babel converting into System.register
  return new Promise(function(resolve, reject) {
    // anonymous module
    if (anonSources[key]) {
      resolve(anonSources[key])
      anonSources[key] = undefined;
    }
    // otherwise we fetch
    else {
      xhrFetch(key, resolve, reject);
    }
  })
  .then(function(source) {
    // check our cache first
    var cacheEntry = localStorage.getItem(key);
    if (cacheEntry) {
      cacheEntry = JSON.parse(cacheEntry);
      // TODO: store a hash instead
      if (cacheEntry.source === source) {
        return Promise.resolve({key: key, code: cacheEntry.code, source: cacheEntry.source});
      }
    }
    return new Promise(function (resolve, reject) {
      promiseMap.set(key, {resolve: resolve, reject: reject});
      babelWorker.postMessage({key: key, source: source});
    });
  }).then(function (data) {
    // evaluate without require, exports and module variables
    // we leave module in for now to allow module.require access
    try {
      var cacheEntry = JSON.stringify({source: data.source, code: data.code});
      localStorage.setItem(key, cacheEntry);
    } catch (e) {
      if (window.console) {
        window.console.warn('Unable to cache transpiled version of ' + key + ': ' + e);
      }
    }
    (0, eval)(data.code + '\n//# sourceURL=' + data.key + '!transpiled');
    processAnonRegister();
  });
};

// create a default loader instance in the browser
if (isBrowser)
  loader = new BrowserESModuleLoader();

export default BrowserESModuleLoader;
