// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
/*-
 * Copyright (c) 2013-2015 Pantheon Developers (https://launchpad.net/switchboard-plug-onlineaccounts)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Authored by: Corentin Noël <corentin@elementary.io>
 */

public class E.PantheonOnlineAccounts : E.Extension, E.OAuth2Support {
    Ag.Manager manager;
    HashTable<uint, string> uoa_to_eds;
    HashTable<string, HashTable<string, Ag.AccountService>> services_from_source;

    static construct {
        extensible_type = typeof (E.SourceRegistryServer);
    }

    construct {
        uoa_to_eds = new HashTable<uint, string> (null, null);
        ((E.DBusServer) get_extensible ()).bus_acquired.connect_after ((connection) => on_bus_acquired ());
    }

    public bool get_access_token_sync (E.Source source, GLib.Cancellable? cancellable, out string? access_token, out int expires_in) throws GLib.Error {
        var account_service = ref_account_service (source);
        if (account_service == null) {
            critical ("Cannot find a corresponding account service in the accounts database from which to obtain an access token for '%s'", source.get_display_name ());
            return false;
        }

        /* XXX This should never happen.  But because libaccounts-glib
         *     splits authentication method by service-type instead of
         *     by provider, and because we broadcast OAuth 2.0 support
         *     across the entire collection (spanning multiple service
         *     types), it's conceivable that not all service-types for
         *     a provider use OAuth 2.0, and an ESource for one of the
         *     ones that DOESN'T could mistakenly request the token. */
        if (!POAUtils.check_oauth2_support (account_service)) {
            critical ("Data source '%s' does not support OAuth 2.0 authentication", source.get_display_name ());
            return false;
        }

        var auth_data = account_service.get_auth_data ();
        var identity = new Signon.Identity.from_db (auth_data.get_credentials_id ());

        try {
            var session = identity.create_session (auth_data.get_method ());
            if (session != null) {
                Variant session_data = null;
                var mechanism = auth_data.get_mechanism ();
                string method;
                if (mechanism == "PLAINTEXT" || mechanism == "HMAC-SHA1" || mechanism == "RSA-SHA1") {
                    method = "oauth1";
                } else if (mechanism == "web_server" || mechanism == "user_agent") {
                    method = "oauth2";
                } else {
                    method = mechanism;
                }

                var loop = new MainLoop ();
                Error error = null;
                session.process_async.begin (auth_data.get_login_parameters (null), method, cancellable, (obj, res) => {
                    try {
                        session_data = session.process_async.end (res);
                    } catch (Error e) {
                        error = e;
                    }
                    loop.quit ();
                });

                loop.run ();
                if (error != null)
                    throw error;

                if (session_data != null) {
                    var secret = session_data.lookup_value ("AccessToken", VariantType.STRING);
                    access_token = secret.get_string ();
                    var expiration = session_data.lookup_value ("Duration", VariantType.INT64);
                    expires_in = (int)expiration.get_int64 ();
                    return true;
                }
            }
        } catch (Error e) {
            throw e;
        }
        return false;
    }

    public async bool get_access_token (E.Source source, GLib.Cancellable? cancellable, out string? access_token, out int expires_in) throws GLib.Error {
        Signon.AuthSession session;
        var account_service = ref_account_service (source);
        if (account_service == null) {
            message ("Cannot find a corresponding account service in the accounts database from which to obtain an access token for '%s'", source.get_display_name ());
            return false;
        }

        /* XXX This should never happen.  But because libaccounts-glib
         *     splits authentication method by service-type instead of
         *     by provider, and because we broadcast OAuth 2.0 support
         *     across the entire collection (spanning multiple service
         *     types), it's conceivable that not all service-types for
         *     a provider use OAuth 2.0, and an ESource for one of the
         *     ones that DOESN'T could mistakenly request the token. */
        if (!POAUtils.check_oauth2_support (account_service)) {
            critical ("Data source '%s' does not support OAuth 2.0 authentication", source.get_display_name ());
            return false;
        }

        var auth_data = account_service.get_auth_data ();
        var identity = new Signon.Identity.from_db (auth_data.get_credentials_id ());

        try {
            session = identity.create_session (auth_data.get_method ());
            if (session != null) {
                var mechanism = auth_data.get_mechanism ();
                string method;
                if (mechanism == "PLAINTEXT" || mechanism == "HMAC-SHA1" || mechanism == "RSA-SHA1") {
                    method = "oauth1";
                } else if (mechanism == "web_server" || mechanism == "user_agent") {
                    method = "oauth2";
                } else {
                    method = mechanism;
                }

                var session_data = yield session.process_async (auth_data.get_login_parameters (null), method, cancellable);
                if (session_data != null) {
                    var secret = session_data.lookup_value ("AccessToken", VariantType.STRING);
                    access_token = secret.get_string ();
                    var expiration = session_data.lookup_value ("Duration", VariantType.INT64);
                    expires_in = (int)expiration.get_int64 ();
                    return true;
                }
            }
        } catch (Error e) {
            throw e;
        }

        return false;
    }

    private void on_bus_acquired () {
        manager = new Ag.Manager ();
        services_from_source = new HashTable<string, HashTable<string, Ag.AccountService>> (str_hash, str_equal);
        populate_accounts_table ();
        manager.list ().foreach ((account_id) => {
            on_account_created (account_id);
        });

        manager.account_created.connect ((id) => on_account_created (id));
        manager.account_deleted.connect ((id) => on_account_deleted (id));
    }

    private void on_account_created (uint account_id) {
        var account = manager.get_account (account_id);
        var source_uid = uoa_to_eds.get (account_id);
        if (source_uid == null) {
            create_collection (account);
        }
    }

    private void populate_accounts_table () {
        var trash = new Queue<E.Source> ();
        var extensible = get_extensible ();
        var id_list = manager.list ();
        ((E.SourceRegistryServer) extensible).list_sources (E.SOURCE_EXTENSION_UOA).foreach ((source) => {
            var uoa_ext = (E.SourceUoa)source.get_extension (E.SOURCE_EXTENSION_UOA);
            var account_id = uoa_ext.get_account_id ();
            if (account_id == 0)
                return;

            var exists = uoa_to_eds.get (account_id);
            if (exists != null)
                return;

            /* Verify the POA account still exists. */
            unowned GLib.List<uint> match = id_list.find (account_id);
            var account = manager.get_account (account_id);
            if (match != null && account != null) {
                uoa_to_eds.set (account_id, source.dup_uid ());
                config_sources (source, account);
            } else {
                trash.push_tail (source);
            }
        });

        while (trash.is_empty () == false) {
            var source = trash.pop_head ();
            try {
                source.remove_sync ();
            } catch (Error e) {
                critical (e.message);
            }
        }
    }

    /* 
     * Populate the hash table with AgAccountService instances by
     * service type. There should only be one AgService per type.
     */
    private HashTable<string, Ag.AccountService> new_account_services (Ag.Account account) {
        var account_services = new HashTable<string, Ag.AccountService> (str_hash, str_equal);
        account.list_services ().foreach ((service) => {
            account_services.set (service.get_service_type (), new Ag.AccountService (account, service));
        });

        return account_services;
    }

    private void create_collection (Ag.Account account) {
        var collection_source = new_source ();
        return_if_fail (collection_source is E.Source);

        config_sources (collection_source, account);
        ((E.SourceRegistryServer) get_extensible ()).add_source (collection_source);
        uoa_to_eds.set (account.id, collection_source.dup_uid ());
    }

    /* 
     * Connect to signals and set all properties accordingly.
     */
    private void config_sources (E.Source collection_source, Ag.Account account) {
        var account_services = new_account_services (account);

        unowned E.SourceExtension source_extension = collection_source.get_extension (E.SOURCE_EXTENSION_AUTHENTICATION);
        ((E.SourceAuthentication)source_extension).set_user (account.display_name);
        collection_source.set_display_name (manager.get_provider (account.get_provider_name ()).get_display_name ());
        account.bind_property ("enabled", collection_source, "enabled", BindingFlags.SYNC_CREATE);

        source_extension = collection_source.get_extension (E.SOURCE_EXTENSION_UOA);
        account.bind_property ("id", source_extension, "account-id", BindingFlags.SYNC_CREATE);

        source_extension = collection_source.get_extension (E.SOURCE_EXTENSION_COLLECTION);
        unowned E.SourceCollection source_collection = (E.SourceCollection)source_extension;
        var user_identity = account.get_display_name ();
        if (user_identity != null) {
            source_collection.set_identity (user_identity);
        }

        bool supports_oauth2 = false;

        var account_service = account_services.get (AG_SERVICE_TYPE_MAIL);
        if (account_service != null) {
            var backend_name_variant = account_service.get_variant ("evolution/BackendName", null);
            if (backend_name_variant != null) {
                if (source_collection.backend_name == null){
                    source_collection.set_backend_name (backend_name_variant.dup_string ());
                }
            } else {
                configure_separate_email (collection_source, account_service);
            }

            account_service.bind_property ("enabled", source_collection, "mail-enabled", BindingFlags.SYNC_CREATE);
            supports_oauth2 |= POAUtils.check_oauth2_support (account_service);
        }

        account_service = account_services.get (AG_SERVICE_TYPE_CALENDAR);
        if (account_service != null) {
            var backend_name_variant = account_service.get_variant ("evolution/BackendName", null);
            if (backend_name_variant != null) {
                if (source_collection.backend_name == null) {
                    source_collection.set_backend_name (backend_name_variant.dup_string ());
                }
            } else {
                configure_separate_calendar (collection_source, account_service);
            }

            account_service.bind_property ("enabled", source_collection, "calendar-enabled", BindingFlags.SYNC_CREATE);
            supports_oauth2 |= POAUtils.check_oauth2_support (account_service);
        }

        account_service = account_services.get (AG_SERVICE_TYPE_CONTACTS);
        if (account_service != null) {
            var backend_name_variant = account_service.get_variant ("evolution/BackendName", null);
            if (backend_name_variant != null) {
                if (source_collection.backend_name == null){
                    source_collection.set_backend_name (backend_name_variant.dup_string ());
                }
            } else {
                configure_separate_contacts (collection_source, account_service);
            }

            account_service.bind_property ("enabled", source_collection, "contacts-enabled", BindingFlags.SYNC_CREATE);
            supports_oauth2 |= POAUtils.check_oauth2_support (account_service);
        }

        /* Stash the AgAccountService hash table in the ESource
         * to keep the property bindings alive.  The hash table
         * will be destroyed along with the ESource. */
        services_from_source.set (collection_source.dup_uid (), account_services);

        /* The data source should not be removable by clients. */
        var server_side_source = (E.ServerSideSource)collection_source;
        server_side_source.set_removable (false);

        if (supports_oauth2 == true) {
            /* This module provides OAuth 2.0 support to the collection.
             * Note, children of the collection source will automatically
             * inherit our EOAuth2Support through the property binding in
             * collection_backend_child_added(). */
            server_side_source.set_oauth2_support (this);
        } 
    }

    /*
     * If there is no BackendName key in the calendar service, try to add a custom one.
     */
    private void configure_separate_calendar (E.Source source, Ag.AccountService account_service) {
        var backend_variant = account_service.get_variant ("evolution/Backend", null);
        if (backend_variant != null) {
            var source_collection = (E.SourceCollection)source.get_extension (E.SOURCE_EXTENSION_COLLECTION);
            if (source_collection.backend_name == null) {
                source_collection.set_backend_name (backend_variant.dup_string ());
            }

            E.SourceCalendar cal = (E.SourceCalendar)source.get_extension (E.SOURCE_EXTENSION_CALENDAR);
            cal.backend_name = backend_variant.dup_string ();
            var collection_backend = ((E.SourceRegistryServer)get_extensible ()).ref_backend_factory (source);
            if (collection_backend == null)
                warning (cal.backend_name);
        }
    }

    /*
     * If there is no BackendName key in the contact service, try to add a custom one.
     */
    private void configure_separate_contacts (E.Source source, Ag.AccountService account_service) {
        var backend_variant = account_service.get_variant ("evolution/Backend", null);
        if (backend_variant != null) {
            var source_collection = (E.SourceCollection)source.get_extension (E.SOURCE_EXTENSION_COLLECTION);
            if (source_collection.backend_name == null) {
                source_collection.set_backend_name (backend_variant.dup_string ());
            }

            E.SourceAddressBook addressbook = (E.SourceAddressBook)source.get_extension (E.SOURCE_EXTENSION_ADDRESS_BOOK);
            addressbook.backend_name = backend_variant.dup_string ();
            var collection_backend = ((E.SourceRegistryServer)get_extensible ()).ref_backend_factory (source);
            if (collection_backend == null)
                warning (addressbook.backend_name);
        }
    }

    /*
     * If there is no BackendName key in the contact service, try to add a custom one.
     */
    private void configure_separate_email (E.Source source, Ag.AccountService account_service) {
        //TODO
    }

    /*
     * Create a new source associated to the POA extension.
     */
    private E.Source? new_source () {
        var file = E.ServerSideSource.new_user_file (null);
        E.Source source = null;
        try {
            source = new E.ServerSideSource ((E.SourceRegistryServer)get_extensible (), file);
        } catch (Error e) {
            critical (e.message);
        }

        return source;
    }

    /*
     * Called when an account is removed from libaccounts-glib.
     * Remove the associated Sources.
     */
    private void on_account_deleted (uint account_id) {
        var source_uid = uoa_to_eds.get (account_id);
        if (source_uid == null)
            return;

        uoa_to_eds.remove (account_id);

        var source = ((E.SourceRegistryServer)get_extensible ()).ref_source (source_uid);
        if (source != null) {
            try {
                source.remove_sync ();
            } catch (Error e) {
                critical (e.message);
            }
        }
    }

    /*
     * Get the AccountService associated to the source (depending on its type).
     */
    private Ag.AccountService? ref_account_service (E.Source source) {
        var uoasource = ((E.SourceRegistryServer)get_extensible ()).find_extension (source, E.SOURCE_EXTENSION_UOA);
        if (uoasource != null) {
            var uid = source.dup_parent ();
            if (uid == null)
                uid = source.dup_uid ();
            var account_services = services_from_source.get (uid);
            if (account_services != null) {
                var service_type = POAUtils.get_service_type_from_source (source);
                if (service_type == null)
                    return null;
                return account_services.get (service_type);
            }
        }

        return null;
    }
}

[ModuleInit]
public static Type e_module_load (GLib.TypeModule type_module) {
    e_fake_module_load (type_module);
    return typeof(E.PantheonOnlineAccounts);
}

public void e_module_unload (TypeModule type_module) {
    
}
