/* -*- Mode: C; c-basic-offset: 4 -*-
 * vim: tabstop=4 shiftwidth=4 expandtab
 *
 * Copyright (C) 2011 John (J5) Palmieri <johnp@redhat.com>
 * Copyright (C) 2014 Simon Feltman <sfeltman@gnome.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "pygi-argument.h"
#include "pygi-util.h"
#include "pygi-cache-private.h"

typedef struct _PyGIHashCache {
    PyGIArgCache arg_cache;
    PyGIArgCache *key_cache;
    PyGIArgCache *value_cache;
} PyGIHashCache;


static void
_hash_cache_free_func (PyGIHashCache *cache)
{
    if (cache != NULL) {
        pygi_arg_cache_free (cache->key_cache);
        pygi_arg_cache_free (cache->value_cache);
        g_slice_free (PyGIHashCache, cache);
    }
}

static gboolean
_pygi_marshal_from_py_ghash (PyGIInvokeState *state,
                             PyGICallableCache *callable_cache,
                             PyGIArgCache *arg_cache, PyObject *py_arg,
                             GIArgument *arg, gpointer *cleanup_data)
{
    PyGIMarshalFromPyFunc key_from_py_marshaller;
    PyGIMarshalFromPyFunc value_from_py_marshaller;

    int i;
    Py_ssize_t length;
    PyObject *py_keys, *py_values;

    GHashFunc hash_func = NULL;
    GEqualFunc equal_func = NULL;
    GDestroyNotify key_destroy_func = NULL;
    GDestroyNotify value_destroy_func = NULL;

    GHashTable *hash_ = NULL;
    PyGIHashCache *hash_cache = (PyGIHashCache *)arg_cache;

    if (Py_IsNone (py_arg)) {
        arg->v_pointer = NULL;
        return TRUE;
    }

    py_keys = PyMapping_Keys (py_arg);
    if (py_keys == NULL) {
        PyErr_Format (PyExc_TypeError, "Must be mapping, not %s",
                      Py_TYPE (py_arg)->tp_name);
        return FALSE;
    }

    length = PyMapping_Length (py_arg);
    if (length < 0) {
        Py_DECREF (py_keys);
        return FALSE;
    }

    py_values = PyMapping_Values (py_arg);
    if (py_values == NULL) {
        Py_DECREF (py_keys);
        return FALSE;
    }

    key_from_py_marshaller = hash_cache->key_cache->from_py_marshaller;
    value_from_py_marshaller = hash_cache->value_cache->from_py_marshaller;

    if (hash_cache->key_cache->type_tag == GI_TYPE_TAG_UTF8
        || hash_cache->key_cache->type_tag == GI_TYPE_TAG_FILENAME) {
        hash_func = g_str_hash;
        equal_func = g_str_equal;
        /* Transfer rule has been updated in hash_table_item_cache_new(). */
        key_destroy_func = g_free;
    }
    if (hash_cache->value_cache->type_tag == GI_TYPE_TAG_UTF8
        || hash_cache->value_cache->type_tag == GI_TYPE_TAG_FILENAME) {
        value_destroy_func = g_free;
    }

    hash_ = g_hash_table_new_full (hash_func, equal_func, key_destroy_func,
                                   value_destroy_func);
    if (hash_ == NULL) {
        PyErr_NoMemory ();
        Py_DECREF (py_keys);
        Py_DECREF (py_values);
        return FALSE;
    }

    for (i = 0; i < length; i++) {
        GIArgument key, value;
        gpointer key_cleanup_data = NULL;
        gpointer value_cleanup_data = NULL;
        PyObject *py_key = PyList_GET_ITEM (py_keys, i);
        PyObject *py_value = PyList_GET_ITEM (py_values, i);
        if (py_key == NULL || py_value == NULL) goto err;

        // LEAK: key_cleanup_data and value_cleanup_data are never cleaned
        if (!key_from_py_marshaller (state, callable_cache,
                                     hash_cache->key_cache, py_key, &key,
                                     &key_cleanup_data))
            goto err;

        if (!value_from_py_marshaller (state, callable_cache,
                                       hash_cache->value_cache, py_value,
                                       &value, &value_cleanup_data))
            goto err;

        g_hash_table_insert (
            hash_,
            _pygi_arg_to_hash_pointer (key, hash_cache->key_cache->type_info),
            _pygi_arg_to_hash_pointer (value,
                                       hash_cache->value_cache->type_info));
        continue;
err:
        /* FIXME: cleanup hash keys and values */
        Py_DECREF (py_keys);
        Py_DECREF (py_values);
        g_hash_table_unref (hash_);
        _PyGI_ERROR_PREFIX ("Item %i: ", i);
        return FALSE;
    }

    arg->v_pointer = hash_;
    Py_DECREF (py_keys);
    Py_DECREF (py_values);

    if (arg_cache->transfer == GI_TRANSFER_NOTHING) {
        /* Free everything in cleanup. */
        *cleanup_data = arg->v_pointer;
    } else if (arg_cache->transfer == GI_TRANSFER_CONTAINER) {
        /* Make a shallow copy so we can free the elements later in cleanup
         * because it is possible invoke will free the list before our cleanup. */
        *cleanup_data = g_hash_table_ref (arg->v_pointer);
    } else { /* GI_TRANSFER_EVERYTHING */
        /* No cleanup, everything is given to the callee.
         * Note that the keys and values will leak for transfer everything because
         * we do not use g_hash_table_new_full and set key/value_destroy_func. */
        *cleanup_data = NULL;
    }

    return TRUE;
}

static void
_pygi_marshal_cleanup_from_py_ghash (PyGIInvokeState *state,
                                     PyGIArgCache *arg_cache, PyObject *py_arg,
                                     gpointer data, gboolean was_processed)
{
    if (data == NULL) return;

    if (was_processed) {
        GHashTable *hash_;
        PyGIHashCache *hash_cache = (PyGIHashCache *)arg_cache;

        hash_ = (GHashTable *)data;

        /* clean up keys and values first */
        if (hash_cache->key_cache->from_py_cleanup != NULL
            || hash_cache->value_cache->from_py_cleanup != NULL) {
            GHashTableIter hiter;
            gpointer key;
            gpointer value;

            PyGIMarshalFromPyCleanupFunc key_cleanup_func =
                hash_cache->key_cache->from_py_cleanup;
            PyGIMarshalFromPyCleanupFunc value_cleanup_func =
                hash_cache->value_cache->from_py_cleanup;

            g_hash_table_iter_init (&hiter, hash_);
            while (g_hash_table_iter_next (&hiter, &key, &value)) {
                if (key != NULL && key_cleanup_func != NULL)
                    key_cleanup_func (state, hash_cache->key_cache, NULL, key,
                                      TRUE);
                if (value != NULL && value_cleanup_func != NULL)
                    value_cleanup_func (state, hash_cache->value_cache, NULL,
                                        value, TRUE);
            }
        }

        g_hash_table_unref (hash_);
    }
}

static PyObject *
_pygi_marshal_to_py_ghash (PyGIInvokeState *state,
                           PyGICallableCache *callable_cache,
                           PyGIArgCache *arg_cache, GIArgument *arg,
                           gpointer *cleanup_data)
{
    GHashTable *hash_;
    GHashTableIter hash_table_iter;

    PyGIMarshalToPyFunc key_to_py_marshaller;
    PyGIMarshalToPyFunc value_to_py_marshaller;

    PyGIArgCache *key_arg_cache;
    PyGIArgCache *value_arg_cache;
    PyGIHashCache *hash_cache = (PyGIHashCache *)arg_cache;

    GIArgument key_arg;
    GIArgument value_arg;

    PyObject *py_obj = NULL;

    hash_ = arg->v_pointer;

    if (hash_ == NULL) {
        py_obj = Py_None;
        Py_INCREF (py_obj);
        return py_obj;
    }

    py_obj = PyDict_New ();
    if (py_obj == NULL) return NULL;

    key_arg_cache = hash_cache->key_cache;
    key_to_py_marshaller = key_arg_cache->to_py_marshaller;

    value_arg_cache = hash_cache->value_cache;
    value_to_py_marshaller = value_arg_cache->to_py_marshaller;

    g_hash_table_iter_init (&hash_table_iter, hash_);
    while (g_hash_table_iter_next (&hash_table_iter, &key_arg.v_pointer,
                                   &value_arg.v_pointer)) {
        gpointer key_cleanup_data = NULL;
        gpointer value_cleanup_data = NULL;
        PyObject *py_key;
        PyObject *py_value;
        int retval;


        _pygi_hash_pointer_to_arg_in_place (&key_arg,
                                            hash_cache->key_cache->type_info);
        py_key = key_to_py_marshaller (state, callable_cache, key_arg_cache,
                                       &key_arg, &key_cleanup_data);

        if (py_key == NULL) {
            Py_CLEAR (py_obj);
            return NULL;
        }

        _pygi_hash_pointer_to_arg_in_place (
            &value_arg, hash_cache->value_cache->type_info);
        py_value = value_to_py_marshaller (state, callable_cache,
                                           value_arg_cache, &value_arg,
                                           &value_cleanup_data);

        if (py_value == NULL) {
            Py_CLEAR (py_obj);
            Py_DECREF (py_key);
            return NULL;
        }

        retval = PyDict_SetItem (py_obj, py_key, py_value);

        Py_DECREF (py_key);
        Py_DECREF (py_value);

        if (retval < 0) {
            Py_CLEAR (py_obj);
            return NULL;
        }
    }

    return py_obj;
}

static void
_pygi_marshal_cleanup_to_py_ghash (PyGIInvokeState *state,
                                   PyGIArgCache *arg_cache,
                                   gpointer cleanup_data, gpointer data,
                                   gboolean was_processed)
{
    if (data == NULL) return;

    /* assume hashtable has boxed key and value */
    if (arg_cache->transfer == GI_TRANSFER_EVERYTHING
        || arg_cache->transfer == GI_TRANSFER_CONTAINER)
        g_hash_table_unref ((GHashTable *)data);
}

static void
_arg_cache_from_py_ghash_setup (PyGIArgCache *arg_cache)
{
    arg_cache->from_py_marshaller = _pygi_marshal_from_py_ghash;
    arg_cache->from_py_cleanup = _pygi_marshal_cleanup_from_py_ghash;
}

static void
_arg_cache_to_py_ghash_setup (PyGIArgCache *arg_cache)
{
    arg_cache->to_py_marshaller = _pygi_marshal_to_py_ghash;
    arg_cache->to_py_cleanup = _pygi_marshal_cleanup_to_py_ghash;
}

static PyGIArgCache *
hash_table_item_cache_new (GITypeInfo *type_info, int index,
                           GITransfer transfer, PyGIDirection direction,
                           PyGICallableCache *callable_cache)
{
    PyGIArgCache *cache;
    GITypeInfo *item_type_info =
        gi_type_info_get_param_type (type_info, index);
    GITypeTag item_type_tag = gi_type_info_get_tag (item_type_info);
    GITransfer item_transfer;

    if (item_type_tag == GI_TYPE_TAG_UTF8
        || item_type_tag == GI_TYPE_TAG_FILENAME)
        /* We assign a destroy notifier to the hash table,
         * so we do not need to free strings ourselves. */
        item_transfer = GI_TRANSFER_EVERYTHING;
    else if (transfer == GI_TRANSFER_CONTAINER)
        item_transfer = GI_TRANSFER_NOTHING;
    else
        item_transfer = transfer;

    cache = pygi_arg_cache_new (item_type_info, NULL, item_transfer, direction,
                                callable_cache, 0, 0);

    gi_base_info_unref ((GIBaseInfo *)item_type_info);

    return cache;
}

PyGIArgCache *
pygi_arg_hash_table_new_from_info (GITypeInfo *type_info, GIArgInfo *arg_info,
                                   GITransfer transfer,
                                   PyGIDirection direction,
                                   PyGICallableCache *callable_cache)
{
    PyGIHashCache *hc = NULL;

    hc = g_slice_new0 (PyGIHashCache);
    if (hc == NULL) return NULL;

    pygi_arg_base_setup ((PyGIArgCache *)hc, type_info, arg_info, transfer,
                         direction);

    ((PyGIArgCache *)hc)->destroy_notify =
        (GDestroyNotify)_hash_cache_free_func;

    hc->key_cache = hash_table_item_cache_new (
        type_info, /*index=*/0, transfer, direction, callable_cache);

    if (hc->key_cache == NULL) {
        pygi_arg_cache_free ((PyGIArgCache *)hc);
        return NULL;
    }

    hc->value_cache = hash_table_item_cache_new (
        type_info, /*index=*/1, transfer, direction, callable_cache);

    if (hc->value_cache == NULL) {
        pygi_arg_cache_free ((PyGIArgCache *)hc);
        return NULL;
    }

    if (direction & PYGI_DIRECTION_FROM_PYTHON) {
        _arg_cache_from_py_ghash_setup ((PyGIArgCache *)hc);
    }

    if (direction & PYGI_DIRECTION_TO_PYTHON) {
        _arg_cache_to_py_ghash_setup ((PyGIArgCache *)hc);
    }

    return (PyGIArgCache *)hc;
}
