var _ = require('lodash'),
    select = require('CSSselect'),
    utils = require('../utils'),
    domEach = utils.domEach,
    isTag = utils.isTag;

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

  return this._make(select(selector, elems));
};

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

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

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

  return this._make(set);
};

var parents = 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);
};

var parentsUntil = exports.parentsUntil = function(selector, filter) {
  var parentNodes = [], untilNode, untilNodes;

  if (typeof selector === 'string') {
    untilNode = select(selector, this.parents().toArray())[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) : 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.
var closest = 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);
};

var next = 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 ? filter.call(elems, selector, this) : this._make(elems);
};

var nextAll = 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 ? filter.call(elems, selector, this) : this._make(elems);
};

var nextUntil = exports.nextUntil = function(selector, filterSelector) {
  if (!this[0]) { return this; }
  var elems = [], untilNode, untilNodes;

  if (typeof selector === 'string') {
    untilNode = select(selector, this.nextAll().get())[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 ?
    filter.call(elems, filterSelector, this) :
    this._make(elems);
};

var prev = 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 ? filter.call(elems, selector, this) : this._make(elems);
};

var prevAll = 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 ? filter.call(elems, selector, this) : this._make(elems);
};

var prevUntil = exports.prevUntil = function(selector, filterSelector) {
  if (!this[0]) { return this; }
  var elems = [], untilNode, untilNodes;

  if (typeof selector === 'string') {
    untilNode = select(selector, this.prevAll().get())[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 ?
    filter.call(elems, filterSelector, this) :
    this._make(elems);
};

var siblings = exports.siblings = function(selector) {
  var parent = this.parent();

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

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

var children = 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);
  else if (_.isNumber(selector)) return this._make(elems[selector]);

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

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

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

var map = 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 filter = exports.filter = function(match, container) {
  container = container || this;

  var make = _.bind(container._make, container);
  var filterFn;

  if (_.isString(match)) {
    filterFn = select.compile(match);
  } else if (_.isFunction(match)) {
    filterFn = function(el, i) {
      return match.call(el, i, el);
    };
  } else if (match.cheerio) {
    filterFn = match.is.bind(match);
  } else {
    filterFn = function(el) {
      return match === el;
    };
  }

  return make(_.filter(this, filterFn));
};

var first = exports.first = function() {
  return this.length > 1 ? this._make(this[0]) : this;
};

var last = 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.
var eq = 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.
var get = exports.get = function(i) {
  if (i == null) {
    return Array.prototype.slice.call(this);
  } else {
    return this[i < 0 ? (this.length + i) : i];
  }
};

var slice = 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 || 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.
var end = exports.end = function() {
  return this.prevObject || this._make([]);
};
