# -*- coding: utf-8 -*-
from inspect import signature, Parameter
from typing import Any, Hashable
from typing import Generic as _GenericAlias


__all__ = ["Signature", "ishashable"]


def ishashable(obj):
    return isinstance(obj, Hashable)


class Signature(dict):

    def __init__(self, *args, **kwargs):
        super().__init__(**kwargs)
        if 'abstract' in args:
            setattr(self, 'isabstract', True)
        else:
            setattr(self, 'isabstract', False)

    @classmethod
    def from_func(cls, funcobj, *axioms, **kwargs):

        if getattr(funcobj, 'isabstractoperation', False):
            sig = Signature('abstract', **kwargs)
        else:
            sig = Signature(**kwargs)

        sig['name'] = funcobj.__name__
        if len(axioms) > 0:
            sig['axioms'] = frozenset(axioms)
        else:
            sig['axioms'] = frozenset()

        params = signature(funcobj).parameters
        if 'args' in params or 'kwargs' in params:
            raise TypeError('Operation can only have a finite number '
                            'of arguments!')
        if 'self' in params or 'cls' in params:
            sig['arity'] = len(params)-1
        else:
            sig['arity'] = len(params)

        if sig['arity'] == 0:
            sig['dtype'] = [Any]
        else:
            sig['dtype'] = []
            for pname, param in params.items():
                if pname not in ['self', 'cls']:
                    if param.annotation is not Parameter.empty:
                        sig['dtype'].append(param.annotation)
                    else:
                        sig['dtype'].append(Any)

        annotations = funcobj.__annotations__
        if 'return' in annotations:
            sig['rtype'] = annotations['return']
        else:
            sig['rtype'] = Any

        return sig

    @classmethod
    def from_property(cls, funcobj, **kwargs):

        if funcobj.isabstractattribute:
            sig = Signature('abstract', **kwargs)
        else:
            sig = Signature(**kwargs)

        sig['name'] = funcobj.__name__

        annotations = funcobj.__annotations__
        if 'return' in annotations:
            sig['rtype'] = annotations['return']
        else:
            sig['rtype'] = Any

        return sig

    def compatible_func(self, other : 'Signature') -> bool:
        """
        Returns True if two instances are compatible in terms of
        domain type and result type.
        """
        if not isinstance(other, Signature) :
            return False

        #check domain types
        for type1, type2 in zip(self['dtype'], other['dtype']):
            if type1 == Any :
                continue
            elif isinstance(type1, _GenericAlias):
                if not isinstance(type2, _GenericAlias):
                    typeargs = type1.__dict__['__args__']
                    if type2 not in typeargs:
                        return False
                else:
                    if type1 != type2 :
                        return False
            else:
                if type1 != type2 :
                    return False

        #check result type
        if isinstance(self['rtype'], _GenericAlias):
            if not isinstance(other['rtype'], _GenericAlias):
                typeargs = self['rtype'].__dict__['__args__']
                if not other['rtype'] in typeargs :
                    return False
            else:
                if self['rtype'] != other['rtype']  :
                    return False
        elif self['rtype'] != Any:
            if self['rtype'] != other['rtype']  :
                return False

        return True

    def accepts_func(self, other : 'Signature') -> bool:
        """
        Returns True if other (implemented operation's signature) is
        compatible to self (abstract operation's signature).
        Every value to every key of self must be present in other.
        For example, here is where axioms are forced to match
        between two signatures.
        """
        if any([not self.isabstract, other.isabstract]):
            return False
        if not self.compatible_op(other) :
            return False
        if not self['name'] == other['name']:
            return False

        for key,value in self.items():
            if key not in ['rtype', 'dtype']:
                if key in other:
                    if isinstance(value, frozenset) and \
                            isinstance(other[key], frozenset):
                        if not value.issubset(other[key]):
                            return False
                    else:
                        if not value == other[key]:
                            return False
                else:
                    return False
        return True

    def update_func(self, other : 'Signature'):
        """
        Updates self with the content of other. Both must be abstracts.
        """
        assert isinstance(other, Signature)
        assert self.isabstract and other.isabstract
        assert self.compatible_op(other)
        for key,value in other.items():
            if key not in ['rtype', 'dtype']:
                if isinstance(value, frozenset):
                    if key in self and isinstance(self[key], frozenset):
                        s = set(self[key])
                        s.update(set(value))
                        self[key] = frozenset(s)
                    else:
                        self[key] = value
                else:
                    self[key] = value
        return

    def accepts_prop(self, value : Any = None) -> bool:
        """
        Returns True if other (implemented attribute's signature) is
        compatible to self (abstract attribute's signature).
        Name and result type must match.
        """
        if not self.isabstract:
            return False
        if value is None:
            return False

        #check result type
        if isinstance(self['rtype'], _GenericAlias):
            if not isinstance(type(value), _GenericAlias):
                typeargs = self['rtype'].__dict__['__args__']
                if not type(value) in typeargs:
                    return False
            else:
                if self['rtype'] != type(value):
                    return False
        elif self['rtype'] != Any:
            if self['rtype'] != type(value):
                return False
        return True

    def accepts_params(self, *args):
        raise NotImplementedError
