"""
@property tutorial
"""
from typing import Any, Callable, Union
from base import BaseLinkedList
from node import Node

class SinglyLinkedList(BaseLinkedList):
    """Implements a Singly Linked List"""

    def __init__(self, head=None, tail=None):
        self._head = head
        self._tail = tail
        self._size = 0

    @property
    def size(self):
        return self._size

    @size.setter
    def size(self, new_size):
        if not isinstance(new_size, int):
            raise ValueError('List size must be int')
        if new_size < 0:
            raise ValueError('List size cannot be negative')
        self._size = new_size

    @property
    def head(self):
        """returns the head of the linked list"""
        return self._head

    @head.setter
    def head(self, node: Union[Node, None]):
        """Sets the head of the linked list"""
        if node is not None and not isinstance(node, Node):
            raise TypeError("Node must be of type None or Node")
        else:
            self._head = node

    @property
    def tail(self):
        """returns the tail of the linked list"""
        return self._tail

    @tail.setter
    def tail(self, node: Union[Node, None]):
        """Sets the tail of the list"""
        if node is not None and not isinstance(node, Node):
            raise TypeError("Node must be of type None or Node")
        elif self.head is not None and node is None:
            raise ValueError("Tail cannot be none if head is not none")
        else:
            self._tail = node

    def append(self, val: Any) -> None:
        """Append a new node with given value to end of list"""
        if self.head is None:
            self.head = Node(val)
            self.tail = self.head
        else:
            self.tail.next_node = Node(val)
            self.tail = self.tail.next_node
        self.size += 1

    def appendleft(self):
        raise NotImplementedError

    def __len__(self) -> int:
        return self._size


    def accepts(*types):
        def check_accepts(f):
            assert len(types) == f.__code__.co_argcount
            def new_f(*args, **kwds):
                for (a, t) in zip(args, types):
                    assert isinstance(a, t), \
                        "arg %r does not match %s" % (a, t)
                return f(*args, **kwds)
            new_f.__name__ = f.__name__
            return new_f
        return check_accepts


    def handle_index(func) -> Callable:
        """Makes sure the indexes are valid are of type int"""

        def new_func(self, *args):
            print(args)
            index = args[0]
            if not isinstance(index, (int, slice)):
                return TypeError(
                    f'list indices must be integers or slices and not {type(args)}'
                )
            if isinstance(index, slice):
                raise NotImplementedError('Slicing is not implemented yet!')
            if self.head is None:
                raise IndexError('Index out of range. List Empty')
            if index < -self.size or index >= self.size:
                raise IndexError('List assignmnent out of range')
            return func(self, *args)
        return new_func

    @handle_index
    def __setitem__(self, index: int, value: Any) -> None:
        if isinstance(index, slice):
            raise NotImplementedError('Slicing is not implemented yet!')
        else:
            if index < 0:
                index += self.size
            node = self.head
            for i in range(index):
                node = node.next_node
            node.val = value

    @handle_index
    def __getitem__(self, index: int) -> Any:
        if isinstance(index, slice):
            raise NotImplementedError
        else:
            if index < 0:
                index += self.size
            node = self.head
            for i in range(index):
                node = node.next_node
            return node.val

    def __iter__(self):
        raise NotImplementedError

    @handle_index
    def pop(self, index=-1) -> Any:
        """Removes the node at the end of the list
        and returns it's value
        """
        pass

    def remove(self, value) -> None:
        raise NotImplementedError

    def sort(self):
        """Sorts the linked list in ascending order"""
        raise NotImplementedError

    def __repr__(self) -> str:
        out = ""
        node = self.head
        while node:
            out += str(node.val) + '->'
            node = node.next_node
        return out + 'None'

"""
@property tutorial
"""
from typing import Any, Callable, Union
from base import BaseLinkedList
from node import Node

class SinglyLinkedList(BaseLinkedList):
    """Implements a Singly Linked List"""

    def __init__(self, head=None, tail=None):
        self._head = head
        self._tail = tail
        self._size = 0

    @property
    def size(self):
        return self._size

    @size.setter
    def size(self, new_size):
        if not isinstance(new_size, int):
            raise ValueError('List size must be int')
        if new_size < 0:
            raise ValueError('List size cannot be negative')
        self._size = new_size

    @property
    def head(self):
        """returns the head of the linked list"""
        return self._head

    @head.setter
    def head(self, node: Union[Node, None]):
        """Sets the head of the linked list"""
        if node is not None and not isinstance(node, Node):
            raise TypeError("Node must be of type None or Node")
        else:
            self._head = node

    @property
    def tail(self):
        """returns the tail of the linked list"""
        return self._tail

    @tail.setter
    def tail(self, node: Union[Node, None]):
        """Sets the tail of the list"""
        if node is not None and not isinstance(node, Node):
            raise TypeError("Node must be of type None or Node")
        elif self.head is not None and node is None:
            raise ValueError("Tail cannot be none if head is not none")
        else:
            self._tail = node

    def append(self, val: Any) -> None:
        """Append a new node with given value to end of list"""
        if self.head is None:
            self.head = Node(val)
            self.tail = self.head
        else:
            self.tail.next_node = Node(val)
            self.tail = self.tail.next_node
        self.size += 1

    def appendleft(self):
        raise NotImplementedError

    def __len__(self) -> int:
        return self._size


    def accepts(*types):
        def check_accepts(f):
            assert len(types) == f.__code__.co_argcount
            def new_f(*args, **kwds):
                for (a, t) in zip(args, types):
                    assert isinstance(a, t), \
                        "arg %r does not match %s" % (a, t)
                return f(*args, **kwds)
            new_f.__name__ = f.__name__
            return new_f
        return check_accepts


    def handle_index(func) -> Callable:
        """Makes sure the indexes are valid are of type int"""

        def new_func(self, *args):
            print(args)
            index = args[0]
            if not isinstance(index, (int, slice)):
                return TypeError(
                    f'list indices must be integers or slices and not {type(args)}'
                )
            if isinstance(index, slice):
                raise NotImplementedError('Slicing is not implemented yet!')
            if self.head is None:
                raise IndexError('Index out of range. List Empty')
            if index < -self.size or index >= self.size:
                raise IndexError('List assignmnent out of range')
            return func(self, *args)
        return new_func

    @handle_index
    def __setitem__(self, index: int, value: Any) -> None:
        if isinstance(index, slice):
            raise NotImplementedError('Slicing is not implemented yet!')
        else:
            if index < 0:
                index += self.size
            node = self.head
            for i in range(index):
                node = node.next_node
            node.val = value

    @handle_index
    def __getitem__(self, index: int) -> Any:
        if isinstance(index, slice):
            raise NotImplementedError
        else:
            if index < 0:
                index += self.size
            node = self.head
            for i in range(index):
                node = node.next_node
            return node.val

    def __iter__(self):
        raise NotImplementedError

    @handle_index
    def pop(self, index=-1) -> Any:
        """Removes the node at the end of the list
        and returns it's value
        """
        pass

    def remove(self, value) -> None:
        raise NotImplementedError

    def sort(self):
        """Sorts the linked list in ascending order"""
        raise NotImplementedError

    def __repr__(self) -> str:
        out = ""
        node = self.head
        while node:
            out += str(node.val) + '->'
            node = node.next_node
        return out + 'None'

@property tutorial Implements a Singly Linked List returns the head of the linked list Sets the head of the linked list returns the tail of the linked list Sets the tail of the list Append a new node with given value to end of list Makes sure the indexes are valid are of type int Removes the node at the end of the list         and returns it's value Sorts the linked list in ascending order