from .object cimport PyObject
from .pyport cimport uint64_t

cdef extern from *:
    # On Python 2, PyDict_GetItemWithError is called _PyDict_GetItemWithError
    # Also backport PyDict_GetItemStringRef and PyDict_SetDefaultRef
    """
    #if PY_MAJOR_VERSION <= 2
    #define PyDict_GetItemWithError _PyDict_GetItemWithError
    #endif

    #if __PYX_LIMITED_VERSION_HEX < 0x030d0000
    static CYTHON_INLINE int
    __Pyx_CAPI_PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result)
    {
        int res;
        PyObject *key_obj = PyUnicode_FromString(key);
        if (key_obj == NULL) {
            *result = NULL;
            return -1;
        }
        res = __Pyx_PyDict_GetItemRef(mp, key_obj, result);
        Py_DECREF(key_obj);
        return res;
    }
    #else
    #define __Pyx_CAPI_PyDict_GetItemStringRef PyDict_GetItemStringRef
    #endif
    #if PY_VERSION_HEX < 0x030d0000 || (CYTHON_COMPILING_IN_LIMITED_API && __PYX_LIMITED_VERSION_HEX < 0x030F0000)
    static CYTHON_INLINE int
    __Pyx_CAPI_PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value,
                        PyObject **result)
    {
        PyObject *value;
        if (__Pyx_PyDict_GetItemRef(d, key, &value) < 0) {
            // get error
            if (result) {
                *result = NULL;
            }
            return -1;
        }
        if (value != NULL) {
            // present
            if (result) {
                *result = value;
            }
            else {
                Py_DECREF(value);
            }
            return 1;
        }

        // missing: set the item
        if (PyDict_SetItem(d, key, default_value) < 0) {
            // set error
            if (result) {
                *result = NULL;
            }
            return -1;
        }
        if (result) {
            Py_INCREF(default_value);
            *result = default_value;
        }
        return 0;
    }
    #else
    #define __Pyx_CAPI_PyDict_SetDefaultRef PyDict_SetDefaultRef
    #endif
    """
    int PyDict_GetItemRef "__Pyx_PyDict_GetItemRef" (object p, object key, PyObject* *result) except -1
    # Return a new strong reference to the object from dictionary p
    # which has a key key:
    # - If the key is present, set *result to a new strong reference to
    #   the value and return 1.
    # - If the key is missing, set *result to NULL and return 0.
    # - On error, raise an exception and return -1.

    int PyDict_GetItemStringRef "__Pyx_CAPI_PyDict_GetItemStringRef" (object p, const char *key, PyObject* *result) except -1
    # Similar to PyDict_GetItemRef(), but key is specified as a const char*
    # UTF-8 encoded bytes string, rather than a PyObject*.

    int PyDict_SetDefaultRef "__Pyx_CAPI_PyDict_SetDefaultRef" (object p, object key, object default_value, PyObject* *result) except -1
    # Inserts default_value into the dictionary p with a key of key if the
    # key is not already present in the dictionary. If result is not NULL,
    # then *result is set to a strong reference to either default_value,
    # if the key was not present, or the existing value, if key was already
    # present in the dictionary. Returns 1 if the key was present and
    # default_value was not inserted, or 0 if the key was not present and
    # default_value was inserted. On failure, returns -1, sets an exception,
    # and sets *result to NULL.
    # For clarity: if you have a strong reference to default_value before
    # calling this function, then after it returns, you hold a strong
    # reference to both default_value and *result (if it’s not NULL). These
    # may refer to the same object: in that case you hold two separate
    # references to it.

cdef extern from "Python.h":
    ############################################################################
    # 7.4.1 Dictionary Objects
    ############################################################################

    # PyDictObject
    #
    # This subtype of PyObject represents a Python dictionary object
    # (i.e. the 'dict' type).

    # PyTypeObject PyDict_Type
    #
    # This instance of PyTypeObject represents the Python dictionary
    # type. This is exposed to Python programs as dict and
    # types.DictType.

    bint PyDict_Check(object p)
    # Return true if p is a dict object or an instance of a subtype of
    # the dict type.

    bint PyDict_CheckExact(object p)
    # Return true if p is a dict object, but not an instance of a
    # subtype of the dict type.

    dict PyDict_New()
    # Return value: New reference.
    # Return a new empty dictionary, or NULL on failure.

    object PyDictProxy_New(object dict)
    # Return value: New reference.
    # Return a proxy object for a mapping which enforces read-only
    # behavior. This is normally used to create a proxy to prevent
    # modification of the dictionary for non-dynamic class types.

    void PyDict_Clear(object p)
    # Empty an existing dictionary of all key-value pairs.

    int PyDict_Contains(object p, object key) except -1
    # Determine if dictionary p contains key. If an item in p is
    # matches key, return 1, otherwise return 0. On error, return
    # -1. This is equivalent to the Python expression "key in p".

    dict PyDict_Copy(object p)
    # Return value: New reference.
    # Return a new dictionary that contains the same key-value pairs as p.

    int PyDict_SetItem(object p, object key, object val) except -1
    # Insert value into the dictionary p with a key of key. key must
    # be hashable; if it isn't, TypeError will be raised. Return 0 on
    # success or -1 on failure.

    int PyDict_SetItemString(object p, const char *key, object val) except -1
    # Insert value into the dictionary p using key as a key. key
    # should be a char*. The key object is created using
    # PyString_FromString(key). Return 0 on success or -1 on failure.

    int PyDict_DelItem(object p, object key) except -1
    # Remove the entry in dictionary p with key key. key must be
    # hashable; if it isn't, TypeError is raised. Return 0 on success
    # or -1 on failure.

    int PyDict_DelItemString(object p, const char *key) except -1
    # Remove the entry in dictionary p which has a key specified by
    # the string key. Return 0 on success or -1 on failure.

    PyObject* PyDict_GetItem(object p, object key)
    # Return value: Borrowed reference.
    # Return the object from dictionary p which has a key key. Return
    # NULL if the key key is not present, but without setting an
    # exception.

    PyObject* PyDict_GetItemWithError(object p, object key) except? NULL
    # Return value: Borrowed reference.
    # Variant of PyDict_GetItem() that does not suppress exceptions. Return
    # NULL with an exception set if an exception occurred. Return NULL
    # without an exception set if the key wasn’t present.

    PyObject* PyDict_GetItemString(object p, const char *key)
    # Return value: Borrowed reference.
    # This is the same as PyDict_GetItem(), but key is specified as a
    # char*, rather than a PyObject*.

    PyObject* PyDict_SetDefault(object p, object key, object default) except NULL
    # Return value: Borrowed reference.
    # This is the same as the Python-level dict.setdefault(). If present, it
    # returns the value corresponding to key from the dictionary p. If the key
    # is not in the dict, it is inserted with value defaultobj and defaultobj
    # is returned. This function evaluates the hash function of key only once,
    # instead of evaluating it independently for the lookup and the insertion.

    list PyDict_Items(object p)
    # Return value: New reference.
    # Return a PyListObject containing all the items from the
    # dictionary, as in the dictionary method items() (see the Python
    # Library Reference).

    list PyDict_Keys(object p)
    # Return value: New reference.
    # Return a PyListObject containing all the keys from the
    # dictionary, as in the dictionary method keys() (see the Python
    # Library Reference).

    list PyDict_Values(object p)
    # Return value: New reference.
    # Return a PyListObject containing all the values from the
    # dictionary p, as in the dictionary method values() (see the
    # Python Library Reference).

    Py_ssize_t PyDict_Size(object p) except -1
    # Return the number of items in the dictionary. This is equivalent
    # to "len(p)" on a dictionary.

    int PyDict_Next(object p, Py_ssize_t *ppos, PyObject* *pkey, PyObject* *pvalue)
    # Iterate over all key-value pairs in the dictionary p. The int
    # referred to by ppos must be initialized to 0 prior to the first
    # call to this function to start the iteration; the function
    # returns true for each pair in the dictionary, and false once all
    # pairs have been reported. The parameters pkey and pvalue should
    # either point to PyObject* variables that will be filled in with
    # each key and value, respectively, or may be NULL. Any references
    # returned through them are borrowed. ppos should not be altered
    # during iteration. Its value represents offsets within the
    # internal dictionary structure, and since the structure is
    # sparse, the offsets are not consecutive.
    # For example:
    #
    #object key, *value;
    #int pos = 0;
    #
    #while (PyDict_Next(self->dict, &pos, &key, &value)) {
    #   /* do something interesting with the values... */
    #    ...
    #}
    # The dictionary p should not be mutated during iteration. It is
    # safe (since Python 2.1) to modify the values of the keys as you
    # iterate over the dictionary, but only so long as the set of keys
    # does not change. For example:
    # object key, *value;
    # int pos = 0;
    # while (PyDict_Next(self->dict, &pos, &key, &value)) {
    #    int i = PyInt_AS_LONG(value) + 1;
    #    object o = PyInt_FromLong(i);
    #    if (o == NULL)
    #        return -1;
    #    if (PyDict_SetItem(self->dict, key, o) < 0) {
    #        Py_DECREF(o);
    #        return -1;
    #    }
    #    Py_DECREF(o);
    # }

    int PyDict_Merge(object a, object b, int override) except -1
    # Iterate over mapping object b adding key-value pairs to
    # dictionary a. b may be a dictionary, or any object supporting
    # PyMapping_Keys() and PyObject_GetItem(). If override is true,
    # existing pairs in a will be replaced if a matching key is found
    # in b, otherwise pairs will only be added if there is not a
    # matching key in a. Return 0 on success or -1 if an exception was
    # raised.

    int PyDict_Update(object a, object b) except -1
    # This is the same as PyDict_Merge(a, b, 1) in C, or a.update(b)
    # in Python. Return 0 on success or -1 if an exception was raised.

    int PyDict_MergeFromSeq2(object a, object seq2, int override) except -1
    # Update or merge into dictionary a, from the key-value pairs in
    # seq2. seq2 must be an iterable object producing iterable objects
    # of length 2, viewed as key-value pairs. In case of duplicate
    # keys, the last wins if override is true, else the first
    # wins. Return 0 on success or -1 if an exception was
    # raised. Equivalent Python (except for the return value):
    #
    #def PyDict_MergeFromSeq2(a, seq2, override):
    #    for key, value in seq2:
    #        if override or key not in a:
    #            a[key] = value
