from numba.core.types.abstract import Callable, Literal, Type, Hashable
from numba.core.types.common import (Dummy, IterableType, Opaque,
                                     SimpleIteratorType)
from numba.core.typeconv import Conversion
from numba.core.errors import TypingError, LiteralTypingError
from numba.core.ir import UndefinedType
from numba.core.utils import get_hashable_key


class PyObject(Dummy):
    """
    A generic CPython object.
    """

    def is_precise(self):
        return False


class Phantom(Dummy):
    """
    A type that cannot be materialized.  A Phantom cannot be used as
    argument or return type.
    """


class Undefined(Dummy):
    """
    A type that is left imprecise.  This is used as a temporaray placeholder
    during type inference in the hope that the type can be later refined.
    """

    def is_precise(self):
        return False


class RawPointer(Opaque):
    """
    A raw pointer without any specific meaning.
    """


class StringLiteral(Literal, Dummy):

    def can_convert_to(self, typingctx, other):
        if isinstance(other, UnicodeType):
            return Conversion.safe


Literal.ctor_map[str] = StringLiteral


def unliteral(lit_type):
    """
    Get base type from Literal type.
    """
    if hasattr(lit_type, '__unliteral__'):
        return lit_type.__unliteral__()
    return getattr(lit_type, 'literal_type', lit_type)


def literal(value):
    """Returns a Literal instance or raise LiteralTypingError
    """
    ty = type(value)
    if isinstance(value, Literal):
        msg = "the function does not accept a Literal type; got {} ({})"
        raise ValueError(msg.format(value, ty))
    try:
        ctor = Literal.ctor_map[ty]
    except KeyError:
        raise LiteralTypingError("{} cannot be used as a literal".format(ty))
    else:
        return ctor(value)


def maybe_literal(value):
    """Get a Literal type for the value or None.
    """
    try:
        return literal(value)
    except LiteralTypingError:
        return


class Omitted(Opaque):
    """
    An omitted function argument with a default value.
    """

    def __init__(self, value):
        self._value = value
        # Use helper function to support both hashable and non-hashable
        # values. See discussion in gh #6957.
        self._value_key = get_hashable_key(value)
        super(Omitted, self).__init__("omitted(default=%r)" % (value,))

    @property
    def key(self):
        return type(self._value), self._value_key

    @property
    def value(self):
        return self._value


class VarArg(Type):
    """
    Special type representing a variable number of arguments at the
    end of a function's signature.  Only used for signature matching,
    not for actual values.
    """

    def __init__(self, dtype):
        self.dtype = dtype
        super(VarArg, self).__init__("*%s" % dtype)

    @property
    def key(self):
        return self.dtype


class Module(Dummy):
    def __init__(self, pymod):
        self.pymod = pymod
        super(Module, self).__init__("Module(%s)" % pymod)

    @property
    def key(self):
        return self.pymod


class MemInfoPointer(Type):
    """
    Pointer to a Numba "meminfo" (i.e. the information for a managed
    piece of memory).
    """
    mutable = True

    def __init__(self, dtype):
        self.dtype = dtype
        name = "memory-managed *%s" % dtype
        super(MemInfoPointer, self).__init__(name)

    @property
    def key(self):
        return self.dtype


class CPointer(Type):
    """
    Type class for pointers to other types.

    Attributes
    ----------
        dtype : The pointee type
        addrspace : int
            The address space pointee belongs to.
    """
    mutable = True

    def __init__(self, dtype, addrspace=None):
        self.dtype = dtype
        self.addrspace = addrspace
        if addrspace is not None:
            name = "%s_%s*" % (dtype, addrspace)
        else:
            name = "%s*" % dtype
        super(CPointer, self).__init__(name)

    @property
    def key(self):
        return self.dtype, self.addrspace


class EphemeralPointer(CPointer):
    """
    Type class for pointers which aren't guaranteed to last long - e.g.
    stack-allocated slots.  The data model serializes such pointers
    by copying the data pointed to.
    """


class EphemeralArray(Type):
    """
    Similar to EphemeralPointer, but pointing to an array of elements,
    rather than a single one.  The array size must be known at compile-time.
    """

    def __init__(self, dtype, count):
        self.dtype = dtype
        self.count = count
        name = "*%s[%d]" % (dtype, count)
        super(EphemeralArray, self).__init__(name)

    @property
    def key(self):
        return self.dtype, self.count


class Object(Type):
    # XXX unused?
    mutable = True

    def __init__(self, clsobj):
        self.cls = clsobj
        name = "Object(%s)" % clsobj.__name__
        super(Object, self).__init__(name)

    @property
    def key(self):
        return self.cls


class Optional(Type):
    """
    Type class for optional types, i.e. union { some type, None }
    """

    def __init__(self, typ):
        assert not isinstance(typ, (Optional, NoneType))
        typ = unliteral(typ)
        self.type = typ
        name = "OptionalType(%s)" % self.type
        super(Optional, self).__init__(name)

    @property
    def key(self):
        return self.type

    def can_convert_to(self, typingctx, other):
        if isinstance(other, Optional):
            return typingctx.can_convert(self.type, other.type)
        else:
            conv = typingctx.can_convert(self.type, other)
            if conv is not None:
                return max(conv, Conversion.safe)

    def can_convert_from(self, typingctx, other):
        if isinstance(other, NoneType):
            return Conversion.promote
        elif isinstance(other, Optional):
            return typingctx.can_convert(other.type, self.type)
        else:
            conv = typingctx.can_convert(other, self.type)
            if conv is not None:
                return max(conv, Conversion.promote)

    def unify(self, typingctx, other):
        if isinstance(other, Optional):
            unified = typingctx.unify_pairs(self.type, other.type)
        else:
            unified = typingctx.unify_pairs(self.type, other)

        if unified is not None:
            if isinstance(unified, Optional):
                return unified
            else:
                return Optional(unified)


class NoneType(Opaque):
    """
    The type for None.
    """

    def unify(self, typingctx, other):
        """
        Turn anything to a Optional type;
        """
        if isinstance(other, (Optional, NoneType)):
            return other
        return Optional(other)


class EllipsisType(Opaque):
    """
    The type for the Ellipsis singleton.
    """


class ExceptionClass(Callable, Phantom):
    """
    The type of exception classes (not instances).
    """

    def __init__(self, exc_class):
        assert issubclass(exc_class, BaseException)
        name = "%s" % (exc_class.__name__)
        self.exc_class = exc_class
        super(ExceptionClass, self).__init__(name)

    def get_call_type(self, context, args, kws):
        return self.get_call_signatures()[0][0]

    def get_call_signatures(self):
        from numba.core import typing
        return_type = ExceptionInstance(self.exc_class)
        return [typing.signature(return_type)], False

    def get_impl_key(self, sig):
        return type(self)

    @property
    def key(self):
        return self.exc_class


class ExceptionInstance(Phantom):
    """
    The type of exception instances.  *exc_class* should be the
    exception class.
    """

    def __init__(self, exc_class):
        assert issubclass(exc_class, BaseException)
        name = "%s(...)" % (exc_class.__name__,)
        self.exc_class = exc_class
        super(ExceptionInstance, self).__init__(name)

    @property
    def key(self):
        return self.exc_class


class SliceType(Type):

    def __init__(self, name, members):
        assert members in (2, 3)
        self.members = members
        self.has_step = members >= 3
        super(SliceType, self).__init__(name)

    @property
    def key(self):
        return self.members


class SliceLiteral(Literal, SliceType):
    def __init__(self, value):
        self._literal_init(value)
        name = 'Literal[slice]({})'.format(value)
        members = 2 if value.step is None else 3
        SliceType.__init__(self, name=name, members=members)

    @property
    def key(self):
        sl = self.literal_value
        return sl.start, sl.stop, sl.step


Literal.ctor_map[slice] = SliceLiteral


class ClassInstanceType(Type):
    """
    The type of a jitted class *instance*.  It will be the return-type
    of the constructor of the class.
    """
    mutable = True
    name_prefix = "instance"

    def __init__(self, class_type):
        self.class_type = class_type
        name = "{0}.{1}".format(self.name_prefix, self.class_type.name)
        super(ClassInstanceType, self).__init__(name)

    def get_data_type(self):
        return ClassDataType(self)

    def get_reference_type(self):
        return self

    @property
    def key(self):
        return self.class_type.key

    @property
    def classname(self):
        return self.class_type.class_name

    @property
    def jit_props(self):
        return self.class_type.jit_props

    @property
    def jit_static_methods(self):
        return self.class_type.jit_static_methods

    @property
    def jit_methods(self):
        return self.class_type.jit_methods

    @property
    def struct(self):
        return self.class_type.struct

    @property
    def methods(self):
        return self.class_type.methods

    @property
    def static_methods(self):
        return self.class_type.static_methods


class ClassType(Callable, Opaque):
    """
    The type of the jitted class (not instance).  When the type of a class
    is called, its constructor is invoked.
    """
    mutable = True
    name_prefix = "jitclass"
    instance_type_class = ClassInstanceType

    def __init__(self, class_def, ctor_template_cls, struct, jit_methods,
                 jit_props, jit_static_methods):
        self.class_name = class_def.__name__
        self.class_doc = class_def.__doc__
        self._ctor_template_class = ctor_template_cls
        self.jit_methods = jit_methods
        self.jit_props = jit_props
        self.jit_static_methods = jit_static_methods
        self.struct = struct
        fielddesc = ','.join("{0}:{1}".format(k, v) for k, v in struct.items())
        name = "{0}.{1}#{2:x}<{3}>".format(self.name_prefix, self.class_name,
                                           id(self), fielddesc)
        super(ClassType, self).__init__(name)

    def get_call_type(self, context, args, kws):
        return self.ctor_template(context).apply(args, kws)

    def get_call_signatures(self):
        return (), True

    def get_impl_key(self, sig):
        return type(self)

    @property
    def methods(self):
        return {k: v.py_func for k, v in self.jit_methods.items()}

    @property
    def static_methods(self):
        return {k: v.py_func for k, v in self.jit_static_methods.items()}

    @property
    def instance_type(self):
        return ClassInstanceType(self)

    @property
    def ctor_template(self):
        return self._specialize_template(self._ctor_template_class)

    def _specialize_template(self, basecls):
        return type(basecls.__name__, (basecls,), dict(key=self))


class DeferredType(Type):
    """
    Represents a type that will be defined later.  It must be defined
    before it is materialized (used in the compiler).  Once defined, it
    behaves exactly as the type it is defining.
    """

    def __init__(self):
        self._define = None
        name = "{0}#{1}".format(type(self).__name__, id(self))
        super(DeferredType, self).__init__(name)

    def get(self):
        if self._define is None:
            raise RuntimeError("deferred type not defined")
        return self._define

    def define(self, typ):
        if self._define is not None:
            raise TypeError("deferred type already defined")
        if not isinstance(typ, Type):
            raise TypeError("arg is not a Type; got: {0}".format(type(typ)))
        self._define = typ

    def unify(self, typingctx, other):
        return typingctx.unify_pairs(self.get(), other)


class ClassDataType(Type):
    """
    Internal only.
    Represents the data of the instance.  The representation of
    ClassInstanceType contains a pointer to a ClassDataType which represents
    a C structure that contains all the data fields of the class instance.
    """

    def __init__(self, classtyp):
        self.class_type = classtyp
        name = "data.{0}".format(self.class_type.name)
        super(ClassDataType, self).__init__(name)


class ContextManager(Callable, Phantom):
    """
    An overly-simple ContextManager type that cannot be materialized.
    """

    def __init__(self, cm):
        self.cm = cm
        super(ContextManager, self).__init__("ContextManager({})".format(cm))

    def get_call_signatures(self):
        if not self.cm.is_callable:
            msg = "contextmanager {} is not callable".format(self.cm)
            raise TypingError(msg)

        return (), False

    def get_call_type(self, context, args, kws):
        from numba.core import typing

        if not self.cm.is_callable:
            msg = "contextmanager {} is not callable".format(self.cm)
            raise TypingError(msg)

        posargs = list(args) + [v for k, v in sorted(kws.items())]
        return typing.signature(self, *posargs)

    def get_impl_key(self, sig):
        return type(self)


class UnicodeType(IterableType, Hashable):

    def __init__(self, name):
        super(UnicodeType, self).__init__(name)

    @property
    def iterator_type(self):
        return UnicodeIteratorType(self)


class UnicodeIteratorType(SimpleIteratorType):

    def __init__(self, dtype):
        name = "iter_unicode"
        self.data = dtype
        super(UnicodeIteratorType, self).__init__(name, dtype)
