from typing import overload, Iterable, Generator, SupportsIndex, Self

from typediter.classes import _MutableTypedIter, Item_T
from typediter._helpers import check_types, check_object_compatibility

class TypedList_lt( _MutableTypedIter[Item_T], list[Item_T] ):
    """ Class based on list, insuring the type safety of its items

    It can be used exactly like the built-in list but its items are type-restricted,
    if the type restriction is broken, it fails with a TypeRestrictionError

    Inherits from list, but overriding methods:
        - That could insert an incompatible item

    (i) All operations returning a new instance will return a new instance of the built-in list,
    not a typed list like in the 'complete' version (TypedList)

    Attributes
    ----------
    - **i_type**
        The type restriction for the items

    Raises
    ------
    - **typediter.exceptions.TypeRestrictionError**
        Raised by a typed-checked operation to prevent
        it from inserting incompatible items in a type-safe iterable.
        (Can be raised at instance creation
        or by every operation performing type checks)
    
    - **typediter.exceptions.InvalidTypeRestrictionError**
        Raised at instance creation if the provided 'i_type':
            - is not a type
    
    - **typediter.exceptions.IterableExpectedError**
        Raised by a type-checked operation if it expected an iterable
        but received something else
        (Can be raised at instance creation
        or by every operation performing type checks)

    - **other exceptions**
        Any exception that can be raised by a built-in 'list'
        for a given operation

    Usage Example
    -------------
    >>> # importing the class
    >>> from typediter import TypedList_lt
    >>>
    >>> # creating an instance
    >>> string_list = TypedList_lt( ('A', 'B', 'C'), i_type=str )
    >>>
    >>> # supports all list operations
    >>> string_list.append( 'D' )
    >>> new_list = string_list + ['E', 'F']
    >>>
    >>> # new instances generated by non-mutating
    >>> # operations are built-ins
    >>> type( new_list ) # -> list
    >>>
    >>> # trying to insert an incompatible item in the instance,
    >>> # will raise a TypeRestrictionError
    >>> invalid_instance = TypedList_lt( ( 1, 2, 3 ), i_type=str ) # will raise TypeRestrictionError
    >>> string_list.append( 12 ) # will raise TypeRestrictionError
    >>>
    >>> # Remark:
    >>> # Operations returning new instances are not type-restricted:
    >>> mixed_list = string_list + [ 1, 2, 3 ] # -> will not fail
    """
    __module__ = 'typediter.light' # shorten class 'print' name

    # -------------------- Representation --------------------
    
    def __repr__( self ) -> str:
        typename = self.i_type.__name__
        default_representation = list(self)
        return f"(lt)List[{typename}]:{default_representation}"
    
    def __str__( self ) -> str:
        return self.__repr__()

    # -------------------- Overriding --------------------
    # -> Everything that could introduce an item of another type

    def copy( self ): # do not type hint return (allows to have correct return type on child classes)
        """ Returns a shallow copy of the typed list """
        new_list = super().copy()
        return self.__class__( new_list, i_type=self.i_type, _skip_type_check=True )
    
    def insert( self, index:SupportsIndex, obj: Item_T, / ) -> None:
        """ (TYPE-CHECKED) Insert object before index """
        check_types( obj, i_type=self.i_type )
        super().insert(index, obj)

    def append( self, obj: Item_T, / ) -> None:
        """ (TYPE-CHECKED) Append object to the end of the list """
        check_types( obj, i_type=self.i_type )
        super().append( obj )

    def extend( self, other:Iterable[Item_T], / ) -> None:
        """ (TYPE-CHECKED) Extend list by appending elements from the iterable """
        if isinstance( other, Generator ):
            # because generators can be only used once
            other = tuple(other)
        check_object_compatibility( other, i_type=self.i_type ) 
        super().extend( other )

    @overload
    def __setitem__( self, key: SupportsIndex, value: Item_T, / ) -> None: ...
    @overload
    def __setitem__( self, key: slice, value: Iterable[Item_T], / ) -> None: ...
    def __setitem__( self, key: SupportsIndex | slice, value: Item_T | Iterable[Item_T], / ) -> None:
        """ (TYPE-CHECKED) Set self[key] to value """
        if isinstance( value, Generator ):
            # because generators can be only used once
            value = tuple(value)

        if isinstance( key, slice ):
            check_object_compatibility( value, i_type=self.i_type )
        else: # SupportsIndex
            check_types( value, i_type=self.i_type )
        
        super().__setitem__( key, value ) # type: ignore[index, assignment]

    def __iadd__( self, other: Iterable[Item_T], / ) -> Self: # type: ignore[override, misc]
        """ (TYPE-CHECKED) Implement self+=value """
        if isinstance( other, Generator ):
            # because generators can be only used once
            other = tuple(other)

        check_object_compatibility( other, i_type=self.i_type )
        return super().__iadd__( other )

