/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 * Copyright (C) 2009 Novell, Inc.
 * Author: Tambet Ingo (tambet@gmail.com).
 *
 * Copyright (C) 2009 - 2010 Red Hat, Inc.
 */

#include "config.h"

#include <string.h>
#include <errno.h>
#include <stdlib.h>

#include <glib/gi18n.h>

#include "shell-mobile-providers.h"

#ifndef MOBILE_BROADBAND_PROVIDER_INFO
#define MOBILE_BROADBAND_PROVIDER_INFO DATADIR "/mobile-broadband-provider-info/serviceproviders.xml"
#endif

#define ISO_3166_COUNTRY_CODES DATADIR "/zoneinfo/iso3166.tab"



static GHashTable *
read_country_codes (void)
{
    GHashTable *table;
    GIOChannel *channel;
    GString *buffer;
    GError *error = NULL;
    GIOStatus status;

    channel = g_io_channel_new_file (ISO_3166_COUNTRY_CODES, "r", &error);
    if (!channel) {
        if (error) {
            g_warning ("Could not read " ISO_3166_COUNTRY_CODES ": %s", error->message);
            g_error_free (error);
        } else
            g_warning ("Could not read " ISO_3166_COUNTRY_CODES ": Unknown error");

        return NULL;
    }

    table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    buffer = g_string_sized_new (32);

    status = G_IO_STATUS_NORMAL;
    while (status == G_IO_STATUS_NORMAL) {
        status = g_io_channel_read_line_string (channel, buffer, NULL, &error);

        switch (status) {
        case G_IO_STATUS_NORMAL:
            if (buffer->str[0] != '#') {
                char **pieces;

                pieces = g_strsplit (buffer->str, "\t", 2);

                /* Hack for rh#556292; iso3166.tab is just wrong */
                pieces[1] = pieces[1] ? g_strchomp (pieces[1]) : NULL;
                if (pieces[1] && !strcmp (pieces[1], "Britain (UK)")) {
                    g_free (pieces[1]);
                    pieces[1] = g_strdup (_("United Kingdom"));
                }

                g_hash_table_insert (table, pieces[0], pieces[1]);
                g_free (pieces);
            }

            g_string_truncate (buffer, 0);
            break;
        case G_IO_STATUS_EOF:
            break;
        case G_IO_STATUS_ERROR:
            g_warning ("Error while reading: %s", error->message);
            g_error_free (error);
            break;
        case G_IO_STATUS_AGAIN:
            /* FIXME: Try again a few times, but really, it never happes, right? */
            break;
        }
    }

    g_string_free (buffer, TRUE);
    g_io_channel_unref (channel);

    return table;
}

/* XML Parser */

typedef enum {
    PARSER_TOPLEVEL = 0,
    PARSER_COUNTRY,
    PARSER_PROVIDER,
    PARSER_METHOD_GSM,
    PARSER_METHOD_GSM_APN,
    PARSER_METHOD_CDMA,
    PARSER_ERROR
} MobileContextState;

typedef struct {
    GHashTable *country_codes;
    GHashTable *table;

    char *current_country;
    GSList *current_providers;
    ShellMobileProvider *current_provider;
    ShellMobileAccessMethod *current_method;

    char *text_buffer;
    MobileContextState state;
} MobileParser;

static ShellGsmMccMnc *
mcc_mnc_new (const char *mcc, const char *mnc)
{
    ShellGsmMccMnc *m;

    m = g_slice_new (ShellGsmMccMnc);
    m->mcc = g_strstrip (g_strdup (mcc));
    m->mnc = g_strstrip (g_strdup (mnc));
    return m;
}

/* added in porting */
static ShellGsmMccMnc *
mcc_mnc_copy (const ShellGsmMccMnc *other) {
    ShellGsmMccMnc *ret;

    ret = g_slice_new (ShellGsmMccMnc);
    ret->mcc = g_strdup (other->mcc);
    ret->mnc = g_strdup (other->mnc);
    return ret;
}

static void
mcc_mnc_free (ShellGsmMccMnc *m)
{
    g_return_if_fail (m != NULL);
    g_free (m->mcc);
    g_free (m->mnc);
    g_slice_free (ShellGsmMccMnc, m);
}

/* added in porting */
G_DEFINE_BOXED_TYPE (ShellGsmMccMnc, shell_gsm_mcc_mnc, mcc_mnc_copy, mcc_mnc_free)

static ShellMobileAccessMethod *
access_method_new (void)
{
    ShellMobileAccessMethod *method;

    method = g_slice_new0 (ShellMobileAccessMethod);
    method->refs = 1;
    method->lcl_names = g_hash_table_new_full (g_str_hash, g_str_equal,
                                               (GDestroyNotify) g_free,
                                               (GDestroyNotify) g_free);

    return method;
}

ShellMobileAccessMethod *
shell_mobile_access_method_ref (ShellMobileAccessMethod *method)
{
    g_return_val_if_fail (method != NULL, NULL);
    g_return_val_if_fail (method->refs > 0, NULL);

    method->refs++;

    return method;
}

void
shell_mobile_access_method_unref (ShellMobileAccessMethod *method)
{
    g_return_if_fail (method != NULL);
    g_return_if_fail (method->refs > 0);

    if (--method->refs == 0) {
        g_free (method->name);
        g_hash_table_destroy (method->lcl_names);
        g_free (method->username);
        g_free (method->password);
        g_free (method->gateway);
        g_free (method->gsm_apn);
        g_slist_foreach (method->dns, (GFunc) g_free, NULL);
        g_slist_free (method->dns);

        g_slice_free (ShellMobileAccessMethod, method);
    }
}

GType
shell_mobile_access_method_get_type (void)
{
    static GType type = 0;

    if (G_UNLIKELY (type == 0)) {
        type = g_boxed_type_register_static ("ShellMobileAccessMethod",
                                             (GBoxedCopyFunc) shell_mobile_access_method_ref,
                                             (GBoxedFreeFunc) shell_mobile_access_method_unref);
    }
    return type;
}


static ShellMobileProvider *
provider_new (void)
{
    ShellMobileProvider *provider;

    provider = g_slice_new0 (ShellMobileProvider);
    provider->refs = 1;
    provider->lcl_names = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                 (GDestroyNotify) g_free,
                                                 (GDestroyNotify) g_free);

    return provider;
}

ShellMobileProvider *
shell_mobile_provider_ref (ShellMobileProvider *provider)
{
    provider->refs++;

    return provider;
}

void
shell_mobile_provider_unref (ShellMobileProvider *provider)
{
    if (--provider->refs == 0) {
        g_free (provider->name);
        g_hash_table_destroy (provider->lcl_names);

        g_slist_foreach (provider->methods, (GFunc) shell_mobile_access_method_unref, NULL);
        g_slist_free (provider->methods);

        g_slist_foreach (provider->gsm_mcc_mnc, (GFunc) mcc_mnc_free, NULL);
        g_slist_free (provider->gsm_mcc_mnc);

        g_slist_free (provider->cdma_sid);

        g_slice_free (ShellMobileProvider, provider);
    }
}

GType
shell_mobile_provider_get_type (void)
{
    static GType type = 0;

    if (G_UNLIKELY (type == 0)) {
        type = g_boxed_type_register_static ("ShellMobileProvider",
                                             (GBoxedCopyFunc) shell_mobile_provider_ref,
                                             (GBoxedFreeFunc) shell_mobile_provider_unref);
    }
    return type;
}

static void
provider_list_free (gpointer data)
{
    GSList *list = (GSList *) data;

    while (list) {
        shell_mobile_provider_unref ((ShellMobileProvider *) list->data);
        list = g_slist_delete_link (list, list);
    }
}

static void
parser_toplevel_start (MobileParser *parser,
                       const char *name,
                       const char **attribute_names,
                       const char **attribute_values)
{
    int i;

    if (!strcmp (name, "serviceproviders")) {
        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (!strcmp (attribute_names[i], "format")) {
                if (strcmp (attribute_values[i], "2.0")) {
                    g_warning ("%s: mobile broadband provider database format '%s'"
                               " not supported.", __func__, attribute_values[i]);
                    parser->state = PARSER_ERROR;
                    break;
                }
            }
        }
    } else if (!strcmp (name, "country")) {
        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (!strcmp (attribute_names[i], "code")) {
                char *country_code;
                char *country;

                country_code = g_ascii_strup (attribute_values[i], -1);
                country = g_hash_table_lookup (parser->country_codes, country_code);
                if (country) {
                    parser->current_country = g_strdup (country);
                    g_free (country_code);
                } else
                    parser->current_country = country_code;

                parser->state = PARSER_COUNTRY;
                break;
            }
        }
    }
}

static void
parser_country_start (MobileParser *parser,
                      const char *name,
                      const char **attribute_names,
                      const char **attribute_values)
{
    if (!strcmp (name, "provider")) {
        parser->state = PARSER_PROVIDER;
        parser->current_provider = provider_new ();
    }
}

static void
parser_provider_start (MobileParser *parser,
                       const char *name,
                       const char **attribute_names,
                       const char **attribute_values)
{
    if (!strcmp (name, "gsm"))
        parser->state = PARSER_METHOD_GSM;
    else if (!strcmp (name, "cdma")) {
        parser->state = PARSER_METHOD_CDMA;
        parser->current_method = access_method_new ();
    }
}

static void
parser_gsm_start (MobileParser *parser,
                  const char *name,
                  const char **attribute_names,
                  const char **attribute_values)
{
    if (!strcmp (name, "network-id")) {
        const char *mcc = NULL, *mnc = NULL;
        int i;

        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (!strcmp (attribute_names[i], "mcc"))
                mcc = attribute_values[i];
            else if (!strcmp (attribute_names[i], "mnc"))
                mnc = attribute_values[i];

            if (mcc && strlen (mcc) && mnc && strlen (mnc)) {
                parser->current_provider->gsm_mcc_mnc = g_slist_prepend (parser->current_provider->gsm_mcc_mnc,
                                                                         mcc_mnc_new (mcc, mnc));
                break;
            }
        }
    } else if (!strcmp (name, "apn")) {
        int i;

        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (!strcmp (attribute_names[i], "value")) {

                parser->state = PARSER_METHOD_GSM_APN;
                parser->current_method = access_method_new ();
                parser->current_method->gsm_apn = g_strstrip (g_strdup (attribute_values[i]));
                break;
            }
        }
    }
}

static void
parser_cdma_start (MobileParser *parser,
                   const char *name,
                   const char **attribute_names,
                   const char **attribute_values)
{
    if (!strcmp (name, "sid")) {
        int i;

        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (!strcmp (attribute_names[i], "value")) {
                unsigned long tmp;

                errno = 0;
                tmp = strtoul (attribute_values[i], NULL, 10);
                if (errno == 0 && tmp > 0)
                    parser->current_provider->cdma_sid = g_slist_prepend (parser->current_provider->cdma_sid,
                                                                          GUINT_TO_POINTER ((guint32) tmp));
                break;
            }
        }
    }
}

static void
mobile_parser_start_element (GMarkupParseContext *context,
                             const gchar *element_name,
                             const gchar **attribute_names,
                             const gchar **attribute_values,
                             gpointer data,
                             GError **error)
{
    MobileParser *parser = (MobileParser *) data;

    if (parser->text_buffer) {
        g_free (parser->text_buffer);
        parser->text_buffer = NULL;
    }

    switch (parser->state) {
    case PARSER_TOPLEVEL:
        parser_toplevel_start (parser, element_name, attribute_names, attribute_values);
        break;
    case PARSER_COUNTRY:
        parser_country_start (parser, element_name, attribute_names, attribute_values);
        break;
    case PARSER_PROVIDER:
        parser_provider_start (parser, element_name, attribute_names, attribute_values);
        break;
    case PARSER_METHOD_GSM:
        parser_gsm_start (parser, element_name, attribute_names, attribute_values);
        break;
    case PARSER_METHOD_CDMA:
        parser_cdma_start (parser, element_name, attribute_names, attribute_values);
        break;
    default:
        break;
    }
}

static void
parser_country_end (MobileParser *parser,
                    const char *name)
{
    if (!strcmp (name, "country")) {
        g_hash_table_insert (parser->table, parser->current_country, parser->current_providers);
        parser->current_country = NULL;
        parser->current_providers = NULL;
        parser->text_buffer = NULL;
        parser->state = PARSER_TOPLEVEL;
    }
}

static void
parser_provider_end (MobileParser *parser,
                     const char *name)
{
    if (!strcmp (name, "name")) {
        if (!parser->current_provider->name) {
            /* Use the first one. */
            parser->current_provider->name = parser->text_buffer;
            parser->text_buffer = NULL;
        }
    } else if (!strcmp (name, "provider")) {
        parser->current_provider->methods = g_slist_reverse (parser->current_provider->methods);

        parser->current_provider->gsm_mcc_mnc = g_slist_reverse (parser->current_provider->gsm_mcc_mnc);
        parser->current_provider->cdma_sid = g_slist_reverse (parser->current_provider->cdma_sid);

        parser->current_providers = g_slist_prepend (parser->current_providers, parser->current_provider);
        parser->current_provider = NULL;
        parser->text_buffer = NULL;
        parser->state = PARSER_COUNTRY;
    }
}

static void
parser_gsm_end (MobileParser *parser,
                 const char *name)
{
    if (!strcmp (name, "gsm")) {
        parser->text_buffer = NULL;
        parser->state = PARSER_PROVIDER;
    }
}

static void
parser_gsm_apn_end (MobileParser *parser,
                    const char *name)
{
    if (!strcmp (name, "name")) {
        if (!parser->current_method->name) {
            /* Use the first one. */
            parser->current_method->name = parser->text_buffer;
            parser->text_buffer = NULL;
        }
    } else if (!strcmp (name, "username")) {
        parser->current_method->username = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "password")) {
        parser->current_method->password = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "dns")) {
        parser->current_method->dns = g_slist_prepend (parser->current_method->dns, parser->text_buffer);
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "gateway")) {
        parser->current_method->gateway = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "apn")) {
        parser->current_method->type = SHELL_MOBILE_ACCESS_METHOD_TYPE_GSM;
        parser->current_method->dns = g_slist_reverse (parser->current_method->dns);

        if (!parser->current_method->name)
            parser->current_method->name = g_strdup (_("Default"));

        parser->current_provider->methods = g_slist_prepend (parser->current_provider->methods,
                                                             parser->current_method);
        parser->current_method = NULL;
        parser->text_buffer = NULL;
        parser->state = PARSER_METHOD_GSM;
    }
}

static void
parser_cdma_end (MobileParser *parser,
                 const char *name)
{
    if (!strcmp (name, "username")) {
        parser->current_method->username = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "password")) {
        parser->current_method->password = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "dns")) {
        parser->current_method->dns = g_slist_prepend (parser->current_method->dns, parser->text_buffer);
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "gateway")) {
        parser->current_method->gateway = parser->text_buffer;
        parser->text_buffer = NULL;
    } else if (!strcmp (name, "cdma")) {
        parser->current_method->type = SHELL_MOBILE_ACCESS_METHOD_TYPE_CDMA;
        parser->current_method->dns = g_slist_reverse (parser->current_method->dns);

        if (!parser->current_method->name)
            parser->current_method->name = g_strdup (parser->current_provider->name);

        parser->current_provider->methods = g_slist_prepend (parser->current_provider->methods,
                                                             parser->current_method);
        parser->current_method = NULL;
        parser->text_buffer = NULL;
        parser->state = PARSER_PROVIDER;
    }
}

static void
mobile_parser_end_element (GMarkupParseContext *context,
                           const gchar *element_name,
                           gpointer data,
                           GError **error)
{
    MobileParser *parser = (MobileParser *) data;

    switch (parser->state) {
    case PARSER_COUNTRY:
        parser_country_end (parser, element_name);
        break;
    case PARSER_PROVIDER:
        parser_provider_end (parser, element_name);
        break;
    case PARSER_METHOD_GSM:
        parser_gsm_end (parser, element_name);
        break;
    case PARSER_METHOD_GSM_APN:
        parser_gsm_apn_end (parser, element_name);
        break;
    case PARSER_METHOD_CDMA:
        parser_cdma_end (parser, element_name);
        break;
    default:
        break;
    }
}

static void
mobile_parser_characters (GMarkupParseContext *context,
                          const gchar *text,
                          gsize text_len,
                          gpointer data,
                          GError **error)
{
    MobileParser *parser = (MobileParser *) data;

    g_free (parser->text_buffer);
    parser->text_buffer = g_strdup (text);
}

static const GMarkupParser mobile_parser = {
    mobile_parser_start_element,
    mobile_parser_end_element,
    mobile_parser_characters,
    NULL, /* passthrough */
    NULL /* error */
};

/**
 * shell_mobile_providers_parse:
 * @out_ccs: (out) (allow-none): (element-type utf8 utf8): a #GHashTable containing
 *   country codes
 *
 * Returns: (element-type utf8 GList) (transfer container): a
 *   hash table where keys are country names #gchar, values are a #GSList
 *   of #ShellMobileProvider. Everything is destroyed with g_hash_table_destroy().
*/
GHashTable *
shell_mobile_providers_parse (GHashTable **out_ccs)
{
    GMarkupParseContext *ctx;
    GIOChannel *channel;
    MobileParser parser;
    GError *error = NULL;
    char buffer[4096];
    GIOStatus status;
    gsize len = 0;

    memset (&parser, 0, sizeof (MobileParser));

    parser.country_codes = read_country_codes ();
    if (!parser.country_codes)
        goto out;

    channel = g_io_channel_new_file (MOBILE_BROADBAND_PROVIDER_INFO, "r", &error);
    if (!channel) {
        if (error) {
            g_warning ("Could not read " MOBILE_BROADBAND_PROVIDER_INFO ": %s", error->message);
            g_error_free (error);
        } else
            g_warning ("Could not read " MOBILE_BROADBAND_PROVIDER_INFO ": Unknown error");

        goto out;
    }

    parser.table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, provider_list_free);
    parser.state = PARSER_TOPLEVEL;

    ctx = g_markup_parse_context_new (&mobile_parser, 0, &parser, NULL);

    status = G_IO_STATUS_NORMAL;
    while (status == G_IO_STATUS_NORMAL) {
        status = g_io_channel_read_chars (channel, buffer, sizeof (buffer), &len, &error);

        switch (status) {
        case G_IO_STATUS_NORMAL:
            if (!g_markup_parse_context_parse (ctx, buffer, len, &error)) {
                status = G_IO_STATUS_ERROR;
                g_warning ("Error while parsing XML: %s", error->message);
                g_error_free (error);;
            }
            break;
        case G_IO_STATUS_EOF:
            break;
        case G_IO_STATUS_ERROR:
            g_warning ("Error while reading: %s", error->message);
            g_error_free (error);
            break;
        case G_IO_STATUS_AGAIN:
            /* FIXME: Try again a few times, but really, it never happes, right? */
            break;
        }
    }

    g_io_channel_unref (channel);
    g_markup_parse_context_free (ctx);

    if (parser.current_provider) {
        g_warning ("pending current provider");
        shell_mobile_provider_unref (parser.current_provider);
    }

    if (parser.current_providers) {
        g_warning ("pending current providers");
        provider_list_free (parser.current_providers);
    }

    g_free (parser.current_country);
    g_free (parser.text_buffer);

 out:
    if (parser.country_codes) {
        if (out_ccs)
            *out_ccs = parser.country_codes;
        else
            g_hash_table_destroy (parser.country_codes);
    }

    return parser.table;
}

static void
dump_generic (ShellMobileAccessMethod *method)
{
    GSList *iter;
    GString *dns;

    g_print ("        username: %s\n", method->username ? method->username : "");
    g_print ("        password: %s\n", method->password ? method->password : "");

    dns = g_string_new (NULL);
    for (iter = method->dns; iter; iter = g_slist_next (iter))
        g_string_append_printf (dns, "%s%s", dns->len ? ", " : "", (char *) iter->data);
    g_print ("        dns     : %s\n", dns->str);
    g_string_free (dns, TRUE);

    g_print ("        gateway : %s\n", method->gateway ? method->gateway : "");
}

static void
dump_cdma (ShellMobileAccessMethod *method)
{
    g_print ("     CDMA: %s\n", method->name);

    dump_generic (method);
}

static void
dump_gsm (ShellMobileAccessMethod *method)
{
    g_print ("     APN: %s (%s)\n", method->name, method->gsm_apn);

    dump_generic (method);
}

static void
dump_country (gpointer key, gpointer value, gpointer user_data)
{
    GSList *citer, *miter;

    for (citer = value; citer; citer = g_slist_next (citer)) {
        ShellMobileProvider *provider = citer->data;

        g_print ("Provider: %s (%s)\n", provider->name, (const char *) key);
        for (miter = provider->methods; miter; miter = g_slist_next (miter)) {
            ShellMobileAccessMethod *method = miter->data;
            GSList *liter;


            for (liter = provider->gsm_mcc_mnc; liter; liter = g_slist_next (liter)) {
                ShellGsmMccMnc *m = liter->data;
                g_print ("        MCC/MNC: %s-%s\n", m->mcc, m->mnc);
            }

            for (liter = provider->cdma_sid; liter; liter = g_slist_next (liter))
                g_print ("        SID: %d\n", GPOINTER_TO_UINT (liter->data));

            switch (method->type) {
            case SHELL_MOBILE_ACCESS_METHOD_TYPE_CDMA:
                dump_cdma (method);
                break;
            case SHELL_MOBILE_ACCESS_METHOD_TYPE_GSM:
                dump_gsm (method);
                break;
            default:
                break;
            }
            g_print ("\n");
        }
    }
}

void
shell_mobile_providers_dump (GHashTable *providers)
{
    g_return_if_fail (providers != NULL);
    g_hash_table_foreach (providers, dump_country, NULL);
}

/* All the following don't exist in nm-applet, because C doesn't need
   those. They're only needed for the introspection annotations
*/

/**
 * shell_mobile_provider_get_gsm_mcc_mnc:
 * @provider: a #ShellMobileProvider
 *
 * Returns: (element-type Shell.GsmMccMnc) (transfer none): the
 *   list of #ShellGsmMccMnc this provider exposes
 */
GSList *
shell_mobile_provider_get_gsm_mcc_mnc (ShellMobileProvider *provider)
{
    return provider->gsm_mcc_mnc;
}

/**
 * shell_mobile_provider_get_cdma_sid:
 * @provider: a #ShellMobileProvider
 *
 * Returns: (element-type guint32) (transfer none): the
 *   list of CDMA sids this provider exposes
 */
GSList *
shell_mobile_provider_get_cdma_sid (ShellMobileProvider *provider)
{
    return provider->cdma_sid;
}
