var fs = require('fs');
var path = require('path');
var URL = require('url');
var pkg = require('../package.json');

var toFileUrl = require('./jsdom/utils').toFileUrl;
var defineGetter = require('./jsdom/utils').defineGetter;
var defineSetter = require('./jsdom/utils').defineSetter;
var style = require('./jsdom/level2/style');
var features = require('./jsdom/browser/documentfeatures');
var dom = exports.dom = require('./jsdom/living/index').dom;
var createWindow = exports.createWindow = require('./jsdom/browser/index').createWindow;


var request = function(options, cb) {
  request = require('request');
  return request(options, cb);
}

exports.defaultLevel = dom.living.html;
exports.browserAugmentation = require('./jsdom/browser/index').browserAugmentation;
exports.windowAugmentation = require('./jsdom/browser/index').windowAugmentation;

// Proxy feature functions to features module.
['availableDocumentFeatures',
 'defaultDocumentFeatures',
 'applyDocumentFeatures'].forEach(function (propName) {
  defineGetter(exports, propName, function () {
    return features[propName];
  });
  defineSetter(exports, propName, function (val) {
    return features[propName] = val;
  });
});

exports.debugMode = false;

defineGetter(exports, 'version', function() {
  return pkg.version;
});

exports.level = function (level, feature) {
  if(!feature) {
    feature = 'core';
  }

  if (String(level) === '1' || String(level) === '2' || String(level) === '3') {
    level = 'level' + level;
  }

  return require('./jsdom/' + level + '/' + feature).dom[level][feature];
};

exports.jsdom = function (html, level, options) {

  options = options || {};
  if(typeof level == 'string') {
    level = exports.level(level, 'html');
  } else {
    level   = level || exports.defaultLevel;
  }

  if (!options.url) {
    options.url = (module.parent.id === 'jsdom') ?
                  module.parent.parent.filename  :
                  module.parent.filename;
    options.url = options.url.replace(/\\/g, '/');
    if (options.url[0] !== '/') {
      options.url = '/' + options.url;
    }
    options.url = 'file://' + options.url;
  }

  var browser = exports.browserAugmentation(level, options),
      doc     = (browser.HTMLDocument)             ?
                 new browser.HTMLDocument(options) :
                 new browser.Document(options);

  require('./jsdom/selectors/index').applyQuerySelectorPrototype(level);

  features.applyDocumentFeatures(doc, options.features);

  if (typeof html === 'undefined' || html === null ||
      (html.trim && !html.trim())) {
    doc.write('<html><head></head><body></body></html>');
  } else {
    doc.write(html + '');
  }

  if (doc.close && !options.deferClose) {
    doc.close();
  }

  // Kept for backwards-compatibility. The window is lazily created when
  // document.parentWindow or document.defaultView is accessed.
  doc.createWindow = function() {
    // Remove ourself
    if (doc.createWindow) {
      delete doc.createWindow;
    }
    return doc.parentWindow;
  };

  return doc;
};

exports.html = function(html, level, options) {
  html += '';

  // TODO: cache a regex and use it here instead
  //       or make the parser handle it
  var htmlLowered = html.toLowerCase();

  // body
  if (!~htmlLowered.indexOf('<body')) {
    html = '<body>' + html + '</body>';
  }

  // html
  if (!~htmlLowered.indexOf('<html')) {
    html = '<html>' + html + '</html>';
  }
  return exports.jsdom(html, level, options);
};

exports.jQueryify = exports.jsdom.jQueryify = function (window /* path [optional], callback */) {

  if (!window || !window.document) { return; }

  var args = Array.prototype.slice.call(arguments),
      callback = (typeof(args[args.length - 1]) === 'function') && args.pop(),
      path,
      jQueryTag = window.document.createElement('script');
      jQueryTag.className = 'jsdom';

  if (args.length > 1 && typeof args[1] === 'string') {
    path = args[1];
  }

  var features = window.document.implementation._features;

  window.document.implementation.addFeature('FetchExternalResources', ['script']);
  window.document.implementation.addFeature('ProcessExternalResources', ['script']);
  window.document.implementation.addFeature('MutationEvents', ['2.0']);
  jQueryTag.src = path || 'http://code.jquery.com/jquery.js';
  window.document.body.appendChild(jQueryTag);

  jQueryTag.onload = function() {
    if (callback) {
      callback(window, window.jQuery);
    }

    window.document.implementation._features = features;
  };

  return window;
};


exports.env = exports.jsdom.env = function () {
  var config = getConfigFromArguments(arguments);
  var callback = config.done;

  if (config.file) {
    fs.readFile(config.file, 'utf-8', function (err, text) {
      if (err) {
        return callback(err);
      }

      config.html = text;
      processHTML(config);
    });
  } else if (config.html) {
    processHTML(config);
  } else if (config.url) {
    handleUrl(config);
  } else if (config.somethingToAutodetect) {
    var url = URL.parse(config.somethingToAutodetect);
    if (url.protocol && url.hostname) {
      config.url = config.somethingToAutodetect;
      handleUrl(config.somethingToAutodetect);
    } else {
      fs.readFile(config.somethingToAutodetect, 'utf-8', function (err, text) {
        if (err) {
          if (err.code === 'ENOENT' || err.code === 'ENAMETOOLONG') {
            config.html = config.somethingToAutodetect;
            processHTML(config);
          } else {
            callback(err);
          }
        } else {
          config.html = text;
          config.url = toFileUrl(config.somethingToAutodetect);
          processHTML(config);
        }
      });
    }
  }

  function handleUrl() {
    var options = {
      uri: config.url,
      encoding: config.encoding || 'utf8',
      headers: config.headers || {},
      proxy: config.proxy || null,
      jar: config.jar !== undefined ? config.jar : true
    };

    request(options, function (err, res, responseText) {
      if (err) {
        return callback(err);
      }

      // The use of `res.request.uri.href` ensures that `window.location.href`
      // is updated when `request` follows redirects.
      config.html = responseText;
      config.url = res.request.uri.href;
      processHTML(config);
    });
  }
};

function processHTML(config) {
  var callback = config.done;
  var options = {
    features: config.features,
    url: config.url,
    parser: config.parser
  };

  if (config.document) {
    options.referrer = config.document.referrer;
    options.cookie = config.document.cookie;
    options.cookieDomain = config.document.cookieDomain;
  }

  var window = exports.html(config.html, null, options).createWindow();
  var features = JSON.parse(JSON.stringify(window.document.implementation._features));

  var docsLoaded = 0;
  var totalDocs = config.scripts.length + config.src.length;
  var readyState = null;
  var errors = [];

  if (!window || !window.document) {
    return callback(new Error('JSDOM: a window object could not be created.'));
  }

  window.document.implementation.addFeature('FetchExternalResources', ['script']);
  window.document.implementation.addFeature('ProcessExternalResources', ['script']);
  window.document.implementation.addFeature('MutationEvents', ['2.0']);

  function scriptComplete() {
    docsLoaded++;

    if (docsLoaded >= totalDocs) {
      window.document.implementation._features = features;

      errors = errors.concat(window.document.errors || []);
      if (errors.length === 0) {
        errors = null;
      }

      process.nextTick(function() {
        callback(errors, window);
      });
    }
  }

  function handleScriptError(e) {
    if (!errors) {
      errors = [];
    }
    errors.push(e.error || e.message);

    // nextTick so that an exception within scriptComplete won't cause
    // another script onerror (which would be an infinite loop)
    process.nextTick(scriptComplete);
  }

  if (config.scripts.length > 0 || config.src.length > 0) {
    config.scripts.forEach(function (scriptSrc) {
      var script = window.document.createElement('script');
      script.className = 'jsdom';
      script.onload = scriptComplete;
      script.onerror = handleScriptError;
      script.src = scriptSrc;

      try {
        // protect against invalid dom
        // ex: http://www.google.com/foo#bar
        window.document.documentElement.appendChild(script);
      } catch (e) {
        handleScriptError(e);
      }
    });

    config.src.forEach(function (scriptText) {
      var script = window.document.createElement('script');
      script.onload = scriptComplete;
      script.onerror = handleScriptError;
      script.text = scriptText;

      window.document.documentElement.appendChild(script);
      window.document.documentElement.removeChild(script);
    });
  } else {
    scriptComplete();
  }
}

function getConfigFromArguments(args, callback) {
  var config = {};
  if (typeof args[0] === 'object') {
    var configToClone = args[0];
    Object.keys(configToClone).forEach(function (key) {
      config[key] = configToClone[key];
    });
  } else {
    var stringToAutodetect = null;

    Array.prototype.forEach.call(args, function (arg) {
      switch (typeof arg) {
        case 'string':
          config.somethingToAutodetect = arg;
          break;
        case 'function':
          config.done = arg;
          break;
        case 'object':
          if (Array.isArray(arg)) {
            config.scripts = arg;
          } else {
            extend(config, arg);
          }
          break;
      }
    });
  }

  if (!config.done) {
    throw new Error('Must pass a "done" option or a callback to jsdom.env.');
  }

  if (!config.somethingToAutodetect && !config.html && !config.file && !config.url) {
    throw new Error('Must pass a "html", "file", or "url" option, or a string, to jsdom.env');
  }

  config.scripts = ensureArray(config.scripts);
  config.src = ensureArray(config.src);

  config.features = config.features || {
    FetchExternalResources: false,
    ProcessExternalResources: false,
    SkipExternalResources: false
  };

  if (!config.url && config.file) {
    config.url = toFileUrl(config.file);
  }

  return config;
}

function ensureArray(value) {
  var array = value || [];
  if (typeof array === 'string') {
    array = [array];
  }
  return array;
}

function extend(config, overrides) {
  Object.keys(overrides).forEach(function (key) {
    config[key] = overrides[key];
  });
}
