""" classes LinkedListNode and LinkedList
"""
from typing import Union, Any


class LinkedListNode:
    """
    Node to be used in linked list

    === Attributes ===
    next_ - successor to this LinkedListNode
    value - data represented by this LinkedListNode
    """
    next_: Union["LinkedListNode", None]

    def __init__(self, value: object,
                 next_: Union["LinkedListNode", None]=None) -> None:
        """
        Create LinkedListNode self with data value and successor next

        >>> LinkedListNode(5).value
        5
        >>> LinkedListNode(5).next_ is None
        True
        """
        self.value, self.next_= value, next_

    def __str__(self) -> str:
        """
        Return a user-friendly representation of this LinkedListNode.

        >>> n = LinkedListNode(5, LinkedListNode(7))
        >>> print(n)
        5 ->7 ->|
        """
        cur_node = self
        result=''
        while cur_node is not None:
            result += '{} ->'.format(cur_node.value)
            cur_node = cur_node.next_
        return result+'|'

    def __eq__(self, other: Any) -> bool:
        """
        Return whether LinkedListNode self is equivalent to other.

        >>> LinkedListNode(5).__eq__(5)
        False
        >>> n1 = LinkedListNode(5, LinkedListNode(7))
        >>> n2 = LinkedListNode(5, LinkedListNode(7, None))
        >>> n1.__eq__(n2)
        True
        """
        left_node=self
        right_node=other
        while (left_node is not None
               and right_node is not None
               and type(left_node) == type(right_node)
               and left_node.value == right_node.value):
            left_node = left_node.next_
            right_node = right_node.next_
        return right_node is None and left_node is None

class LinkedList:
    """
    Collection of LinkedListNodes

    === Attributes ==
    front - first node of this LinkedList
    back - last node of this LinkedList
    size - number of nodes in this LinkedList, >= 0
    """
    front: Union[LinkedListNode, None]
    back: Union[LinkedListNode, None]
    size: int

    def __init__(self) -> None:
        """
        Create an empty linked list.
        """
        self.front,self.back = None, None
        self.size = 0


    def __str__(self) -> str:
        """
        Return a human-friendly string representation of
        LinkedList self.

        >>> lnk = LinkedList()
        >>> lnk.front = LinkedListNode(5)
        >>> lnk.back = lnk.front
        >>> print(lnk)
        5 ->|
        """
        return str(self.front)

    def __eq__(self, other: Any) -> bool:
        """
        Return whether LinkedList self is equivalent to
        other.

        >>> LinkedList().__eq__(None)
        False
        >>> lnk = LinkedList()
        >>> lnk.prepend(5)
        >>> lnk2 = LinkedList()
        >>> lnk2.prepend(5)
        >>> lnk.__eq__(lnk2)
        True
        """
        return (type(self) == type(other) and
                self.front == other.front)

    def append(self, value: object) -> None:
        """
        Insert a new LinkedListNode with value after self.back.

        >>> lnk = LinkedList()
        >>> lnk.append(5)
        >>> lnk.size
        1
        >>> print(lnk.front)
        5 ->|
        >>> lnk.append(6)
        >>> lnk.size
        2
        >>> print(lnk.front)
        5 ->6 ->|
        """
        if self.size==0:
            n1=LinkedListNode(value, None)
            self.front=n1
            self.back=n1
            self.size+=1
        else:
            n2=LinkedListNode(value,None)
            self.back.next_ = n2
            self.back=n2
            self.size+=1

    def prepend(self, value: object) -> None:
        """
        Insert value before LinkedList self.front.

        >>> lnk = LinkedList()
        >>> lnk.prepend(0)
        >>> lnk.prepend(1)
        >>> lnk.prepend(2)
        >>> str(lnk.front)
        '2 ->1 ->0 ->|'
        >>> lnk.size
        3
        """
        if self.size==0:
            n1=LinkedListNode(value, None)
            self.front=n1
            self.back=n1
            self.size+=1
        else:
            n2=LinkedListNode(value,self.front)
            self.front=n2
            self.size+=1

    def delete_front(self) -> None:
        """
        Delete LinkedListNode self.front from self.

        Assume self.front is not None

        >>> lnk = LinkedList()
        >>> lnk.prepend(0)
        >>> lnk.prepend(1)
        >>> lnk.prepend(2)
        >>> lnk.delete_front()
        >>> str(lnk.front)
        '1 -> 0 ->|'
        >>> lnk.size
        2
        >>> lnk.delete_front()
        >>> lnk.delete_front()
        >>> str(lnk.front)
        'None'
        """
        pass

    def __getitem__(self, index: int) -> object:
        """
        Return the value at LinkedList self's position index,
        which must be a valid position in LinkedList self.

        >>> lnk = LinkedList()
        >>> lnk.append(0)
        >>> lnk.append(1)
        >>> lnk.append(2)
        >>> lnk.__getitem__(1)
        1
        >>> lnk[0]
        0
        >>> lnk[2]
        2
        >>> lnk[-1]
        2
        >>> lnk[-2]
        1
        >>> lnk[-3]
        0
        """
        if index < 0:
            index += self.size
        if index >= self.size or self.size == 0 or index < 0:
            raise IndexError('index is out of bounds')
        current = self.front
        for steps in range(index):
            current = current.next_
            # assert current is not None, 'reached invalid index'
        return current.value

    def __contains__(self, value: object) -> bool:
        """
        Return whether LinkedList self contains value.

        >>> lnk = LinkedList()
        >>> lnk.prepend(0)
        >>> lnk.prepend(1)
        >>> lnk.prepend(2)
        >>> lnk.__contains__(1)
        True
        >>> lnk.__contains__(3)
        False
        >>> 2 in lnk
        True
        >>> 3 in lnk
        False
        """
        current = self.front
        while current is not None:
            if current.value == value:
                return True
            current = current.next_
        return False


if __name__ == '__main__':
    import doctest
    doctest.testmod()
