/*
 * Copyright (C) 2012-2014 Red Hat, Inc.
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <Python.h>
#include <solv/poolid.h>
#include <solv/solver.h>
#include <solv/util.h>
#include <time.h>

#include "error.hpp"
#include "nevra.hpp"
#include "hy-query-private.hpp"
#include "hy-selector.h"
#include "hy-subject.h"
#include "dnf-reldep.h"
#include "dnf-reldep-list.h"
#include "repo/solvable/DependencyContainer.hpp"
#include "transaction/Swdb.hpp"

#include "exception-py.hpp"
#include "hawkey-pysys.hpp"
#include "iutil-py.hpp"
#include "package-py.hpp"
#include "query-py.hpp"
#include "reldep-py.hpp"
#include "sack-py.hpp"
#include "pycomp.hpp"
#include "sack/advisorypkg.hpp"
#include "sack/packageset.hpp"
#include "sack/selector.hpp"

#include <algorithm>
#include <functional>

typedef struct {
    PyObject_HEAD
    HyQuery query;
    PyObject *sack;
} _QueryObject;

static const int keyname_int_matches[] = {
    HY_PKG,
    HY_PKG_ADVISORY,
    HY_PKG_ADVISORY_BUG,
    HY_PKG_ADVISORY_CVE,
    HY_PKG_ADVISORY_SEVERITY,
    HY_PKG_ADVISORY_TYPE,
    HY_PKG_ARCH,
    HY_PKG_CONFLICTS,
    HY_PKG_DESCRIPTION,
    HY_PKG_DOWNGRADABLE,
    HY_PKG_DOWNGRADES,
    HY_PKG_EMPTY,
    HY_PKG_ENHANCES,
    HY_PKG_EPOCH,
    HY_PKG_EVR,
    HY_PKG_FILE,
    HY_PKG_LATEST,
    HY_PKG_LATEST_PER_ARCH,
    HY_PKG_LATEST_PER_ARCH_BY_PRIORITY,
    HY_PKG_LOCATION,
    HY_PKG_NAME,
    HY_PKG_NEVRA,
    HY_PKG_NEVRA_STRICT,
    HY_PKG_OBSOLETES,
    HY_PKG_OBSOLETES_BY_PRIORITY,
    HY_PKG_PROVIDES,
    HY_PKG_RECOMMENDS,
    HY_PKG_RELEASE,
    HY_PKG_REPONAME,
    HY_PKG_REQUIRES,
    HY_PKG_SOURCERPM,
    HY_PKG_SUGGESTS,
    HY_PKG_SUMMARY,
    HY_PKG_SUPPLEMENTS,
    HY_PKG_UPGRADABLE,
    HY_PKG_UPGRADES,
    HY_PKG_UPGRADES_BY_PRIORITY,
    HY_PKG_URL,
    HY_PKG_VERSION
};

static const char * const keyname_char_matches[] = {
    "pkg",
    "advisory",
    "advisory_bug",
    "advisory_cve",
    "advisory_severity",
    "advisory_type",
    "arch",
    "conflicts",
    "description",
    "downgradable",
    "downgrades",
    "empty",
    "enhances",
    "epoch",
    "evr",
    "file",
    "latest",
    "latest_per_arch",
    "latest_per_arch_by_priority",
    "location",
    "name",
    "nevra",
    "nevra_strict",
    "obsoletes",
    "obsoletes_by_priority",
    "provides",
    "recommends",
    "release",
    "reponame",
    "requires",
    "sourcerpm",
    "suggests",
    "summary",
    "supplements",
    "upgradable",
    "upgrades",
    "upgrades_by_priority",
    "url",
    "version",
    NULL
};

static const char * const query_cmp_map_char[] = {
    "eq",
    "gt",
    "lt",
    "neq",
    "not",
    "gte",
    "lte",
    "substr",
    "glob",
    "eqg",
    "upgrade",
    NULL
};

static const int query_cmp_map_int[] = {
    HY_EQ,
    HY_GT,
    HY_LT,
    HY_NEQ,
    HY_NOT,
    HY_EQ | HY_GT,
    HY_EQ | HY_LT,
    HY_SUBSTR,
    HY_GLOB,
    HY_EQG,
    HY_UPGRADE
};

HyQuery
queryFromPyObject(PyObject *o)
{
    if (!PyType_IsSubtype(o->ob_type, &query_Type)) {
        PyErr_SetString(PyExc_TypeError, "Expected a Query object.");
        return NULL;
    }
    return ((_QueryObject *)o)->query;
}

PyObject *
queryToPyObject(HyQuery query, PyObject *sack, PyTypeObject *custom_object_type)
{
    _QueryObject *self = (_QueryObject *)custom_object_type->tp_alloc(custom_object_type, 0);
    if (self) {
        self->query = query;
        self->sack = sack;
        Py_INCREF(sack);
    }
    return (PyObject *) self;
}

int
query_converter(PyObject *o, HyQuery *query_ptr)
{
    HyQuery query = queryFromPyObject(o);
    if (query == NULL)
        return 0;
    *query_ptr = query;
    return 1;
}

/* functions on the type */

static PyObject *
query_new(PyTypeObject *type, PyObject *args, PyObject *kwds) try
{
    _QueryObject *self = (_QueryObject *)type->tp_alloc(type, 0);
    if (self) {
        self->query = NULL;
        self->sack = NULL;
    }
    return (PyObject *)self;
} CATCH_TO_PYTHON

static void
query_dealloc(_QueryObject *self)
{
    if (self->query)
        delete self->query;
    Py_XDECREF(self->sack);
    Py_TYPE(self)->tp_free(self);
}

static int
query_init(_QueryObject * self, PyObject *args, PyObject *kwds) try
{
    const char *kwlist[] = {"sack", "flags", "query", NULL};
    PyObject *sack = NULL;
    PyObject *query = NULL;
    int flags = 0;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiO", (char**) kwlist, &sack, &flags, &query))
        return -1;

    if (query && (!sack || sack == Py_None) && queryObject_Check(query)) {
        _QueryObject *query_obj = (_QueryObject*)query;
        self->sack = query_obj->sack;
        self->query = new libdnf::Query(*query_obj->query);
    } else if (sack && (!query || query == Py_None) && sackObject_Check(sack)) {
        DnfSack *csack = sackFromPyObject(sack);
        assert(csack);
        self->sack = sack;
        self->query = new libdnf::Query(csack, static_cast<libdnf::Query::ExcludeFlags>(flags));
    } else {
        const char *msg = "Expected a _hawkey.Sack or a _hawkey.Query object.";
        PyErr_SetString(PyExc_TypeError, msg);
        return -1;
    }
    Py_INCREF(self->sack);
    return 0;
} CATCH_TO_PYTHON_INT

/* object attributes */

static PyObject *
get_evaluated(_QueryObject *self, void *unused) try
{
    HyQuery q = self->query;
    return PyBool_FromLong((long) q->getApplied());
} CATCH_TO_PYTHON

static PyObject *
clear(_QueryObject *self, PyObject *unused) try
{
    self->query->clear();
    Py_RETURN_NONE;
} CATCH_TO_PYTHON

static int
raise_bad_filter(void)
{
    PyErr_SetString(HyExc_Query, "Invalid filter key or match type.");
    return 0;
}

static int
filter_add(HyQuery query, key_t keyname, int cmp_type, PyObject *match)
{
    if (keyname == HY_PKG_DOWNGRADABLE ||
        keyname == HY_PKG_DOWNGRADES ||
        keyname == HY_PKG_EMPTY ||
        keyname == HY_PKG_LATEST_PER_ARCH ||
        keyname == HY_PKG_LATEST_PER_ARCH_BY_PRIORITY ||
        keyname == HY_PKG_LATEST ||
        keyname == HY_PKG_UPGRADABLE ||
        keyname == HY_PKG_UPGRADES ||
        keyname == HY_PKG_UPGRADES_BY_PRIORITY) {
        int val;

        if (!PyInt_Check(match) || cmp_type != HY_EQ) {
            PyErr_SetString(HyExc_Value, "Invalid boolean filter query.");
            return 0;
        }
        long val_long = PyLong_AsLong(match);
        if (val_long < INT_MIN) {
            val = INT_MIN;
        } else if (val_long > INT_MAX) {
            val = INT_MAX;
        } else {
            val = val_long;
        }
        if (keyname == HY_PKG_EMPTY) {
            if (!val) {
                PyErr_SetString(HyExc_Value, "Invalid boolean filter query.");
                return 0;
            }
            query->addFilter(HY_PKG_EMPTY, HY_EQ, 1);
        } else {
            query->addFilter(keyname, HY_EQ, val);
        }
        return 1;
    }
    if (PyUnicode_Check(match) || PyString_Check(match)) {
        PycompString cmatch(match);
        if (!cmatch.getCString())
            return 0;
        int query_filter_ret = query->addFilter(keyname, cmp_type, cmatch.getCString());

        if (query_filter_ret)
            return raise_bad_filter();
        return 1;
    }
    if (PyInt_Check(match)) {
        long val = PyLong_AsLong(match);
        if (cmp_type == HY_GLOB) // Workaround: Python can send integer with HY_GLOB
            cmp_type = HY_EQ;
        if (val > INT_MAX || val < INT_MIN) {
            PyErr_SetString(HyExc_Value, "Numeric argument out of range.");
            return 0;
        }
        if (query->addFilter(keyname, cmp_type, val))
            return raise_bad_filter();
        return 1;
    }
    if (queryObject_Check(match)) {
        HyQuery target = queryFromPyObject(match);
        const DnfPackageSet * pset = target->runSet();
        int ret = query->addFilter(keyname, cmp_type, pset);

        if (ret)
            return raise_bad_filter();
        return 1;
    }
    if (reldepObject_Check(match)) {
        DnfReldep *reldep = reldepFromPyObject(match);

	/* A reldep cannot be used across sack objects. If there is an attempt
         * to do so, the underlying libsolv structures are incomplete and a SEGFAULT is
         * likely to occur. */

        if (query->getSack() != reldep->getSack()) {
            PyErr_SetString(HyExc_Query, "Direct dependency lookups must originate from the same sack.");
            return 0;
        }

        if (cmp_type != HY_EQ || query->addFilter(keyname, reldep))
            return raise_bad_filter();
        return 1;
    }
    // match is a sequence now:
    switch (keyname) {
    case HY_PKG:
    case HY_PKG_OBSOLETES:
    case HY_PKG_OBSOLETES_BY_PRIORITY:
    case HY_PKG_CONFLICTS:
    case HY_PKG_REQUIRES:
    case HY_PKG_ENHANCES:
    case HY_PKG_RECOMMENDS:
    case HY_PKG_SUGGESTS:
    case HY_PKG_SUPPLEMENTS: {
        // It could be a sequence of packages or reldep/strings. Lets try packages first.
        auto pset = pyseq_to_packageset(match, query->getSack());
        if (!pset) {
            if (auto PyError = PyErr_Occurred()) {
                // It was not a sequence of packages.
                if (PyErr_GivenExceptionMatches(PyError, PyExc_TypeError)) {
                    PyErr_Clear();
                    auto reldeplist = pyseq_to_reldeplist(match, query->getSack(), cmp_type);
                    if (reldeplist == NULL)
                        return 1;

                    int ret = query->addFilter(keyname, reldeplist.get());
                    if (ret) {
                        return raise_bad_filter();
                    }
                    break;
                }
            }
            return 1;
        }
        int ret = query->addFilter(keyname, cmp_type, pset.get());
        if (ret)
            return raise_bad_filter();

        break;
    }
    case HY_PKG_PROVIDES: {
        auto reldeplist = pyseq_to_reldeplist(match, query->getSack(), cmp_type);
        if (reldeplist == NULL)
            return 1;

        int ret = query->addFilter(keyname, reldeplist.get());
        if (ret)
            return raise_bad_filter();
        break;
    }
    default: {
        std::vector<std::string> matches;
        try {
            matches = pySequenceConverter(match);
        } catch (std::runtime_error &) {
            return 0;
        }
        std::vector<const char *> matchesCString(matches.size() + 1);
        std::transform(matches.begin(), matches.end(), matchesCString.begin(),
            std::mem_fn(&std::string::c_str));
        int filter_in_ret = query->addFilter(keyname, cmp_type, matchesCString.data());
        if (filter_in_ret)
            return raise_bad_filter();
        break;
    }
    }
    return 1;
}

static char *
filter_key_splitter(char** key)
{
    char *sbegin = *key;
	char *end;

    if (sbegin == NULL)
		return NULL;
    int index;

    for (index = 0; sbegin[index] != '\0'; ++index) {
        if ((sbegin[index] == '_') &&  (sbegin[index + 1] == '_')) {
            end = sbegin + index;
            *end++ = '\0';
            *key = ++end;
            return sbegin;
        }
    }
    *key = NULL;
    return sbegin;
}

gboolean
filter_internal(HyQuery query, HySelector sltr, PyObject *sack, PyObject *args, PyObject *kwds)
{
    PyObject *key, *value;
    Py_ssize_t pos = 0;
    key_t keyname;
    int cmp_type;
    PyObject *tuple_item;
    int argument_number, presence_cmp_type;
    int cmp_type_flag = 0;

    if (args != NULL) {
        Py_ssize_t tuple_size = PyTuple_Size(args);
        for (int x = 0; x < tuple_size; ++x) {
            tuple_item = PyTuple_GetItem(args, x);
            if (PyInt_Check(tuple_item)) {
                long c_int = PyLong_AsLong(tuple_item);
                if (c_int == HY_ICASE) {
                    cmp_type_flag = HY_ICASE;
                } else {
                    PyErr_SetString(HyExc_Value, "Invalid flag. Only HY_ICASE allowed");
                    return FALSE;
                }
            }
        }
    }

    if (kwds != NULL) {
        while (PyDict_Next(kwds, &pos, &key, &value)) {
            keyname = -1;
            argument_number = 0;
            PycompString cmatch(key);
            if (!cmatch.getCString())
                return FALSE;
            auto parsed_string = cmatch.getString();
            char *tmp_string = &parsed_string.front();
            cmp_type = 0;
            char *parcial_string;
            while ((parcial_string = filter_key_splitter(&tmp_string)) != NULL) {
                if (!argument_number) {
                    for (unsigned int i = 0; keyname_char_matches[i] != NULL; ++i) {
                        if (strcmp(keyname_char_matches[i], parcial_string) == 0) {
                            keyname = keyname_int_matches[i];
                            argument_number = 1;
                            break;
                        }
                    }
                    if (!argument_number) {
                        PyErr_SetString(HyExc_Value, g_strdup_printf(
                            "Unrecognized key name: %s", parcial_string));
                        return FALSE;
                    }
                } else {
                    presence_cmp_type = FALSE;
                    for (unsigned int i = 0; query_cmp_map_char[i] != NULL; ++i) {
                        if (strcmp(query_cmp_map_char[i], parcial_string) == 0) {
                            cmp_type |= query_cmp_map_int[i];
                            presence_cmp_type = TRUE;
                            break;
                        }
                    }
                    if (!presence_cmp_type) {
                        PyErr_SetString(HyExc_Value, g_strdup_printf(
                            "Unrecognized filter type: %s", parcial_string));
                        return FALSE;
                    }
                }
            }
            if (cmp_type == 0) {
                cmp_type = HY_EQ;
            }
            if (keyname != -1) {
                if (query != NULL) {
                    if (filter_add(query, keyname, cmp_type|cmp_type_flag, value) == 0) {
                        return FALSE;
                    }
                } else {
                    if (keyname == HY_PKG) {
                        DnfSack *c_sack = sackFromPyObject(sack);
                        assert(c_sack);
                        auto pset = pyseq_to_packageset(value, c_sack);
                        if (!pset) {
                            ret2e(DNF_ERROR_BAD_SELECTOR, "Invalid value type: Only List and Query supported");
                            return FALSE;
                        }
                        if (!sltr) {
                            PyErr_SetString(HyExc_Value, "Selector is nulptr");
                            return FALSE;
                        }
                        if (ret2e(sltr->set(pset.get()),
                            "Invalid Selector spec." )) {
                            return FALSE;
                        }
                    } else {
                        PycompString c_sltr_match(value);
                        if (!c_sltr_match.getCString())
                            return FALSE;
                        if (ret2e(hy_selector_set(sltr, keyname, cmp_type, c_sltr_match.getCString()),
                            "Invalid Selector spec." )) {
                            return FALSE;
                        }
                    }
                }
            }
        }
    }
    return TRUE;
}

static PyObject *
filter(_QueryObject *self, PyObject *args, PyObject *kwds) try {
    auto query = std::unique_ptr<libdnf::Query>(new libdnf::Query(*self->query));
    gboolean ret = filter_internal(query.get(), NULL, self->sack, args, kwds);
    if (!ret)
        return NULL;
    PyObject *final_query = queryToPyObject(query.release(), self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static _QueryObject *
filterm(_QueryObject *self, PyObject *args, PyObject *kwds) try {
    gboolean ret = filter_internal(self->query, NULL, self->sack, args, kwds);
    if (!ret)
        return NULL;
    Py_INCREF(self);
    return self;
} CATCH_TO_PYTHON

static PyObject *
add_available_filter(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = new libdnf::Query(*self->query);
    query->available();
    PyObject *final_query = queryToPyObject(query, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_downgrades_filter(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = new libdnf::Query(*self->query);
    query->addFilter(HY_PKG_DOWNGRADES, HY_EQ, 1);
    PyObject *final_query = queryToPyObject(query, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
duplicated_filter(_QueryObject *self, PyObject *unused) try
{
    HyQuery self_query_copy = new libdnf::Query(*self->query);
    self_query_copy->filterDuplicated();
    PyObject *final_query = queryToPyObject(self_query_copy, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_filter_extras(_QueryObject *self, PyObject *unused) try
{
    HyQuery self_query_copy = new libdnf::Query(*self->query);
    self_query_copy->filterExtras();
    PyObject *final_query = queryToPyObject(self_query_copy, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_installed_filter(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = new libdnf::Query(*self->query);
    query->installed();
    PyObject *final_query = queryToPyObject(query, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_filter_latest(_QueryObject *self, PyObject *args) try
{
    int value = 1;

    if (!PyArg_ParseTuple(args, "|i", &value))
        return NULL;

    HyQuery query = new libdnf::Query(*self->query);
    query->addFilter(HY_PKG_LATEST_PER_ARCH, HY_EQ, value);
    PyObject *final_query = queryToPyObject(query, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_upgrades_filter(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = new libdnf::Query(*self->query);
    query->addFilter(HY_PKG_UPGRADES, HY_EQ, 1);
    PyObject *final_query = queryToPyObject(query, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON


static PyObject *
run(_QueryObject *self, PyObject *unused) try
{
    PyObject *list;

    const DnfPackageSet * pset = self->query->runSet();
    list = packageset_to_pylist(pset, self->sack);
    return list;
} CATCH_TO_PYTHON

static PyObject *
apply(PyObject *self, PyObject *unused) try
{
    ((_QueryObject *) self)->query->apply();
    Py_INCREF(self);
    return self;
} CATCH_TO_PYTHON

static PyObject *
q_union(PyObject *self, PyObject *args) try
{
    PyObject *other;
    if (!PyArg_ParseTuple(args, "O!", &query_Type, &other))
            return NULL;

    HyQuery self_query_copy = new libdnf::Query(*((_QueryObject *) self)->query);
    HyQuery other_q = ((_QueryObject *) other)->query;
    self_query_copy->queryUnion(*other_q);
    PyObject *final_query = queryToPyObject(self_query_copy, ((_QueryObject *) self)->sack,
                                            Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
q_intersection(PyObject *self, PyObject *args) try
{
    PyObject *other;
    if (!PyArg_ParseTuple(args, "O!", &query_Type, &other))
            return NULL;

    HyQuery self_query_copy = new libdnf::Query(*((_QueryObject *) self)->query);
    HyQuery other_q = ((_QueryObject *) other)->query;
    self_query_copy->queryIntersection(*other_q);
    PyObject *final_query = queryToPyObject(self_query_copy, ((_QueryObject *) self)->sack,
                                            Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
q_difference(PyObject *self, PyObject *args) try
{
    PyObject *other;
    if (!PyArg_ParseTuple(args, "O!", &query_Type, &other))
            return NULL;

    HyQuery self_query_copy = new libdnf::Query(*((_QueryObject *) self)->query);
    HyQuery other_q = ((_QueryObject *) other)->query;
    self_query_copy->queryDifference(*other_q);
    PyObject *final_query = queryToPyObject(self_query_copy, ((_QueryObject *) self)->sack,
                                            Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

typedef struct {
    PyObject_HEAD
    libdnf::Swdb *ptr;
    void *ty;
    int own;
    PyObject *next;
} SwdbSwigPyObject;

static PyObject *
get_advisory_pkgs(_QueryObject *self, PyObject *args) try
{
    int cmpType;

    if (!PyArg_ParseTuple(args, "i", &cmpType))
        return NULL;

    std::vector<libdnf::AdvisoryPkg> advisoryPkgs;

    self->query->getAdvisoryPkgs(cmpType, advisoryPkgs);
    return advisoryPkgVectorToPylist(advisoryPkgs);
} CATCH_TO_PYTHON

static PyObject *
filter_userinstalled(PyObject *self, PyObject *args, PyObject *kwds) try
{
    const char *kwlist[] = {"swdb", NULL};
    PyObject *pySwdb;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", (char **)kwlist, &pySwdb)) {
        return NULL;
    }

    UniquePtrPyObject thisPySwdb(PyObject_GetAttrString(pySwdb, "this"));
    auto swigSwdb = reinterpret_cast< SwdbSwigPyObject * >(thisPySwdb.get());

    if (swigSwdb == nullptr) {
        PyErr_SetString(PyExc_SystemError, "Unable to parse SwigPyObject");
        return NULL;
    }

    libdnf::Swdb * swdb = swigSwdb->ptr;

    if (swdb == NULL) {
        PyErr_SetString(PyExc_SystemError, "Unable to parse swig object");
        return NULL;
    }
    HyQuery self_query_copy = new libdnf::Query(*((_QueryObject *) self)->query);
    self_query_copy->filterUserInstalled(*swdb);

    PyObject *final_query = queryToPyObject(self_query_copy, ((_QueryObject *) self)->sack,
                                            Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
filter_unneeded_or_safe_to_remove(PyObject *self, PyObject *args, PyObject *kwds, bool SafeToRemove)
{
    const char *kwlist[] = {"swdb", "debug_solver", NULL};
    PyObject *pySwdb;
    PyObject *debug_solver = NULL;

    if (!PyArg_ParseTupleAndKeywords(
            args, kwds, "O|O!", (char **)kwlist, &pySwdb, &PyBool_Type, &debug_solver)) {
        return NULL;
    }

    UniquePtrPyObject thisPySwdb(PyObject_GetAttrString(pySwdb, "this"));
    auto swigSwdb = reinterpret_cast< SwdbSwigPyObject * >(thisPySwdb.get());

    if (swigSwdb == nullptr) {
        PyErr_SetString(PyExc_SystemError, "Unable to parse SwigPyObject");
        return NULL;
    }

    libdnf::Swdb *swdb = swigSwdb->ptr;

    if (swdb == NULL) {
        PyErr_SetString(PyExc_SystemError, "Unable to parse swig object");
        return NULL;
    }
    std::unique_ptr<libdnf::Query> self_query_copy(new libdnf::Query(*((_QueryObject *) self)->query));
    gboolean c_debug_solver = debug_solver != NULL && PyObject_IsTrue(debug_solver);

    int ret;
    if (SafeToRemove) {
        ret = self_query_copy->filterSafeToRemove(*swdb, c_debug_solver);
    } else {
        ret = self_query_copy->filterUnneeded(*swdb, c_debug_solver);
    }
    if (ret == -1) {
        PyErr_SetString(PyExc_SystemError, "Unable to provide query with unneded filter");
        return NULL;
    }

    PyObject *final_query = queryToPyObject(self_query_copy.release(), ((_QueryObject *) self)->sack,
                                            Py_TYPE(self));
    return final_query;
}

static PyObject *
filter_safe_to_remove(PyObject *self, PyObject *args, PyObject *kwds) try
{
    return filter_unneeded_or_safe_to_remove(self, args, kwds, true);
} CATCH_TO_PYTHON


static PyObject *
filter_unneeded(PyObject *self, PyObject *args, PyObject *kwds) try
{
    return filter_unneeded_or_safe_to_remove(self, args, kwds, false);
} CATCH_TO_PYTHON

static PyObject *
q_add(_QueryObject *self, PyObject *list) try
{
    if (!PyList_Check(list)) {
        PyErr_SetString(PyExc_TypeError, "Only a list can be concatenated to a Query");
        return NULL;
    }
    PyObject *unused = NULL;
    PyObject *query_list = run(self, unused);

    int list_count = PyList_Size(list);
    for (int index = 0; index < list_count; ++index)
        PyList_Append(query_list, PyList_GetItem(list, index));
    return query_list;
} CATCH_TO_PYTHON

static int
query_contains(PyObject *self, PyObject *pypkg) try
{
    HyQuery q = ((_QueryObject *) self)->query;
    DnfPackage *pkg = packageFromPyObject(pypkg);

    if (pkg) {
        Id id = dnf_package_get_id(pkg);
        q->apply();
        if (MAPTST(q->getResult(), id))
            return 1;
    }
    return 0;
} CATCH_TO_PYTHON_INT

static size_t
query_len(PyObject *self) try
{
    HyQuery q = ((_QueryObject *) self)->query;
    return q->size();
} CATCH_TO_PYTHON_INT

static PyObject *
q_length(PyObject *self, PyObject *unused) try
{
    return PyLong_FromLong(query_len(self));
} CATCH_TO_PYTHON

static PyObject *
query_get_item(PyObject *self, int index) try
{
    HyQuery query = ((_QueryObject *) self)->query;
    Id id = query->getIndexItem(index);
    if (id == -1) {
        PyErr_SetString(PyExc_IndexError, "list index out of range");
        return NULL;
    }
    PyObject *package = new_package(((_QueryObject *) self)->sack, id);
    return package;
} CATCH_TO_PYTHON

static PyObject *
query_iter(PyObject *self) try
{
    const DnfPackageSet * pset = ((_QueryObject *) self)->query->runSet();
    UniquePtrPyObject list(packageset_to_pylist(pset, ((_QueryObject *) self)->sack));
    if (!list)
        return NULL;
    PyObject *iter = PyObject_GetIter(list.get());
    return iter;
} CATCH_TO_PYTHON

static PyObject *
query_to_name_dict(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = ((_QueryObject *) self)->query;
    Pool *pool = dnf_sack_get_pool(query->getSack());

    libdnf::IdQueue samename;
    hy_query_to_name_ordered_queue(query, &samename);

    Solvable *considered;
    Id name = 0;
    UniquePtrPyObject list(PyList_New(0));
    UniquePtrPyObject ret_dict(PyDict_New());

    for (int i = 0; i < samename.size(); ++i) {
        Id package_id = samename[i];
        considered = pool->solvables + package_id;
        if (name == 0) {
            name = considered->name;
        } else if (name != considered->name) {
            PyDict_SetItemString(ret_dict.get(), pool_id2str(pool, name), list.get());
            list.reset(PyList_New(0));
            name = considered->name;
        }
        UniquePtrPyObject package(new_package(self->sack, package_id));
        if (!package)
            goto fail;

        int rc = PyList_Append(list.get(), package.get());
        if (rc == -1)
            goto fail;
    }
    if (name)
        PyDict_SetItemString(ret_dict.get(), pool_id2str(pool, name), list.get());
    return ret_dict.release();

    fail:
        PyErr_SetString(PyExc_SystemError, "Unable to create name_dict");
        return NULL;
} CATCH_TO_PYTHON

static PyObject *
query_to_name_arch_dict(_QueryObject *self, PyObject *unused) try
{
    HyQuery query = ((_QueryObject *) self)->query;
    Pool *pool = dnf_sack_get_pool(query->getSack());

    libdnf::IdQueue samename;

    hy_query_to_name_arch_ordered_queue(query, &samename);

    Solvable *considered;
    Id name = 0;
    Id arch = 0;
    UniquePtrPyObject key(PyTuple_New(2));
    UniquePtrPyObject list(PyList_New(0));
    UniquePtrPyObject ret_dict(PyDict_New());

    for (int i = 0; i < samename.size(); ++i) {
        Id package_id = samename[i];
        considered = pool->solvables + package_id;
        if (name == 0) {
            name = considered->name;
            arch = considered->arch;
        } else if ((name != considered->name) || (arch != considered->arch)) {
            if (PyTuple_SetItem(key.get(), 0, PyString_FromString(pool_id2str(pool, name))))
                goto fail;
            if (PyTuple_SetItem(key.get(), 1, PyString_FromString(pool_id2str(pool, arch))))
                goto fail;
            PyDict_SetItem(ret_dict.get(), key.get(), list.get());
            key.reset(PyTuple_New(2));
            list.reset(PyList_New(0));
            name = considered->name;
            arch = considered->arch;
        }
        UniquePtrPyObject package(new_package(self->sack, package_id));
        if (!package)
            goto fail;

        int rc = PyList_Append(list.get(), package.get());
        if (rc == -1)
            goto fail;
    }
    if (name) {
        if (PyTuple_SetItem(key.get(), 0, PyString_FromString(pool_id2str(pool, name))))
            goto fail;
        if (PyTuple_SetItem(key.get(), 1, PyString_FromString(pool_id2str(pool, arch))))
            goto fail;
        PyDict_SetItem(ret_dict.get(), key.get(), list.get());
    }

    return ret_dict.release();

    fail:
        PyErr_SetString(PyExc_SystemError, "Unable to create name_arch_dict");
        return NULL;
} CATCH_TO_PYTHON

static PyObject *
add_nevra_or_other_filter(_QueryObject *self, PyObject *args) try
{
    auto self_query_copy = std::unique_ptr<libdnf::Query>(new libdnf::Query(*self->query));

    int arguments_count = PyTuple_Size(args);
    if (arguments_count == 1) {
        const char *name;
        if (!PyArg_ParseTuple(args, "s", &name))
            return NULL;
        libdnf::Nevra nevra;
        if (nevra.parse(name, HY_FORM_NEVRA))
            self_query_copy->addFilter(&nevra, false);
        else
            self_query_copy->addFilter(HY_PKG_EMPTY, HY_EQ, 1);
    } else if (arguments_count == 3) {
        const char *name;
        const char *evr;
        const char *arch;

        if (!PyArg_ParseTuple(args, "sss", &name, &evr, &arch))
            return NULL;
        self_query_copy->addFilter(HY_PKG_NAME, HY_EQ, name);
        self_query_copy->addFilter(HY_PKG_EVR, HY_EQ, evr);
        self_query_copy->addFilter(HY_PKG_ARCH, HY_EQ, arch);
    } else {
        PyErr_SetString(PyExc_TypeError,
                        "nevra() takes 1 (NEVRA), or 3 (name, evr, arch) str params");
        return NULL;
    }
    PyObject *final_query = queryToPyObject(self_query_copy.release(), self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyObject *
add_filter_recent(_QueryObject *self, PyObject *args) try
{
    long recent;
    if (!PyArg_ParseTuple(args, "l", &recent))
        return NULL;

    self->query->apply();
    HyQuery self_query_copy = new libdnf::Query(*self->query);
    time_t now = time(NULL);
    time_t recent_limit = now - (recent*86400);
    self_query_copy->filterRecent((recent_limit < 0) ? 0 : recent_limit);
    PyObject *final_query = queryToPyObject(self_query_copy, self->sack, Py_TYPE(self));
    return final_query;
} CATCH_TO_PYTHON

static PyGetSetDef query_getsetters[] = {
    {(char*)"evaluated",  (getter)get_evaluated, NULL, NULL, NULL},
    {NULL}                        /* sentinel */
};

PySequenceMethods query_sequence = {
    (lenfunc)query_len,               /* sq_length */
    (binaryfunc)q_add,                /* sq_concat */
    0,                                /* sq_repeat */
    (ssizeargfunc) query_get_item,    /* sq_item */
    0,                                /* sq_slice */
    0,                                /* sq_ass_item */
    0,                                /* sq_ass_slice */
    (objobjproc)query_contains,       /* sq_contains */
};

static struct PyMethodDef query_methods[] = {
    {"clear", (PyCFunction)clear, METH_NOARGS,
     NULL},
    {"filter", (PyCFunction)filter, METH_KEYWORDS|METH_VARARGS,
     NULL},
    {"filterm", (PyCFunction)filterm, METH_KEYWORDS|METH_VARARGS,
     NULL},
    {"run", (PyCFunction)run, METH_NOARGS,
     NULL},
    {"apply", (PyCFunction)apply, METH_NOARGS,
     NULL},
    {"available", (PyCFunction)add_available_filter, METH_NOARGS, NULL},
    {"downgrades", (PyCFunction)add_downgrades_filter, METH_NOARGS, NULL},
    {"duplicated", (PyCFunction)duplicated_filter, METH_NOARGS, NULL},
    {"extras", (PyCFunction)add_filter_extras, METH_NOARGS, NULL},
    {"installed", (PyCFunction)add_installed_filter, METH_NOARGS, NULL},
    {"latest", (PyCFunction)add_filter_latest, METH_VARARGS, NULL},
    {"union", (PyCFunction)q_union, METH_VARARGS, NULL},
    {"upgrades", (PyCFunction)add_upgrades_filter, METH_NOARGS, NULL},
    {"intersection", (PyCFunction)q_intersection, METH_VARARGS, NULL},
    {"difference", (PyCFunction)q_difference, METH_VARARGS, NULL},
    {"count", (PyCFunction)q_length, METH_NOARGS,
        NULL},
    {"get_advisory_pkgs", (PyCFunction)get_advisory_pkgs, METH_VARARGS, NULL},
    {"userinstalled", (PyCFunction)filter_userinstalled, METH_KEYWORDS|METH_VARARGS, NULL},
    {"_na_dict", (PyCFunction)query_to_name_arch_dict, METH_NOARGS, NULL},
    {"_name_dict", (PyCFunction)query_to_name_dict, METH_NOARGS, NULL},
    {"_nevra", (PyCFunction)add_nevra_or_other_filter, METH_VARARGS, NULL},
    {"_recent", (PyCFunction)add_filter_recent, METH_VARARGS, NULL},
    {"_unneeded", (PyCFunction)filter_unneeded, METH_KEYWORDS|METH_VARARGS, NULL},
    {"_safe_to_remove", (PyCFunction)filter_safe_to_remove, METH_KEYWORDS|METH_VARARGS, NULL},
    {NULL}                      /* sentinel */
};

PyTypeObject query_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "_hawkey.Query",                /*tp_name*/
    sizeof(_QueryObject),        /*tp_basicsize*/
    0,                                /*tp_itemsize*/
    (destructor) query_dealloc, /*tp_dealloc*/
    0,                                /*tp_print*/
    0,                                /*tp_getattr*/
    0,                                /*tp_setattr*/
    0,                                /*tp_compare*/
    0,                                /*tp_repr*/
    0,                                /*tp_as_number*/
    &query_sequence,                  /*tp_as_sequence*/
    0,                                /*tp_as_mapping*/
    0,                                /*tp_hash */
    0,                                /*tp_call*/
    0,                                /*tp_str*/
    0,                                /*tp_getattro*/
    0,                                /*tp_setattro*/
    0,                                /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE,        /*tp_flags*/
    "Query object",                /* tp_doc */
    0,                                /* tp_traverse */
    0,                                /* tp_clear */
    0,                                /* tp_richcompare */
    0,                                /* tp_weaklistoffset */
    query_iter,                       /* tp_iter */
    0,                                /* tp_iternext */
    query_methods,                /* tp_methods */
    0,                                /* tp_members */
    query_getsetters,                /* tp_getset */
    0,                                /* tp_base */
    0,                                /* tp_dict */
    0,                                /* tp_descr_get */
    0,                                /* tp_descr_set */
    0,                                /* tp_dictoffset */
    (initproc)query_init,        /* tp_init */
    0,                                /* tp_alloc */
    query_new,                        /* tp_new */
    0,                                /* tp_free */
    0,                                /* tp_is_gc */
};
