"""
Template for maps

WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in
"""


from cpython.ref cimport Py_INCREF,Py_DECREF




{{py:

# map_name, name, key_type, val_type
map_types       = [('Int64toInt64', 'int64toint64', 'int64_t', 'int64_t'),
                   ('Int64toFloat64', 'int64tofloat64', 'int64_t', 'float64_t'),
                   ('Float64toInt64', 'float64toint64', 'float64_t', 'int64_t'),
                   ('Float64toFloat64', 'float64tofloat64', 'float64_t', 'float64_t'),
                   ('Int32toInt32', 'int32toint32', 'int32_t', 'int32_t'),
                   ('Int32toFloat32', 'int32tofloat32', 'int32_t', 'float32_t'),
                   ('Float32toInt32', 'float32toint32', 'float32_t', 'int32_t'),
                   ('Float32toFloat32', 'float32tofloat32', 'float32_t', 'float32_t'),
                   ('PyObject', 'pyobject', 'object', 'object'),
                  ]
}}


{{for map_name, name, key_type, val_type in map_types}}


cdef class {{map_name}}Map:

    {{if map_name != 'PyObject'}}
    @classmethod
    def fromkeys(cls, iterable, value):
        return {{map_name}}Map(((key, value) for key in iterable))

    def __cinit__(self, iterable=None, *, number_of_elements_hint=None):
        """
        number_of_elements_hint - number of elements without the need of reallocation.
        """
        self.table = kh_init_{{name}}map()
        if number_of_elements_hint is not None:
            kh_resize_{{name}}map(self.table, element_n_to_bucket_n(number_of_elements_hint))
        cdef {{key_type}} key
        cdef {{val_type}} val
        if iterable is not None:
            for key, val in iterable:
                    self.cput(key, val)

    def __dealloc__(self):
        if self.table is not NULL:
            kh_destroy_{{name}}map(self.table)
            self.table = NULL

    cpdef void discard(self, {{key_type}} key) except *:
        cdef khint_t k
        k = kh_get_{{name}}map(self.table, key)
        if k != self.table.n_buckets:
            kh_del_{{name}}map(self.table, k)

    cdef bint contains(self, {{key_type}} key) except *:
        cdef khint_t k
        k = kh_get_{{name}}map(self.table, key)
        return k != self.table.n_buckets

    def __contains__(self, {{key_type}} key):
        return self.contains(key)

    {{else}}
    @classmethod
    def fromkeys(cls, iterable, value):
        return {{map_name}}Map((key, value) for key in iterable)

    def __cinit__(self, iterable=None, *, number_of_elements_hint=None):
        """
        number_of_elements_hint - number of elements without the need of reallocation.
        """
        self.table = kh_init_pyobjectmap()
        if number_of_elements_hint is not None:
            kh_resize_pyobjectmap(self.table, element_n_to_bucket_n(number_of_elements_hint))
        if iterable is not None:
            for key, val in iterable:
                    self.cput(key, val)

 
    cpdef void discard(self, object key) except *:
        cdef khint_t k
        k = kh_get_pyobjectmap(self.table, <pyobject_t>key)
        if k != self.table.n_buckets:
            Py_DECREF(<object>(self.table.keys[k]))
            Py_DECREF(<object>(self.table.vals[k]))
            kh_del_pyobjectmap(self.table, k)

    def __dealloc__(self):
        cdef Py_ssize_t i
        if self.table is not NULL:
            for i in range(self.table.size):
                if kh_exist_pyobjectmap(self.table, i):
                    Py_DECREF(<object>(self.table.keys[i]))
                    Py_DECREF(<object>(self.table.vals[i]))
            kh_destroy_pyobjectmap(self.table)
            self.table = NULL

    cdef bint contains(self, pyobject_t key) except *:
        cdef khint_t k
        k = kh_get_pyobjectmap(self.table, key)
        return k != self.table.n_buckets

    def __contains__(self, object key):
        return self.contains(<pyobject_t>key)

    {{endif}}

    def __len__(self):
        return self.size()
  
    cdef khint_t size(self):
        return self.table.size

    {{if map_name != 'PyObject'}}
    cpdef void cput(self, {{key_type}} key, {{val_type}} val) except *:
        cdef:
            khint_t k
            int ret = 0

        k = kh_put_{{name}}map(self.table, key, &ret)
        self.table.keys[k] = key
        self.table.vals[k] = val

    def __setitem__(self, key, val):
        self.cput(key, val)

    cpdef {{val_type}} cget(self, {{key_type}} key) except *:
        k = kh_get_{{name}}map(self.table, key)
        if k != self.table.n_buckets:
            return self.table.vals[k]
        else:
            raise KeyError(key)

    def __getitem__(self, key):
        return self.cget(key)
        

    {{else}}
    cpdef void cput(self, object key, object val) except *:
        cdef:
            khint_t k
            int ret = 0
        k = kh_put_pyobjectmap(self.table, <pyobject_t>key, &ret)
        if not ret:
            Py_DECREF(<object>(self.table.vals[k]))
        else:
            Py_INCREF(key)
        Py_INCREF(val)
        self.table.vals[k] = <pyobject_t> val
 
    def __setitem__(self, key, val):
        self.cput(key, val)

    cpdef object cget(self, object key):
        k = kh_get_pyobjectmap(self.table, <pyobject_t>key)
        if k != self.table.n_buckets:
            return <object>self.table.vals[k]
        else:
            raise KeyError(key)

    def __getitem__(self, key):
        return self.cget(key)
    {{endif}}

    cdef {{map_name}}MapIterator get_iter(self, int view_type):
        return {{map_name}}MapIterator(self, view_type)

    def clear(self):
        cdef {{map_name}}Map tmp={{map_name}}Map()
        swap_{{name}}map(self, tmp)

    def copy(self):
        return copy_{{name}}map(self)

    def update(self, other):
        if isinstance(other, {{map_name}}Map):
            update_{{name}}map(self, other)
            return
        for key,val in other:
            self[key]=val

    def setdefault(self, key, default):
        try:
            return self[key]
        except KeyError:
            self[key]=default
            return default

    def get(self, *args, **kwargs):
        if len(args)==0:
            raise TypeError("get() expected at least 1 arguments, got 0")
        if len(args)>2:
            raise TypeError("get() expected at most 2 arguments, got {0}".format(len(args)))
        if kwargs:
            raise TypeError("get() takes no keyword arguments")
        key = args[0]
        try:
            return self[key]
        except KeyError:
            if len(args)==1:
                return None
            return args[1]

    def pop(self, *args, **kwargs):
        if len(args)==0:
            raise TypeError("pop() expected at least 1 arguments, got 0")
        if len(args)>2:
            raise TypeError("pop() expected at most 2 arguments, got {0}".format(len(args)))
        if kwargs:
            raise TypeError("pop() takes no keyword arguments")
        key = args[0]
        try:
            val = self[key]
        except KeyError as e:
            if len(args)==1:
                raise e from None
            return args[1]
        del self[key]
        return val

    def popitem(self):
        if self.size()== 0:
            raise KeyError("popitem(): dictionary is empty")
        key = next(iter(self))
        val = self.pop(key)
        return (key, val)

    def keys(self):
        return {{map_name}}MapView(self, 0)

    def values(self):
        return {{map_name}}MapView(self, 1)

    def items(self):
        return {{map_name}}MapView(self, 2)

    def __iter__(self):
        return iter(self.keys())

    def __delitem__(self, key):
        cdef size_t old=self.size()
        self.discard(key)
        if old==self.size():
            raise KeyError(key)

    def __eq__(self, other):
        return are_equal_{{name}}map(self,other)


### Iterator:
cdef class {{map_name}}MapIterator:

    cdef void __move(self) except *:
        while self.it<self.parent.table.n_buckets and not kh_exist_{{name}}map(self.parent.table, self.it):
              self.it+=1       

    cdef bint has_next(self) except *:
        self.__move()
        return self.it < self.parent.table.n_buckets
      
    # doesn't work if there was change between last has_next() and next()       
    cdef {{name}}_key_val_pair next(self) except *:
        cdef {{name}}_key_val_pair result 
        result.key = self.parent.table.keys[self.it]
        result.val = self.parent.table.vals[self.it]
        self.it+=1#ensure at least one move!
        return result

    def __cinit__(self, {{map_name}}Map parent, view_type):
        self.parent = parent
        self.view_type = view_type
        #search the start:
        self.it = 0
        self.__move()

    def __next__(self):
        cdef {{name}}_key_val_pair pair
        if self.has_next():
            pair=self.next()

            {{if map_name == 'PyObject'}}
            if self.view_type == 0:           # keys
                return <object>pair.key
            if self.view_type == 1:           # vals
                return <object>pair.val
            else:                            # items
                return (<object>pair.key, <object>pair.val)

            {{else}}
            if self.view_type == 0:           # keys
                return pair.key
            if self.view_type == 1:           # vals
                return pair.val
            else:                        # items
                return (pair.key, pair.val)
           {{endif}}
        else:
            raise StopIteration


cdef class {{map_name}}MapView:
    cdef {{map_name}}MapIterator get_iter(self):
        return {{map_name}}MapIterator(self.parent, self.view_type)  

    def __cinit__(self, {{map_name}}Map parent, view_type):
        self.parent = parent
        self.view_type = view_type

    def __iter__(self):
        return self.get_iter()

    def __len__(self):
        return self.parent.size()

    def __contains__(self, x):
        for y in self:
            if x==y:
                return True
        return False

##########################      Utils:

{{if map_name != 'PyObject'}}
cpdef {{map_name}}Map {{map_name}}Map_from_buffers({{key_type}}[:] keys, {{val_type}}[:] vals, double size_hint=0.0):
{{else}}
cpdef PyObjectMap PyObjectMap_from_buffers(object[:] keys, object[:] vals, double size_hint=0.0):
{{endif}}
    cdef Py_ssize_t n = len(keys)
    cdef Py_ssize_t b = len(vals)
    if b < n:
        n = b
    cdef Py_ssize_t at_least_needed = element_n_from_size_hint(<khint_t>n, size_hint)
    res={{map_name}}Map(number_of_elements_hint=at_least_needed)
    cdef Py_ssize_t i
    for i in range(n):
        res.cput(keys[i], vals[i])
    return res


{{if val_type == 'object'}}
cdef object DEFAULT_VALUE_{{name}} = None
{{elif val_type.startswith('float')}}
cdef {{val_type}} DEFAULT_VALUE_{{name}} = float("nan")
{{else}}
cdef {{val_type}} DEFAULT_VALUE_{{name}} = 0
{{endif}}

{{if map_name != 'PyObject'}}
cpdef size_t {{map_name}}Map_to({{map_name}}Map map, {{key_type}}[:] keys, {{val_type}}[:] vals, bint stop_at_unknown=True, {{val_type}} default_value=DEFAULT_VALUE_{{name}}) except *:
{{else}}
cpdef size_t PyObjectMap_to(PyObjectMap map, object[:] keys, object[:] vals, bint stop_at_unknown=True, object default_value=None) except *:
{{endif}}
    """returns number of found keys"""
    if map is None:
        raise TypeError("'NoneType' is not a map")
    cdef size_t n = len(keys)
    if n != len(vals):
        raise ValueError("Different lengths of keys and vals arrays")
    cdef size_t i
    cdef khint_t k
    cdef size_t res = 0
    for i in range(n):
        {{if map_name != 'PyObject'}}
        k = kh_get_{{name}}map(map.table, keys[i])
        {{else}}
        k = kh_get_{{name}}map(map.table,<pyobject_t> keys[i])
        {{endif}}
        if k != map.table.n_buckets:
            {{if map_name != 'PyObject'}}
            vals[i] = map.table.vals[k]
            {{else}}
            vals[i] = <object>map.table.vals[k]
            {{endif}}
            res += 1
        else:
            vals[i] = default_value
            if stop_at_unknown:
                return res
    return res

cpdef void swap_{{name}}map({{map_name}}Map a, {{map_name}}Map b) except *:
    if a is None or b is None:
        raise TypeError("'NoneType' object is not iterable")

    cdef kh_{{name}}map_t *tmp=a.table
    a.table=b.table
    b.table=tmp


cpdef {{map_name}}Map copy_{{name}}map({{map_name}}Map s):
    if s is None:
        return None
    cdef {{map_name}}Map result = {{map_name}}Map(number_of_elements_hint=s.size())
    cdef {{map_name}}MapIterator it=s.get_iter(2)
    cdef {{name}}_key_val_pair p
    while it.has_next():
        p = it.next()
        {{if map_name == 'PyObject'}}
        result.cput(<object>p.key, <object>p.val)
        {{else}}
        result.cput(p.key, p.val)
        {{endif}}
    return result


cpdef bint are_equal_{{name}}map({{map_name}}Map a, {{map_name}}Map b) except *:
    if a is None or b is None:
        raise TypeError("'NoneType' object is not iterable")
    if a.size()!=b.size():
        return False
    cdef {{map_name}}MapIterator it=a.get_iter(2)
    cdef {{name}}_key_val_pair p
    while it.has_next():
        p = it.next()
        if not b.contains(p.key):
            return False
    return True


cpdef void update_{{name}}map({{map_name}}Map a, {{map_name}}Map b) except *:
    if a is None or b is None:
        raise TypeError("'NoneType' object is not iterable")
    cdef {{map_name}}MapIterator it=b.get_iter(2)
    cdef {{name}}_key_val_pair p
    while it.has_next():
        p = it.next()
        {{if map_name == 'PyObject'}}
        a.cput(<object>p.key, <object>p.val)
        {{else}}
        a.cput(p.key, p.val)
        {{endif}}

{{endfor}}
