"""
- A linked list is similar to an array, it holds values. However, links in a linked
    list do not have indexes.
- This is an example of a double ended, doubly linked list.
- Each link references the next link and the previous one.
- A Doubly Linked List (DLL) contains an extra pointer, typically called previous
    pointer, together with next pointer and data which are there in singly linked list.
 - Advantages over SLL - It can be traversed in both forward and backward direction.
     Delete operation is more efficient"""


class LinkedList:
    """
    >>> linked_list = LinkedList()
    >>> linked_list.insert_at_head("a")
    >>> linked_list.insert_at_tail("b")
    >>> linked_list.delete_tail()
    'b'
    >>> linked_list.is_empty
    False
    >>> linked_list.delete_head()
    'a'
    >>> linked_list.is_empty
    True
    """

    def __init__(self):
        self.head = None  # First node in list
        self.tail = None  # Last node in list

    def __str__(self):
        current = self.head
        nodes = []
        while current is not None:
            nodes.append(current)
            current = current.next
        return " ".join(str(node) for node in nodes)

    def insert_at_head(self, data):
        new_node = Node(data)
        if self.is_empty:
            self.tail = new_node
            self.head = new_node
        else:
            self.head.previous = new_node
            new_node.next = self.head
            self.head = new_node

    def delete_head(self) -> str:
        if self.is_empty:
            return "List is empty"

        head_data = self.head.data
        if self.head.next:
            self.head = self.head.next
            self.head.previous = None

        else:  # If there is no next previous node
            self.head = None
            self.tail = None

        return head_data

    def insert_at_tail(self, data):
        new_node = Node(data)
        if self.is_empty:
            self.tail = new_node
            self.head = new_node
        else:
            self.tail.next = new_node
            new_node.previous = self.tail
            self.tail = new_node

    def delete_tail(self) -> str:
        if self.is_empty:
            return "List is empty"

        tail_data = self.tail.data
        if self.tail.previous:
            self.tail = self.tail.previous
            self.tail.next = None
        else:  # if there is no previous node
            self.head = None
            self.tail = None

        return tail_data

    def delete(self, data) -> str:
        current = self.head

        while current.data != data:  # Find the position to delete
            if current.next:
                current = current.next
            else:  # We have reached the end an no value matches
                return "No data matching given value"

        if current == self.head:
            self.delete_head()

        elif current == self.tail:
            self.delete_tail()

        else:  # Before: 1 <--> 2(current) <--> 3
            current.previous.next = current.next  # 1 --> 3
            current.next.previous = current.previous  # 1 <--> 3
        return data

    @property
    def is_empty(self):  # return True if the list is empty
        return self.head is None


class Node:
    def __init__(self, data):
        self.data = data
        self.previous = None
        self.next = None

    def __str__(self):
        return f"{self.data}"