/**
 * Linked list.
 *
 * @example
 *
 * var LL = require('path-to-algorithms/src/data-structures/linked-list');
 *
 * var linkedList = new LL.LinkedList();
 *
 * linkedList.push({
 *   name: 'John',
 *   birthyear: 1981
 * });
 * linkedList.push({
 *   name: 'Pavlo',
 *   birthyear: 2000
 * });
 * linkedList.push({
 *   name: 'Garry',
 *   birthyear: 1989
 * });
 * linkedList.push({
 *   name: 'Derek',
 *   birthyear: 1990
 * });
 * linkedList.push({
 *   name: 'Ivan',
 *   birthyear: 1966
 * });
 *
 * console.log(linkedList.shift().data); // { name: 'John', birthyear: 1981 }
 * console.log(linkedList.pop().data);   // { name: 'Ivan', birthyear: 1966 }
 *
 * @module data-structures/linked-list
 */
(function (exports) {

  'use strict';

  /**
   * Linked list node.
   *
   * @public
   * @constructor
   * @param {Object} data Data of the node.
   */
  exports.Node = function (data) {
    /**
     * Data of the node.
     * @member {Object}
     */
    this.data = data;
    /**
     * Next node.
     * @member {Node}
     */
    this.next = null;
    /**
     * Previous node.
     * @member {Node}
     */
    this.prev = null;
  };

  /**
   * Linked list.
   *
   * @public
   * @constructor
   */
  exports.LinkedList = function () {
    this.first = null;
    this.last = null;
  };

  /**
   * Add data to the end of linked list.
   *
   * @public
   * @method
   * @param {Object} data Data which should be added.
   */
  exports.LinkedList.prototype.push = function (data) {
    var node = new exports.Node(data);
    if (this.first === null) {
      this.first = this.last = node;
    } else {
      var temp = this.last;
      this.last = node;
      node.prev = temp;
      temp.next = node;
    }
  };

  /**
   * Add data to the beginning of linked list.
   *
   * @public
   * @method
   * @param {Object} data Data which should be added.
   */
  exports.LinkedList.prototype.unshift = function (data) {
    var node = new exports.Node(data);
    if (this.first === null) {
      this.first = this.last = node;
    } else {
      var temp = this.first;
      this.first = node;
      node.next = temp;
      temp.prev = node;
    }
  };

  /**
   * In order traversal of the linked list.
   *
   * @public
   * @method
   * @param {Function} cb Callback which should be executed on each node.
   */
  exports.LinkedList.prototype.inorder = function (cb) {
    var temp = this.first;
    while (temp) {
      cb(temp);
      temp = temp.next;
    }
  };

  /**
   * Remove data from the linked list.
   *
   * @public
   * @method
   * @param {Object} data Data which should be removed.
   * @return {Boolean} Returns true if data has been removed.
   */
  exports.LinkedList.prototype.remove = function (data) {
    if (this.first === null) {
      return false;
    }
    var temp = this.first;
    var next;
    var prev;
    while (temp) {
      if (temp.data === data) {
        next = temp.next;
        prev = temp.prev;
        if (next) {
          next.prev = prev;
        }
        if (prev) {
          prev.next = next;
        }
        if (temp === this.first) {
          this.first = next;
        }
        if (temp === this.last) {
          this.last = prev;
        }
        return true;
      }
      temp = temp.next;
    }
    return false;
  };

  /**
   * Check if linked list contains cycle.
   *
   * @public
   * @method
   * @return {Boolean} Returns true if linked list contains cycle.
   */
  exports.LinkedList.prototype.hasCycle = function () {
    var fast = this.first;
    var slow = this.first;
    while (true) {
      if (fast === null) {
        return false;
      }
      fast = fast.next;
      if (fast === null) {
        return false;
      }
      fast = fast.next;
      slow = slow.next;
      if (fast === slow) {
        return true;
      }
    }
  };

  /**
   * Return last node from the linked list.
   *
   * @public
   * @method
   * @return {Node} Last node.
   */
  exports.LinkedList.prototype.pop = function () {
    if (this.last === null) {
      return null;
    }
    var temp = this.last;
    this.last = this.last.prev;
    return temp;
  };

  /**
   * Return first node from the linked list.
   *
   * @public
   * @method
   * @return {Node} First node.
   */
  exports.LinkedList.prototype.shift = function () {
    if (this.first === null) {
      return null;
    }
    var temp = this.first;
    this.first = this.first.next;
    return temp;
  };

  /**
   * Reverses the linked list recursively
   *
   * @public
   * @method
   */
  exports.LinkedList.prototype.recursiveReverse = function () {

    function inverse(current, next) {
      if (!next) {
        return;
      }
      inverse(next, next.next);
      next.next = current;
    }

    if (!this.first) {
      return;
    }
    inverse(this.first, this.first.next);
    this.first.next = null;
    var temp = this.first;
    this.first = this.last;
    this.last = temp;
  };

  /**
   * Reverses the linked list iteratively
   *
   * @public
   * @method
   */
  exports.LinkedList.prototype.reverse = function () {
    if (!this.first || !this.first.next) {
      return;
    }
    var current = this.first.next;
    var prev = this.first;
    var temp;
    while (current) {
      temp = current.next;
      current.next = prev;
      prev.prev = current;
      prev = current;
      current = temp;
    }
    this.first.next = null;
    this.last.prev = null;
    temp = this.first;
    this.first = prev;
    this.last = temp;
  };

})(typeof window === 'undefined' ? module.exports : window);
