from typing import Iterable, AbstractSet, Generator, Self

from typediter.classes import _MutableTypedIter, Item_T
from typediter._helpers import (
    check_types,
    check_object_compatibility,
    contains_generators,
    unpack_generators
)

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

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

    Inherits from set, 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 set,
    not a typed set like in the 'complete' version (TypedSet)

    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
            - or is not a hashable 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 'set'
        for a given operation

    Usage Example
    -------------
    >>> # importing the class
    >>> from typediter import TypedSet_lt
    >>>
    >>> # creating an instance
    >>> string_set = TypedSet_lt( ('A', 'B', 'C'), i_type=str )
    >>>
    >>> # supports all set operations
    >>> string_set.add( 'D' )
    >>> new_set = string_set & { 'E', 'F' }
    >>>
    >>> # new instances generated by non-mutating
    >>> # operations are built-ins
    >>> type( new_set ) # -> set
    >>>
    >>> # trying to insert an incompatible item in the instance,
    >>> # will raise a TypeRestrictionError
    >>> invalid_instance = TypedSet_lt( ( 1, 2, 3 ), i_type=str ) # will raise TypeRestrictionError
    >>> string_set.add( 12 ) # will raise TypeRestrictionError
    >>>
    >>> # Remark:
    >>> # Operations returning new instances are not type-restricted:
    >>> mixed_set = string_set & { 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 = set(self)
        return f"(lt)Set[{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 set """
        new_set = super().copy()
        return self.__class__( new_set, i_type=self.i_type, _skip_type_check=True )
    
    def add( self, element:Item_T, / ) -> None:
        """ (TYPE-CHECKED) Add an element to a set

        This has no effect if the element is already present
        """
        check_types( element, i_type=self.i_type )
        super().add( element )
    
    def symmetric_difference_update( self, value: Iterable[Item_T], / ) -> None:
        """ (TYPE-CHECKED) Update a set with the symmetric difference of itself and another """
        if isinstance( value, Generator ):
            # because generators can be only used once
            value = tuple(value)
        
        check_object_compatibility( value, i_type=self.i_type )
        super().symmetric_difference_update( value )

    def update( self, *iterables: Iterable[Item_T] ) -> None:
        """ (TYPE-CHECKED) Update a set with the union of itself and others """
        if contains_generators(iterables):
            # because generators can be only used once
            iterables = unpack_generators( iterables )
        
        for iterable_obj in iterables:
            check_object_compatibility( iterable_obj, i_type=self.i_type )
        super().update( *iterables )
    
    def __ior__( self, value: AbstractSet[Item_T], / ) -> Self: # type: ignore[override, misc]
        """ (TYPE-CHECKED) Return self|=value """
        check_object_compatibility( value, i_type=self.i_type )
        return super().__ior__( value )

    def __ixor__( self, value: AbstractSet[Item_T], / ) -> Self: # type: ignore[override, misc]
        """ (TYPE-CHECKED) Return self^=value """
        check_object_compatibility( value, i_type=self.i_type )
        return super().__ixor__( value )

