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

    === Attributes ===
    @param LinkedListNode next_: successor to this LinkedListNode
    @param object value: data this LinkedListNode represents
    """
    def __init__(self, value, next_=None):
        """
        Create LinkedListNode self with data value and successor next_.

        @param LinkedListNode self: this LinkedListNode
        @param object value: data of this linked list node
        @param LinkedListNode|None next_: successor to this LinkedListNode.
        @rtype: None
        """
        self.value, self.next_ = value, next_

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

        @param LinkedListNode self: this LinkedListNode
        @rtype: str

        >>> n = LinkedListNode(5, LinkedListNode(7))
        >>> print(n)
        5 -> 7 ->|
        """
        s = "{} ->".format(self.value)
        # create a reference to "walk" along the list
        current_node = self.next_
        # build string s for each remaining node
        while current_node is not None:
            s += " {} ->".format(current_node.value)
            current_node = current_node.next_
        return s + "|"

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

        Assume self and other are not nodes in an looped list.

        @param LinkedListNode self: this LinkedListNode
        @param LinkedListNode|object other: object to compare to self.
        @rtype: bool

        >>> LinkedListNode(5).__eq__(5)
        False
        >>> n1 = LinkedListNode(5, LinkedListNode(7))
        >>> n2 = LinkedListNode(5, LinkedListNode(7, None))
        >>> n1.__eq__(n2)
        True
        """
        if not type(other) == LinkedListNode:
            return False
        current_node = self
        other_node = other
        while (current_node is not None
               and other_node is not None):
            if current_node.value != other_node.value:
                return False
            current_node = current_node.next_
            other_node = other_node.next_
        assert current_node is None or other_node is None
        return current_node is None and other_node is None
        # exercise: write this recursively (I claim it's much
        # simpler


class LinkedList:
    """
    Collection of LinkedListNodes

    === Attributes ==
    @param: LinkedListNode front: first node of this LinkedList
    @param LinkedListNode back: last node of this LinkedList
    @param int size: number of nodes in this LinkedList
                        a non-negative integer
    """
    def __init__(self):
        """
        Create an empty linked list.

        @param LinkedList self: this LinkedList
        @rtype: None
        """
        self.front, self.back, self.size = None, None, 0

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

        @param LinkedList self: this LinkedList
        @rtype: str

        >>> lnk = LinkedList()
        >>> lnk.prepend(5)
        >>> print(lnk)
        5 ->|
        """
        pass


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

        @param LinkedList self: this LinkedList
        @param LinkedList|object other: object to compare to self
        @rtype: bool

        >>> LinkedList().__eq__(None)
        False
        >>> lnk = LinkedList()
        >>> lnk.prepend(5)
        >>> lnk2 = LinkedList()
        >>> lnk2.prepend(5)
        >>> lnk.__eq__(lnk2)
        True
        """
        pass

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

        @param LinkedList self: this LinkedList.
        @param object value: value of new LinkedListNode
        @rtype: None

        >>> lnk = LinkedList()
        >>> lnk.append(5)
        >>> lnk.size
        1
        >>> print(lnk.front)
        5 ->|
        >>> lnk.append(6)
        >>> lnk.size
        2
        >>> print(lnk.front)
        5 -> 6 ->|
        """
        self.size += 1
        new_node = LinkedListNode(value)
        if self.front is None:
            assert self.back is None and self.size == 1
            self.front, self.back = new_node, new_node
        else:
            assert self.back is not None
            self.back.next_ = new_node
            self.back = new_node
            # switch the order of the two lines above
            # for spectacular error!

    def prepend(self, value):
        """
        Insert value before LinkedList self.front.

        @param LinkedList self: this LinkedList
        @param object value: value for new LinkedList.front
        @rtype: None

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


    # def delete_front(self):
    #     """
    #     Delete LinkedListNode self.front from self.
    #
    #     Assume self.front is not None
    #
    #     @param LinkedList self: this LinkedList
    #     @rtype: 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):
        """
        Return the value at LinkedList self's position index.

        @param LinkedList self: this LinkedList
        @param int index: position to retrieve value from
        @rtype: object

        >>> lnk.prepend(1)
        >>> lnk.prepend(0)
        >>> lnk.__getitem__(1)
        1
        >>> lnk[-2]
        0
        """
        if not (-self.size <= index < self.size):
            raise IndexError("bad index!!!")
        elif index < 0:
            index += self.size
        assert 0 <= index < self.size
        current_node = self.front
        for _ in range(index):
            assert current_node is not None
            current_node = current_node.next_
        return current_node.value


    def __contains__(self, value):
        """
        Return whether LinkedList self contains value.

        @param LinkedList self: this LinkedList.
        @param object value: value to search for in self
        @rtype: bool

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


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