class LinkedList:
    """Implements our own recursive list data structure"""
    __slots__ = ['_value', '_rest']

    def __init__(self, value=None, rest=None):
        self._value = value
        self._rest = rest
       
    # getters/setters
    def getRest(self):
        return self._rest
    
    def getValue(self):
        return self._value

    def setValue(self, val):
        self._value = val

    def __strElements(self):
    # helper function for __str__()
        if self._rest is None:
            return str(self._value)
        else:
            return str(self._value) + ", " + self._rest.__strElements()
        
    def __str__(self):
        return "[" + self.__strElements() + "]"
    
    # repr() function calls __repr__() method
    # return value should be a string that is a valid Python 
    # expression that can be used to recreate the LinkedList
    def __repr__(self):
        return "LinkedList({}, {})".format(self._value, repr(self._rest))

    # len() function calls __len__() method
    # slightly updated version that accounts for empty list
    def __len__(self):
        # base case: i'm the last item
        if self._rest is None and self._value is None:
            return 0
        # i am an empty list
        elif self._rest is None and self._value is not None:
            return 1
        else:
            # same as return 1 + self._rest.__len__()
            return 1 + len(self._rest)  
    
    # in operator calls __contains__() method
    def __contains__(self, val):
        if self._value == val:
            return True
        elif self._rest is None:
            return False
        else:
            # same as calling self.__contains__(val)
            return val in self._rest
        
    # + operator calls __add__() method
    # + operator returns a mutated LinkedList (not a new instance!)
    # (so this actually behaves more like list.extend() method) 
    def __add__(self, other):
        # other is another instance of LinkedList
        # if we are the last item in the list
        if self._rest is None:
            # set _rest to other
            self._rest = other
        else:
            # else, recurse until we reach the last item
            self._rest.__add__(other)
        return self

    # + operator calls __add__() method
    # + operator returns a new instance of LinkedList
    # trickier implementation!
    def __add2__(self, other):
        # other is another instance of LinkedList
        
        # if we've reached the last two items
        if self.getRest() is None and other.getRest() is None:
            return LinkedList(self.getValue(), LinkedList(other.getValue()))

        # else if only one list is empty, move on to other
        elif self.getRest() is None and other.getRest() is not None:
             return LinkedList(self.getValue(), LinkedList(other.getValue())+other.getRest())

        # else, recurse until we reach the last item in first list,
        # creating list elements as we go
        else:
            return LinkedList(self.getValue(), (self.getRest()+other))
                 
    # [] list index notation calls __getitem__() method
    # index specifies which item we want
    def __getitem__(self, index):
        # if index is 0, we found the item we need to return
        if index == 0:
            return self._value
        else:
            # else we recurse until index reaches 0
            # remember that this implicitly calls __getitem__
            return self._rest[index - 1]
        
    # [] list index notation also calls __setitem__() method
    # index specifies which item we want, val is new value
    def __setitem__(self, index, val):
        # if index is 0, we found the item we need to update
        if index == 0:
            self._value = val
        else:
            # else we recurse until index reaches 0
            # remember that this implicitly calls __setitem__
            self._rest[index - 1] = val
            
    # == operator calls __eq__() method
    # if we want to test two LinkedLists for equality, we test 
    # if all items are the same
    # other is another LinkedList
    def __eq__(self, other):
        # If both lists are empty
        if self._rest is None and other.getRest() is None:
            return self._value == other.getValue()

        # If both lists are not empty, then value of current list elements 
        # must match, and same should be recursively true for 
        # rest of the list
        elif self._rest is not None and other.getRest() is not None :
            return self._value == other.getValue() and self._rest == other.getRest()

        # If we reach here, then one of the lists is empty and other is not
        else:
            return False
   
    # append is not a special method, but it is a method
    # that we know and love from the Python list class.
    # unlike __add__, we do not return a new LinkedList instance
    def append(self, val):
        # if am at the list item
        if self._rest is None:
            # add a new LinkedList to the end
            self._rest = LinkedList(val)
        else:
            # else recurse until we find the end
            self._rest.append(val)

if __name__ == "__main__":
    myList = LinkedList("a")
    myList = LinkedList("b", myList)
    myList = LinkedList("c", myList)

    print("myList:", myList)
    print("value:",myList.getValue())
    print("rest:", myList.getRest())

    myList2 = LinkedList("d")
    myList2 = LinkedList("e", myList2)
    print("myList2:", myList2)
    
    myList3 = myList + myList2
    print("myList3 = myList + myList2: ",myList3)
    print("myList",myList)
    print("myList2",myList2)
    
    print("Length:", len(myList))
    print("c in list?", "c" in myList)
    print("d in list?", "d" in myList)
    print("myList[1]:", myList[1])

    myList2 = LinkedList("c", LinkedList("b", LinkedList("a")))
    print("myList2:", myList2)

    print("myList == myList2?", myList2 == myList)

    myList3 = myList + myList2
    print("myList3:", myList3)
    print("myList2 == myList3?", myList2 == myList3)
    print("myList == myList3?", myList3 == myList)

    myList3[1] = "d"
    print("myList3 with d:", myList3)

    myList3.append("e")
    print("myList3 with e:", myList3)

