/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright 2009-2010  Red Hat, Inc,
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include <cups/cups.h>
#include <cups/ppd.h>
#include <glib.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>

#include "pp-utils.h"

#define DBUS_TIMEOUT 120000
#define DBUS_TIMEOUT_LONG 600000

#if (CUPS_VERSION_MAJOR > 1) || (CUPS_VERSION_MINOR > 5)
#define HAVE_CUPS_1_6 1
#endif

#ifndef HAVE_CUPS_1_6
#define ippGetCount(attr) attr->num_values
#define ippGetGroupTag(attr) attr->group_tag
#define ippGetValueTag(attr) attr->value_tag
#define ippGetName(attr) attr->name
#define ippGetStatusCode(ipp) ipp->request.status.status_code
#define ippGetInteger(attr, element) attr->values[element].integer
#define ippGetString(attr, element, language) attr->values[element].string.text
#define ippGetBoolean(attr, element) attr->values[element].boolean

static int
ippGetRange (ipp_attribute_t *attr, int element, int *upper)
{
    *upper = attr->values[element].range.upper;
    return (attr->values[element].range.lower);
}

static ipp_attribute_t *
ippFirstAttribute (ipp_t *ipp)
{
    if (!ipp)
        return (NULL);
    return (ipp->current = ipp->attrs);
}

static ipp_attribute_t *
ippNextAttribute (ipp_t *ipp)
{
    if (!ipp || !ipp->current)
        return (NULL);
    return (ipp->current = ipp->current->next);
}
#endif

#if (CUPS_VERSION_MAJOR == 1) && (CUPS_VERSION_MINOR <= 6)
#define HTTP_URI_STATUS_OK HTTP_URI_OK
#endif

gchar *
get_tag_value (const gchar *tag_string, const gchar *tag_name)
{
    gchar **tag_string_splitted = NULL;
    gchar *tag_value = NULL;
    gint tag_name_length;
    gint i;

    if (tag_string && tag_name) {
        tag_name_length = strlen (tag_name);
        tag_string_splitted = g_strsplit (tag_string, ";", 0);
        if (tag_string_splitted) {
            for (i = 0; i < g_strv_length (tag_string_splitted); i++)
                if (g_ascii_strncasecmp (tag_string_splitted[i], tag_name, tag_name_length) == 0)
                    if (strlen (tag_string_splitted[i]) > tag_name_length + 1)
                        tag_value = tag_string_splitted[i] + tag_name_length + 1;

            tag_value = g_strdup (tag_value);
            g_strfreev (tag_string_splitted);
        }
    }

    return tag_value;
}

/*
 * Normalize given string so that it is lowercase, doesn't
 * have trailing or leading whitespaces and digits doesn't
 * neighbour with alphabetic.
 * (see cupshelpers/ppds.py from system-config-printer)
 */
static gchar *
normalize (const gchar *input_string)
{
    gchar *result = NULL;
    gint i, j = 0, k = -1;

    if (input_string) {
        g_autofree gchar *tmp = g_strstrip (g_ascii_strdown (input_string, -1));
        if (tmp) {
            g_autofree gchar *res = g_new (gchar, 2 * strlen (tmp) + 1);

            for (i = 0; i < strlen (tmp); i++) {
                if ((g_ascii_isalpha (tmp[i]) && k >= 0 && g_ascii_isdigit (res[k]))
                    || (g_ascii_isdigit (tmp[i]) && k >= 0 && g_ascii_isalpha (res[k]))) {
                    res[j] = ' ';
                    k = j++;
                    res[j] = tmp[i];
                    k = j++;
                } else {
                    if (g_ascii_isspace (tmp[i]) || !g_ascii_isalnum (tmp[i])) {
                        if (!(k >= 0 && res[k] == ' ')) {
                            res[j] = ' ';
                            k = j++;
                        }
                    } else {
                        res[j] = tmp[i];
                        k = j++;
                    }
                }
            }

            res[j] = '\0';

            result = g_strdup (res);
        }
    }

    return result;
}

char *
get_dest_attr (const char *dest_name, const char *attr)
{
    cups_dest_t *dests;
    int num_dests;
    cups_dest_t *dest;
    const char *value;
    char *ret;

    if (dest_name == NULL)
        return NULL;

    ret = NULL;

    num_dests = cupsGetDests (&dests);
    if (num_dests < 1) {
        g_debug ("Unable to get printer destinations");
        return NULL;
    }

    dest = cupsGetDest (dest_name, NULL, num_dests, dests);
    if (dest == NULL) {
        g_debug ("Unable to find a printer named '%s'", dest_name);
        goto out;
    }

    value = cupsGetOption (attr, dest->num_options, dest->options);
    if (value == NULL) {
        g_debug ("Unable to get %s for '%s'", attr, dest_name);
        goto out;
    }
    ret = g_strdup (value);
out:
    cupsFreeDests (num_dests, dests);

    return ret;
}

gchar *
get_ppd_attribute (const gchar *ppd_file_name, const gchar *attribute_name)
{
    ppd_file_t *ppd_file = NULL;
    ppd_attr_t *ppd_attr = NULL;
    gchar *result = NULL;

    if (ppd_file_name) {
        ppd_file = ppdOpenFile (ppd_file_name);

        if (ppd_file) {
            ppd_attr = ppdFindAttr (ppd_file, attribute_name, NULL);
            if (ppd_attr != NULL)
                result = g_strdup (ppd_attr->value);
            ppdClose (ppd_file);
        }
    }

    return result;
}

/*  Set default destination in ~/.cups/lpoptions.
 *  Unset default destination if "dest" is NULL.
 */
void
set_local_default_printer (const gchar *printer_name)
{
    cups_dest_t *dests = NULL;
    int num_dests = 0;
    int i;

    num_dests = cupsGetDests (&dests);

    for (i = 0; i < num_dests; i++) {
        if (printer_name && g_strcmp0 (dests[i].name, printer_name) == 0)
            dests[i].is_default = 1;
        else
            dests[i].is_default = 0;
    }

    cupsSetDests (num_dests, dests);
}

/*
 * This function does something which should be provided by CUPS...
 * It returns FALSE if the renaming fails.
 */
gboolean
printer_rename (const gchar *old_name, const gchar *new_name)
{
    ipp_attribute_t *attr = NULL;
    cups_ptype_t printer_type = 0;
    cups_dest_t *dests = NULL;
    cups_dest_t *dest = NULL;
    cups_job_t *jobs = NULL;
    g_autoptr(GDBusConnection) bus = NULL;
    const gchar *printer_location = NULL;
    const gchar *printer_info = NULL;
    const gchar *printer_uri = NULL;
    const gchar *device_uri = NULL;
    const gchar *job_sheets = NULL;
    gboolean result = FALSE;
    gboolean accepting = TRUE;
    gboolean printer_paused = FALSE;
    gboolean default_printer = FALSE;
    gboolean printer_shared = FALSE;
    g_autoptr(GError) error = NULL;
    http_t *http;
    g_autofree gchar *ppd_link = NULL;
    g_autofree gchar *ppd_filename = NULL;
    gchar **sheets = NULL;
    gchar **users_allowed = NULL;
    gchar **users_denied = NULL;
    gchar **member_names = NULL;
    const gchar *start_sheet = NULL;
    const gchar *end_sheet = NULL;
    g_autofree gchar *error_policy = NULL;
    g_autofree gchar *op_policy = NULL;
    ipp_t *request;
    ipp_t *response;
    gint i;
    int num_dests = 0;
    int num_jobs = 0;
    static const char *const requested_attrs[] = { "printer-error-policy", "printer-op-policy",
                                                   "requesting-user-name-allowed", "requesting-user-name-denied",
                                                   "member-names" };

    if (old_name == NULL || old_name[0] == '\0' || new_name == NULL || new_name[0] == '\0'
        || g_strcmp0 (old_name, new_name) == 0)
        return FALSE;

    num_dests = cupsGetDests (&dests);

    dest = cupsGetDest (new_name, NULL, num_dests, dests);
    if (dest) {
        cupsFreeDests (num_dests, dests);
        return FALSE;
    }

    num_jobs = cupsGetJobs (&jobs, old_name, 0, CUPS_WHICHJOBS_ACTIVE);
    cupsFreeJobs (num_jobs, jobs);
    if (num_jobs > 1) {
        g_warning ("There are queued jobs on printer %s!", old_name);
        cupsFreeDests (num_dests, dests);
        return FALSE;
    }

    /*
     * Gather some informations about the original printer
     */
    dest = cupsGetDest (old_name, NULL, num_dests, dests);
    if (dest) {
        for (i = 0; i < dest->num_options; i++) {
            if (g_strcmp0 (dest->options[i].name, "printer-is-accepting-jobs") == 0)
                accepting = g_strcmp0 (dest->options[i].value, "true") == 0;
            else if (g_strcmp0 (dest->options[i].name, "printer-is-shared") == 0)
                printer_shared = g_strcmp0 (dest->options[i].value, "true") == 0;
            else if (g_strcmp0 (dest->options[i].name, "device-uri") == 0)
                device_uri = dest->options[i].value;
            else if (g_strcmp0 (dest->options[i].name, "printer-uri-supported") == 0)
                printer_uri = dest->options[i].value;
            else if (g_strcmp0 (dest->options[i].name, "printer-info") == 0)
                printer_info = dest->options[i].value;
            else if (g_strcmp0 (dest->options[i].name, "printer-location") == 0)
                printer_location = dest->options[i].value;
            else if (g_strcmp0 (dest->options[i].name, "printer-state") == 0)
                printer_paused = g_strcmp0 (dest->options[i].value, "5") == 0;
            else if (g_strcmp0 (dest->options[i].name, "job-sheets") == 0)
                job_sheets = dest->options[i].value;
            else if (g_strcmp0 (dest->options[i].name, "printer-type") == 0)
                printer_type = atoi (dest->options[i].value);
        }
        default_printer = dest->is_default;
    }
    cupsFreeDests (num_dests, dests);

    if (accepting) {
        printer_set_accepting_jobs (old_name, FALSE, NULL);

        num_jobs = cupsGetJobs (&jobs, old_name, 0, CUPS_WHICHJOBS_ACTIVE);
        cupsFreeJobs (num_jobs, jobs);
        if (num_jobs > 1) {
            printer_set_accepting_jobs (old_name, accepting, NULL);
            g_warning ("There are queued jobs on printer %s!", old_name);
            return FALSE;
        }
    }

    /*
     * Gather additional informations about the original printer
     */
#ifdef HAVE_CUPS_HTTPCONNECT2
    http = httpConnect2 (cupsServer (), ippPort (), NULL, AF_UNSPEC, cupsEncryption (), 1, 30000, NULL);
#else
    http = httpConnectEncrypt (cupsServer (), ippPort (), cupsEncryption ());
#endif
    if (http != NULL) {
        request = ippNewRequest (IPP_GET_PRINTER_ATTRIBUTES);
        ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, printer_uri);
        ippAddStrings (request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes",
                       G_N_ELEMENTS (requested_attrs), NULL, requested_attrs);
        response = cupsDoRequest (http, request, "/");

        if (response) {
            if (ippGetStatusCode (response) <= IPP_OK_CONFLICT) {
                attr = ippFindAttribute (response, "printer-error-policy", IPP_TAG_NAME);
                if (attr)
                    error_policy = g_strdup (ippGetString (attr, 0, NULL));

                attr = ippFindAttribute (response, "printer-op-policy", IPP_TAG_NAME);
                if (attr)
                    op_policy = g_strdup (ippGetString (attr, 0, NULL));

                attr = ippFindAttribute (response, "requesting-user-name-allowed", IPP_TAG_NAME);
                if (attr && ippGetCount (attr) > 0) {
                    users_allowed = g_new0 (gchar *, ippGetCount (attr) + 1);
                    for (i = 0; i < ippGetCount (attr); i++)
                        users_allowed[i] = g_strdup (ippGetString (attr, i, NULL));
                }

                attr = ippFindAttribute (response, "requesting-user-name-denied", IPP_TAG_NAME);
                if (attr && ippGetCount (attr) > 0) {
                    users_denied = g_new0 (gchar *, ippGetCount (attr) + 1);
                    for (i = 0; i < ippGetCount (attr); i++)
                        users_denied[i] = g_strdup (ippGetString (attr, i, NULL));
                }

                attr = ippFindAttribute (response, "member-names", IPP_TAG_NAME);
                if (attr && ippGetCount (attr) > 0) {
                    member_names = g_new0 (gchar *, ippGetCount (attr) + 1);
                    for (i = 0; i < ippGetCount (attr); i++)
                        member_names[i] = g_strdup (ippGetString (attr, i, NULL));
                }
            }
            ippDelete (response);
        }
        httpClose (http);
    }

    if (job_sheets) {
        sheets = g_strsplit (job_sheets, ",", 0);
        if (g_strv_length (sheets) > 1) {
            start_sheet = sheets[0];
            end_sheet = sheets[1];
        }
    }

    ppd_link = g_strdup (cupsGetPPD (old_name));
    if (ppd_link) {
        ppd_filename = g_file_read_link (ppd_link, NULL);

        if (!ppd_filename)
            ppd_filename = g_strdup (ppd_link);
    }

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
    } else {
        if (printer_type & CUPS_PRINTER_CLASS) {
            if (member_names)
                for (i = 0; i < g_strv_length (member_names); i++)
                    class_add_printer (new_name, member_names[i]);
        } else {
            g_autoptr(GVariant) output = NULL;
            g_autoptr(GError) add_error = NULL;

            output = g_dbus_connection_call_sync (
                bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterAddWithPpdFile",
                g_variant_new ("(sssss)", new_name, device_uri ? device_uri : "", ppd_filename ? ppd_filename : "",
                               printer_info ? printer_info : "", printer_location ? printer_location : ""),
                G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &add_error);

            if (output) {
                const gchar *ret_error;

                g_variant_get (output, "(&s)", &ret_error);
                if (ret_error[0] != '\0')
                    g_warning ("cups-pk-helper: rename of printer %s to %s failed: %s", old_name, new_name, ret_error);
            } else {
                g_warning ("%s", add_error->message);
            }
        }
    }

    if (ppd_link) {
        g_unlink (ppd_link);
    }

    num_dests = cupsGetDests (&dests);
    dest = cupsGetDest (new_name, NULL, num_dests, dests);
    if (dest) {
        printer_set_accepting_jobs (new_name, accepting, NULL);
        printer_set_enabled (new_name, !printer_paused);
        printer_set_shared (new_name, printer_shared);
        printer_set_job_sheets (new_name, start_sheet, end_sheet);
        printer_set_policy (new_name, op_policy, FALSE);
        printer_set_policy (new_name, error_policy, TRUE);
        printer_set_users (new_name, users_allowed, TRUE);
        printer_set_users (new_name, users_denied, FALSE);
        if (default_printer)
            printer_set_default (new_name);

        printer_delete (old_name);

        result = TRUE;
    } else
        printer_set_accepting_jobs (old_name, accepting, NULL);

    cupsFreeDests (num_dests, dests);
    if (sheets)
        g_strfreev (sheets);
    if (users_allowed)
        g_strfreev (users_allowed);
    if (users_denied)
        g_strfreev (users_denied);

    return result;
}

gboolean
printer_set_location (const gchar *printer_name, const gchar *location)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name || !location)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetLocation",
                                          g_variant_new ("(ss)", printer_name, location), G_VARIANT_TYPE ("(s)"),
                                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of location for printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_accepting_jobs (const gchar *printer_name, gboolean accepting_jobs, const gchar *reason)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetAcceptJobs",
                                          g_variant_new ("(sbs)", printer_name, accepting_jobs, reason ? reason : ""),
                                          G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of acceptance of jobs for printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_enabled (const gchar *printer_name, gboolean enabled)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetEnabled",
                                          g_variant_new ("(sb)", printer_name, enabled), G_VARIANT_TYPE ("(s)"),
                                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of enablement of printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_delete (const gchar *printer_name)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterDelete",
                                          g_variant_new ("(s)", printer_name), G_VARIANT_TYPE ("(s)"),
                                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: removing of printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_default (const gchar *printer_name)
{
    const char *cups_server;
    g_autoptr(GError) error = NULL;

    if (!printer_name)
        return TRUE;

    cups_server = cupsServer ();
    if (g_ascii_strncasecmp (cups_server, "localhost", 9) == 0 || g_ascii_strncasecmp (cups_server, "127.0.0.1", 9) == 0
        || g_ascii_strncasecmp (cups_server, "::1", 3) == 0 || cups_server[0] == '/') {
        g_autoptr(GDBusConnection) bus = NULL;
        g_autoptr(GVariant) output = NULL;
        const gchar *ret_error;

        /* Clean .cups/lpoptions before setting
         * default printer on local CUPS server.
         */
        set_local_default_printer (NULL);

        bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
        if (!bus) {
            g_warning ("Failed to get system bus: %s", error->message);
            return FALSE;
        }

        output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetDefault",
                                              g_variant_new ("(s)", printer_name), G_VARIANT_TYPE ("(s)"),
                                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

        if (output == NULL) {
            g_warning ("%s", error->message);
            return FALSE;
        }

        g_variant_get (output, "(&s)", &ret_error);
        if (ret_error[0] != '\0') {
            g_warning ("cups-pk-helper: setting default printer to %s failed: %s", printer_name, ret_error);
            return FALSE;
        }

        return TRUE;
    } else
    /* Store default printer to .cups/lpoptions
     * if we are connected to a remote CUPS server.
     */
    {
        set_local_default_printer (printer_name);
        return TRUE;
    }
}

gboolean
printer_set_shared (const gchar *printer_name, gboolean shared)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetShared",
                                          g_variant_new ("(sb)", printer_name, shared), G_VARIANT_TYPE ("(s)"),
                                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of sharing of printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_job_sheets (const gchar *printer_name, const gchar *start_sheet, const gchar *end_sheet)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name || !start_sheet || !end_sheet)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetJobSheets",
                                          g_variant_new ("(sss)", printer_name, start_sheet, end_sheet),
                                          G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of job sheets for printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_policy (const gchar *printer_name, const gchar *policy, gboolean error_policy)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name || !policy)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    if (error_policy)
        output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetErrorPolicy",
                                              g_variant_new ("(ss)", printer_name, policy), G_VARIANT_TYPE ("(s)"),
                                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
    else
        output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetOpPolicy",
                                              g_variant_new ("(ss)", printer_name, policy), G_VARIANT_TYPE ("(s)"),
                                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of a policy for printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_set_users (const gchar *printer_name, gchar **users, gboolean allowed)
{
    g_autoptr(GDBusConnection) bus = NULL;
    GVariantBuilder array_builder;
    gint i;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!printer_name || !users)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    g_variant_builder_init (&array_builder, G_VARIANT_TYPE ("as"));
    for (i = 0; users[i]; i++)
        g_variant_builder_add (&array_builder, "s", users[i]);

    if (allowed)
        output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetUsersAllowed",
                                              g_variant_new ("(sas)", printer_name, &array_builder),
                                              G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
    else
        output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterSetUsersDenied",
                                              g_variant_new ("(sas)", printer_name, &array_builder),
                                              G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: setting of access list for printer %s failed: %s", printer_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
class_add_printer (const gchar *class_name, const gchar *printer_name)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GVariant) output = NULL;
    const gchar *ret_error;
    g_autoptr(GError) error = NULL;

    if (!class_name || !printer_name)
        return TRUE;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        return TRUE;
    }

    output = g_dbus_connection_call_sync (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "ClassAddPrinter",
                                          g_variant_new ("(ss)", class_name, printer_name), G_VARIANT_TYPE ("(s)"),
                                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

    if (output == NULL) {
        g_warning ("%s", error->message);
        return FALSE;
    }

    g_variant_get (output, "(&s)", &ret_error);
    if (ret_error[0] != '\0') {
        g_warning ("cups-pk-helper: adding of printer %s to class %s failed: %s", printer_name, class_name, ret_error);
        return FALSE;
    }

    return TRUE;
}

gboolean
printer_is_local (cups_ptype_t printer_type, const gchar *device_uri)
{
    gboolean result = TRUE;
    char scheme[HTTP_MAX_URI];
    char username[HTTP_MAX_URI];
    char hostname[HTTP_MAX_URI];
    char resource[HTTP_MAX_URI];
    int port;

    if (printer_type & (CUPS_PRINTER_DISCOVERED | CUPS_PRINTER_REMOTE | CUPS_PRINTER_IMPLICIT))
        result = FALSE;

    if (device_uri == NULL || !result)
        return result;

    httpSeparateURI (HTTP_URI_CODING_ALL, device_uri, scheme, sizeof (scheme), username, sizeof (username), hostname,
                     sizeof (hostname), &port, resource, sizeof (resource));

    if (g_str_equal (scheme, "ipp") || g_str_equal (scheme, "smb") || g_str_equal (scheme, "socket")
        || g_str_equal (scheme, "lpd"))
        result = FALSE;

    return result;
}

gchar *
printer_get_hostname (cups_ptype_t printer_type, const gchar *device_uri, const gchar *printer_uri)
{
    gboolean local = TRUE;
    gchar *result = NULL;
    char scheme[HTTP_MAX_URI];
    char username[HTTP_MAX_URI];
    char hostname[HTTP_MAX_URI];
    char resource[HTTP_MAX_URI];
    int port;

    if (device_uri == NULL)
        return result;

    if (printer_type & (CUPS_PRINTER_DISCOVERED | CUPS_PRINTER_REMOTE | CUPS_PRINTER_IMPLICIT)) {
        if (printer_uri) {
            httpSeparateURI (HTTP_URI_CODING_ALL, printer_uri, scheme, sizeof (scheme), username, sizeof (username),
                             hostname, sizeof (hostname), &port, resource, sizeof (resource));

            if (hostname[0] != '\0')
                result = g_strdup (hostname);
        }

        local = FALSE;
    }

    if (result == NULL && device_uri) {
        httpSeparateURI (HTTP_URI_CODING_ALL, device_uri, scheme, sizeof (scheme), username, sizeof (username),
                         hostname, sizeof (hostname), &port, resource, sizeof (resource));

        if (g_str_equal (scheme, "ipp") || g_str_equal (scheme, "smb") || g_str_equal (scheme, "socket")
            || g_str_equal (scheme, "lpd")) {
            if (hostname[0] != '\0')
                result = g_strdup (hostname);

            local = FALSE;
        }
    }

    if (local)
        result = g_strdup ("localhost");

    return result;
}

/* Returns default page size for current locale */
const gchar *
get_page_size_from_locale (void)
{
    if (g_str_equal (gtk_paper_size_get_default (), GTK_PAPER_NAME_LETTER))
        return "Letter";
    else
        return "A4";
}

typedef struct {
    gchar *printer_name;
    gchar **attributes_names;
    GHashTable *result;
    GIACallback callback;
    gpointer user_data;
    GMainContext *context;
} GIAData;

static GIAData *
gia_data_new (const gchar *printer_name, gchar **attributes_names, GIACallback callback, gpointer user_data)
{
    GIAData *data;

    data = g_new0 (GIAData, 1);
    data->printer_name = g_strdup (printer_name);
    data->attributes_names = g_strdupv (attributes_names);
    data->callback = callback;
    data->user_data = user_data;
    data->context = g_main_context_ref_thread_default ();

    return data;
}

static void
gia_data_free (GIAData *data)
{
    g_free (data->printer_name);
    if (data->attributes_names)
        g_strfreev (data->attributes_names);
    if (data->result)
        g_hash_table_unref (data->result);
    if (data->context)
        g_main_context_unref (data->context);
    g_free (data);
}

static gboolean
get_ipp_attributes_idle_cb (gpointer user_data)
{
    GIAData *data = (GIAData *) user_data;

    data->callback (data->result, data->user_data);
    data->result = NULL;

    return FALSE;
}

static void
get_ipp_attributes_cb (gpointer user_data)
{
    GIAData *data = user_data;
    g_autoptr(GSource) idle_source = NULL;

    idle_source = g_idle_source_new ();
    g_source_set_callback (idle_source, get_ipp_attributes_idle_cb, data, (GDestroyNotify) gia_data_free);
    g_source_attach (idle_source, data->context);
}

static void
ipp_attribute_free2 (gpointer attr)
{
    IPPAttribute *attribute = (IPPAttribute *) attr;
    ipp_attribute_free (attribute);
}

static gpointer
get_ipp_attributes_func (gpointer user_data)
{
    ipp_attribute_t *attr = NULL;
    GIAData *data = user_data;
    ipp_t *request;
    ipp_t *response = NULL;
    g_autofree gchar *printer_uri = NULL;
    char **requested_attrs = NULL;
    gint i, j, length = 0;

    printer_uri = g_strdup_printf ("ipp://localhost/printers/%s", data->printer_name);

    if (data->attributes_names) {
        length = g_strv_length (data->attributes_names);

        requested_attrs = g_new0 (char *, length);
        for (i = 0; data->attributes_names[i]; i++)
            requested_attrs[i] = g_strdup (data->attributes_names[i]);

        request = ippNewRequest (IPP_GET_PRINTER_ATTRIBUTES);
        ippAddString (request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, printer_uri);
        ippAddStrings (request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", length, NULL,
                       (const char **) requested_attrs);
        response = cupsDoRequest (CUPS_HTTP_DEFAULT, request, "/");
    }

    if (response) {
        if (ippGetStatusCode (response) <= IPP_OK_CONFLICT) {
            for (j = 0; j < length; j++) {
                attr = ippFindAttribute (response, requested_attrs[j], IPP_TAG_ZERO);
                if (attr && ippGetCount (attr) > 0 && ippGetValueTag (attr) != IPP_TAG_NOVALUE) {
                    IPPAttribute *attribute;

                    attribute = g_new0 (IPPAttribute, 1);
                    attribute->attribute_name = g_strdup (requested_attrs[j]);
                    attribute->attribute_values = g_new0 (IPPAttributeValue, ippGetCount (attr));
                    attribute->num_of_values = ippGetCount (attr);

                    if (ippGetValueTag (attr) == IPP_TAG_INTEGER || ippGetValueTag (attr) == IPP_TAG_ENUM) {
                        attribute->attribute_type = IPP_ATTRIBUTE_TYPE_INTEGER;

                        for (i = 0; i < ippGetCount (attr); i++)
                            attribute->attribute_values[i].integer_value = ippGetInteger (attr, i);
                    } else if (ippGetValueTag (attr) == IPP_TAG_NAME || ippGetValueTag (attr) == IPP_TAG_STRING
                               || ippGetValueTag (attr) == IPP_TAG_TEXT || ippGetValueTag (attr) == IPP_TAG_URI
                               || ippGetValueTag (attr) == IPP_TAG_KEYWORD
                               || ippGetValueTag (attr) == IPP_TAG_URISCHEME) {
                        attribute->attribute_type = IPP_ATTRIBUTE_TYPE_STRING;

                        for (i = 0; i < ippGetCount (attr); i++)
                            attribute->attribute_values[i].string_value = g_strdup (ippGetString (attr, i, NULL));
                    } else if (ippGetValueTag (attr) == IPP_TAG_RANGE) {
                        attribute->attribute_type = IPP_ATTRIBUTE_TYPE_RANGE;

                        for (i = 0; i < ippGetCount (attr); i++) {
                            attribute->attribute_values[i].lower_range =
                                ippGetRange (attr, i, &(attribute->attribute_values[i].upper_range));
                        }
                    } else if (ippGetValueTag (attr) == IPP_TAG_BOOLEAN) {
                        attribute->attribute_type = IPP_ATTRIBUTE_TYPE_BOOLEAN;

                        for (i = 0; i < ippGetCount (attr); i++)
                            attribute->attribute_values[i].boolean_value = ippGetBoolean (attr, i);
                    }

                    if (!data->result)
                        data->result = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, ipp_attribute_free2);

                    g_hash_table_insert (data->result, g_strdup (requested_attrs[j]), attribute);
                }
            }
        }

        ippDelete (response);
    }

    for (i = 0; i < length; i++)
        g_free (requested_attrs[i]);
    g_free (requested_attrs);

    get_ipp_attributes_cb (data);

    return NULL;
}

void
get_ipp_attributes_async (const gchar *printer_name, gchar **attributes_names, GIACallback callback, gpointer user_data)
{
    GIAData *data;
    g_autoptr(GThread) thread = NULL;
    g_autoptr(GError) error = NULL;

    data = gia_data_new (printer_name, attributes_names, callback, user_data);

    thread = g_thread_try_new ("get-ipp-attributes", get_ipp_attributes_func, data, &error);

    if (!thread) {
        g_warning ("%s", error->message);
        callback (NULL, user_data);

        gia_data_free (data);
    }
}

IPPAttribute *
ipp_attribute_copy (IPPAttribute *attr)
{
    IPPAttribute *result = NULL;
    gint i;

    if (attr) {
        result = g_new0 (IPPAttribute, 1);

        *result = *attr;
        result->attribute_name = g_strdup (attr->attribute_name);
        result->attribute_values = g_new0 (IPPAttributeValue, attr->num_of_values);
        for (i = 0; i < attr->num_of_values; i++) {
            result->attribute_values[i] = attr->attribute_values[i];
            if (attr->attribute_values[i].string_value)
                result->attribute_values[i].string_value = g_strdup (attr->attribute_values[i].string_value);
        }
    }

    return result;
}

void
ipp_attribute_free (IPPAttribute *attr)
{
    gint i;

    if (attr) {
        for (i = 0; i < attr->num_of_values; i++)
            g_free (attr->attribute_values[i].string_value);

        g_free (attr->attribute_values);
        g_free (attr->attribute_name);
        g_free (attr);
    }
}

typedef struct {
    gchar *printer_name;
    gchar *ppd_copy;
    GCancellable *cancellable;
    PSPCallback callback;
    gpointer user_data;
} PSPData;

static PSPData *
psp_data_new (const gchar *printer_name, const gchar *ppd_copy, GCancellable *cancellable, PSPCallback callback,
              gpointer user_data)
{
    PSPData *data;

    data = g_new0 (PSPData, 1);
    data->printer_name = g_strdup (printer_name);
    data->ppd_copy = g_strdup (ppd_copy);
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;
    return data;
}

static void
psp_data_free (PSPData *data)
{
    g_free (data->printer_name);
    if (data->ppd_copy != NULL) {
        g_unlink (data->ppd_copy);
        g_free (data->ppd_copy);
    }
    g_clear_object (&data->cancellable);
    g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (PSPData, psp_data_free)

static void
printer_set_ppd_async_dbus_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GVariant) output = NULL;
    gboolean result = FALSE;
    g_autoptr(PSPData) data = user_data;
    g_autoptr(GError) error = NULL;

    output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);

    if (output) {
        const gchar *ret_error;

        g_variant_get (output, "(&s)", &ret_error);
        if (ret_error[0] != '\0')
            g_warning ("cups-pk-helper: setting of driver for printer %s failed: %s", data->printer_name, ret_error);
        else
            result = TRUE;
    } else {
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("%s", error->message);
    }

    /* Don't call callback if cancelled */
    if (!data->cancellable || !g_cancellable_is_cancelled (data->cancellable))
        data->callback (data->printer_name, result, data->user_data);
}

/*
 * Set ppd for given printer.
 * Don't use this for classes, just for printers.
 */
void
printer_set_ppd_async (const gchar *printer_name, const gchar *ppd_name, GCancellable *cancellable,
                       PSPCallback callback, gpointer user_data)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GError) error = NULL;

    if (printer_name == NULL || printer_name[0] == '\0') {
        callback (printer_name, FALSE, user_data);
        return;
    }

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        callback (printer_name, FALSE, user_data);
        return;
    }

    g_dbus_connection_call (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterAdd",
                            g_variant_new ("(sssss)", printer_name, "", ppd_name, "", ""), G_VARIANT_TYPE ("(s)"),
                            G_DBUS_CALL_FLAGS_NONE, -1, cancellable, printer_set_ppd_async_dbus_cb,
                            psp_data_new (printer_name, NULL, cancellable, callback, user_data));
}

static void
printer_set_ppd_file_async_scb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GDBusConnection) bus = NULL;
    gboolean success;
    g_autoptr(PSPData) data = user_data;
    g_autoptr(GError) error = NULL;

    success = g_file_copy_finish (G_FILE (source_object), res, &error);

    if (!success) {
        g_warning ("%s", error->message);
        data->callback (data->printer_name, FALSE, data->user_data);
        return;
    }

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        data->callback (data->printer_name, FALSE, data->user_data);
        return;
    }

    g_dbus_connection_call (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "PrinterAddWithPpdFile",
                            g_variant_new ("(sssss)", data->printer_name, "", data->ppd_copy, "", ""),
                            G_VARIANT_TYPE ("(s)"), G_DBUS_CALL_FLAGS_NONE, -1, data->cancellable,
                            printer_set_ppd_async_dbus_cb, data);
    g_steal_pointer (&data);
}

/*
 * Set ppd for given printer.
 * Don't use this for classes, just for printers.
 */
void
printer_set_ppd_file_async (const gchar *printer_name, const gchar *ppd_filename, GCancellable *cancellable,
                            PSPCallback callback, gpointer user_data)
{
    g_autoptr(GFileIOStream) stream = NULL;
    g_autoptr(GFile) source_ppd_file = NULL;
    g_autoptr(GFile) destination_ppd_file = NULL;

    if (printer_name == NULL || printer_name[0] == '\0') {
        callback (printer_name, FALSE, user_data);
        return;
    }

    /*
     * We need to copy the PPD to temp directory at first.
     * This is needed because of SELinux.
     */
    source_ppd_file = g_file_new_for_path (ppd_filename);
    destination_ppd_file = g_file_new_tmp ("g-c-c-XXXXXX.ppd", &stream, NULL);

    g_file_copy_async (
        source_ppd_file, destination_ppd_file, G_FILE_COPY_OVERWRITE, G_PRIORITY_DEFAULT, cancellable, NULL, NULL,
        printer_set_ppd_file_async_scb,
        psp_data_new (printer_name, g_file_get_path (destination_ppd_file), cancellable, callback, user_data));
}

typedef void (*GPACallback) (gchar **attribute_values, gpointer user_data);

typedef struct {
    gchar **ppds_names;
    gchar *attribute_name;
    gchar **result;
    GPACallback callback;
    gpointer user_data;
    GMainContext *context;
} GPAData;

static GPAData *
gpa_data_new (gchar **ppds_names, gchar *attribute_name, GPACallback callback, gpointer user_data)
{
    GPAData *data;

    data = g_new0 (GPAData, 1);
    data->ppds_names = g_strdupv (ppds_names);
    data->attribute_name = g_strdup (attribute_name);
    data->callback = callback;
    data->user_data = user_data;
    data->context = g_main_context_ref_thread_default ();

    return data;
}

static void
gpa_data_free (GPAData *data)
{
    g_free (data->attribute_name);
    g_strfreev (data->ppds_names);
    if (data->result != NULL)
        g_strfreev (data->result);
    if (data->context)
        g_main_context_unref (data->context);
    g_free (data);
}

static gboolean
get_ppds_attribute_idle_cb (gpointer user_data)
{
    GPAData *data = (GPAData *) user_data;

    data->callback (data->result, data->user_data);

    return FALSE;
}

static void
get_ppds_attribute_cb (gpointer user_data)
{
    GPAData *data = (GPAData *) user_data;
    g_autoptr(GSource) idle_source = NULL;

    idle_source = g_idle_source_new ();
    g_source_set_callback (idle_source, get_ppds_attribute_idle_cb, data, (GDestroyNotify) gpa_data_free);
    g_source_attach (idle_source, data->context);
}

static gpointer
get_ppds_attribute_func (gpointer user_data)
{
    ppd_file_t *ppd_file;
    ppd_attr_t *ppd_attr;
    GPAData *data = user_data;
    gint i;

    data->result = g_new0 (gchar *, g_strv_length (data->ppds_names) + 1);
    for (i = 0; data->ppds_names[i]; i++) {
        g_autofree gchar *ppd_filename = g_strdup (cupsGetServerPPD (CUPS_HTTP_DEFAULT, data->ppds_names[i]));
        if (ppd_filename) {
            ppd_file = ppdOpenFile (ppd_filename);
            if (ppd_file) {
                ppd_attr = ppdFindAttr (ppd_file, data->attribute_name, NULL);
                if (ppd_attr != NULL)
                    data->result[i] = g_strdup (ppd_attr->value);

                ppdClose (ppd_file);
            }

            g_unlink (ppd_filename);
        }
    }

    get_ppds_attribute_cb (data);

    return NULL;
}

/*
 * Get values of requested PPD attribute for given PPDs.
 */
static void
get_ppds_attribute_async (gchar **ppds_names, gchar *attribute_name, GPACallback callback, gpointer user_data)
{
    GPAData *data;
    g_autoptr(GThread) thread = NULL;
    g_autoptr(GError) error = NULL;

    if (!ppds_names || !attribute_name) {
        callback (NULL, user_data);
        return;
    }

    data = gpa_data_new (ppds_names, attribute_name, callback, user_data);

    thread = g_thread_try_new ("get-ppds-attribute", get_ppds_attribute_func, data, &error);

    if (!thread) {
        g_warning ("%s", error->message);
        callback (NULL, user_data);

        gpa_data_free (data);
    }
}

typedef void (*GDACallback) (gchar *device_id, gchar *device_make_and_model, gchar *device_uri, gpointer user_data);

typedef struct {
    gchar *printer_name;
    gchar *device_uri;
    GList *backend_list;
    GCancellable *cancellable;
    GDACallback callback;
    gpointer user_data;
} GDAData;

static GDAData *
gda_data_new (const gchar *printer_name, GCancellable *cancellable, GDACallback callback, gpointer user_data)
{
    GDAData *data;

    data = g_new0 (GDAData, 1);
    data->printer_name = g_strdup (printer_name);
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;

    return data;
}

static void
gda_data_free (GDAData *data)
{
    g_free (data->printer_name);
    g_free (data->device_uri);
    g_list_free_full (data->backend_list, g_free);
    g_clear_object (&data->cancellable);
    g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (GDAData, gda_data_free)

typedef struct {
    gchar *printer_name;
    gint count;
    PPDName **result;
    GCancellable *cancellable;
    GPNCallback callback;
    gpointer user_data;
} GPNData;

static GPNData *
gpn_data_new (const gchar *printer_name, gint count, GCancellable *cancellable, GPNCallback callback,
              gpointer user_data)
{
    GPNData *data;

    data = g_new0 (GPNData, 1);
    data->printer_name = g_strdup (printer_name);
    data->count = count;
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;

    return data;
}

static void
gpn_data_free (GPNData *data)
{
    g_free (data->printer_name);
    if (data->result != NULL) {
        for (int i = 0; data->result[i]; i++) {
            g_free (data->result[i]->ppd_name);
            g_free (data->result[i]->ppd_display_name);
            g_free (data->result[i]);
        }
        g_free (data->result);
    }
    g_clear_object (&data->cancellable);
    g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (GPNData, gpn_data_free)

static void
get_ppd_names_async_cb (gchar **attribute_values, gpointer user_data)
{
    g_autoptr(GPNData) data = user_data;
    gint i;

    if (g_cancellable_is_cancelled (data->cancellable)) {
        data->callback (NULL, data->printer_name, TRUE, data->user_data);
        return;
    }

    if (attribute_values) {
        for (i = 0; attribute_values[i]; i++)
            data->result[i]->ppd_display_name = g_strdup (attribute_values[i]);
    }

    data->callback (data->result, data->printer_name, FALSE, data->user_data);
}

static void
get_ppd_names_async_dbus_scb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GVariant) output = NULL;
    PPDName *ppd_item;
    PPDName **result = NULL;
    g_autoptr(GPNData) data = user_data;
    g_autoptr(GError) error = NULL;
    GList *driver_list = NULL;
    GList *iter;
    gint i, j, n = 0;
    static const char *const match_levels[] = { "exact-cmd", "exact", "close", "generic", "none" };

    output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);

    if (output) {
        g_autoptr(GVariant) array = NULL;

        g_variant_get (output, "(@a(ss))", &array);

        for (j = 0; j < G_N_ELEMENTS (match_levels) && n < data->count; j++) {
            g_autoptr(GVariantIter) iter = NULL;
            const gchar *driver, *match;

            g_variant_get (array, "a(ss)", &iter);

            while (g_variant_iter_next (iter, "(&s&s)", &driver, &match)) {
                if (g_str_equal (match, match_levels[j]) && n < data->count) {
                    ppd_item = g_new0 (PPDName, 1);
                    ppd_item->ppd_name = g_strdup (driver);

                    if (g_strcmp0 (match, "exact-cmd") == 0)
                        ppd_item->ppd_match_level = PPD_EXACT_CMD_MATCH;
                    else if (g_strcmp0 (match, "exact") == 0)
                        ppd_item->ppd_match_level = PPD_EXACT_MATCH;
                    else if (g_strcmp0 (match, "close") == 0)
                        ppd_item->ppd_match_level = PPD_CLOSE_MATCH;
                    else if (g_strcmp0 (match, "generic") == 0)
                        ppd_item->ppd_match_level = PPD_GENERIC_MATCH;
                    else if (g_strcmp0 (match, "none") == 0)
                        ppd_item->ppd_match_level = PPD_NO_MATCH;

                    driver_list = g_list_append (driver_list, ppd_item);

                    n++;
                }
            }
        }
    } else {
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("%s", error->message);
    }

    if (n > 0) {
        result = g_new0 (PPDName *, n + 1);
        i = 0;
        for (iter = driver_list; iter; iter = iter->next) {
            result[i] = iter->data;
            i++;
        }
    }

    if (result) {
        g_auto(GStrv) ppds_names = NULL;

        data->result = result;

        ppds_names = g_new0 (gchar *, n + 1);
        for (i = 0; i < n; i++)
            ppds_names[i] = g_strdup (result[i]->ppd_name);

        get_ppds_attribute_async (ppds_names, "NickName", get_ppd_names_async_cb, data);
        g_steal_pointer (&data);
    } else {
        data->callback (NULL, data->printer_name, g_cancellable_is_cancelled (data->cancellable), data->user_data);
    }
}

static void
get_device_attributes_cb (gchar *device_id, gchar *device_make_and_model, gchar *device_uri, gpointer user_data)
{
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GError) error = NULL;
    g_autoptr(GPNData) data = user_data;

    if (g_cancellable_is_cancelled (data->cancellable)) {
        data->callback (NULL, data->printer_name, TRUE, data->user_data);
        return;
    }

    if (!device_id || !device_make_and_model || !device_uri) {
        data->callback (NULL, data->printer_name, FALSE, data->user_data);
        return;
    }

    bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        data->callback (NULL, data->printer_name, FALSE, data->user_data);
        return;
    }

    g_dbus_connection_call (bus, SCP_BUS, SCP_PATH, SCP_IFACE, "GetBestDrivers",
                            g_variant_new ("(sss)", device_id, device_make_and_model, device_uri),
                            G_VARIANT_TYPE ("(a(ss))"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_LONG, data->cancellable,
                            get_ppd_names_async_dbus_scb, data);
    g_steal_pointer (&data);
}

/*
 * Special item for the list of backends. It represents
 * backends not present in the list itself.
 */
#define OTHER_BACKENDS "other-backends"

/*
 * List of CUPS backends sorted according to their speed,
 * the fastest is the first one. The last item represents
 * backends not present in the list.
 */
const gchar *cups_backends[] = { "usb",   "socket", "serial",    "parallel", "lpd", "ipp",   "hp",
                                 "dnssd", "snmp",   "bluetooth", "beh",      "ncp", "hpfax", OTHER_BACKENDS };

static GList *
create_backends_list ()
{
    GList *list = NULL;
    gint i;

    for (i = 0; i < G_N_ELEMENTS (cups_backends); i++)
        list = g_list_prepend (list, g_strdup (cups_backends[i]));
    list = g_list_reverse (list);

    return list;
}

static GVariantBuilder *
create_other_backends_array ()
{
    GVariantBuilder *builder;
    gint i;

    builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
    for (i = 0; i < G_N_ELEMENTS (cups_backends) - 1; i++)
        g_variant_builder_add (builder, "s", cups_backends[i]);

    return builder;
}

static void
get_device_attributes_async_dbus_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)

{
    g_autoptr(GVariant) output = NULL;
    g_autoptr(GDAData) data = user_data;
    g_autoptr(GError) error = NULL;
    g_autofree gchar *device_id = NULL;
    g_autofree gchar *device_make_and_model = NULL;

    output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);

    if (output) {
        const gchar *ret_error;
        g_autoptr(GVariant) devices_variant = NULL;
        gint index = -1;

        g_variant_get (output, "(&s@a{ss})", &ret_error, &devices_variant);

        if (ret_error[0] != '\0') {
            g_warning ("cups-pk-helper: getting of attributes for printer %s failed: %s", data->printer_name,
                       ret_error);
        }

        if (data->device_uri) {
            g_autoptr(GVariantIter) iter = NULL;
            const gchar *key, *value;
            g_autofree gchar *suffix = NULL;

            g_variant_get (devices_variant, "a{ss}", &iter);

            while (g_variant_iter_next (iter, "{&s&s}", &key, &value)) {
                if (g_str_equal (value, data->device_uri)) {
                    gchar *number = g_strrstr (key, ":");
                    if (number != NULL) {
                        gchar *endptr;

                        number++;
                        index = g_ascii_strtoll (number, &endptr, 10);
                        if (index == 0 && endptr == (number))
                            index = -1;
                    }
                }
            }

            suffix = g_strdup_printf (":%d", index);

            g_variant_get (devices_variant, "a{ss}", &iter);

            while (g_variant_iter_next (iter, "{&s&s}", &key, &value)) {
                if (g_str_has_suffix (key, suffix)) {
                    if (g_str_has_prefix (key, "device-id")) {
                        device_id = g_strdup (value);
                    }

                    if (g_str_has_prefix (key, "device-make-and-model")) {
                        device_make_and_model = g_strdup (value);
                    }
                }
            }
        }
    } else {
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("%s", error->message);
    }

    if (!device_id || !device_make_and_model) {
        GVariantBuilder *include_scheme_builder = NULL;
        GVariantBuilder *exclude_scheme_builder = NULL;

        if (data->backend_list && !g_cancellable_is_cancelled (data->cancellable)) {
            const gchar *backend_name;

            backend_name = data->backend_list->data;

            if (g_strcmp0 (backend_name, OTHER_BACKENDS) != 0) {
                include_scheme_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
                g_variant_builder_add (include_scheme_builder, "s", backend_name);
            } else {
                exclude_scheme_builder = create_other_backends_array ();
            }

            data->backend_list = g_list_delete_link (data->backend_list, data->backend_list);

            g_dbus_connection_call (G_DBUS_CONNECTION (g_object_ref (source_object)), MECHANISM_BUS, "/", MECHANISM_BUS,
                                    "DevicesGet",
                                    g_variant_new ("(iiasas)", 0, 0, include_scheme_builder, exclude_scheme_builder),
                                    G_VARIANT_TYPE ("(sa{ss})"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT,
                                    data->cancellable, get_device_attributes_async_dbus_cb, data);
            g_steal_pointer (&data);

            if (include_scheme_builder)
                g_variant_builder_unref (include_scheme_builder);

            if (exclude_scheme_builder)
                g_variant_builder_unref (exclude_scheme_builder);

            return;
        }
    }

    data->callback (device_id, device_make_and_model, data->device_uri, data->user_data);
}

static void
get_device_attributes_async_scb (GHashTable *result, gpointer user_data)
{
    g_autoptr(GDBusConnection) bus = NULL;
    GVariantBuilder include_scheme_builder;
    IPPAttribute *attr;
    g_autoptr(GDAData) data = user_data;
    g_autoptr(GError) error = NULL;

    if (result) {
        attr = g_hash_table_lookup (result, "device-uri");
        if (attr && attr->attribute_type == IPP_ATTRIBUTE_TYPE_STRING && attr->num_of_values > 0)
            data->device_uri = g_strdup (attr->attribute_values[0].string_value);
        g_hash_table_unref (result);
    }

    if (g_cancellable_is_cancelled (data->cancellable)) {
        data->callback (NULL, NULL, NULL, data->user_data);
        return;
    }

    if (!data->device_uri) {
        data->callback (NULL, NULL, NULL, data->user_data);
        return;
    }

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        data->callback (NULL, NULL, NULL, data->user_data);
        return;
    }

    data->backend_list = create_backends_list ();

    g_variant_builder_init (&include_scheme_builder, G_VARIANT_TYPE ("as"));
    g_variant_builder_add (&include_scheme_builder, "s", data->backend_list->data);

    data->backend_list = g_list_delete_link (data->backend_list, data->backend_list);

    g_dbus_connection_call (g_object_ref (bus), MECHANISM_BUS, "/", MECHANISM_BUS, "DevicesGet",
                            g_variant_new ("(iiasas)", 0, 0, &include_scheme_builder, NULL),
                            G_VARIANT_TYPE ("(sa{ss})"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, data->cancellable,
                            get_device_attributes_async_dbus_cb, data);
    g_steal_pointer (&data);
}

/*
 * Get device-id, device-make-and-model and device-uri for given printer.
 */
static void
get_device_attributes_async (const gchar *printer_name, GCancellable *cancellable, GDACallback callback,
                             gpointer user_data)
{
    g_auto(GStrv) attributes = NULL;

    if (!printer_name) {
        callback (NULL, NULL, NULL, user_data);
        return;
    }

    attributes = g_new0 (gchar *, 2);
    attributes[0] = g_strdup ("device-uri");

    get_ipp_attributes_async (printer_name, attributes, get_device_attributes_async_scb,
                              gda_data_new (printer_name, cancellable, callback, user_data));
}

/*
 * Return "count" best matching driver names for given printer.
 */
void
get_ppd_names_async (gchar *printer_name, gint count, GCancellable *cancellable, GPNCallback callback,
                     gpointer user_data)
{
    if (!printer_name) {
        callback (NULL, NULL, TRUE, user_data);
        return;
    }

    /*
     * We have to find out device-id for this printer at first.
     */
    get_device_attributes_async (printer_name, cancellable, get_device_attributes_cb,
                                 gpn_data_new (printer_name, count, cancellable, callback, user_data));
}

typedef struct {
    PPDList *result;
    GCancellable *cancellable;
    GAPCallback callback;
    gpointer user_data;
    GMainContext *context;
} GAPData;

static GAPData *
gap_data_new (GCancellable *cancellable, GAPCallback callback, gpointer user_data)
{
    GAPData *data;

    data = g_new0 (GAPData, 1);
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;
    data->context = g_main_context_ref_thread_default ();

    return data;
}

static void
gap_data_free (GAPData *data)
{
    if (data->result != NULL)
        ppd_list_free (data->result);
    g_clear_object (&data->cancellable);
    if (data->context)
        g_main_context_unref (data->context);
    g_free (data);
}

static gboolean
get_all_ppds_idle_cb (gpointer user_data)
{
    GAPData *data = user_data;

    if (!g_cancellable_is_cancelled (data->cancellable))
        data->callback (data->result, data->user_data);

    return FALSE;
}

static void
get_all_ppds_cb (gpointer user_data)
{
    GAPData *data = user_data;
    g_autoptr(GSource) idle_source = NULL;

    idle_source = g_idle_source_new ();
    g_source_set_callback (idle_source, get_all_ppds_idle_cb, data, (GDestroyNotify) gap_data_free);
    g_source_attach (idle_source, data->context);
}

static const struct {
    const char *normalized_name;
    const char *display_name;
} manufacturers_names[] = {
    { "alps", "Alps" },
    { "anitech", "Anitech" },
    { "apple", "Apple" },
    { "apollo", "Apollo" },
    { "brother", "Brother" },
    { "canon", "Canon" },
    { "citizen", "Citizen" },
    { "citoh", "Citoh" },
    { "compaq", "Compaq" },
    { "dec", "DEC" },
    { "dell", "Dell" },
    { "dnp", "DNP" },
    { "dymo", "Dymo" },
    { "epson", "Epson" },
    { "fujifilm", "Fujifilm" },
    { "fujitsu", "Fujitsu" },
    { "gelsprinter", "Ricoh" },
    { "generic", "Generic" },
    { "genicom", "Genicom" },
    { "gestetner", "Gestetner" },
    { "hewlett packard", "Hewlett-Packard" },
    { "heidelberg", "Heidelberg" },
    { "hitachi", "Hitachi" },
    { "hp", "Hewlett-Packard" },
    { "ibm", "IBM" },
    { "imagen", "Imagen" },
    { "imagistics", "Imagistics" },
    { "infoprint", "InfoPrint" },
    { "infotec", "Infotec" },
    { "intellitech", "Intellitech" },
    { "kodak", "Kodak" },
    { "konica minolta", "Minolta" },
    { "kyocera", "Kyocera" },
    { "kyocera mita", "Kyocera" },
    { "lanier", "Lanier" },
    { "lexmark international", "Lexmark" },
    { "lexmark", "Lexmark" },
    { "minolta", "Minolta" },
    { "minolta qms", "Minolta" },
    { "mitsubishi", "Mitsubishi" },
    { "nec", "NEC" },
    { "nrg", "NRG" },
    { "oce", "Oce" },
    { "oki", "Oki" },
    { "oki data corp", "Oki" },
    { "olivetti", "Olivetti" },
    { "olympus", "Olympus" },
    { "panasonic", "Panasonic" },
    { "pcpi", "PCPI" },
    { "pentax", "Pentax" },
    { "qms", "QMS" },
    { "raven", "Raven" },
    { "raw", "Raw" },
    { "ricoh", "Ricoh" },
    { "samsung", "Samsung" },
    { "savin", "Savin" },
    { "seiko", "Seiko" },
    { "sharp", "Sharp" },
    { "shinko", "Shinko" },
    { "sipix", "SiPix" },
    { "sony", "Sony" },
    { "star", "Star" },
    { "tally", "Tally" },
    { "tektronix", "Tektronix" },
    { "texas instruments", "Texas Instruments" },
    { "toshiba", "Toshiba" },
    { "toshiba tec corp.", "Toshiba" },
    { "xante", "Xante" },
    { "xerox", "Xerox" },
    { "zebra", "Zebra" },
};

static gpointer
get_all_ppds_func (gpointer user_data)
{
    ipp_attribute_t *attr;
    GHashTable *ppds_hash = NULL;
    GHashTable *manufacturers_hash = NULL;
    GAPData *data = user_data;
    PPDName *item;
    ipp_t *request;
    ipp_t *response;
    GList *list;
    gchar *manufacturer_display_name;
    gint i, j;

    request = ippNewRequest (CUPS_GET_PPDS);
    response = cupsDoRequest (CUPS_HTTP_DEFAULT, request, "/");

    if (response && ippGetStatusCode (response) <= IPP_OK_CONFLICT) {
        /*
         * This hash contains names of manufacturers as keys and
         * values are GLists of PPD names.
         */
        ppds_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

        /*
         * This hash contains all possible names of manufacturers as keys
         * and values are just first occurrences of their equivalents.
         * This is for mapping of e.g. "Hewlett Packard" and "HP" to the same name
         * (the one which comes first).
         */
        manufacturers_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

        for (i = 0; i < G_N_ELEMENTS (manufacturers_names); i++) {
            g_hash_table_insert (manufacturers_hash, g_strdup (manufacturers_names[i].normalized_name),
                                 g_strdup (manufacturers_names[i].display_name));
        }

        for (attr = ippFirstAttribute (response); attr != NULL; attr = ippNextAttribute (response)) {
            const gchar *ppd_device_id = NULL;
            const gchar *ppd_make_and_model = NULL;
            const gchar *ppd_name = NULL;
            const gchar *ppd_product = NULL;
            const gchar *ppd_make = NULL;
            g_autofree gchar *mdl = NULL;
            g_autofree gchar *mfg = NULL;
            g_autofree gchar *mfg_normalized = NULL;

            while (attr != NULL && ippGetGroupTag (attr) != IPP_TAG_PRINTER)
                attr = ippNextAttribute (response);

            if (attr == NULL)
                break;

            while (attr != NULL && ippGetGroupTag (attr) == IPP_TAG_PRINTER) {
                if (g_strcmp0 (ippGetName (attr), "ppd-device-id") == 0 && ippGetValueTag (attr) == IPP_TAG_TEXT)
                    ppd_device_id = ippGetString (attr, 0, NULL);
                else if (g_strcmp0 (ippGetName (attr), "ppd-make-and-model") == 0
                         && ippGetValueTag (attr) == IPP_TAG_TEXT)
                    ppd_make_and_model = ippGetString (attr, 0, NULL);
                else if (g_strcmp0 (ippGetName (attr), "ppd-name") == 0 && ippGetValueTag (attr) == IPP_TAG_NAME)
                    ppd_name = ippGetString (attr, 0, NULL);
                else if (g_strcmp0 (ippGetName (attr), "ppd-product") == 0 && ippGetValueTag (attr) == IPP_TAG_TEXT)
                    ppd_product = ippGetString (attr, 0, NULL);
                else if (g_strcmp0 (ippGetName (attr), "ppd-make") == 0 && ippGetValueTag (attr) == IPP_TAG_TEXT)
                    ppd_make = ippGetString (attr, 0, NULL);

                attr = ippNextAttribute (response);
            }

            /* Get manufacturer's name */
            if (ppd_device_id && ppd_device_id[0] != '\0') {
                mfg = get_tag_value (ppd_device_id, "mfg");
                if (!mfg)
                    mfg = get_tag_value (ppd_device_id, "manufacturer");
                mfg_normalized = normalize (mfg);
            }

            if (!mfg && ppd_make && ppd_make[0] != '\0') {
                mfg = g_strdup (ppd_make);
                mfg_normalized = normalize (ppd_make);
            }

            /* Get model */
            if (ppd_make_and_model && ppd_make_and_model[0] != '\0') {
                mdl = g_strdup (ppd_make_and_model);
            }

            if (!mdl && ppd_product && ppd_product[0] != '\0') {
                mdl = g_strdup (ppd_product);
            }

            if (!mdl && ppd_device_id && ppd_device_id[0] != '\0') {
                mdl = get_tag_value (ppd_device_id, "mdl");
                if (!mdl)
                    mdl = get_tag_value (ppd_device_id, "model");
            }

            if (ppd_name && ppd_name[0] != '\0' && mdl && mdl[0] != '\0' && mfg && mfg[0] != '\0') {
                manufacturer_display_name = g_hash_table_lookup (manufacturers_hash, mfg_normalized);
                if (!manufacturer_display_name) {
                    g_hash_table_insert (manufacturers_hash, g_strdup (mfg_normalized), g_strdup (mfg));
                } else {
                    g_free (mfg_normalized);
                    mfg_normalized = normalize (manufacturer_display_name);
                }

                item = g_new0 (PPDName, 1);
                item->ppd_name = g_strdup (ppd_name);
                item->ppd_display_name = g_strdup (mdl);
                item->ppd_match_level = -1;

                list = g_hash_table_lookup (ppds_hash, mfg_normalized);
                if (list) {
                    list = g_list_append (list, item);
                } else {
                    list = g_list_append (list, item);
                    g_hash_table_insert (ppds_hash, g_strdup (mfg_normalized), list);
                }
            }

            if (attr == NULL)
                break;
        }
    }

    if (response)
        ippDelete (response);

    if (ppds_hash && manufacturers_hash) {
        GHashTableIter iter;
        gpointer key;
        gpointer value;
        GList *ppd_item;
        GList *sort_list = NULL;
        GList *list_iter;
        gchar *name;

        data->result = g_new0 (PPDList, 1);
        data->result->num_of_manufacturers = g_hash_table_size (ppds_hash);
        data->result->manufacturers = g_new0 (PPDManufacturerItem *, data->result->num_of_manufacturers);

        g_hash_table_iter_init (&iter, ppds_hash);
        while (g_hash_table_iter_next (&iter, &key, &value)) {
            sort_list = g_list_append (sort_list, g_strdup (key));
        }

        /* Sort list of manufacturers */
        sort_list = g_list_sort (sort_list, (GCompareFunc) g_strcmp0);

        /*
         * Fill resulting list of lists (list of manufacturers where
         * each item contains list of PPD names)
         */
        i = 0;
        for (list_iter = sort_list; list_iter; list_iter = list_iter->next) {
            name = (gchar *) list_iter->data;
            value = g_hash_table_lookup (ppds_hash, name);

            data->result->manufacturers[i] = g_new0 (PPDManufacturerItem, 1);
            data->result->manufacturers[i]->manufacturer_name = g_strdup (name);
            data->result->manufacturers[i]->manufacturer_display_name =
                g_strdup (g_hash_table_lookup (manufacturers_hash, name));
            data->result->manufacturers[i]->num_of_ppds = g_list_length ((GList *) value);
            data->result->manufacturers[i]->ppds = g_new0 (PPDName *, data->result->manufacturers[i]->num_of_ppds);

            for (ppd_item = (GList *) value, j = 0; ppd_item; ppd_item = ppd_item->next, j++) {
                data->result->manufacturers[i]->ppds[j] = ppd_item->data;
            }

            g_list_free ((GList *) value);

            i++;
        }

        g_list_free_full (sort_list, g_free);
        g_hash_table_destroy (ppds_hash);
        g_hash_table_destroy (manufacturers_hash);
    }

    get_all_ppds_cb (data);

    return NULL;
}

/*
 * Get names of all installed PPDs sorted by manufacturers names.
 */
void
get_all_ppds_async (GCancellable *cancellable, GAPCallback callback, gpointer user_data)
{
    GAPData *data;
    g_autoptr(GThread) thread = NULL;
    g_autoptr(GError) error = NULL;

    data = gap_data_new (cancellable, callback, user_data);

    thread = g_thread_try_new ("get-all-ppds", get_all_ppds_func, data, &error);

    if (!thread) {
        g_warning ("%s", error->message);
        callback (NULL, user_data);

        gap_data_free (data);
    }
}

PPDList *
ppd_list_copy (PPDList *list)
{
    PPDList *result = NULL;
    gint i, j;

    if (list) {
        result = g_new0 (PPDList, 1);
        result->num_of_manufacturers = list->num_of_manufacturers;
        result->manufacturers = g_new0 (PPDManufacturerItem *, list->num_of_manufacturers);

        for (i = 0; i < result->num_of_manufacturers; i++) {
            result->manufacturers[i] = g_new0 (PPDManufacturerItem, 1);
            result->manufacturers[i]->num_of_ppds = list->manufacturers[i]->num_of_ppds;
            result->manufacturers[i]->ppds = g_new0 (PPDName *, result->manufacturers[i]->num_of_ppds);

            result->manufacturers[i]->manufacturer_display_name =
                g_strdup (list->manufacturers[i]->manufacturer_display_name);

            result->manufacturers[i]->manufacturer_name = g_strdup (list->manufacturers[i]->manufacturer_name);

            for (j = 0; j < result->manufacturers[i]->num_of_ppds; j++) {
                result->manufacturers[i]->ppds[j] = g_new0 (PPDName, 1);

                result->manufacturers[i]->ppds[j]->ppd_display_name =
                    g_strdup (list->manufacturers[i]->ppds[j]->ppd_display_name);

                result->manufacturers[i]->ppds[j]->ppd_name = g_strdup (list->manufacturers[i]->ppds[j]->ppd_name);

                result->manufacturers[i]->ppds[j]->ppd_match_level = list->manufacturers[i]->ppds[j]->ppd_match_level;
            }
        }
    }

    return result;
}

void
ppd_list_free (PPDList *list)
{
    gint i, j;

    if (list) {
        for (i = 0; i < list->num_of_manufacturers; i++) {
            for (j = 0; j < list->manufacturers[i]->num_of_ppds; j++) {
                g_free (list->manufacturers[i]->ppds[j]->ppd_name);
                g_free (list->manufacturers[i]->ppds[j]->ppd_display_name);
                g_free (list->manufacturers[i]->ppds[j]);
            }

            g_free (list->manufacturers[i]->manufacturer_name);
            g_free (list->manufacturers[i]->manufacturer_display_name);
            g_free (list->manufacturers[i]->ppds);
            g_free (list->manufacturers[i]);
        }

        g_free (list->manufacturers);
        g_free (list);
    }
}

gchar *
get_standard_manufacturers_name (const gchar *name)
{
    g_autofree gchar *normalized_name = NULL;
    gint i;

    if (name == NULL)
        return NULL;

    normalized_name = normalize (name);

    for (i = 0; i < G_N_ELEMENTS (manufacturers_names); i++) {
        if (g_strcmp0 (manufacturers_names[i].normalized_name, normalized_name) == 0) {
            return g_strdup (manufacturers_names[i].display_name);
        }
    }

    return NULL;
}

typedef struct {
    gchar *printer_name;
    gchar *host_name;
    gint port;
    gchar *result;
    PGPCallback callback;
    gpointer user_data;
    GMainContext *context;
} PGPData;

static PGPData *
pgp_data_new (const gchar *printer_name, const gchar *host_name, gint port, PGPCallback callback, gpointer user_data)
{
    PGPData *data;

    data = g_new0 (PGPData, 1);
    data->printer_name = g_strdup (printer_name);
    data->host_name = g_strdup (host_name);
    data->port = port;
    data->callback = callback;
    data->user_data = user_data;
    data->context = g_main_context_ref_thread_default ();

    return data;
}

static void
pgp_data_free (PGPData *data)
{
    g_free (data->printer_name);
    g_free (data->host_name);
    g_free (data->result);
    if (data->context)
        g_main_context_unref (data->context);
    g_free (data);
}

static gboolean
printer_get_ppd_idle_cb (gpointer user_data)
{
    PGPData *data = user_data;

    data->callback (data->result, data->user_data);

    return FALSE;
}

static void
printer_get_ppd_cb (gpointer user_data)
{
    PGPData *data = user_data;
    g_autoptr(GSource) idle_source = NULL;

    idle_source = g_idle_source_new ();
    g_source_set_callback (idle_source, printer_get_ppd_idle_cb, data, (GDestroyNotify) pgp_data_free);
    g_source_attach (idle_source, data->context);
}

static gpointer
printer_get_ppd_func (gpointer user_data)
{
    PGPData *data = user_data;

    if (data->host_name) {
        http_t *http;

#ifdef HAVE_CUPS_HTTPCONNECT2
        http =
            httpConnect2 (data->host_name, data->port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_IF_REQUESTED, 1, 30000, NULL);
#else
        http = httpConnect (data->host_name, data->port);
#endif
        if (http) {
            data->result = g_strdup (cupsGetPPD2 (http, data->printer_name));
            httpClose (http);
        }
    } else {
        data->result = g_strdup (cupsGetPPD (data->printer_name));
    }

    printer_get_ppd_cb (data);

    return NULL;
}

void
printer_get_ppd_async (const gchar *printer_name, const gchar *host_name, gint port, PGPCallback callback,
                       gpointer user_data)
{
    PGPData *data;
    g_autoptr(GThread) thread = NULL;
    g_autoptr(GError) error = NULL;

    data = pgp_data_new (printer_name, host_name, port, callback, user_data);

    thread = g_thread_try_new ("printer-get-ppd", printer_get_ppd_func, data, &error);

    if (!thread) {
        g_warning ("%s", error->message);
        callback (NULL, user_data);

        pgp_data_free (data);
    }
}

typedef struct {
    gchar *printer_name;
    cups_dest_t *result;
    GNDCallback callback;
    gpointer user_data;
    GMainContext *context;
} GNDData;

static GNDData *
gnd_data_new (const gchar *printer_name, GNDCallback callback, gpointer user_data)
{
    GNDData *data;

    data = g_new0 (GNDData, 1);
    data->printer_name = g_strdup (printer_name);
    data->callback = callback;
    data->user_data = user_data;
    data->context = g_main_context_ref_thread_default ();

    return data;
}

static void
gnd_data_free (GNDData *data)
{
    g_free (data->printer_name);
    if (data->context)
        g_main_context_unref (data->context);
    g_free (data);
}

static gboolean
get_named_dest_idle_cb (gpointer user_data)
{
    GNDData *data = user_data;

    data->callback (data->result, data->user_data);

    return FALSE;
}

static void
get_named_dest_cb (gpointer user_data)
{
    GNDData *data = user_data;
    g_autoptr(GSource) idle_source = NULL;

    idle_source = g_idle_source_new ();
    g_source_set_callback (idle_source, get_named_dest_idle_cb, data, (GDestroyNotify) gnd_data_free);
    g_source_attach (idle_source, data->context);
}

static gpointer
get_named_dest_func (gpointer user_data)
{
    GNDData *data = user_data;

    data->result = cupsGetNamedDest (CUPS_HTTP_DEFAULT, data->printer_name, NULL);

    get_named_dest_cb (data);

    return NULL;
}

void
get_named_dest_async (const gchar *printer_name, GNDCallback callback, gpointer user_data)
{
    GNDData *data;
    g_autoptr(GThread) thread = NULL;
    g_autoptr(GError) error = NULL;

    data = gnd_data_new (printer_name, callback, user_data);

    thread = g_thread_try_new ("get-named-dest", get_named_dest_func, data, &error);

    if (!thread) {
        g_warning ("%s", error->message);
        callback (NULL, user_data);

        gnd_data_free (data);
    }
}

typedef struct {
    GCancellable *cancellable;
    PAOCallback callback;
    gpointer user_data;
} PAOData;

static PAOData *
pao_data_new (GCancellable *cancellable, PAOCallback callback, gpointer user_data)
{
    PAOData *data;

    data = g_new0 (PAOData, 1);
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;
    return data;
}

static void
pao_data_free (PAOData *data)
{
    g_clear_object (&data->cancellable);
    g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (PAOData, pao_data_free)

static void
printer_add_option_async_dbus_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    g_autoptr(GVariant) output = NULL;
    gboolean success = FALSE;
    g_autoptr(PAOData) data = user_data;
    g_autoptr(GError) error = NULL;

    output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);

    if (output) {
        const gchar *ret_error;

        g_variant_get (output, "(&s)", &ret_error);
        if (ret_error[0] != '\0')
            g_warning ("cups-pk-helper: setting of an option failed: %s", ret_error);
        else
            success = TRUE;
    } else {
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("%s", error->message);
    }

    if (!g_cancellable_is_cancelled (data->cancellable))
        data->callback (success, data->user_data);
}

void
printer_add_option_async (const gchar *printer_name, const gchar *option_name, gchar **values, gboolean set_default,
                          GCancellable *cancellable, PAOCallback callback, gpointer user_data)
{
    GVariantBuilder array_builder;
    g_autoptr(GDBusConnection) bus = NULL;
    g_autoptr(GError) error = NULL;
    gint i;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        callback (FALSE, user_data);
        return;
    }

    g_variant_builder_init (&array_builder, G_VARIANT_TYPE ("as"));
    if (values) {
        for (i = 0; values[i]; i++)
            g_variant_builder_add (&array_builder, "s", values[i]);
    }

    g_dbus_connection_call (bus, MECHANISM_BUS, "/", MECHANISM_BUS,
                            set_default ? "PrinterAddOptionDefault" : "PrinterAddOption",
                            g_variant_new ("(ssas)", printer_name, option_name, &array_builder), G_VARIANT_TYPE ("(s)"),
                            G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, cancellable, printer_add_option_async_dbus_cb,
                            pao_data_new (cancellable, callback, user_data));
}

typedef struct {
    GList *backend_list;
    GCancellable *cancellable;
    GCDCallback callback;
    gpointer user_data;
} GCDData;

static GCDData *
gcd_data_new (GList *backend_list, GCancellable *cancellable, GCDCallback callback, gpointer user_data)
{
    GCDData *data;

    data = g_new0 (GCDData, 1);
    data->backend_list = backend_list;
    if (cancellable)
        data->cancellable = g_object_ref (cancellable);
    data->callback = callback;
    data->user_data = user_data;

    return data;
}

static void
gcd_data_free (GCDData *data)
{
    g_list_free_full (data->backend_list, g_free);
    g_clear_object (&data->cancellable);
    g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (GCDData, gcd_data_free)

static gint
get_suffix_index (const gchar *string)
{
    gchar *number;
    gchar *endptr;
    gint index = -1;

    number = g_strrstr (string, ":");
    if (number) {
        number++;
        index = g_ascii_strtoll (number, &endptr, 10);
        if (index == 0 && endptr == number)
            index = -1;
    }

    return index;
}

static void
get_cups_devices_async_dbus_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)

{
    g_autoptr(GPtrArray) devices = NULL;
    g_autoptr(GVariant) output = NULL;
    g_autoptr(GCDData) data = user_data;
    g_autoptr(GError) error = NULL;
    gint num_of_devices = 0;

    output = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error);

    if (output) {
        const gchar *ret_error;
        g_autoptr(GVariant) devices_variant = NULL;
        gboolean is_network_device;
        g_autoptr(GVariantIter) iter = NULL;
        const gchar *key, *value;
        gint index = -1, max_index = -1, i;

        g_variant_get (output, "(&s@a{ss})", &ret_error, &devices_variant);

        if (ret_error[0] != '\0') {
            g_warning ("cups-pk-helper: getting of CUPS devices failed: %s", ret_error);
        }

        g_variant_get (devices_variant, "a{ss}", &iter);
        while (g_variant_iter_next (iter, "{&s&s}", &key, &value)) {
            index = get_suffix_index (key);
            if (index > max_index)
                max_index = index;
        }

        if (max_index >= 0) {
            g_autoptr(GVariantIter) iter2 = NULL;

            num_of_devices = max_index + 1;
            devices = g_ptr_array_new_with_free_func (g_object_unref);
            for (i = 0; i < num_of_devices; i++)
                g_ptr_array_add (devices, pp_print_device_new ());

            g_variant_get (devices_variant, "a{ss}", &iter2);
            while (g_variant_iter_next (iter2, "{&s&s}", &key, &value)) {
                PpPrintDevice *device;

                index = get_suffix_index (key);
                if (index >= 0) {
                    device = g_ptr_array_index (devices, index);
                    if (g_str_has_prefix (key, "device-class")) {
                        is_network_device = g_strcmp0 (value, "network") == 0;
                        g_object_set (device, "is-network-device", is_network_device, NULL);
                    } else if (g_str_has_prefix (key, "device-id"))
                        g_object_set (device, "device-id", value, NULL);
                    else if (g_str_has_prefix (key, "device-info"))
                        g_object_set (device, "device-info", value, NULL);
                    else if (g_str_has_prefix (key, "device-make-and-model")) {
                        g_object_set (device, "device-make-and-model", value, "device-name", value, NULL);
                    } else if (g_str_has_prefix (key, "device-uri"))
                        g_object_set (device, "device-uri", value, NULL);
                    else if (g_str_has_prefix (key, "device-location"))
                        g_object_set (device, "device-location", value, NULL);

                    g_object_set (device, "acquisition-method", ACQUISITION_METHOD_DEFAULT_CUPS_SERVER, NULL);
                }
            }
        }
    } else {
        if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
            g_warning ("%s", error->message);

        data->callback (devices, TRUE, g_cancellable_is_cancelled (data->cancellable), data->user_data);
        return;
    }

    if (data->backend_list) {
        if (!g_cancellable_is_cancelled (data->cancellable)) {
            GVariantBuilder *include_scheme_builder = NULL;
            GVariantBuilder *exclude_scheme_builder = NULL;
            g_autofree gchar *backend_name = NULL;

            backend_name = data->backend_list->data;

            data->callback (devices, FALSE, FALSE, data->user_data);

            if (g_strcmp0 (backend_name, OTHER_BACKENDS) != 0) {
                include_scheme_builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
                g_variant_builder_add (include_scheme_builder, "s", backend_name);
            } else {
                exclude_scheme_builder = create_other_backends_array ();
            }

            data->backend_list = g_list_delete_link (data->backend_list, data->backend_list);

            g_dbus_connection_call (G_DBUS_CONNECTION (g_object_ref (source_object)), MECHANISM_BUS, "/", MECHANISM_BUS,
                                    "DevicesGet",
                                    g_variant_new ("(iiasas)", 0, 0, include_scheme_builder, exclude_scheme_builder),
                                    G_VARIANT_TYPE ("(sa{ss})"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT,
                                    data->cancellable, get_cups_devices_async_dbus_cb, data);
            g_steal_pointer (&data);

            if (include_scheme_builder)
                g_variant_builder_unref (include_scheme_builder);

            if (exclude_scheme_builder)
                g_variant_builder_unref (exclude_scheme_builder);

            return;
        } else {
            data->callback (devices, TRUE, TRUE, data->user_data);
        }
    } else {
        data->callback (devices, TRUE, g_cancellable_is_cancelled (data->cancellable), data->user_data);
    }
}

void
get_cups_devices_async (GCancellable *cancellable, GCDCallback callback, gpointer user_data)
{
    g_autoptr(GDBusConnection) bus = NULL;
    GVariantBuilder include_scheme_builder;
    GList *backend_list;
    g_autoptr(GError) error = NULL;
    g_autofree gchar *backend_name = NULL;

    bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
    if (!bus) {
        g_warning ("Failed to get system bus: %s", error->message);
        callback (NULL, TRUE, FALSE, user_data);
        return;
    }

    backend_list = create_backends_list ();

    backend_name = backend_list->data;

    g_variant_builder_init (&include_scheme_builder, G_VARIANT_TYPE ("as"));
    g_variant_builder_add (&include_scheme_builder, "s", backend_name);

    backend_list = g_list_delete_link (backend_list, backend_list);

    g_dbus_connection_call (bus, MECHANISM_BUS, "/", MECHANISM_BUS, "DevicesGet",
                            g_variant_new ("(iiasas)", 0, 0, &include_scheme_builder, NULL),
                            G_VARIANT_TYPE ("(sa{ss})"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT, cancellable,
                            get_cups_devices_async_dbus_cb,
                            gcd_data_new (backend_list, cancellable, callback, user_data));
}

gchar *
guess_device_hostname (PpPrintDevice *device)
{
    http_uri_status_t status;
    char scheme[HTTP_MAX_URI];
    char username[HTTP_MAX_URI];
    char hostname[HTTP_MAX_URI];
    char resource[HTTP_MAX_URI];
    int port;
    gchar *result = NULL;
    gchar *hostname_begin;
    gchar *hostname_end = NULL;

    if (device != NULL && pp_print_device_get_device_uri (device) != NULL) {
        if (g_str_has_prefix (pp_print_device_get_device_uri (device), "socket")
            || g_str_has_prefix (pp_print_device_get_device_uri (device), "lpd")
            || g_str_has_prefix (pp_print_device_get_device_uri (device), "ipp")
            || g_str_has_prefix (pp_print_device_get_device_uri (device), "smb")) {
            status =
                httpSeparateURI (HTTP_URI_CODING_ALL, pp_print_device_get_device_uri (device), scheme, HTTP_MAX_URI,
                                 username, HTTP_MAX_URI, hostname, HTTP_MAX_URI, &port, resource, HTTP_MAX_URI);

            if (status >= HTTP_URI_STATUS_OK && hostname[0] != '\0')
                result = g_strdup (hostname);
        } else if ((g_str_has_prefix (pp_print_device_get_device_uri (device), "dnssd")
                    || g_str_has_prefix (pp_print_device_get_device_uri (device), "mdns"))
                   && pp_print_device_get_device_info (device) != NULL) {
            /*
             * CUPS browses its printers as
             * "PrinterName @ ComputerName" or "PrinterInfo @ ComputerName"
             * through DNS-SD.
             */
            hostname_begin = g_strrstr (pp_print_device_get_device_info (device), " @ ");
            if (hostname_begin != NULL)
                result = g_strdup (hostname_begin + 3);
        } else if (g_str_has_prefix (pp_print_device_get_device_uri (device), "hp:/net/")
                   || g_str_has_prefix (pp_print_device_get_device_uri (device), "hpfax:/net/")) {
            /*
             * HPLIP printers have URI of form hp:/net/%s?ip=%s&port=%d
             * or hp:/net/%s?ip=%s.
             */
            hostname_begin = g_strrstr (pp_print_device_get_device_uri (device), "ip=");
            if (hostname_begin != NULL) {
                hostname_begin += 3;
                hostname_end = strstr (hostname_begin, "&");
            }

            if (hostname_end != NULL)
                result = g_strndup (hostname_begin, hostname_end - hostname_begin);
            else
                result = g_strdup (hostname_begin);
        }
    }

    return result;
}

gchar *
canonicalize_device_name (GList *device_names, GPtrArray *local_cups_devices, cups_dest_t *dests, gint num_of_dests,
                          PpPrintDevice *device)
{
    PpPrintDevice *item;
    gboolean already_present;
    GList *iter;
    gsize len;
    g_autofree gchar *name = NULL;
    gchar *occurrence;
    gint name_index, j;
    static const char *const residues[] = { "-foomatic",   "-hpijs",  "-hpcups",        "-cups",
                                            "-gutenprint", "-series", "-label-printer", "-dot-matrix",
                                            "-ps3",        "-ps2",    "-br-script",     "-kpdl",
                                            "-pcl3",       "-pcl",    "-zxs",           "-pxl" };

    if (pp_print_device_get_device_id (device) != NULL) {
        name = get_tag_value (pp_print_device_get_device_id (device), "mdl");
        if (name == NULL)
            name = get_tag_value (pp_print_device_get_device_id (device), "model");
    }

    if (name == NULL && pp_print_device_get_device_make_and_model (device) != NULL
        && pp_print_device_get_device_make_and_model (device)[0] != '\0') {
        name = g_strdup (pp_print_device_get_device_make_and_model (device));
    }

    if (name == NULL && pp_print_device_get_device_original_name (device) != NULL
        && pp_print_device_get_device_original_name (device)[0] != '\0') {
        name = g_strdup (pp_print_device_get_device_original_name (device));
    }

    if (name == NULL && pp_print_device_get_device_info (device) != NULL
        && pp_print_device_get_device_info (device)[0] != '\0') {
        name = g_strdup (pp_print_device_get_device_info (device));
    }

    if (name == NULL)
        return NULL;

    g_strstrip (name);
    g_strcanon (name, ALLOWED_CHARACTERS, '-');

    /* Remove common strings found in driver names */
    for (j = 0; j < G_N_ELEMENTS (residues); j++) {
        g_autofree gchar *lower_name = g_ascii_strdown (name, -1);

        occurrence = g_strrstr (lower_name, residues[j]);
        if (occurrence != NULL) {
            occurrence[0] = '\0';
            name[strlen (lower_name)] = '\0';
        }
    }

    /* Remove trailing "-" */
    len = strlen (name);
    while (len-- && name[len] == '-')
        name[len] = '\0';

    /* Merge "--" to "-" */
    occurrence = g_strrstr (name, "--");
    while (occurrence != NULL) {
        shift_string_left (occurrence);
        occurrence = g_strrstr (name, "--");
    }

    /* Remove leading "-" */
    if (name[0] == '-')
        shift_string_left (name);

    name_index = 2;
    already_present = FALSE;
    while (TRUE) {
        g_autofree gchar *new_name = NULL;

        if (already_present) {
            new_name = g_strdup_printf ("%s-%d", name, name_index);
            name_index++;
        } else {
            new_name = g_strdup (name);
        }

        already_present = FALSE;
        for (j = 0; j < num_of_dests; j++)
            if (g_strcmp0 (dests[j].name, new_name) == 0)
                already_present = TRUE;

        for (iter = device_names; iter; iter = iter->next) {
            gchar *device_original_name = iter->data;
            if (g_strcmp0 (device_original_name, new_name) == 0)
                already_present = TRUE;
        }

        for (guint i = 0; i < local_cups_devices->len; i++) {
            item = g_ptr_array_index (local_cups_devices, i);
            if (g_strcmp0 (pp_print_device_get_device_original_name (item), new_name) == 0)
                already_present = TRUE;
        }

        if (!already_present)
            return g_steal_pointer (&new_name);
    }
}

void
shift_string_left (gchar *str)
{
    gchar *next;

    if (str != NULL && str[0] != '\0') {
        next = g_utf8_find_next_char (str, NULL);
        memmove (str, next, strlen (next) + 1);
    }
}

gboolean
printer_name_is_valid (const gchar *str)
{
    const gchar *invalid_chars = " \t#/";
    return strlen (str) == strcspn (str, invalid_chars);
}
