/* Copyright 2009-2010 Yorba Foundation
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution. 
 */

#if !NO_PUBLISHING

namespace FacebookConnector {
// this should not be changed by anyone unless they know what they're doing
private const string API_KEY = "3afe0a1888bd340254b1587025f8d1a5";
private const int MAX_PHOTO_DIMENSION = 720;
private const string DEFAULT_ALBUM_NAME = _("Shotwell Connect");

private const string SERVICE_WELCOME_MESSAGE = 
    _("You are not currently logged into Facebook.\n\nIf you don't yet have a Facebook account, you can create one during the login process.");
private const string RESTART_ERROR_MESSAGE = 
    _("You have already logged in and out of Facebook during this Shotwell session.\nTo continue publishing to Facebook, quit and restart Shotwell, then try publishing again.");

private struct Album {
    string name;
    string id;
    
    Album(string creator_name, string creator_id) {
        name = creator_name;
        id = creator_id;
    }
}

public class Interactor : ServiceInteractor {
    private const int NO_ALBUM = -1;

    private Session session;
    // we need to hold a reference to the web_auth_pane to ensure that it is not destroyed after
    // it's unhooked from the publishing dialog. This is necessary, otherwise WebKit will get
    // very angry.
    private WebAuthenticationPane web_auth_pane = null;
    private ProgressPane progress_pane = null;
    private Album[] albums = null;
    private int publish_to_album = NO_ALBUM;
    private bool cancelled = false;

    public Interactor(PublishingDialog host) {
        base(host);

        session = new Session(API_KEY);
    }

    private int lookup_album(string name) {
        for (int i = 0; i < albums.length; i++) {
            if (albums[i].name == name)
                return i;
        }
        return NO_ALBUM;
    }

    private void on_service_welcome_login() {
        // ignore all events if the user has cancelled or we have and error situation
        if (has_error() || cancelled)
            return;

        do_test_connection_to_endpoint();
    }

    private void on_web_auth_pane_login_succeeded(string success_url) {
        if (has_error() || cancelled)
            return;

        web_auth_pane.hide();
        do_authenticate_session(success_url);
    }

    private void on_web_auth_pane_login_failed() {
        if (has_error() || cancelled)
            return;

        // In this case, "failed" doesn't mean that the user didn't enter the right username and
        // password -- Facebook handles that case inside the Facebook Connect web control. Instead,
        // it means that no session was initiated in response to our login request. The only
        // way this happens is if the user clicks the "Cancel" button that appears inside
        // the web control. In this case, the correct behavior is to return the user to the
        // service welcome pane so that they can start the web interaction again.
        do_show_service_welcome_pane();
    }

    private void on_publishing_options_pane_logout() {
        if (has_error() || cancelled)
            return;

        do_logout();
    }

    private void on_publishing_options_pane_publish(string album_name) {
        if (has_error() || cancelled)
            return;

        if (lookup_album(album_name) != NO_ALBUM) {
            publish_to_album = lookup_album(album_name);
            do_upload();
        } else {
            do_create_album(album_name);
        }
    }

    private void on_session_authenticated() {
        if (has_error() || cancelled)
            return;

        assert(session.is_authenticated());
        do_fetch_album_descriptions();
    }

    private void on_session_authentication_failed(PublishingError err) {
        if (has_error() || cancelled)
            return;

        post_error(err);
    }

    private void on_albums_extracted() {
        if (has_error() || cancelled)
            return;

        do_show_publishing_options_pane();
    }

    private void on_fetch_album_descriptions_completed(RESTTransaction txn) {
        txn.completed -= on_fetch_album_descriptions_completed;
        txn.network_error -= on_fetch_album_descriptions_error;

        if (has_error() || cancelled)
            return;

        do_extract_albums_from_xml(txn.get_response());
    }

    private void on_fetch_album_descriptions_error(RESTTransaction bad_txn, PublishingError err) {
        bad_txn.completed -= on_fetch_album_descriptions_completed;
        bad_txn.network_error -= on_fetch_album_descriptions_error;

        if (has_error() || cancelled)
            return;

        post_error(err);
    }

    private void on_create_album_txn_completed(RESTTransaction txn) {
        txn.completed -= on_create_album_txn_completed;
        txn.network_error -= on_create_album_txn_error;

        if (has_error() || cancelled)
            return;

        do_extract_aid_from_xml(txn.get_response());
    }

    private void on_create_album_txn_error(RESTTransaction bad_txn, PublishingError err) {
        bad_txn.completed -= on_create_album_txn_completed;
        bad_txn.network_error -= on_create_album_txn_error;

        if (has_error() || cancelled)
            return;

        post_error(err);
    }

    private void on_album_name_extracted() {
        if (has_error() || cancelled)
            return;

        do_upload();
    }

    private void on_upload_complete(BatchUploader uploader) {
        uploader.status_updated -= progress_pane.set_status;
        uploader.upload_complete -= on_upload_complete;
        uploader.upload_error -= on_upload_error;

        if (has_error() || cancelled)
            return;

        do_show_success_pane();
    }

    private void on_upload_error(BatchUploader uploader, PublishingError err) {
        uploader.status_updated -= progress_pane.set_status;
        uploader.upload_complete -= on_upload_complete;
        uploader.upload_error -= on_upload_error;

        if (has_error() || cancelled)
            return;

        post_error(err);
    }

    private void on_endpoint_test_completed(RESTTransaction txn) {
        txn.completed -= on_endpoint_test_completed;
        txn.network_error -= on_endpoint_test_error;

        if (has_error() || cancelled)
            return;

        do_hosted_web_authentication();
    }

    private void on_endpoint_test_error(RESTTransaction bad_txn, PublishingError err) {
        bad_txn.completed -= on_endpoint_test_completed;
        bad_txn.network_error -= on_endpoint_test_error;

        if (has_error() || cancelled)
            return;

        post_error(err);
    }

    private void do_show_publishing_options_pane() {
        get_host().set_cancel_button_mode();
        get_host().unlock_service();

        PublishingOptionsPane publishing_options_pane =
            new PublishingOptionsPane(session.get_user_name(), albums);
        publishing_options_pane.logout += on_publishing_options_pane_logout;
        publishing_options_pane.publish += on_publishing_options_pane_publish;
        get_host().install_pane(publishing_options_pane);
    }

    private void do_show_service_welcome_pane() {
        get_host().set_cancel_button_mode();
        get_host().unlock_service();

        LoginWelcomePane service_welcome_pane = new LoginWelcomePane(SERVICE_WELCOME_MESSAGE);
        service_welcome_pane.login_requested += on_service_welcome_login;
        get_host().install_pane(service_welcome_pane);
    }

    private void do_authenticate_session(string success_url) {
        debug("Facebook.Interactor: do_authenticate_session( ): ACTION: preparing to extract " +
            ("session key information encoded in url = '%s'").printf(success_url));

        get_host().set_cancel_button_mode();
        get_host().lock_service();
        get_host().install_pane(new AccountFetchWaitPane());

        session.authenticated += on_session_authenticated;
        session.authentication_failed += on_session_authentication_failed;

        try {
            session.authenticate(success_url);
        } catch (PublishingError err) {
            post_error(err);
        }
    }

    private void do_test_connection_to_endpoint() {
        get_host().set_cancel_button_mode();
        get_host().lock_service();

        get_host().install_pane(new StaticMessagePane(_("Testing connection to Facebook...")));

        EndpointTestTransaction txn = new EndpointTestTransaction(session);
        txn.completed += on_endpoint_test_completed;
        txn.network_error += on_endpoint_test_error;

        txn.execute();
    }

    private void do_hosted_web_authentication() {
        get_host().set_cancel_button_mode();
        get_host().unlock_service();

        web_auth_pane = new WebAuthenticationPane();
        web_auth_pane.login_succeeded += on_web_auth_pane_login_succeeded;
        web_auth_pane.login_failed += on_web_auth_pane_login_failed;

        get_host().install_pane(web_auth_pane);
    }

    private void do_fetch_album_descriptions() {
        get_host().set_cancel_button_mode();
        get_host().lock_service();

        Transaction albums_transaction = new AlbumsFetchTransaction(session);
        albums_transaction.completed += on_fetch_album_descriptions_completed;
        albums_transaction.network_error += on_fetch_album_descriptions_error;

        albums_transaction.execute();
    }

    private void do_extract_albums_from_xml(string xml) {
        debug("Facebook.Interactor: do_extract_albums_from_xml( ): ACTION: extracting album info " +
            ("from xml response '%s'").printf(xml));

        Album[] extracted = new Album[0];

        try {
            RESTXmlDocument response_doc =
                RESTXmlDocument.parse_string(xml, Transaction.check_response);

            Xml.Node* root = response_doc.get_root_node();

            if (root->name != "photos_getAlbums_response")
               throw new PublishingError.MALFORMED_RESPONSE("Document root node has unexpected name '%s'",
                   root->name);

            Xml.Node* doc_node_iter = root->children;
            for ( ; doc_node_iter != null; doc_node_iter = doc_node_iter->next) {
                if (doc_node_iter->name != "album")
                    continue;

                string name_val = null;
                string aid_val = null;
                Xml.Node* album_node_iter = doc_node_iter->children;
                for ( ; album_node_iter != null; album_node_iter = album_node_iter->next) {
                    if (album_node_iter->name == "name") {
                        name_val = album_node_iter->get_content();
                    } else if (album_node_iter->name == "aid") {
                        aid_val = album_node_iter->get_content();
                    }
                }

                if (name_val != "Profile Pictures")
                    extracted += Album(name_val, aid_val);
            }
        } catch (PublishingError err) {
            post_error(err);
            return;
        }

        albums = extracted;

        on_albums_extracted();
    }

    private void do_create_album(string album_name) {
        albums += Album(album_name, "");

        get_host().set_cancel_button_mode();
        get_host().lock_service();

        get_host().install_pane(new StaticMessagePane(_("Creating album...")));

        Transaction create_txn = new AlbumCreationTransaction(session, album_name);
        create_txn.completed += on_create_album_txn_completed;
        create_txn.network_error += on_create_album_txn_error;

        create_txn.execute();
    }

    private void do_extract_aid_from_xml(string xml) {
        try {
            RESTXmlDocument response_doc = RESTXmlDocument.parse_string(xml,
                Transaction.check_response);

            Xml.Node* root = response_doc.get_root_node();
            Xml.Node* aid_node = response_doc.get_named_child(root, "aid");

            assert(albums[albums.length - 1].id == "");

            publish_to_album = albums.length - 1;
            albums[publish_to_album].id = aid_node->get_content();
        } catch (PublishingError err) {
            post_error(err);
            return;
        }

        on_album_name_extracted();
    }

    private void do_upload() {
        assert(publish_to_album != NO_ALBUM);

        get_host().lock_service();
        get_host().set_cancel_button_mode();

        progress_pane = new ProgressPane();
        get_host().install_pane(progress_pane);

        TransformablePhoto[] photos = get_host().get_photos();
        Uploader uploader = new Uploader(session, albums[publish_to_album].id, photos);
        uploader.status_updated += progress_pane.set_status;
        uploader.upload_complete += on_upload_complete;
        uploader.upload_error += on_upload_error;

        uploader.upload();
    }

    private void do_show_success_pane() {
        get_host().unlock_service();
        get_host().set_close_button_mode();

        get_host().install_pane(new SuccessPane());
    }

    private void do_logout() {
        session.deauthenticate();
        start_interaction();
    }

    public override string get_name() {
        return "Facebook";
    }

    public override void cancel_interaction() {
        session.stop_transactions();
        cancelled = true;
    }

    public override void start_interaction() {
        get_host().set_standard_window_mode();

        if (session.is_authenticated()) {
            // if a fully authenticated session has been loaded from GConf, then simulate a
            // session authenticated event
            on_session_authenticated();
        } else {
            if (WebAuthenticationPane.is_cache_dirty()) {
                get_host().set_cancel_button_mode();
                get_host().unlock_service();
                get_host().install_pane(new StaticMessagePane(RESTART_ERROR_MESSAGE));
            } else {
                do_show_service_welcome_pane();
            }
        }
    }

    public void logout_user() {
        do_logout();
    }
}

private class Session : RESTSession {
    private const string API_VERSION = "1.0";
    private const string USER_AGENT = "Java/1.6.0_16";
    private const string ENDPOINT_URL = "http://api.facebook.com/restserver.php";

    private string session_key = null;
    private string uid = null;
    private string secret = null;
    private string api_key = null;
    private string user_name = null;

    public signal void authenticated();
    public signal void authentication_failed(PublishingError err);

    public Session(string api_key) {
        base(ENDPOINT_URL, USER_AGENT);

        this.api_key = api_key;

        if (is_persistent_session_valid()) {
            Config config = Config.get_instance();

            session_key = config.get_facebook_session_key();
            secret = config.get_facebook_session_secret();
            uid = config.get_facebook_uid();
            user_name = config.get_facebook_user_name();
        }
    }

    private static bool is_persistent_session_valid() {
        Config config = Config.get_instance();

        string session_key = config.get_facebook_session_key();
        string session_secret = config.get_facebook_session_secret();
        string uid = config.get_facebook_uid();
        string user_name = config.get_facebook_user_name();
       
        return ((session_key != null) && (session_secret != null) && (uid != null) &&
            (user_name != null));
    }

    private static void invalidate_persistent_session() {
        Config config = Config.get_instance();
        
        config.clear_facebook_session_key();
        config.clear_facebook_session_secret();
        config.clear_facebook_uid();
        config.clear_facebook_user_name();
    }

    private void on_user_info_txn_completed(RESTTransaction txn) {
        txn.completed -= on_user_info_txn_completed;
        txn.network_error -= on_user_info_txn_error;

        try {
            RESTXmlDocument response_doc = RESTXmlDocument.parse_string(txn.get_response(),
                Transaction.check_response);

            Xml.Node* root_node = response_doc.get_root_node();
            Xml.Node* user_node = response_doc.get_named_child(root_node, "user");
            Xml.Node* name_node = response_doc.get_named_child(user_node, "name");

            user_name = name_node->get_content();
        } catch (PublishingError err) {
            authentication_failed(err);
            return;
        }

        authenticated();

        Config config = Config.get_instance();

        config.set_facebook_session_key(session_key);
        config.set_facebook_session_secret(secret);
        config.set_facebook_uid(uid);
        config.set_facebook_user_name(user_name);
    }

    private void on_user_info_txn_error(RESTTransaction txn, PublishingError err) {
        txn.completed -= on_user_info_txn_completed;
        txn.network_error -= on_user_info_txn_error;

        authentication_failed(err);
    }

    public void authenticate(string good_login_uri) throws PublishingError {       
        // the raw uri is percent-encoded, so decode it
        string decoded_uri = Soup.URI.decode(good_login_uri);

        // locate the session object description string within the decoded uri
        string session_desc = decoded_uri.str("session={");
        if (session_desc == null)
            throw new PublishingError.MALFORMED_RESPONSE("Server redirect URL contained no session description");

        // remove any trailing parameters from the session description string
        string trailing_params = session_desc.chr(-1, '&');
        if (trailing_params != null)
            session_desc = session_desc.replace(trailing_params, "");

        // remove the key from the session description string
        session_desc = session_desc.replace("session=", "");
        
        // remove the group open, group close, quote, list separator, and key-value
        // delimiter characters from the session description string
        session_desc = session_desc.replace("{", "");
        session_desc = session_desc.replace("}", "");
        session_desc = session_desc.replace("\"", "");
        session_desc = session_desc.replace(",", " ");
        session_desc = session_desc.replace(":", " ");
        
        // parse the session description string
        string[] session_tokens = session_desc.split(" ");
        for (int i = 0; i < session_tokens.length; i++) {
            if (session_tokens[i] == "session_key") {
                session_key = session_tokens[++i];
            } else if (session_tokens[i] == "uid") {
                uid = session_tokens[++i];
            } else if (session_tokens[i] == "secret") {
                secret = session_tokens[++i];
            }
        }

        if (session_key == null)
            throw new PublishingError.MALFORMED_RESPONSE("Session description object has no session key");
        if (uid == null)
            throw new PublishingError.MALFORMED_RESPONSE("Session description object has no user ID");
        if (secret == null)
            throw new PublishingError.MALFORMED_RESPONSE("Session description object has no session secret");

        UserInfoTransaction user_info_txn = new UserInfoTransaction(this, get_user_id());
        user_info_txn.completed += on_user_info_txn_completed;
        user_info_txn.network_error += on_user_info_txn_error;
        user_info_txn.execute();
    }

    public bool is_authenticated() {
        return ((session_key != null) && (uid != null) && (secret != null) && (api_key != null) &&
                (user_name != null));
    }

    public void deauthenticate() {
        session_key = null;
        uid = null;
        secret = null;
        user_name = null;

        invalidate_persistent_session();
    }

    public string get_api_key() {
        return api_key;
    }

    public string get_session_key() {
        assert(session_key != null);
        return session_key;
    }

    public string get_user_id() {
        assert(uid != null);
        return uid;
    }

    public string get_session_secret() {
        assert(secret != null);
        return secret;
    }

    public string get_next_call_id() {
        TimeVal currtime = TimeVal();
        currtime.get_current_time();

        return "%u.%u".printf((uint) currtime.tv_sec, (uint) currtime.tv_usec);
    }

    public string get_api_version() {
        return API_VERSION;
    }

    public string get_user_name() {
        assert(user_name != null);
        return user_name;
    }
}

private class Uploader : BatchUploader {
    private Session session;
    private string aid;

    public Uploader(Session session, string aid, TransformablePhoto[] photos) {
        base(photos);

        this.session = session;
        this.aid = aid;
    }

    protected override void prepare_file(BatchUploader.TemporaryFileDescriptor file) {
        Scaling scaling = Scaling.for_constraint(ScaleConstraint.DIMENSIONS, MAX_PHOTO_DIMENSION,
            false);
        
        try {
            file.source_photo.export(file.temp_file, scaling, Jpeg.Quality.MAXIMUM,
                PhotoFileFormat.JFIF);
        } catch (Error e) {
            error("FacebookConnector.Uploader: can't create temporary files");
        }
    }

    protected override RESTTransaction create_transaction_for_file(
        BatchUploader.TemporaryFileDescriptor file) {
        return new UploadTransaction(session, aid, file.temp_file.get_path(), file.source_photo);
    }
}

private class WebAuthenticationPane : PublishingDialogPane {
    private WebKit.WebView webview = null;
    private Gtk.ScrolledWindow webview_frame = null;
    private static bool cache_dirty = false;

    public signal void login_succeeded(string success_url);
    public signal void login_failed();

    public WebAuthenticationPane() {
        set_size_request(476, 360);

        webview_frame = new Gtk.ScrolledWindow(null, null);
        webview_frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN);
        webview_frame.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);

        webview = new WebKit.WebView();
        webview.load_finished += on_page_load;
        webview.load_started += on_load_started;

        webview_frame.add(webview);
        add(webview_frame);
    }

    private string get_login_url() {
        return "http://www.facebook.com/login.php?api_key=%s&connect_display=popup&v=1.0&next=http://www.facebook.com/connect/login_success.html&cancel_url=http://www.facebook.com/connect/login_failure.html&fbconnect=true&return_session=true&req_perms=read_stream,publish_stream,offline_access,photo_upload".printf(FacebookConnector.API_KEY);
    }

    private void on_page_load(WebKit.WebFrame origin_frame) {
        window.set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));

        string loaded_url = origin_frame.get_uri().dup();

        // strip parameters from the loaded url
        if (loaded_url.contains("?")) {
            string params = loaded_url.chr(-1, '?');
            loaded_url = loaded_url.replace(params, "");
        }

        // were we redirected to the facebook login success page?
        if (loaded_url.contains("login_success")) {
            cache_dirty = true;
            login_succeeded(origin_frame.get_uri());
            return;
        }

        // were we redirected to the login total failure page?
        if (loaded_url.contains("login_failure")) {
            login_failed();
            return;
        }
    }
    
    private void on_load_started(WebKit.WebFrame frame) {
        window.set_cursor(new Gdk.Cursor(Gdk.CursorType.WATCH));
    }

    public static bool is_cache_dirty() {
        return cache_dirty;
    }
    
    public override void installed() {
        webview.open(get_login_url());
    }
}

private class PublishingOptionsPane : PublishingDialogPane {
    private const string HEADER_LABEL_TEXT = _("You are logged into Facebook as %s.\n\nWhere would you like to publish the selected photos?");

    private Gtk.RadioButton use_existing_radio = null;
    private Gtk.RadioButton create_new_radio = null;
    private Gtk.ComboBox existing_albums_combo = null;
    private Gtk.Entry new_album_entry = null;
    private Gtk.Button publish_button = null;
    private Gtk.Button logout_button = null;
    private Gtk.Label how_to_label = null;
    private Album[] albums = null;

    public signal void logout();
    public signal void publish(string target_album);

    public PublishingOptionsPane(string username, Album[] albums) {
        this.albums = albums;

        set_border_width(16);

        Gtk.SeparatorToolItem top_padding = new Gtk.SeparatorToolItem();
        top_padding.set_size_request(-1, 50);
        top_padding.set_draw(false);
        add(top_padding);

        Gtk.HBox how_to_label_layouter = new Gtk.HBox(false, 8);
        how_to_label = new Gtk.Label(HEADER_LABEL_TEXT.printf(username));
        Gtk.SeparatorToolItem how_to_pusher = new Gtk.SeparatorToolItem();
        how_to_pusher.set_draw(false);
        how_to_label_layouter.add(how_to_label);
        how_to_label_layouter.add(how_to_pusher);
        how_to_pusher.set_size_request(100, -1);
        add(how_to_label_layouter);

        Gtk.VBox album_mode_layouter = new Gtk.VBox(false, 8);
        album_mode_layouter.set_border_width(44);
        use_existing_radio = new Gtk.RadioButton.with_mnemonic(null,
            _("Publish to an e_xisting album:"));
        use_existing_radio.clicked += on_use_existing_toggled;
        create_new_radio = new Gtk.RadioButton.with_mnemonic(use_existing_radio.get_group(),
            _("Create a _new album named:"));
        create_new_radio.clicked += on_create_new_toggled;

        Gtk.HBox use_existing_layouter = new Gtk.HBox(false, 8);
        use_existing_layouter.add(use_existing_radio);
        existing_albums_combo = new Gtk.ComboBox.text();
        Gtk.Alignment existing_combo_aligner = new Gtk.Alignment(1.0f, 0.5f, 0.0f, 0.0f);
        existing_combo_aligner.add(existing_albums_combo);
        use_existing_layouter.add(existing_combo_aligner);

        Gtk.HBox create_new_layouter = new Gtk.HBox(false, 8);
        create_new_layouter.add(create_new_radio);
        new_album_entry = new Gtk.Entry();
        create_new_layouter.add(new_album_entry);
        new_album_entry.set_size_request(142, -1);

        publish_button = new Gtk.Button.with_mnemonic(_("_Publish"));
        publish_button.clicked += on_publish_button_clicked;
        logout_button = new Gtk.Button.with_mnemonic(_("_Logout"));
        logout_button.clicked += on_logout_button_clicked;
        Gtk.HBox buttons_layouter = new Gtk.HBox(false, 8);
        Gtk.SeparatorToolItem buttons_left_padding = new Gtk.SeparatorToolItem();
        buttons_left_padding.set_draw(false);
        Gtk.SeparatorToolItem buttons_right_padding = new Gtk.SeparatorToolItem();
        buttons_right_padding.set_draw(false);
        Gtk.SeparatorToolItem buttons_central_padding = new Gtk.SeparatorToolItem();
        buttons_central_padding.set_draw(false);
        buttons_layouter.add(buttons_left_padding);
        Gtk.Alignment logout_button_aligner = new Gtk.Alignment(0.5f, 0.5f, 0.0f, 0.0f);
        logout_button_aligner.add(logout_button);
        Gtk.Alignment publish_button_aligner = new Gtk.Alignment(0.5f, 0.5f, 0.0f, 0.0f);
        publish_button_aligner.add(publish_button);
        buttons_layouter.add(logout_button_aligner);
        buttons_layouter.add(buttons_central_padding);
        buttons_layouter.add(publish_button_aligner);
        buttons_layouter.add(buttons_right_padding);
        publish_button.set_size_request(PublishingDialog.STANDARD_ACTION_BUTTON_WIDTH, -1);
        logout_button.set_size_request(PublishingDialog.STANDARD_ACTION_BUTTON_WIDTH, -1);

        album_mode_layouter.add(use_existing_layouter);
        album_mode_layouter.add(create_new_layouter);

        add(album_mode_layouter);
        add(buttons_layouter);
        
        Gtk.SeparatorToolItem bottom_padding = new Gtk.SeparatorToolItem();
        bottom_padding.set_size_request(-1, 50);
        bottom_padding.set_draw(false);
        add(bottom_padding);
    }
  
    private void on_use_existing_toggled() {
        if (use_existing_radio.active) {
            existing_albums_combo.set_sensitive(true);
            new_album_entry.set_sensitive(false);
            existing_albums_combo.grab_focus();            
        }
    }
    
    private void on_create_new_toggled() {
        if (create_new_radio.active) {
            existing_albums_combo.set_sensitive(false);
            new_album_entry.set_sensitive(true);
            new_album_entry.grab_focus();
        }
    }
    
    private void on_logout_button_clicked() {
        logout();
    }
    
    private void on_publish_button_clicked() {
        if (use_existing_radio.active) {
            publish(existing_albums_combo.get_active_text());
        } else {
            publish(new_album_entry.get_text());
        }
    }

    public override void installed() {
        if (albums.length == 0) {
            create_new_radio.set_active(true);
            new_album_entry.set_text(DEFAULT_ALBUM_NAME);
            existing_albums_combo.set_sensitive(false);
            use_existing_radio.set_sensitive(false);
        } else {
            int default_album_seq_num = -1;
            int ticker = 0;
            foreach (Album album in albums) {
                existing_albums_combo.append_text(album.name);
                if (album.name == DEFAULT_ALBUM_NAME)
                    default_album_seq_num = ticker;
                ticker++;
            }
            if (default_album_seq_num != -1) {
                existing_albums_combo.set_active(default_album_seq_num);
                use_existing_radio.set_active(true);
                new_album_entry.set_sensitive(false);
            }
            else {
                create_new_radio.set_active(true);
                existing_albums_combo.set_active(0);
                existing_albums_combo.set_sensitive(false);
                new_album_entry.set_text(DEFAULT_ALBUM_NAME);
            }
        }
 
       publish_button.grab_focus();
    }
}

private class Transaction : RESTTransaction {
    public const string SIGNATURE_KEY = "sig";

    public Transaction(Session session) {
        base(session);
    }

    protected override void sign() {
        Session facebook_session = (Session) get_parent_session();
        add_argument("api_key", facebook_session.get_api_key());
        add_argument("session_key", facebook_session.get_session_key());
        add_argument("v", facebook_session.get_api_version());
        add_argument("call_id", facebook_session.get_next_call_id());

        string sig = generate_signature(get_sorted_arguments(), facebook_session);
       
        set_signature_key(SIGNATURE_KEY);
        set_signature_value(sig);
    }

    public static new string? check_response(RESTXmlDocument doc) {
        Xml.Node* root = doc.get_root_node();
        if (root->name != "error_response")
            return null;
        
        Xml.Node* error_code = null;
        try {
            error_code = doc.get_named_child(root, "error_code");
        } catch (PublishingError err) {
            warning("Unable to parse error response for error code");
        }
        
        Xml.Node* error_msg = null;
        try {
            error_msg = doc.get_named_child(root, "error_msg");
        } catch (PublishingError err) {
            warning("Unable to parse error response for error message");
        }

        // 102 errors occur when the session key has become invalid -- the correct behavior in this
        // case is to log the user out
        if ((error_code != null) && (error_code->get_content() == "102")) {
            PublishingDialog shell = PublishingDialog.get_active_instance();
            Interactor interactor = (Interactor) shell.get_interactor();
            interactor.logout_user();
        }

        return "%s (error code %s)".printf(error_msg != null ? error_msg->get_content() : "(unknown)",
            error_code != null ? error_code->get_content() : "(unknown)");
    }

    public static string generate_signature(RESTArgument[] sorted_args, Session session) {
        string hash_string = "";
        foreach (RESTArgument arg in sorted_args)
            hash_string = hash_string + ("%s=%s".printf(arg.key, arg.value));

        return Checksum.compute_for_string(ChecksumType.MD5, (hash_string +
            session.get_session_secret()));
    }
}

private class UserInfoTransaction : Transaction {
    public UserInfoTransaction(Session session, string user_id) {
        base(session);

        add_argument("method", "users.getInfo");
        add_argument("uids", user_id);
        add_argument("fields", "name");
    }
}

private class AlbumsFetchTransaction : Transaction {
    public AlbumsFetchTransaction(Session session) {
        base(session);

        assert(session.is_authenticated());

        add_argument("method", "photos.getAlbums");
    }
}

private class AlbumCreationTransaction : Transaction {
    public AlbumCreationTransaction(Session session, string album_name) {
        base(session);

        assert(session.is_authenticated());

        add_argument("method", "photos.createAlbum");
        add_argument("name", album_name);
    }
}

private class UploadTransaction : PhotoUploadTransaction {
    public UploadTransaction(Session session, string aid, string source_file_path,
        TransformablePhoto source_photo) {
        base(session, source_file_path, source_photo);

        add_argument("api_key", session.get_api_key());
        add_argument("session_key", session.get_session_key());
        add_argument("v", session.get_api_version());
        add_argument("method", "photos.upload");
        add_argument("aid", aid);
    }

    protected override void sign() {
        Session facebook_session = (Session) get_parent_session();
        add_argument("call_id", facebook_session.get_next_call_id());

        string sig = Transaction.generate_signature(get_sorted_arguments(),
            facebook_session);
       
        set_signature_key(Transaction.SIGNATURE_KEY);
        set_signature_value(sig);
    }
}

}

#endif

