from typing import Iterable, AbstractSet, Generator

from typediter.classes import Item_T
from typediter.classes.light import TypedSet_lt
from typediter._helpers import (
    check_object_compatibility,
    contains_generators,
    unpack_generators
)

class TypedSet( TypedSet_lt[Item_T] ):
    """ Class based on set, insuring the type safety of its items, and the type safety on operations returning a new set

    It can be used exactly like the built-in set but its items, and some operations, 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 (overridden in parent 'light' version class)
        - That used to return a set, to make it return a TypedSet
        (for example, set union returns a new set instance, so TypedSet union
        returns a new TypedSet instance)

    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
    >>>
    >>> # creating an instance
    >>> string_set = TypedSet( ('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 also type-safe
    >>> type( new_set ) # -> TypedSet
    >>>
    >>> # Remark: 
    >>> # if the non-mutating operation is handled by the built-in, a built-in is returned
    >>> builtin_and_result = { 'E', 'F' } & string_set
    >>> type( builtin_and_result ) # -> set
    >>>
    >>> # trying to insert an incompatible item in the instance,
    >>> # or in a typed iterable instance generated by an operation,
    >>> # will raise a TypeRestrictionError
    >>> invalid_instance = TypedSet( ( 1, 2, 3 ), i_type=str ) # will raise TypeRestrictionError
    >>> string_set.add( 12 ) # will raise TypeRestrictionError
    >>> invalid_operation_result = string_set & { 1, 2, 3 } # will raise TypeRestrictionError
    """
    __module__ = 'typediter.complete' # shorten class 'print' name

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

    # -------------------- Overriding --------------------
    # (mutating methods overridden in parent 'light' verion class) 
    # -> Operations handled by current class should return current class instance

    def difference( self, *iterables: Iterable ) -> 'TypedSet[Item_T]':
        """ (NO TYPE CHECKS NEEDED) Return the difference of two or more sets as a new typed set

        (i.e. all elements that are in this set but not the others)
        """
        # does not need type check
        # every item retured are already present in self
        new_set = super().difference( *iterables )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )

    def intersection( self, *iterables: Iterable ) -> 'TypedSet[Item_T]':
        """ (NO TYPE CHECKS NEEDED) Return the intersection of two or more sets as a new typed set

        (i.e. all elements that are in all sets)
        """
        # doesn't need type check
        # if it intersects it's already in self
        new_set = super().intersection( *iterables )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )
    
    def symmetric_difference( self, value: Iterable[Item_T], / ) -> 'TypedSet[Item_T]':
        """ (TYPE-CHECKED) Return the symmetric difference of two sets as a new typed set

        (i.e. all elements that are in exactly one of the sets)
        """
        if isinstance( value, Generator ):
            # because generators can be only used once
            value = tuple(value)

        check_object_compatibility( value, i_type=self.i_type )
        new_set = super().symmetric_difference( value )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )

    def union( self, *iterables: Iterable[Item_T] ) -> 'TypedSet[Item_T]':  # type: ignore[override]
        """ (TYPE-CHECKED) Return the union of sets as a new typed set

        (i.e. all elements that are in either set)
        """
        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 )

        new_set = super().union( *iterables )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )

    def __sub__( self, value: AbstractSet, / ) -> 'TypedSet[Item_T]':
        """ (NO TYPE CHECKS NEEDED) Return self-value as a new typed set """
        # does not need type check
        # every item retured are already present in self
        new_set = super().__sub__( value )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )
    
    def __and__( self, value: AbstractSet, / ) -> 'TypedSet[Item_T]':
        """ (NO TYPE CHECKS NEEDED) Return self&value as a new typed set """
        # does not need type check
        # if it intersects it's already in self
        new_set = super().__and__( value )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )
    
    def __or__( self, value: AbstractSet[Item_T], / ) -> 'TypedSet[Item_T]': # type: ignore[override]
        """ (TYPE-CHECKED) Return self|value as a new typed set """
        check_object_compatibility( value, i_type=self.i_type )
        new_set = super().__or__( value )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )

    def __xor__( self, value: AbstractSet[Item_T], / ) -> 'TypedSet[Item_T]': # type: ignore[override]
        """ (TYPE-CHECKED) Return self^value as a new typed set """
        check_object_compatibility( value, i_type=self.i_type )
        new_set = super().__xor__( value )
        return TypedSet( new_set, i_type=self.i_type, _skip_type_check=True )
