/* SinglyLinkedList!!
 * A linked list is similar to an array, it holds a list of values.
 * However, links in a linked list do not have indexes. With
 * a linked list you do not need to predetermine its size as
 * it grows and shrinks as it is edited. This is an example of
 * a singly linked list.
 */

// Methods - size, head, addLast, addFirst, addAt, removeFirst, removeLast, remove, removeAt, indexOf, isEmpty,  elementAt, findMiddle, get, clean, rotateListRight

class Node {
  constructor(data) {
    this.data = data
    this.next = null
  }
}

class LinkedList {
  constructor(listOfValues) {
    this.headNode = null
    this.tailNode = null
    this.length = 0

    if (listOfValues instanceof Array) {
      for (const value of listOfValues) {
        this.addLast(value)
      }
    }
  }

  // initiates the currentNode and currentIndex and return as an object
  initiateNodeAndIndex() {
    return { currentNode: this.headNode, currentIndex: 0 }
  }

  // Returns length
  size() {
    return this.length
  }

  // Returns the head
  head() {
    return this.headNode?.data ?? null
  }

  // Returns the tail
  tail() {
    return this.tailNode?.data ?? null
  }

  // Return if the list is empty
  isEmpty() {
    return this.length === 0
  }

  // add a node at last it to linklist
  addLast(element) {
    // Check if its the first element
    if (this.headNode === null) {
      return this.addFirst(element)
    }
    const node = new Node(element)
    // Adding node at the end of the list and increase the length
    this.tailNode.next = node
    this.tailNode = node
    this.length++
    return this.size()
  }

  // add a node at first it to linklist
  addFirst(element) {
    const node = new Node(element)
    // Check if its the first element
    if (this.headNode === null) {
      this.tailNode = node
    }
    // Adding node at the start of the list and increase the length
    node.next = this.headNode
    this.headNode = node
    this.length++
    return this.size()
  }

  // remove the first from the linklist
  removeFirst() {
    // Check if head is null
    if (this.headNode === null) {
      return null
    }
    // Removing node at the start of the list and decrease the length
    const removedNode = this.headNode
    this.headNode = this.headNode.next
    this.length--
    // Check if the list is empty
    if (this.isEmpty()) {
      this.tailNode = null
    }
    return removedNode?.data
  }

  // remove the last from the linklist
  removeLast() {
    if (this.isEmpty()) return null
    // Check if there is only one element
    if (this.length === 1) {
      return this.removeFirst()
    }
    // Removing node at the end of the list and decrease the length
    const removedNode = this.tailNode
    let { currentNode } = this.initiateNodeAndIndex()
    while (currentNode.next.next) {
      currentNode = currentNode.next
    }
    currentNode.next = null
    this.tailNode = currentNode
    this.length--
    return removedNode.data
  }

  // Removes the node with the value as param
  remove(element) {
    if (this.isEmpty()) return null
    let { currentNode } = this.initiateNodeAndIndex()
    let removedNode = null
    // Check if the head node is the element to remove
    if (currentNode.data === element) {
      return this.removeFirst()
    }
    // Check if the tail node is the element to remove
    if (this.tailNode.data === element) {
      return this.removeLast()
    }
    // Check which node is the node to remove
    while (currentNode.next) {
      if (currentNode.next.data === element) {
        removedNode = currentNode.next
        currentNode.next = currentNode.next.next
        this.length--
        return removedNode.data
      }
      currentNode = currentNode.next
    }
    return removedNode?.data || null
  }

  // Returns the index of the element passed as param otherwise -1
  indexOf(element) {
    if (this.isEmpty()) return -1
    let { currentNode, currentIndex } = this.initiateNodeAndIndex()
    while (currentNode) {
      if (currentNode.data === element) {
        return currentIndex
      }
      currentNode = currentNode.next
      currentIndex++
    }
    return -1
  }

  // Returns the element at an index
  elementAt(index) {
    if (index >= this.length || index < 0) {
      throw new RangeError('Out of Range index')
    }
    let { currentIndex, currentNode } = this.initiateNodeAndIndex()
    while (currentIndex < index) {
      currentIndex++
      currentNode = currentNode.next
    }
    return currentNode.data
  }

  // Adds the element at specified index
  addAt(index, element) {
    // Check if index is out of bounds of list
    if (index > this.length || index < 0) {
      throw new RangeError('Out of Range index')
    }
    if (index === 0) return this.addFirst(element)
    if (index === this.length) return this.addLast(element)
    let { currentIndex, currentNode } = this.initiateNodeAndIndex()
    const node = new Node(element)

    while (currentIndex !== index - 1) {
      currentIndex++
      currentNode = currentNode.next
    }

    // Adding the node at specified index
    const tempNode = currentNode.next
    currentNode.next = node
    node.next = tempNode
    // Incrementing the length
    this.length++
    return this.size()
  }

  // Removes the node at specified index
  removeAt(index) {
    // Check if index is present in list
    if (index < 0 || index >= this.length) {
      throw new RangeError('Out of Range index')
    }
    if (index === 0) return this.removeFirst()
    if (index === this.length - 1) return this.removeLast()

    let { currentIndex, currentNode } = this.initiateNodeAndIndex()
    while (currentIndex !== index - 1) {
      currentIndex++
      currentNode = currentNode.next
    }
    const removedNode = currentNode.next
    currentNode.next = currentNode.next.next
    this.length--
    return removedNode.data
  }

  // Returns a reference to middle node of linked list
  findMiddle() {
    // If there are two middle nodes, return the second middle node.
    let fast = this.headNode
    let slow = this.headNode

    while (fast !== null && fast.next !== null) {
      fast = fast.next.next
      slow = slow.next
    }
    return slow
  }

  // make the linkedList Empty
  clean() {
    this.headNode = null
    this.tailNode = null
    this.length = 0
  }

  // Method to get the LinkedList
  get() {
    const list = []
    let { currentNode } = this.initiateNodeAndIndex()
    while (currentNode) {
      list.push(currentNode.data)
      currentNode = currentNode.next
    }
    return list
  }

  // Method for Rotating a List to the right by k places
  rotateListRight(k) {
    if (!this.headNode) return
    let current = this.headNode
    let tail = this.tailNode
    let count = 1
    while (current.next) {
      count++
      current = current.next
    }
    current.next = this.headNode
    tail = current
    k %= count
    while (count - k > 0) {
      tail = tail.next
      count--
    }
    this.headNode = tail.next
    tail.next = null
  }

  // Method to iterate over the LinkedList
  iterator() {
    let { currentNode } = this.initiateNodeAndIndex()
    if (currentNode === null) return -1

    const iterate = function* () {
      while (currentNode) {
        yield currentNode.data
        currentNode = currentNode.next
      }
    }
    return iterate()
  }

  // Method to log the LinkedList
  log() {
    console.log(JSON.stringify(this.headNode, null, 2))
  }

  // Method to reverse the LinkedList
  reverse() {
    let head = this.headNode
    let prev = null
    let next = null
    while (head) {
      next = head.next
      head.next = prev
      prev = head
      head = next
    }
    this.tailNode = this.headNode
    this.headNode = prev
  }
}

export { Node, LinkedList }