var select = require('css-select'),

utils = require('../utils'),
domEach = utils.domEach,
uniqueSort = require('htmlparser2').DomUtils.uniqueSort,
isTag = utils.isTag,
_ = {
  bind: require('lodash.bind'),
  forEach: require('lodash.foreach'),
  reject: require('lodash.reject'),
  filter: require('lodash.filter'),
  reduce: require('lodash.reduce')
};

exports.find = function(selectorOrHaystack) {

var elems = _.reduce(this, function(memo, elem) {
  return memo.concat(_.filter(elem.children, isTag));
}, []);
var contains = this.constructor.contains;
var haystack;

if (selectorOrHaystack && typeof selectorOrHaystack !== 'string') {
  if (selectorOrHaystack.cheerio) {
    haystack = selectorOrHaystack.get();
  } else {
    haystack = [selectorOrHaystack];
  }

  return this._make(haystack.filter(function(elem) {
    var idx, len;
    for (idx = 0, len = this.length; idx < len; ++idx) {
      if (contains(this[idx], elem)) {
        return true;
      }
    }
  }, this));
}

var options = {__proto__: this.options, context: this.toArray()};

return this._make(select(selectorOrHaystack, elems, options));

};

// Get the parent of each element in the current set of matched elements, // optionally filtered by a selector. exports.parent = function(selector) {

var set = [];

domEach(this, function(idx, elem) {
  var parentElem = elem.parent;
  if (parentElem && set.indexOf(parentElem) < 0) {
    set.push(parentElem);
  }
});

if (arguments.length) {
  set = exports.filter.call(set, selector, this);
}

return this._make(set);

};

exports.parents = function(selector) {

var parentNodes = [];

// When multiple DOM elements are in the original set, the resulting set will
// be in *reverse* order of the original elements as well, with duplicates
// removed.
this.get().reverse().forEach(function(elem) {
  traverseParents(this, elem.parent, selector, Infinity)
    .forEach(function(node) {
      if (parentNodes.indexOf(node) === -1) {
        parentNodes.push(node);
      }
    }
  );
}, this);

return this._make(parentNodes);

};

exports.parentsUntil = function(selector, filter) {

var parentNodes = [], untilNode, untilNodes;

if (typeof selector === 'string') {
  untilNode = select(selector, this.parents().toArray(), this.options)[0];
} else if (selector && selector.cheerio) {
  untilNodes = selector.toArray();
} else if (selector) {
  untilNode = selector;
}

// When multiple DOM elements are in the original set, the resulting set will
// be in *reverse* order of the original elements as well, with duplicates
// removed.

this.toArray().reverse().forEach(function(elem) {
  while ((elem = elem.parent)) {
    if ((untilNode && elem !== untilNode) ||
      (untilNodes && untilNodes.indexOf(elem) === -1) ||
      (!untilNode && !untilNodes)) {
      if (isTag(elem) && parentNodes.indexOf(elem) === -1) { parentNodes.push(elem); }
    } else {
      break;
    }
  }
}, this);

return this._make(filter ? select(filter, parentNodes, this.options) : parentNodes);

};

// For each element in the set, get the first element that matches the selector // by testing the element itself and traversing up through its ancestors in the // DOM tree. exports.closest = function(selector) {

var set = [];

if (!selector) {
  return this._make(set);
}

domEach(this, function(idx, elem) {
  var closestElem = traverseParents(this, elem, selector, 1)[0];

  // Do not add duplicate elements to the set
  if (closestElem && set.indexOf(closestElem) < 0) {
    set.push(closestElem);
  }
}.bind(this));

return this._make(set);

};

exports.next = function(selector) {

if (!this[0]) { return this; }
var elems = [];

_.forEach(this, function(elem) {
  while ((elem = elem.next)) {
    if (isTag(elem)) {
      elems.push(elem);
      return;
    }
  }
});

return selector ?
  exports.filter.call(elems, selector, this) :
  this._make(elems);

};

exports.nextAll = function(selector) {

if (!this[0]) { return this; }
var elems = [];

_.forEach(this, function(elem) {
  while ((elem = elem.next)) {
    if (isTag(elem) && elems.indexOf(elem) === -1) {
      elems.push(elem);
    }
  }
});

return selector ?
  exports.filter.call(elems, selector, this) :
  this._make(elems);

};

exports.nextUntil = function(selector, filterSelector) {

if (!this[0]) { return this; }
var elems = [], untilNode, untilNodes;

if (typeof selector === 'string') {
  untilNode = select(selector, this.nextAll().get(), this.options)[0];
} else if (selector && selector.cheerio) {
  untilNodes = selector.get();
} else if (selector) {
  untilNode = selector;
}

_.forEach(this, function(elem) {
  while ((elem = elem.next)) {
    if ((untilNode && elem !== untilNode) ||
      (untilNodes && untilNodes.indexOf(elem) === -1) ||
      (!untilNode && !untilNodes)) {
      if (isTag(elem) && elems.indexOf(elem) === -1) {
        elems.push(elem);
      }
    } else {
      break;
    }
  }
});

return filterSelector ?
  exports.filter.call(elems, filterSelector, this) :
  this._make(elems);

};

exports.prev = function(selector) {

if (!this[0]) { return this; }
var elems = [];

_.forEach(this, function(elem) {
  while ((elem = elem.prev)) {
    if (isTag(elem)) {
      elems.push(elem);
      return;
    }
  }
});

return selector ?
  exports.filter.call(elems, selector, this) :
  this._make(elems);

};

exports.prevAll = function(selector) {

if (!this[0]) { return this; }
var elems = [];

_.forEach(this, function(elem) {
  while ((elem = elem.prev)) {
    if (isTag(elem) && elems.indexOf(elem) === -1) {
      elems.push(elem);
    }
  }
});

return selector ?
  exports.filter.call(elems, selector, this) :
  this._make(elems);

};

exports.prevUntil = function(selector, filterSelector) {

if (!this[0]) { return this; }
var elems = [], untilNode, untilNodes;

if (typeof selector === 'string') {
  untilNode = select(selector, this.prevAll().get(), this.options)[0];
} else if (selector && selector.cheerio) {
  untilNodes = selector.get();
} else if (selector) {
  untilNode = selector;
}

_.forEach(this, function(elem) {
  while ((elem = elem.prev)) {
    if ((untilNode && elem !== untilNode) ||
      (untilNodes && untilNodes.indexOf(elem) === -1) ||
      (!untilNode && !untilNodes)) {
      if (isTag(elem) && elems.indexOf(elem) === -1) {
        elems.push(elem);
      }
    } else {
      break;
    }
  }
});

return filterSelector ?
  exports.filter.call(elems, filterSelector, this) :
  this._make(elems);

};

exports.siblings = function(selector) {

var parent = this.parent();

var elems = _.filter(
  parent ? parent.children() : this.siblingsAndMe(),
  _.bind(function(elem) { return isTag(elem) && !this.is(elem); }, this)
);

if (selector !== undefined) {
  return exports.filter.call(elems, selector, this);
} else {
  return this._make(elems);
}

};

exports.children = function(selector) {

var elems = _.reduce(this, function(memo, elem) {
  return memo.concat(_.filter(elem.children, isTag));
}, []);

if (selector === undefined) return this._make(elems);

return exports.filter.call(elems, selector, this);

};

exports.contents = function() {

return this._make(_.reduce(this, function(all, elem) {
  all.push.apply(all, elem.children);
  return all;
}, []));

};

exports.each = function(fn) {

var i = 0, len = this.length;
while (i < len && fn.call(this[i], i, this[i]) !== false) ++i;
return this;

};

exports.map = function(fn) {

return this._make(_.reduce(this, function(memo, el, i) {
  var val = fn.call(el, i, el);
  return val == null ? memo : memo.concat(val);
}, []));

};

var makeFilterMethod = function(filterFn) {

return function(match, container) {
  var testFn;
  container = container || this;

  if (typeof match === 'string') {
    testFn = select.compile(match, container.options);
  } else if (typeof match === 'function') {
    testFn = function(el, i) {
      return match.call(el, i, el);
    };
  } else if (match.cheerio) {
    testFn = match.is.bind(match);
  } else {
    testFn = function(el) {
      return match === el;
    };
  }

  return container._make(filterFn(this, testFn));
};

};

exports.filter = makeFilterMethod(_.filter); exports.not = makeFilterMethod(_.reject);

exports.has = function(selectorOrHaystack) {

var that = this;
return exports.filter.call(this, function() {
  return that._make(this).find(selectorOrHaystack).length > 0;
});

};

exports.first = function() {

return this.length > 1 ? this._make(this[0]) : this;

};

exports.last = function() {

return this.length > 1 ? this._make(this[this.length - 1]) : this;

};

// Reduce the set of matched elements to the one at the specified index. exports.eq = function(i) {

i = +i;

// Use the first identity optimization if possible
if (i === 0 && this.length <= 1) return this;

if (i < 0) i = this.length + i;
return this[i] ? this._make(this[i]) : this._make([]);

};

// Retrieve the DOM elements matched by the jQuery object. exports.get = function(i) {

if (i == null) {
  return Array.prototype.slice.call(this);
} else {
  return this[i < 0 ? (this.length + i) : i];
}

};

// Search for a given element from among the matched elements. exports.index = function(selectorOrNeedle) {

var $haystack, needle;

if (arguments.length === 0) {
  $haystack = this.parent().children();
  needle = this[0];
} else if (typeof selectorOrNeedle === 'string') {
  $haystack = this._make(selectorOrNeedle);
  needle = this[0];
} else {
  $haystack = this;
  needle = selectorOrNeedle.cheerio ? selectorOrNeedle[0] : selectorOrNeedle;
}

return $haystack.get().indexOf(needle);

};

exports.slice = function() {

return this._make([].slice.apply(this, arguments));

};

function traverseParents(self, elem, selector, limit) {

var elems = [];
while (elem && elems.length < limit) {
  if (!selector || exports.filter.call([elem], selector, self).length) {
    elems.push(elem);
  }
  elem = elem.parent;
}
return elems;

}

// End the most recent filtering operation in the current chain and return the // set of matched elements to its previous state. exports.end = function() {

return this.prevObject || this._make([]);

};

exports.add = function(other, context) {

var selection = this._make(other, context);
var contents = uniqueSort(selection.get().concat(this.get()));

for (var i = 0; i < contents.length; ++i) {
  selection[i] = contents[i];
}
selection.length = contents.length;

return selection;

};

// Add the previous set of elements on the stack to the current set, optionally // filtered by a selector. exports.addBack = function(selector) {

return this.add(
  arguments.length ? this.prevObject.filter(selector) : this.prevObject
);

};