/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

public class FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service {
    private const string ICON_FILENAME = "facebook.png";

    private static Gdk.Pixbuf[] icon_pixbuf_set = null;
    
    public FacebookService(GLib.File resource_directory) {
        if (icon_pixbuf_set == null)
            icon_pixbuf_set = Resources.load_from_resource
                (Resources.RESOURCE_PATH + "/" + ICON_FILENAME);
    }
    
    public int get_pluggable_interface(int min_host_interface, int max_host_interface) {
        return Spit.negotiate_interfaces(min_host_interface, max_host_interface,
            Spit.Publishing.CURRENT_INTERFACE);
    }

    public unowned string get_id() {
        return "org.yorba.shotwell.publishing.facebook";
    }

    public unowned string get_pluggable_name() {
        return "Facebook";
    }

    public void get_info(ref Spit.PluggableInfo info) {
        info.authors = "Lucas Beeler";
        info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc.");
        info.translators = Resources.TRANSLATORS;
        info.version = _VERSION;
        info.website_name = Resources.WEBSITE_NAME;
        info.website_url = Resources.WEBSITE_URL;
        info.is_license_wordwrapped = false;
        info.license = Resources.LICENSE;
        info.icons = icon_pixbuf_set;
    }

    public void activation(bool enabled) {
    }

    public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) {
        return new Publishing.Facebook.FacebookPublisher(this, host);
    }

    public Spit.Publishing.Publisher.MediaType get_supported_media() {
        return (Spit.Publishing.Publisher.MediaType.PHOTO |
            Spit.Publishing.Publisher.MediaType.VIDEO);
    }
}

namespace Publishing.Facebook {
// global parameters for the Facebook publishing plugin -- don't touch these (unless you really,
// truly, deep-down know what you're doing)
public const string SERVICE_NAME = "facebook";
internal const string USER_VISIBLE_NAME = "Facebook";
internal const string DEFAULT_ALBUM_NAME = _("Shotwell Connect");
internal const int EXPIRED_SESSION_STATUS_CODE = 400;

internal class Album {
    public string name;
    public string id;

    public Album(string name, string id) {
        this.name = name;
        this.id = id;
    }
}

internal enum Resolution {
    STANDARD,
    HIGH;

    public string get_name() {
        switch (this) {
            case STANDARD:
                return _("Standard (720 pixels)");

            case HIGH:
                return _("Large (2048 pixels)");

            default:
                error("Unknown resolution %s", this.to_string());
        }
    }

    public int get_pixels() {
        switch (this) {
            case STANDARD:
                return 720;

            case HIGH:
                return 2048;

            default:
                error("Unknown resolution %s", this.to_string());
        }
    }
}

internal class PublishingParameters {
    public const int UNKNOWN_ALBUM = -1;
    
    public bool strip_metadata;
    public Album[] albums;
    public int target_album;
    public string? new_album_name;  // the name of the new album being created during this
                                    // publishing interaction or null if publishing to an existing
                                    // album

    public string? privacy_object;  // a serialized JSON object encoding the privacy settings of the
                                    // published resources
    public Resolution resolution;
    
    public PublishingParameters() {
        this.albums = null;
        this.privacy_object = null;
        this.target_album = UNKNOWN_ALBUM;
        this.new_album_name = null;
        this.strip_metadata = false;
        this.resolution = Resolution.HIGH;
    }
    
    public void add_album(string name, string id) {
        if (albums == null)
            albums = new Album[0];
        
        Album new_album = new Album(name, id);
        albums += new_album;
    }
    
    public void set_target_album_by_name(string? name) {
        if (name == null) {
            target_album = UNKNOWN_ALBUM;
            return;
        }
    
        for (int i = 0; i < albums.length; i++) {

            if (albums[i].name == name) {
                target_album = i;
                return;
            }
        }
        
        target_album = UNKNOWN_ALBUM;
    }
    
    public string? get_target_album_name() {
        if (albums == null || target_album == UNKNOWN_ALBUM)
            return null;

        return albums[target_album].name;
    }
    
    public string? get_target_album_id() {
        if (albums == null || target_album == UNKNOWN_ALBUM)
            return null;

        return albums[target_album].id;    
    }
}

public class FacebookPublisher : Spit.Publishing.Publisher, GLib.Object {
    private PublishingParameters publishing_params;
    private weak Spit.Publishing.PluginHost host = null;
    private Spit.Publishing.ProgressCallback progress_reporter = null;
    private weak Spit.Publishing.Service service = null;
    private Spit.Publishing.Authenticator authenticator = null;
    private bool running = false;
    private GraphSession graph_session;
    private PublishingOptionsPane? publishing_options_pane = null;
    private Uploader? uploader = null;
    private string? uid = null;
    private string? username = null;

    public FacebookPublisher(Spit.Publishing.Service service,
                             Spit.Publishing.PluginHost host) {
        debug("FacebookPublisher instantiated.");

        this.service = service;
        this.host = host;

        this.publishing_params = new PublishingParameters();
        this.authenticator =
            Publishing.Authenticator.Factory.get_instance().create("facebook",
                    host);

        this.graph_session = new GraphSession();
        graph_session.authenticated.connect(on_session_authenticated);
    }

    private bool get_persistent_strip_metadata() {
        return host.get_config_bool("strip_metadata", false);
    }

    private void set_persistent_strip_metadata(bool strip_metadata) {
        host.set_config_bool("strip_metadata", strip_metadata);
    }

    // Part of the fix for #3232. These have to be 
    // public so the legacy options pane may use them.
    public int get_persistent_default_size() {
        return host.get_config_int("default_size", 0);
    }
    
    public void set_persistent_default_size(int size) {
        host.set_config_int("default_size", size);
    }

    /*
    private void do_test_connection_to_endpoint() {
        debug("ACTION: testing connection to Facebook endpoint.");
        host.set_service_locked(true);
        
        host.install_static_message_pane(_("Testing connection to Facebook…"));
        
        GraphMessage endpoint_test_message = graph_session.new_endpoint_test();
        endpoint_test_message.completed.connect(on_endpoint_test_completed);
        endpoint_test_message.failed.connect(on_endpoint_test_error);
        
        graph_session.send_message(endpoint_test_message);
    }
    */
    
    private void do_fetch_user_info() {
        debug("ACTION: fetching user information.");
        
        host.set_service_locked(true);
        host.install_account_fetch_wait_pane();
        
        GraphMessage user_info_message = graph_session.new_query("/me");
        
        user_info_message.completed.connect(on_fetch_user_info_completed);
        user_info_message.failed.connect(on_fetch_user_info_error);
        
        graph_session.send_message(user_info_message);
    }

    private void do_fetch_album_descriptions() {
        debug("ACTION: fetching album list.");

        host.set_service_locked(true);
        host.install_account_fetch_wait_pane();
        
        GraphMessage albums_message = graph_session.new_query("/%s/albums".printf(uid));
        
        albums_message.completed.connect(on_fetch_albums_completed);
        albums_message.failed.connect(on_fetch_albums_error);
        
        graph_session.send_message(albums_message);
    }
    
    private void do_extract_user_info_from_json(string json) {
        debug("ACTION: extracting user info from JSON response.");
        
        try {
            Json.Parser parser = new Json.Parser();
            parser.load_from_data(json);
            
            Json.Node root = parser.get_root();
            Json.Object response_object = root.get_object();
            uid = response_object.get_string_member("id");
            username = response_object.get_string_member("name");
        } catch (Error error) {
            host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message));
            return;
        }
        
        on_user_info_extracted();
    }

    private void do_extract_albums_from_json(string json) {
        debug("ACTION: extracting album info from JSON response.");

        try {
            Json.Parser parser = new Json.Parser();
            parser.load_from_data(json);
            
            Json.Node root = parser.get_root();
            Json.Object response_object = root.get_object();
            Json.Array album_list = response_object.get_array_member("data");
            
            publishing_params.albums = new Album[0];

            for (int i = 0; i < album_list.get_length(); i++) {
                Json.Object current_album = album_list.get_object_element(i);
                string album_id = current_album.get_string_member("id");
                string album_name = current_album.get_string_member("name");

                // Note that we are completely ignoring the "can_upload" flag in the list of albums
                // that we pulled from facebook eariler -- effectively, we add every album to the
                // publishing_params album list regardless of the value of its can_upload flag. In
                // the future we may wish to make adding to the publishing_params album list
                // conditional on the value of the can_upload flag being true
                publishing_params.add_album(album_name, album_id);
            }
        } catch (Error error) {
            host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message));
            return;
        }

        on_albums_extracted();
    }
    
    private void do_create_new_album() {
        debug("ACTION: creating a new album named \"%s\".\n", publishing_params.new_album_name);
        
        host.set_service_locked(true);
        host.install_static_message_pane(_("Creating album…"));
        
        GraphMessage create_album_message = graph_session.new_create_album(
            publishing_params.new_album_name, publishing_params.privacy_object);
        
        create_album_message.completed.connect(on_create_album_completed);
        create_album_message.failed.connect(on_create_album_error);

        graph_session.send_message(create_album_message);
    }

    private void do_show_publishing_options_pane() {
        debug("ACTION: showing publishing options pane.");

        host.set_service_locked(false);
        Gtk.Builder builder = new Gtk.Builder();

        try {
            // the trailing get_path() is required, since add_from_file can't cope
            // with File objects directly and expects a pathname instead.
            builder.add_from_resource (Resources.RESOURCE_PATH + "/" +
                    "facebook_publishing_options_pane.ui");
        } catch (Error e) {
            warning("Could not parse UI file! Error: %s.", e.message);
            host.post_error(
                new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
                    _("A file required for publishing is unavailable. Publishing to Facebook can’t continue.")));
            return;
        }
        
        publishing_options_pane = new PublishingOptionsPane(username, publishing_params.albums,
            host.get_publishable_media_type(), this, builder, get_persistent_strip_metadata(),
            authenticator.can_logout());
        publishing_options_pane.logout.connect(on_publishing_options_pane_logout);
        publishing_options_pane.publish.connect(on_publishing_options_pane_publish);
        host.install_dialog_pane(publishing_options_pane,
            Spit.Publishing.PluginHost.ButtonMode.CANCEL);
    }

    private void do_logout() {
        debug("ACTION: clearing persistent session information and restaring interaction.");
        this.authenticator.logout();

        running = false;
        start();
    }

    private void do_add_new_local_album_from_json(string album_name, string json) {
        try {
            Json.Parser parser = new Json.Parser();
            parser.load_from_data(json);
            
            Json.Node root = parser.get_root();
            Json.Object response_object = root.get_object();
            string album_id = response_object.get_string_member("id");
            
            publishing_params.add_album(album_name, album_id);
        } catch (Error error) {
            host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message));
            return;
        }
        
        publishing_params.set_target_album_by_name(album_name);
        do_upload();
    }


    private void on_authenticator_succeeded() {
        debug("EVENT: Authenticator login succeeded.");

        do_authenticate_session();
    }

    private void on_authenticator_failed() {
    }

    private void do_authenticate_session() {
        var parameter = this.authenticator.get_authentication_parameter();
        Variant access_token;
        if (!parameter.lookup_extended("AccessToken", null, out access_token)) {
            critical("Authenticator signalled success, but does not provide access token");
            assert_not_reached();
        }
        graph_session.authenticated.connect(on_session_authenticated);
        graph_session.authenticate(access_token.get_string());
    }

    private void do_upload() {
        debug("ACTION: uploading photos to album '%s'",
            publishing_params.target_album == PublishingParameters.UNKNOWN_ALBUM ? "(none)" :
            publishing_params.get_target_album_name());

        host.set_service_locked(true);

        progress_reporter = host.serialize_publishables(publishing_params.resolution.get_pixels(),
            publishing_params.strip_metadata);

        // Serialization is a long and potentially cancellable operation, so before we use
        // the publishables, make sure that the publishing interaction is still running. If it
        // isn't the publishing environment may be partially torn down so do a short-circuit
        // return
        if (!is_running())
            return;

        Spit.Publishing.Publishable[] publishables = host.get_publishables();
        uploader = new Uploader(graph_session, publishing_params, publishables);

        uploader.upload_complete.connect(on_upload_complete);
        uploader.upload_error.connect(on_upload_error);

        uploader.upload(on_upload_status_updated);
    }
    
    private void do_show_success_pane() {
        debug("ACTION: showing success pane.");

        host.set_service_locked(false);
        host.install_success_pane();
    }

    private void on_generic_error(Spit.Publishing.PublishingError error) {
        if (error is Spit.Publishing.PublishingError.EXPIRED_SESSION)
            do_logout();
        else
            host.post_error(error);
    }

#if 0
    private void on_endpoint_test_completed(GraphMessage message) {
        message.completed.disconnect(on_endpoint_test_completed);
        message.failed.disconnect(on_endpoint_test_error);

        if (!is_running())
            return;

        debug("EVENT: endpoint test transaction detected that the Facebook endpoint is alive.");

        do_hosted_web_authentication();
    }

    private void on_endpoint_test_error(GraphMessage message,
        Spit.Publishing.PublishingError error) {
        message.completed.disconnect(on_endpoint_test_completed);
        message.failed.disconnect(on_endpoint_test_error);

        if (!is_running())
            return;

        debug("EVENT: endpoint test transaction failed to detect a connection to the Facebook " +
            "endpoint" + error.message);

        on_generic_error(error);
    }
#endif

    private void on_session_authenticated() {
        graph_session.authenticated.disconnect(on_session_authenticated);
        
        if (!is_running())
            return;

        assert(graph_session.is_authenticated());
        debug("EVENT: an authenticated session has become available.");

        do_fetch_user_info();
    }
    
    private void on_fetch_user_info_completed(GraphMessage message) {
        message.completed.disconnect(on_fetch_user_info_completed);
        message.failed.disconnect(on_fetch_user_info_error);

        if (!is_running())
            return;

        debug("EVENT: user info fetch completed; response = '%s'.", message.get_response_body());
        
        do_extract_user_info_from_json(message.get_response_body());
    }
    
    private void on_fetch_user_info_error(GraphMessage message,
        Spit.Publishing.PublishingError error) {
        message.completed.disconnect(on_fetch_user_info_completed);
        message.failed.disconnect(on_fetch_user_info_error);
        
        if (!is_running())
            return;

        debug("EVENT: fetching user info generated and error.");

        on_generic_error(error);
    }
    
    private void on_user_info_extracted() {
        if (!is_running())
            return;

        debug("EVENT: user info extracted from JSON response: uid = %s; name = %s.", uid, username);

        do_fetch_album_descriptions();
    }
    
    private void on_fetch_albums_completed(GraphMessage message) {
        message.completed.disconnect(on_fetch_albums_completed);
        message.failed.disconnect(on_fetch_albums_error);
        
        if (!is_running())
            return;

        debug("EVENT: album descriptions fetch transaction completed; response = '%s'.",
            message.get_response_body());

        do_extract_albums_from_json(message.get_response_body());
    }
    
    private void on_fetch_albums_error(GraphMessage message,
        Spit.Publishing.PublishingError err) {
        message.completed.disconnect(on_fetch_albums_completed);
        message.failed.disconnect(on_fetch_albums_error);
        
        if (!is_running())
            return;

        debug("EVENT: album description fetch attempt generated an error.");

        on_generic_error(err);
    }

    private void on_albums_extracted() {
        if (!is_running())
            return;

        debug("EVENT: successfully extracted %d albums from JSON response",
            publishing_params.albums.length);

        do_show_publishing_options_pane();
    }

    private void on_publishing_options_pane_logout() {
        publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish);
        publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout);

        if (!is_running())
            return;

        debug("EVENT: user clicked 'Logout' in publishing options pane.");

        do_logout();
    }

    private void on_publishing_options_pane_publish(string? target_album, string privacy_setting,
        Resolution resolution, bool strip_metadata) {
        publishing_options_pane.publish.disconnect(on_publishing_options_pane_publish);
        publishing_options_pane.logout.disconnect(on_publishing_options_pane_logout);
        
        if (!is_running())
            return;
        
        debug("EVENT: user clicked 'Publish' in publishing options pane.");
        
        publishing_params.strip_metadata = strip_metadata;
        set_persistent_strip_metadata(strip_metadata);
        publishing_params.resolution = resolution;
        set_persistent_default_size(resolution);
        publishing_params.privacy_object = privacy_setting;
        
        if (target_album != null) {
            // we are publishing at least one photo so we need the name of an album to which
            // we'll upload the photo(s)
            publishing_params.set_target_album_by_name(target_album);
            if (publishing_params.target_album != PublishingParameters.UNKNOWN_ALBUM) {
                do_upload();
            } else {
                publishing_params.new_album_name = target_album;
                do_create_new_album();
            }
        } else {
            // we're publishing only videos and we don't need an album name
            do_upload();
        }
    }
    
    private void on_create_album_completed(GraphMessage message) {
        message.completed.disconnect(on_create_album_completed);
        message.failed.disconnect(on_create_album_error);
        
        assert(publishing_params.new_album_name != null);
        
        if (!is_running())
            return;

        debug("EVENT: created new album resource on remote host; response body = %s.\n",
            message.get_response_body());

        do_add_new_local_album_from_json(publishing_params.new_album_name,
            message.get_response_body());
    }
    
    private void on_create_album_error(GraphMessage message, Spit.Publishing.PublishingError err) {
        message.completed.disconnect(on_create_album_completed);
        message.failed.disconnect(on_create_album_error);

        if (!is_running())
            return;

        debug("EVENT: attempt to create new album generated an error.");

        on_generic_error(err);
    }
    
    private void on_upload_status_updated(int file_number, double completed_fraction) {
        if (!is_running())
            return;

        debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction);

        assert(progress_reporter != null);

        progress_reporter(file_number, completed_fraction);
    }

    private void on_upload_complete(Uploader uploader, int num_published) {
        uploader.upload_complete.disconnect(on_upload_complete);
        uploader.upload_error.disconnect(on_upload_error);
        
        if (!is_running())
            return;

        debug("EVENT: uploader reports upload complete; %d items published.", num_published);

        do_show_success_pane();
    }

    private void on_upload_error(Uploader uploader, Spit.Publishing.PublishingError err) {
        uploader.upload_complete.disconnect(on_upload_complete);
        uploader.upload_error.disconnect(on_upload_error);
        
        if (!is_running())
            return;

        debug("EVENT: uploader reports upload error = '%s'.", err.message);

        host.post_error(err);
    }

    public Spit.Publishing.Service get_service() {
        return service;
    }

    public string get_service_name() {
        return SERVICE_NAME;
    }

    public string get_user_visible_name() {
        return USER_VISIBLE_NAME;
    }

    public void start() {
        if (is_running())
            return;

        debug("FacebookPublisher: starting interaction.");

        running = true;

        // reset all publishing parameters to their default values -- in case this start is
        // actually a restart
        publishing_params = new PublishingParameters();

        this.authenticator.authenticated.connect(on_authenticator_succeeded);
        this.authenticator.authentication_failed.connect(on_authenticator_failed);
        this.authenticator.authenticate();
    }

    public void stop() {
        debug("FacebookPublisher: stop( ) invoked.");

        if (graph_session != null)
            graph_session.stop_transactions();

        host = null;
        running = false;
    }
    
    public bool is_running() {
        return running;
    }
}

internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object {
    private Gtk.Builder builder;
    private Gtk.Box pane_widget = null;
    private Gtk.RadioButton use_existing_radio = null;
    private Gtk.RadioButton create_new_radio = null;
    private Gtk.ComboBoxText existing_albums_combo = null;
    private Gtk.ComboBoxText visibility_combo = null;
    private Gtk.Entry new_album_entry = null;
    private Gtk.CheckButton strip_metadata_check = null;
    private Gtk.Button publish_button = null;
    private Gtk.Button logout_button = null;
    private Gtk.Label how_to_label = null;
    private Album[] albums = null;
    private FacebookPublisher publisher = null;
    private PrivacyDescription[] privacy_descriptions;

    private Resolution[] possible_resolutions;
    private Gtk.ComboBoxText resolution_combo = null;

    private Spit.Publishing.Publisher.MediaType media_type;

    private const string HEADER_LABEL_TEXT = _("You are logged into Facebook as %s.\n\n");
    private const string PHOTOS_LABEL_TEXT = _("Where would you like to publish the selected photos?");
    private const string RESOLUTION_LABEL_TEXT = _("Upload _size:");
    private const int CONTENT_GROUP_SPACING = 32;
    private const int STANDARD_ACTION_BUTTON_WIDTH = 128;

    public signal void logout();
    public signal void publish(string? target_album, string privacy_setting,
        Resolution target_resolution, bool strip_metadata);

    private class PrivacyDescription {
        public string description;
        public string privacy_setting;

        public PrivacyDescription(string description, string privacy_setting) {
            this.description = description;
            this.privacy_setting = privacy_setting;
        }
    }

    public PublishingOptionsPane(string username, Album[] albums,
        Spit.Publishing.Publisher.MediaType media_type, FacebookPublisher publisher,
        Gtk.Builder builder, bool strip_metadata, bool can_logout) {

        this.builder = builder;
        assert(builder != null);
        assert(builder.get_objects().length() > 0);

        this.albums = albums;
        this.privacy_descriptions = create_privacy_descriptions();

        this.possible_resolutions = create_resolution_list();
        this.publisher = publisher;

        // we'll need to know if the user is importing video or not when sorting out visibility.
        this.media_type = media_type;

        pane_widget = (Gtk.Box) builder.get_object("facebook_pane_box");
        pane_widget.set_border_width(16);

        use_existing_radio = (Gtk.RadioButton) this.builder.get_object("use_existing_radio");
        create_new_radio = (Gtk.RadioButton) this.builder.get_object("create_new_radio");
        existing_albums_combo = (Gtk.ComboBoxText) this.builder.get_object("existing_albums_combo");
        visibility_combo = (Gtk.ComboBoxText) this.builder.get_object("visibility_combo");
        publish_button = (Gtk.Button) this.builder.get_object("publish_button");
        logout_button = (Gtk.Button) this.builder.get_object("logout_button");
        if (!can_logout) {
            logout_button.parent.remove (logout_button);
        }
        new_album_entry = (Gtk.Entry) this.builder.get_object("new_album_entry");
        resolution_combo = (Gtk.ComboBoxText) this.builder.get_object("resolution_combo");
        how_to_label = (Gtk.Label) this.builder.get_object("how_to_label");
        strip_metadata_check = (Gtk.CheckButton) this.builder.get_object("strip_metadata_check");

        create_new_radio.clicked.connect(on_create_new_toggled);
        use_existing_radio.clicked.connect(on_use_existing_toggled);

        string label_text = HEADER_LABEL_TEXT.printf(username);
        if ((media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0)
            label_text += PHOTOS_LABEL_TEXT;
        how_to_label.set_label(label_text);
        strip_metadata_check.set_active(strip_metadata);

        setup_visibility_combo();
        visibility_combo.set_active(0);

        publish_button.clicked.connect(on_publish_button_clicked);
        logout_button.clicked.connect(on_logout_button_clicked);
        
        setup_resolution_combo();
        resolution_combo.set_active(publisher.get_persistent_default_size());
        resolution_combo.changed.connect(on_size_changed);
        
        // Ticket #3175, part 2: make sure this widget starts out sensitive
        // if it needs to by checking whether we're starting with a video
        // or a new gallery.
        visibility_combo.set_sensitive(
            (create_new_radio != null && create_new_radio.active) ||
            ((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0));
        
        // if publishing only videos, disable all photo-specific controls
        if (media_type == Spit.Publishing.Publisher.MediaType.VIDEO) {
            strip_metadata_check.set_active(false);
            strip_metadata_check.set_sensitive(false);
            resolution_combo.set_sensitive(false);
            use_existing_radio.set_sensitive(false);
            create_new_radio.set_sensitive(false);
            existing_albums_combo.set_sensitive(false);
            new_album_entry.set_sensitive(false);
        }
    }
    
    private bool publishing_photos() {
        return (media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0;
    }

    private void setup_visibility_combo() {
        foreach (PrivacyDescription p in privacy_descriptions)
            visibility_combo.append_text(p.description);
    }

    private void setup_resolution_combo() {
        foreach (Resolution res in possible_resolutions)
            resolution_combo.append_text(res.get_name());
    }

    private void on_use_existing_toggled() {
        if (use_existing_radio.active) {
            existing_albums_combo.set_sensitive(true);
            new_album_entry.set_sensitive(false);

            // Ticket #3175 - if we're not adding a new gallery
            // or a video, then we shouldn't be allowed tof
            // choose visibility, since it has no effect.
            visibility_combo.set_sensitive((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0);

            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();

            // Ticket #3175 - if we're creating a new gallery, make sure this is
            // active, since it may have possibly been set inactive.
            visibility_combo.set_sensitive(true);
        }
    }

    private void on_size_changed() {
        publisher.set_persistent_default_size(resolution_combo.get_active());
    }

    private void on_logout_button_clicked() {
        logout();
    }

    private void on_publish_button_clicked() {
        string album_name;
        string privacy_setting = privacy_descriptions[visibility_combo.get_active()].privacy_setting;

        Resolution resolution_setting;

        if (publishing_photos()) {        
            resolution_setting = possible_resolutions[resolution_combo.get_active()];
            if (use_existing_radio.active) {
                album_name = existing_albums_combo.get_active_text();
            } else {
                album_name = new_album_entry.get_text();
            }
        } else {
            resolution_setting = Resolution.STANDARD;
            album_name = null;
        }

        publish(album_name, privacy_setting, resolution_setting, strip_metadata_check.get_active());
    }

    private PrivacyDescription[] create_privacy_descriptions() {
        PrivacyDescription[] result = new PrivacyDescription[0];

        result += new PrivacyDescription(_("Just me"), "{ 'value' : 'SELF' }");
        result += new PrivacyDescription(_("Friends"), "{ 'value' : 'ALL_FRIENDS' }");
        result += new PrivacyDescription(_("Everyone"), "{ 'value' : 'EVERYONE' }");

        return result;
    }

    private Resolution[] create_resolution_list() {
        Resolution[] result = new Resolution[0];

        result += Resolution.STANDARD;
        result += Resolution.HIGH;

        return result;
    }

    public void installed() {
        if (publishing_photos()) {
            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 void notify_logout() {
        logout();
    }

    private void notify_publish(string? target_album, string privacy_setting, Resolution target_resolution) {
        publish(target_album, privacy_setting, target_resolution, strip_metadata_check.get_active());
    }

    public Gtk.Widget get_widget() {
        return pane_widget;
    }

    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
    }

    public void on_pane_installed() {
        logout.connect(notify_logout);
        publish.connect(notify_publish);

        installed();
    }

    public void on_pane_uninstalled() {
        logout.disconnect(notify_logout);
        publish.disconnect(notify_publish);
    }
}

internal enum Endpoint {
    DEFAULT,
    VIDEO,
    TEST_CONNECTION;
    
    public string to_uri() {
        switch (this) {
            case DEFAULT:
                return "https://graph.facebook.com/";
                
            case VIDEO:
                return "https://graph-video.facebook.com/";
                
            case TEST_CONNECTION:
                return "https://www.facebook.com/";
                
            default:
                assert_not_reached();
        }
    }
}

internal abstract class GraphMessage {
    public signal void completed();
    public signal void failed(Spit.Publishing.PublishingError err);
    public signal void data_transmitted(int bytes_sent_so_far, int total_bytes);

    public abstract string get_uri();
    public abstract string get_response_body();
}

internal class GraphSession {
    private abstract class GraphMessageImpl : GraphMessage {
        public Publishing.RESTSupport.HttpMethod method;
        public string uri;
        public string access_token;
        public Soup.Message soup_message;
        public weak GraphSession host_session;
        public int bytes_so_far;
        
        public GraphMessageImpl(GraphSession host_session, Publishing.RESTSupport.HttpMethod method,
            string relative_uri, string access_token, Endpoint endpoint = Endpoint.DEFAULT) {
            this.method = method;
            this.access_token = access_token;
            this.host_session = host_session;
            this.bytes_so_far = 0;
            
            string endpoint_uri = endpoint.to_uri();
            try {
                Regex starting_slashes = new Regex("^/+");
                this.uri = endpoint_uri + starting_slashes.replace(relative_uri, -1, 0, "");
            } catch (RegexError err) {
                assert_not_reached();
            }
        }
        
        public virtual bool prepare_for_transmission() {
            return true;
        }

        public override string get_uri() {
            return uri;
        }
    
        public override string get_response_body() {
            return (string) soup_message.response_body.data;
        }
        
        public void on_wrote_body_data(Soup.Buffer chunk) {
            bytes_so_far += (int) chunk.length;
            
            data_transmitted(bytes_so_far, (int) soup_message.request_body.length);
        }
    }
    
    private class GraphQueryMessage : GraphMessageImpl {
        public GraphQueryMessage(GraphSession host_session, string relative_uri,
            string access_token) {
            base(host_session, Publishing.RESTSupport.HttpMethod.GET, relative_uri, access_token);

            Soup.URI destination_uri = new Soup.URI(uri + "?access_token=" + access_token);
            soup_message = new Soup.Message.from_uri(method.to_string(), destination_uri);
            soup_message.wrote_body_data.connect(on_wrote_body_data);
        }
    }
    
    private class GraphEndpointProbeMessage : GraphMessageImpl {
        public GraphEndpointProbeMessage(GraphSession host_session) {
            base(host_session, Publishing.RESTSupport.HttpMethod.GET, "/", "",
                Endpoint.TEST_CONNECTION);

            soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri));
            soup_message.wrote_body_data.connect(on_wrote_body_data);
        }
    }
    
    private class GraphUploadMessage : GraphMessageImpl {
        private MappedFile mapped_file = null;
        private Spit.Publishing.Publishable publishable;
        
        public GraphUploadMessage(GraphSession host_session, string access_token,
            string relative_uri, Spit.Publishing.Publishable publishable,
            bool suppress_titling, string? resource_privacy = null) {
            base(host_session, Publishing.RESTSupport.HttpMethod.POST, relative_uri, access_token,
                (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ?
                Endpoint.VIDEO : Endpoint.DEFAULT);
            
            // Video uploads require a privacy string at the per-resource level. Since they aren't
            // placed in albums, they can't inherit their privacy settings from their containing
            // album like photos do
            assert(publishable.get_media_type() != Spit.Publishing.Publisher.MediaType.VIDEO ||
                resource_privacy != null);
            
            this.publishable = publishable;
            
            // attempt to map the binary payload from disk into memory
            try {
                this.mapped_file = new MappedFile(publishable.get_serialized_file().get_path(),
                    false);
            } catch (FileError e) {
                return;
            }
            
            this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri));
            soup_message.wrote_body_data.connect(on_wrote_body_data);
            
            unowned uint8[] payload = (uint8[]) mapped_file.get_contents();
            payload.length = (int) mapped_file.get_length();
            
            Soup.Buffer image_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, payload);
            
            Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data");
            
            mp_envelope.append_form_string("access_token", access_token);
            
            if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO)
                mp_envelope.append_form_string("privacy", resource_privacy);
            
            //Get photo title and post it as message on FB API
            string publishable_title = publishable.get_param_string("title");
            if (!suppress_titling && publishable_title != null)
                mp_envelope.append_form_string("name", publishable_title);
                
            //Set 'message' data field with EXIF comment field. Title has precedence.
            string publishable_comment = publishable.get_param_string("comment");
            if (!suppress_titling && publishable_comment != null)
               mp_envelope.append_form_string("message", publishable_comment);
            
            //Sets correct date of the picture
            if (!suppress_titling)
               mp_envelope.append_form_string("backdated_time", publishable.get_exposure_date_time().to_string());

            string source_file_mime_type =
                (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ?
                "video" : "image/jpeg";
            mp_envelope.append_form_file("source", publishable.get_serialized_file().get_basename(),
                source_file_mime_type, image_data);
            
            mp_envelope.to_message(soup_message.request_headers, soup_message.request_body);
        }
        
        public override bool prepare_for_transmission() {
            if (mapped_file == null) {
                failed(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
                    "File %s is unavailable.".printf(publishable.get_serialized_file().get_path())));
                return false;
            } else {
                return true;
            }
        }
    }

    private class GraphCreateAlbumMessage : GraphMessageImpl {
        public GraphCreateAlbumMessage(GraphSession host_session, string access_token,
            string album_name, string album_privacy) {
            base(host_session, Publishing.RESTSupport.HttpMethod.POST, "/me/albums", access_token);
            
            assert(album_privacy != null && album_privacy != "");
            
            this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri));
            
            Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data");

            mp_envelope.append_form_string("access_token", access_token);
            mp_envelope.append_form_string("name", album_name);
            mp_envelope.append_form_string("privacy", album_privacy);
            
            mp_envelope.to_message(soup_message.request_headers, soup_message.request_body);
        }
    }

    public signal void authenticated();
    
    private Soup.Session soup_session;
    private string? access_token;
    private GraphMessage? current_message;
    
    public GraphSession() {
        this.soup_session = new Soup.Session ();
        this.soup_session.request_unqueued.connect (on_request_unqueued);
        this.soup_session.timeout = 15;
        this.access_token = null;
        this.current_message = null;
        this.soup_session.ssl_use_system_ca_file = true;
    }

    ~GraphSession() {
         soup_session.request_unqueued.disconnect (on_request_unqueued);
     }
    
    private void manage_message(GraphMessage msg) {
        assert(current_message == null);
        
        current_message = msg;
    }
    
    private void unmanage_message(GraphMessage msg) {
        assert(current_message != null);
        
        current_message = null;
    }

     private void on_request_unqueued(Soup.Message msg) {
        assert(current_message != null);
        GraphMessageImpl real_message = (GraphMessageImpl) current_message;
        assert(real_message.soup_message == msg);
        
        // these error types are always recoverable given the unique behavior of the Facebook
        // endpoint, so try again
        if (msg.status_code == Soup.KnownStatusCode.IO_ERROR ||
            msg.status_code == Soup.KnownStatusCode.MALFORMED ||
            msg.status_code == Soup.KnownStatusCode.TRY_AGAIN) {
            real_message.bytes_so_far = 0;
            soup_session.queue_message(msg, null);
            return;
        }
        
        unmanage_message(real_message);
        msg.wrote_body_data.disconnect(real_message.on_wrote_body_data);
        
        Spit.Publishing.PublishingError? error = null;
        switch (msg.status_code) {
            case Soup.KnownStatusCode.OK:
            case Soup.KnownStatusCode.CREATED: // HTTP code 201 (CREATED) signals that a new
                                               // resource was created in response to a PUT
                                               // or POST
            break;
            
            case EXPIRED_SESSION_STATUS_CODE:
                error = new Spit.Publishing.PublishingError.EXPIRED_SESSION(
                    "OAuth Access Token has Expired. Logout user.");
            break;
            
            case Soup.KnownStatusCode.CANT_RESOLVE:
            case Soup.KnownStatusCode.CANT_RESOLVE_PROXY:
                error = new Spit.Publishing.PublishingError.NO_ANSWER(
                    "Unable to resolve %s (error code %u)", real_message.get_uri(), msg.status_code);
            break;
            
            case Soup.KnownStatusCode.CANT_CONNECT:
            case Soup.KnownStatusCode.CANT_CONNECT_PROXY:
                error = new Spit.Publishing.PublishingError.NO_ANSWER(
                    "Unable to connect to %s (error code %u)", real_message.get_uri(), msg.status_code);
            break;
            
            default:
                // status codes below 100 are used by Soup, 100 and above are defined HTTP
                // codes
                if (msg.status_code >= 100) {
                    error = new Spit.Publishing.PublishingError.NO_ANSWER(
                        "Service %s returned HTTP status code %u %s", real_message.get_uri(),
                        msg.status_code, msg.reason_phrase);
                } else {
                    debug(msg.reason_phrase);
                    error = new Spit.Publishing.PublishingError.NO_ANSWER(
                        "Failure communicating with %s (error code %u)", real_message.get_uri(),
                        msg.status_code);
                }
            break;
        }

        // All valid communication with Facebook involves body data in the response
        if (error == null)
            if (msg.response_body.data == null || msg.response_body.data.length == 0)
                error = new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
                    "No response data from %s", real_message.get_uri());
        
        if (error == null)
            real_message.completed();
        else
            real_message.failed(error);
     }
    
    public void authenticate(string access_token) {
        this.access_token = access_token;
        authenticated();
    }
    
    public bool is_authenticated() {
        return access_token != null;
    }
    
#if 0
    public GraphMessage new_endpoint_test() {
        return new GraphEndpointProbeMessage(this);
    }
#endif
    
    public GraphMessage new_query(string resource_path) {
        return new GraphQueryMessage(this, resource_path, access_token);
    }

    public GraphMessage new_upload(string resource_path, Spit.Publishing.Publishable publishable,
        bool suppress_titling, string? resource_privacy = null) {
        return new GraphUploadMessage(this, access_token, resource_path, publishable,
            suppress_titling, resource_privacy);
    }
    
    public GraphMessage new_create_album(string album_name, string privacy) {
        return new GraphSession.GraphCreateAlbumMessage(this, access_token, album_name, privacy);
    }
    
    public void send_message(GraphMessage message) {
        GraphMessageImpl real_message = (GraphMessageImpl) message;
        
        debug("making HTTP request to URI: " + real_message.soup_message.uri.to_string(false));
        
        if (real_message.prepare_for_transmission()) {
            manage_message(message);
            soup_session.queue_message(real_message.soup_message, null);
        }
    }
    
    public void stop_transactions() {
        soup_session.abort();
    }
}

internal class Uploader {
    private int current_file;
    private Spit.Publishing.Publishable[] publishables;
    private GraphSession session;
    private PublishingParameters publishing_params;
	private unowned Spit.Publishing.ProgressCallback? status_updated = null;

    public signal void upload_complete(int num_photos_published);
    public signal void upload_error(Spit.Publishing.PublishingError err);

    public Uploader(GraphSession session, PublishingParameters publishing_params,
        Spit.Publishing.Publishable[] publishables) {
        this.current_file = 0;
        this.publishables = publishables;
        this.session = session;
        this.publishing_params = publishing_params;
    }
    
    private void send_current_file() {
        Spit.Publishing.Publishable publishable = publishables[current_file];
        GLib.File? file = publishable.get_serialized_file();
            
        // if the current publishable hasn't been serialized, then skip it
        if (file == null) {
            current_file++;
            return;
        }
        
        string resource_uri =
            (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.PHOTO) ?
            "/%s/photos".printf(publishing_params.get_target_album_id()) : "/me/videos";
        string? resource_privacy =
            (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ?
            publishing_params.privacy_object : null;
        GraphMessage upload_message = session.new_upload(resource_uri, publishable,
            publishing_params.strip_metadata, resource_privacy);

        upload_message.data_transmitted.connect(on_chunk_transmitted);
        upload_message.completed.connect(on_message_completed);
        upload_message.failed.connect(on_message_failed);
        
        session.send_message(upload_message);
    }

    private void send_files() {
        current_file = 0;
        send_current_file();
    }
    
    private void on_chunk_transmitted(int bytes_written_so_far, int total_bytes) {
        double file_span = 1.0 / publishables.length;
        double this_file_fraction_complete = ((double) bytes_written_so_far) / total_bytes;
        double fraction_complete = (current_file * file_span) + (this_file_fraction_complete *
            file_span);

		if (status_updated != null)
	        status_updated(current_file + 1, fraction_complete);
    }
    
    private void on_message_completed(GraphMessage message) {
        message.data_transmitted.disconnect(on_chunk_transmitted);
        message.completed.disconnect(on_message_completed);
        message.failed.disconnect(on_message_failed);

        current_file++;
        if (current_file < publishables.length) {
            send_current_file();
        } else {
            upload_complete(current_file);
        }
    }
    
    private void on_message_failed(GraphMessage message, Spit.Publishing.PublishingError error) {
        message.data_transmitted.disconnect(on_chunk_transmitted);
        message.completed.disconnect(on_message_completed);
        message.failed.disconnect(on_message_failed);
        
        upload_error(error);
    }
        
    public void upload(Spit.Publishing.ProgressCallback? status_updated = null) {
        this.status_updated = status_updated;

        if (publishables.length > 0)
           send_files();
    }
}

}

