/*
 * Copyright (C) 2012 Canonical, Ltd.
 *
 * Authors:
 *  Renato Araujo Oliveira Filho <renato@canonical.com>
 *  Ricardo Mendoza <ricmm@canonical.com>
 *
 * 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; version 3.
 *
 * 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 "account-proxy.h"

#include <glib.h>
#include <gio/gio.h>

#define UBUNTU_ANDROID_ACCOUNTS_UPDATED_SIGNAL  "AccountsUpdated"
#define UBUNTU_ANDROID_ACCOUNT_CHANGED_SIGNAL   "EmailAccountUpdated"
#define UBUNTU_ANDROID_ACCOUNT_CREATED_SIGNAL   "EmailAccountsAdded"
#define UBUNTU_ANDROID_ACCOUNT_REMOVED_SIGNAL   "EmailAccountsDeleted"
#define UBUNTU_ANDROID_SERVICE_NAME             "com.canonical.Android"
#define UBUNTU_ANDROID_OBJECT_PATH              "/com/canonical/android/accounts/Accounts"
#define UBUNTU_ANDROID_INTERFACE_NAME           "com.canonical.android.accounts.Accounts"

G_DEFINE_TYPE (AndroidAccountManagerProxy, android_account_manager_proxy, G_TYPE_OBJECT)

enum
{
    ANDROID_ACCOUNT_TYPE,
    ANDROID_ACCOUNT_DISPLAY_NAME,
    ANDROID_ACCOUNT_EMAIL_ADDR,
    ANDROID_ACCOUNT_SENDER_NAME = 9,
    ANDROID_ACCOUNT_SIGNATURE = 13
};

enum
{
    ANDROID_ACCOUNT_HOST_PROTOCOL = 1,
    ANDROID_ACCOUNT_HOST_ADDRESS,
    ANDROID_ACCOUNT_HOST_PORT,
    ANDROID_ACCOUNT_HOST_FLAGS,
    ANDROID_ACCOUNT_HOST_LOGIN,
    ANDROID_ACCOUNT_HOST_DOMAIN
};

enum
{
    ACCOUNT_UPDATED,
    ACCOUNT_CREATED,
    ACCOUNT_REMOVED,
    LAST_SIGNAL
};

static gint compare_fields[NUM_FIELDS] = { ACCOUNT_SENDER_NAME,
                                           ACCOUNT_INCOMING_HOST_LOGIN,
                                           ACCOUNT_INCOMING_HOST_PROTOCOL,
                                           ACCOUNT_INCOMING_HOST_PORT,
                                           ACCOUNT_INCOMING_HOST_ADDRESS,
                                           ACCOUNT_OUTGOING_HOST_LOGIN,
                                           ACCOUNT_OUTGOING_HOST_PORT,
                                           ACCOUNT_OUTGOING_HOST_ADDRESS };


static guint proxy_signals[LAST_SIGNAL] = { 0 };
const gchar * null_string = "ufa-NULL";
const gint32 null_integer = 2147483647;

struct _AndroidAccountManagerProxyPrivate {
    GList *registered_accounts;
    GList *server_accounts;
    GDBusProxy *proxy;
    gboolean first_sync;
};


AndroidAccountManagerProxy*
android_account_manager_proxy_new (void)
{
    return g_object_new (TYPE_ANDROID_ACCOUNT_MANAGER_PROXY, NULL);
}

static void
android_account_manager_proxy_dispose (GObject *object)
{
    AndroidAccountManagerProxy *self = ANDROID_ACCOUNT_MANAGER_PROXY (object);
    AndroidAccountManagerProxyPrivate *priv = self->priv;

    if (priv) {
        g_list_free_full (priv->registered_accounts, (GDestroyNotify) account_info_destroy);
        g_list_free_full (priv->server_accounts, (GDestroyNotify) account_info_destroy);
    }

    G_OBJECT_CLASS (android_account_manager_proxy_parent_class)->dispose (object);
}

static void
android_account_manager_proxy_finalize (GObject *object)
{
    G_OBJECT_CLASS (android_account_manager_proxy_parent_class)->finalize (object);
}

static void
android_account_manager_proxy_class_init (AndroidAccountManagerProxyClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    g_type_class_add_private (klass, sizeof (AndroidAccountManagerProxyPrivate));

    object_class->dispose   = android_account_manager_proxy_dispose;
    object_class->finalize  = android_account_manager_proxy_finalize;

    proxy_signals[ACCOUNT_UPDATED] = g_signal_new ("account-updated",
                                                   G_OBJECT_CLASS_TYPE (klass),
                                                   G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                                   G_STRUCT_OFFSET (AndroidAccountManagerProxyClass, account_updated),
                                                   NULL, NULL, NULL,
                                                   G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_ULONG);

    proxy_signals[ACCOUNT_CREATED] = g_signal_new ("account-created",
                                                   G_OBJECT_CLASS_TYPE (klass),
                                                   G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                                   G_STRUCT_OFFSET (AndroidAccountManagerProxyClass, account_created),
                                                   NULL, NULL,
                                                   g_cclosure_marshal_VOID__STRING,
                                                   G_TYPE_NONE, 1, G_TYPE_STRING);

    proxy_signals[ACCOUNT_REMOVED] = g_signal_new ("account-removed",
                                                   G_OBJECT_CLASS_TYPE (klass),
                                                   G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
                                                   G_STRUCT_OFFSET (AndroidAccountManagerProxyClass, account_removed),
                                                   NULL, NULL,
                                                   g_cclosure_marshal_VOID__STRING,
                                                   G_TYPE_NONE, 1, G_TYPE_STRING);
}

static void
android_account_manager_proxy_init (AndroidAccountManagerProxy *self)
{
    self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                              TYPE_ANDROID_ACCOUNT_MANAGER_PROXY,
                                              AndroidAccountManagerProxyPrivate);

}

static AccountInfo*
android_account_manager_proxy_find_account_by_email (GList *accounts,
                                                     const gchar *email)
{
    GList *acc;

    for (acc = accounts; acc != NULL; acc = acc->next) {
        AccountInfo *data = (AccountInfo*) acc->data;
        GVariant *email_field = account_info_get_field (data, ACCOUNT_EMAIL);
        if (email_field &&
            g_str_equal (g_variant_get_string (email_field, NULL), email)) {
            return data;
        }
    }

    return NULL;
}

gboolean
android_account_manager_proxy_account_exists (GList *accounts,
                                              AccountInfo *account)
{
    GVariant *email = account_info_get_field(account, ACCOUNT_EMAIL);
    const gchar *email_str = g_variant_get_string(email, NULL);
    AccountInfo *acc = android_account_manager_proxy_find_account_by_email(accounts, email_str);

    if (acc)
        return 1;

    return 0;
}

static GVariant*
android_account_manager_proxy_serialize_account (AccountInfo *account)
{
    GVariant *vAccount;
    GVariant *incomingHost;
    GVariant *outgoingHost;
    GVariant *policy;

    incomingHost = g_variant_new("(bssiisss)",
                              0,
                              null_string,
                              g_variant_get_string (account_info_get_field (account, ACCOUNT_INCOMING_HOST_ADDRESS), NULL),
                              g_variant_get_int32 (account_info_get_field (account, ACCOUNT_INCOMING_HOST_PORT)),
                              null_integer,
                              g_variant_get_string (account_info_get_field (account, ACCOUNT_INCOMING_HOST_LOGIN), NULL),
                              null_string,
                              null_string);

    outgoingHost = g_variant_new("(bssiisss)",
                              0,
                              null_string,
                              g_variant_get_string (account_info_get_field (account, ACCOUNT_OUTGOING_HOST_ADDRESS), NULL),
                              g_variant_get_int32 (account_info_get_field (account, ACCOUNT_OUTGOING_HOST_PORT)),
                              null_integer,
                              g_variant_get_string (account_info_get_field (account, ACCOUNT_OUTGOING_HOST_LOGIN), NULL),
                              null_string,
                              null_string);

    policy = g_variant_new("(biiiiiiibbbbbbbiiiiib)",
                            0, null_integer, null_integer, null_integer, null_integer,
                            null_integer, null_integer, null_integer, 0, 0, 0, 0, 0,
                            0, 0, null_integer, null_integer, null_integer, null_integer,
                            null_integer, 0);

    GVariant **accountTuple = g_new(GVariant *, 17);

    accountTuple[0] = g_variant_new_string(null_string);
    accountTuple[1] = g_variant_new_string(null_string);
    accountTuple[2] = account_info_get_field (account, ANDROID_ACCOUNT_EMAIL_ADDR);
    accountTuple[3] = g_variant_new_string(null_string);
    accountTuple[4] = g_variant_new_int32(null_integer);
    accountTuple[5] = g_variant_new_int32(null_integer);
    accountTuple[6] = g_variant_new_int32(null_integer);
    accountTuple[7] = g_variant_new_int32(null_integer);
    accountTuple[8] = g_variant_new_string(null_string);
    accountTuple[9] = account_info_get_field (account, ACCOUNT_SENDER_NAME);
    accountTuple[10] = g_variant_new_string(null_string);
    accountTuple[11] = g_variant_new_string(null_string);
    accountTuple[12] = g_variant_new_string(null_string);
    accountTuple[13] = g_variant_new_string(null_string);
    accountTuple[14] = incomingHost;
    accountTuple[15] = outgoingHost;
    accountTuple[16] = policy;

    vAccount = g_variant_new_tuple(accountTuple, 17);

    GVariant **inside = g_new(GVariant *, 1);
    inside[0] = vAccount;
    GVariant *final = g_variant_new_tuple(inside, 1);

    return final;
}

static AccountInfo*
android_account_manager_proxy_parse_account (GVariant *recAccount)
{
    AccountInfo *acc;
    GVariant *base, *host;
    int i;

    acc = account_info_new ();

    // Base account info
    if (g_variant_is_of_type(g_variant_get_child_value(recAccount, 0), "s"))
        base = recAccount;
    else
        base = g_variant_get_child_value(recAccount, 0);

    account_info_set_field(acc, ACCOUNT_TYPE,
            g_variant_get_child_value(base, ANDROID_ACCOUNT_TYPE));
    account_info_set_field(acc, ACCOUNT_DISPLAY_NAME,
            g_variant_get_child_value(base, ANDROID_ACCOUNT_DISPLAY_NAME));
    account_info_set_field(acc, ACCOUNT_EMAIL,
            g_variant_get_child_value(base, ANDROID_ACCOUNT_EMAIL_ADDR));
    account_info_set_field(acc, ACCOUNT_SENDER_NAME,
            g_variant_get_child_value(base, ANDROID_ACCOUNT_SENDER_NAME));
    account_info_set_field(acc, ACCOUNT_SIGNATURE,
            g_variant_get_child_value(base, ANDROID_ACCOUNT_SIGNATURE));

    // Incoming server info
    host = g_variant_get_child_value(base, 14);
    for (i = ANDROID_ACCOUNT_HOST_PROTOCOL; i <= ANDROID_ACCOUNT_HOST_DOMAIN; i++)
        account_info_set_field(acc, ACCOUNT_INCOMING_HOST_BASE + i, \
                               g_variant_get_child_value(host, i));

    // Outgoing server info
    host = g_variant_get_child_value(base, 15);
    for (i = ANDROID_ACCOUNT_HOST_PROTOCOL; i <= ANDROID_ACCOUNT_HOST_DOMAIN; i++)
        account_info_set_field(acc, ACCOUNT_OUTGOING_HOST_BASE + i,  \
                               g_variant_get_child_value(host, i));
    
    return acc;
}

static void
android_account_manager_proxy_create_local (AndroidAccountManagerProxy *self, GVariant *accountsVar)
{
    AndroidAccountManagerProxyPrivate *priv = self->priv;

    GVariant *serverAcc;
    GVariant *accountVar;
    GVariantIter *iter;

    g_variant_get(g_variant_get_child_value(accountsVar, 0),
            "a(ssssiiiissssss(bssiisss)(bssiisss)(biiiiiiibbbbbbbiiiiib))", &iter);

    while (accountVar = g_variant_iter_next_value(iter)) {
        AccountInfo *recAccount;

        recAccount = android_account_manager_proxy_parse_account (accountVar);
        
        priv->server_accounts = g_list_append (priv->server_accounts, recAccount);
        
        GVariant *email = account_info_get_field (recAccount, ACCOUNT_EMAIL);
        const gchar *email_str = g_variant_get_string (email, NULL);
    
        GVariant *type = account_info_get_field (recAccount, ACCOUNT_INCOMING_HOST_PROTOCOL);
        const gchar *type_str = g_variant_get_string(type, NULL);

        if (g_str_equal(type_str, "imap") || g_str_equal(type_str, "pop3")) {
            priv->server_accounts = g_list_append (priv->server_accounts, recAccount);
            g_signal_emit (G_OBJECT (self), proxy_signals[ACCOUNT_CREATED], 0, email_str);
        }
    }
}

static void
android_account_manager_proxy_remove_local (AndroidAccountManagerProxy *self, GVariant *emails)
{
    AndroidAccountManagerProxyPrivate *priv = self->priv;
    GVariantIter *iter;
    gchar *email_str;

    g_variant_get (g_variant_get_child_value(emails, 0), "as", &iter);
    while (g_variant_iter_loop (iter, "s", &email_str)) {
        AccountInfo *acc = android_account_manager_proxy_find_account_by_email (priv->server_accounts, email_str);
        priv->server_accounts = g_list_remove (priv->server_accounts, acc);
        account_info_destroy (acc);
        g_signal_emit (G_OBJECT (self), proxy_signals[ACCOUNT_REMOVED], 0, email_str);
    }
    g_variant_iter_free (iter);

}

void
android_account_manager_proxy_update_local (AndroidAccountManagerProxy *self,
                                              GVariant *accountVar)
{
    AccountInfo *localAcc, *recAccount;
    AndroidAccountManagerProxyPrivate *priv = self->priv;
    unsigned long update_mask = 0;
    int field;

    recAccount = android_account_manager_proxy_parse_account (accountVar);
    GVariant *email = account_info_get_field (recAccount, ACCOUNT_EMAIL);
    const gchar *email_str = g_variant_get_string (email, NULL);
    localAcc = android_account_manager_proxy_find_account_by_email (priv->registered_accounts, email_str);
    
    if (!localAcc)
        return;

    for (field = 0; field < NUM_FIELDS; field++)
        if (!account_info_compare_field(localAcc, recAccount, compare_fields[field])) 
            update_mask |= (1 << field);

    if (update_mask) {
        localAcc = android_account_manager_proxy_find_account_by_email (priv->server_accounts, email_str);
        priv->server_accounts = g_list_remove (priv->server_accounts, localAcc);
        priv->server_accounts = g_list_append (priv->server_accounts, recAccount);
        g_signal_emit (G_OBJECT (self), proxy_signals[ACCOUNT_UPDATED], 0, email_str, update_mask);
    }
}

static void
android_account_manager_proxy_sync (AndroidAccountManagerProxy *self)
{
    AndroidAccountManagerProxyPrivate *priv = self->priv;
    GError *error = NULL;

    GVariant *result = g_dbus_proxy_call_sync (priv->proxy,
                                               "getAllEmailAccounts",
                                               NULL,
                                               G_DBUS_CALL_FLAGS_NONE,
                                               -1,
                                               NULL,
                                               &error);
    if (error) {
        g_warning ("Fail to call getAllEmailAccounts: %s\n", error->message);
        g_error_free (error);
        return;
    }

    if (priv->server_accounts) {
        g_list_free_full (priv->server_accounts, (GDestroyNotify) account_info_destroy);
        priv->server_accounts = NULL;
    }

    GVariantIter *iter;
    GVariant *accountVar;
    GList *walk;

    g_variant_get(g_variant_get_child_value(result, 0),
            "a(ssssiiiissssss(bssiisss)(bssiisss)(biiiiiiibbbbbbbiiiiib))", &iter);
    while (accountVar = g_variant_iter_next_value(iter)) {
        AccountInfo *recAccount;
        gboolean exists;

        recAccount = android_account_manager_proxy_parse_account (accountVar);
        exists = android_account_manager_proxy_account_exists (priv->registered_accounts, recAccount);
        
        priv->server_accounts = g_list_append (priv->server_accounts, recAccount);
        if (!exists) {
            GVariant *email = account_info_get_field (recAccount, ACCOUNT_EMAIL);
            const gchar *email_str = g_variant_get_string (email, NULL);

            g_signal_emit (G_OBJECT (self), proxy_signals[ACCOUNT_CREATED], 0, email_str);
        } else {
            android_account_manager_proxy_update_local(self, accountVar);
        }
    }

    for (walk = priv->registered_accounts; walk != NULL; walk = walk->next) {
        AccountInfo *clientAcc;
        gboolean exists_on_server;

        clientAcc =  (AccountInfo*) walk->data;
        exists_on_server = android_account_manager_proxy_account_exists (priv->server_accounts, clientAcc);

        if (!exists_on_server) {
            GVariant *email = account_info_get_field (clientAcc, ACCOUNT_EMAIL);
            const gchar *email_str = g_variant_get_string (email, NULL);

            g_signal_emit (G_OBJECT (self), proxy_signals[ACCOUNT_REMOVED], 0, email_str);
        }
    }

    g_variant_unref (result);
}

void
android_account_manager_proxy_handle_update(AndroidAccountManagerProxy *self,
                                                  AccountInfo *server,
                                                  AccountInfo *local)
{  
    AndroidAccountManagerProxyPrivate *priv = self->priv;

    GVariant *email = account_info_get_field (server, ACCOUNT_EMAIL);
    const gchar *email_str = g_variant_get_string (email, NULL);
    email = account_info_get_field (local, ACCOUNT_EMAIL);
    const gchar *email_str2 = g_variant_get_string (email, NULL);
    int differs = 0;
    int field;

    for (field = 0; field < NUM_FIELDS; field++)
        if (!account_info_compare_field(local, server, compare_fields[field]))
            differs++; 
        else {
            if (compare_fields[field] == ACCOUNT_INCOMING_HOST_PORT ||
                compare_fields[field] == ACCOUNT_OUTGOING_HOST_PORT) {
                account_info_set_field(local, compare_fields[field], g_variant_new_int32(null_integer));
            } else
                account_info_set_field(local, compare_fields[field], g_variant_new_string(null_string));
        }


    if (differs) {
        GError *error = NULL;
        AccountInfo *recAccount;
        GVariant *parameters;
        
        parameters = android_account_manager_proxy_serialize_account (local);
        AccountInfo *parsed = android_account_manager_proxy_parse_account(parameters);
        
        GVariant *result = g_dbus_proxy_call_sync (priv->proxy,
                                                   "updateEmailAccountSettings",
                                                   parameters,
                                                   G_DBUS_CALL_FLAGS_NONE,
                                                   -1,
                                                   NULL,
                                                   &error);
       
        g_variant_unref(parameters);
 
        if (error) {
            g_warning ("Fail to call updateEmailAccountSettings: %s\n", error->message);
            g_error_free (error);
            return;
        }

        AccountInfo *acc = android_account_manager_proxy_get_account(self, email_str);
        priv->server_accounts = g_list_remove(priv->server_accounts, acc);
        acc = android_account_manager_proxy_get_account(self, email_str);
        priv->server_accounts = g_list_append(priv->server_accounts, account_info_copy(acc));
    }
}

static void
android_account_manager_proxy_on_dbus_signal (GDBusProxy *proxy,
                                              gchar *sender_name,
                                              gchar *signal_name,
                                              GVariant *parameters,
                                              gpointer user_data)
{
    if (g_str_equal (signal_name, UBUNTU_ANDROID_ACCOUNT_CREATED_SIGNAL)) {
        android_account_manager_proxy_create_local (ANDROID_ACCOUNT_MANAGER_PROXY (user_data), parameters);
    } else if (g_str_equal (signal_name, UBUNTU_ANDROID_ACCOUNT_CHANGED_SIGNAL)) {
        android_account_manager_proxy_update_local (ANDROID_ACCOUNT_MANAGER_PROXY (user_data), parameters);
    } else if (g_str_equal (signal_name, UBUNTU_ANDROID_ACCOUNT_REMOVED_SIGNAL)) {
        android_account_manager_proxy_remove_local (ANDROID_ACCOUNT_MANAGER_PROXY (user_data), parameters);
    }
}

void
android_account_manager_proxy_start_sync (AndroidAccountManagerProxy *self)
{

    GError *error = NULL;
    GDBusConnection *bus;

    if (self->priv->proxy)
        return;
    
    bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);

    if (!bus) {
        g_warning ("Fail to get dbus session: %s\n", error->message);
    } else {
        self->priv->proxy = g_dbus_proxy_new_sync (bus, G_DBUS_PROXY_FLAGS_NONE, NULL,
                                                   UBUNTU_ANDROID_SERVICE_NAME,
                                                   UBUNTU_ANDROID_OBJECT_PATH,
                                                   UBUNTU_ANDROID_INTERFACE_NAME,
                                                   NULL, &error);
        if (!self->priv->proxy) {
            g_warning ("Fail to connect with android service: %s\n", error->message);
        } else {
            g_signal_connect (self->priv->proxy, "g-signal",
                              G_CALLBACK (android_account_manager_proxy_on_dbus_signal), self);
        }

        g_object_unref (bus);
        android_account_manager_proxy_sync (self);
    }
}

void
android_account_manager_proxy_register_account (AndroidAccountManagerProxy *self,
                                                AccountInfo *account)
{
    AndroidAccountManagerProxyPrivate *priv = self->priv;

    if (account == NULL) {
        return;
    }

    if (!android_account_manager_proxy_account_exists (priv->registered_accounts, account)) {
        priv->registered_accounts = g_list_append (priv->registered_accounts,
                                                   account_info_copy (account));
    }
}

void
android_account_manager_proxy_unregister_account (AndroidAccountManagerProxy *self,
                                                  const char *email)
{
    AndroidAccountManagerProxyPrivate *priv = self->priv;
    AccountInfo *acc = android_account_manager_proxy_find_account_by_email (priv->registered_accounts, email);

    if (acc) {
        priv->registered_accounts = g_list_remove (priv->registered_accounts, acc);
        account_info_destroy (acc);
    }
}

const AccountInfo*
android_account_manager_proxy_get_account (AndroidAccountManagerProxy *self,
                                           const gchar *email)
{
    // Find account details on Android
    AccountInfo *acc =  android_account_manager_proxy_find_account_by_email (self->priv->server_accounts, email);

    // Find account details on Thunderbird
    if (!acc) {
        acc =  android_account_manager_proxy_find_account_by_email (self->priv->registered_accounts, email);
    }

    return acc;
}


