/**
 * Module dependencies
 */

var serialize = require('dom-serializer'),
    defaultOptions = require('./options').default,
    flattenOptions = require('./options').flatten,
    select = require('css-select'),
    parse = require('./parse'),
    _ = {
      merge: require('lodash/merge'),
      defaults: require('lodash/defaults')
    };

/**
 * $.load(str)
 */

exports.load = function(content, options, isDocument) {
  var Cheerio = require('./cheerio');

  options = _.defaults(flattenOptions(options || {}), defaultOptions);

  if (isDocument === void 0)
    isDocument = true;

  var root = parse(content, options, isDocument);

  var initialize = function(selector, context, r, opts) {
    if (!(this instanceof initialize)) {
      return new initialize(selector, context, r, opts);
    }
    opts = _.defaults(opts || {}, options);
    return Cheerio.call(this, selector, context, r || root, opts);
  };

  // Ensure that selections created by the "loaded" `initialize` function are
  // true Cheerio instances.
  initialize.prototype = Object.create(Cheerio.prototype);
  initialize.prototype.constructor = initialize;

  // Mimic jQuery's prototype alias for plugin authors.
  initialize.fn = initialize.prototype;

  // Keep a reference to the top-level scope so we can chain methods that implicitly
  // resolve selectors; e.g. $("<span>").(".bar"), which otherwise loses ._root
  initialize.prototype._originalRoot = root;

  // Add in the static methods
  _.merge(initialize, exports);

  // Add in the root
  initialize._root = root;
  // store options
  initialize._options = options;

  return initialize;
};

/*
* Helper function
*/

function render(that, dom, options) {
  if (!dom) {
    if (that._root && that._root.children) {
      dom = that._root.children;
    } else {
      return '';
    }
  } else if (typeof dom === 'string') {
    dom = select(dom, that._root, options);
  }

  return serialize(dom, options);
}

/**
 * $.html([selector | dom], [options])
 */

exports.html = function(dom, options) {
  // be flexible about parameters, sometimes we call html(),
  // with options as only parameter
  // check dom argument for dom element specific properties
  // assume there is no 'length' or 'type' properties in the options object
  if (Object.prototype.toString.call(dom) === '[object Object]' && !options && !('length' in dom) && !('type' in dom))
  {
    options = dom;
    dom = undefined;
  }

  // sometimes $.html() used without preloading html
  // so fallback non existing options to the default ones
  options = _.defaults(flattenOptions(options || {}), this._options, defaultOptions);

  return render(this, dom, options);
};

/**
 * $.xml([selector | dom])
 */

exports.xml = function(dom) {
  var options = _.defaults({xml: true}, this._options);

  return render(this, dom, options);
};

/**
 * $.text(dom)
 */

exports.text = function(elems) {
  if (!elems) {
    elems = this.root();
  }

  var ret = '',
      len = elems.length,
      elem;

  for (var i = 0; i < len; i++) {
    elem = elems[i];
    if (elem.type === 'text') ret += elem.data;
    else if (elem.children && elem.type !== 'comment' && elem.tagName !== 'script' && elem.tagName !== 'style') {
      ret += exports.text(elem.children);
    }
  }

  return ret;
};

/**
 * $.parseHTML(data [, context ] [, keepScripts ])
 * Parses a string into an array of DOM nodes. The `context` argument has no
 * meaning for Cheerio, but it is maintained for API compatibility with jQuery.
 */
exports.parseHTML = function(data, context, keepScripts) {
  var parsed;

  if (!data || typeof data !== 'string') {
    return null;
  }

  if (typeof context === 'boolean') {
    keepScripts = context;
  }

  parsed = this.load(data, defaultOptions, false);
  if (!keepScripts) {
    parsed('script').remove();
  }

  // The `children` array is used by Cheerio internally to group elements that
  // share the same parents. When nodes created through `parseHTML` are
  // inserted into previously-existing DOM structures, they will be removed
  // from the `children` array. The results of `parseHTML` should remain
  // constant across these operations, so a shallow copy should be returned.
  return parsed.root()[0].children.slice();
};

/**
 * $.root()
 */
exports.root = function() {
  return this(this._root);
};

/**
 * $.contains()
 */
exports.contains = function(container, contained) {

  // According to the jQuery API, an element does not "contain" itself
  if (contained === container) {
    return false;
  }

  // Step up the descendants, stopping when the root element is reached
  // (signaled by `.parent` returning a reference to the same object)
  while (contained && contained !== contained.parent) {
    contained = contained.parent;
    if (contained === container) {
      return true;
    }
  }

  return false;
};

/**
 * $.merge()
 */

exports.merge = function(arr1, arr2) {
  if(!(isArrayLike(arr1) && isArrayLike(arr2))){
    return;
  }
  var newLength = arr1.length + arr2.length;
  var i = 0;
  while(i < arr2.length){
    arr1[i + arr1.length] = arr2[i];
    i++;
  }
  arr1.length = newLength;
  return arr1;
};

function isArrayLike(item){
  if(Array.isArray(item)){
    return true;
  }
  if(typeof item !== 'object'){
    return false;
  }
  if(!item.hasOwnProperty('length')){
    return false;
  }
  if(typeof item.length !== 'number') {
    return false;
  }
  if(item.length < 0){
    return false;
  }
  var i = 0;
  while(i < item.length){
    if(!(i in item)){
      return false;
    }
    i++;
  }
  return true;
}
