/* Copyright 2016 Software Freedom Conservancy Inc.
 * Copyright 2017 Jens Georg <mail@jensge.org>
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

public class ExportDialog : Gtk.Dialog {
    public const int DEFAULT_SCALE = 1200;

    // "Unmodified" and "Current," though they appear in the "Format:" popup menu, really
    // aren't formats so much as they are operating modes that determine specific formats.
    // Hereafter we'll refer to these as "special formats."
    public const int NUM_SPECIAL_FORMATS = 2;
    public const string UNMODIFIED_FORMAT_LABEL = _("Unmodified");
    public const string CURRENT_FORMAT_LABEL = _("Current");

    public const ScaleConstraint[] CONSTRAINT_ARRAY = { ScaleConstraint.ORIGINAL,
        ScaleConstraint.DIMENSIONS, ScaleConstraint.WIDTH, ScaleConstraint.HEIGHT };

    public const Jpeg.Quality[] QUALITY_ARRAY = { Jpeg.Quality.LOW, Jpeg.Quality.MEDIUM,
        Jpeg.Quality.HIGH, Jpeg.Quality.MAXIMUM };

    private static ScaleConstraint current_constraint = ScaleConstraint.ORIGINAL;
    private static ExportFormatParameters current_parameters = ExportFormatParameters.current();
    private static int current_scale = DEFAULT_SCALE;

    private Gtk.Grid table = new Gtk.Grid();
    private Gtk.DropDown quality_combo;
    private Gtk.DropDown constraint_combo;
    private Gtk.DropDown format_combo;
    private Gtk.Switch export_metadata;
    private Gee.ArrayList<string> format_options = new Gee.ArrayList<string>();
    private Gtk.Entry pixels_entry;
    private Gtk.Widget ok_button;
    private bool in_insert = false;

    public ExportDialog(string title) {
        Object (title: title, use_header_bar: Resources.use_header_bar(), resizable: false);

        set_transient_for(AppWindow.get_instance());
        set_modal(true);


        //get information about the export settings out of our config backend
        Config.Facade config = Config.Facade.get_instance();
        current_parameters.mode = config.get_export_export_format_mode(); //ExportFormatMode
        current_parameters.specified_format = config.get_export_photo_file_format(); //PhotoFileFormat
        current_parameters.quality = config.get_export_quality(); //quality
        current_parameters.export_metadata = config.get_export_export_metadata(); //export metadata
        current_constraint = config.get_export_constraint(); //constraint
        current_scale = config.get_export_scale(); //scale

        var model = new Gtk.StringList(null);
        quality_combo = new Gtk.DropDown(model, null);
        int ctr = 0;
        foreach (Jpeg.Quality quality in QUALITY_ARRAY) {
            model.append(quality.to_string());
            if (quality == current_parameters.quality)
                quality_combo.set_selected(ctr);
            ctr++;
        }

        model = new Gtk.StringList(null);
        constraint_combo = new Gtk.DropDown(model, null);
        ctr = 0;
        foreach (ScaleConstraint constraint in CONSTRAINT_ARRAY) {
            model.append(constraint.to_string());
            if (constraint == current_constraint)
                constraint_combo.set_selected(ctr);
            ctr++;
        }

        model = new Gtk.StringList(null);
        format_combo = new Gtk.DropDown(model, null);
        format_add_option(UNMODIFIED_FORMAT_LABEL);
        format_add_option(CURRENT_FORMAT_LABEL);
        foreach (PhotoFileFormat format in PhotoFileFormat.get_image_writeable()) {
            format_add_option(format.get_properties().get_user_visible_name());
        }

        pixels_entry = new Gtk.Entry();
        pixels_entry.set_max_length(6);
        pixels_entry.set_text("%d".printf(current_scale));

        // register after preparation to avoid signals during init
        constraint_combo.notify["selected-item"].connect(on_constraint_changed);
        format_combo.notify["selected-item"].connect(on_format_changed);
        pixels_entry.changed.connect(on_pixels_changed);
        pixels_entry.insert_text.connect(on_pixels_insert_text);
        pixels_entry.activate.connect(on_activate);

        // layout controls
        add_label(_("_Format:"), 0, 0, format_combo);
        add_control(format_combo, 1, 0);

        add_label(_("_Quality:"), 0, 1, quality_combo);
        add_control(quality_combo, 1, 1);

        add_label(_("_Scaling constraint:"), 0, 2, constraint_combo);
        add_control(constraint_combo, 1, 2);

        add_label(_("_Pixels:"), 0, 3, pixels_entry);
        add_control(pixels_entry, 1, 3);

        export_metadata = new Gtk.Switch ();
        add_label(_("Export _metadata:"), 0, 4, export_metadata);
        add_control(export_metadata, 1, 4);
        export_metadata.active = true;
        export_metadata.halign = Gtk.Align.START;

        table.set_row_spacing(6);
        table.set_column_spacing(12);

        ((Gtk.Box) get_content_area()).append(table);

        // add buttons to action area
        add_button(Resources.CANCEL_LABEL, Gtk.ResponseType.CANCEL);
        ok_button = add_button(Resources.OK_LABEL, Gtk.ResponseType.OK);
        set_default_response(Gtk.ResponseType.OK);

        response.connect(on_response);

        if (current_constraint == ScaleConstraint.ORIGINAL) {
            pixels_entry.sensitive = false;
            quality_combo.sensitive = false;
        }

        ok_button.add_css_class("suggested-action");
        ok_button.grab_focus();
    }

    private void on_response(int res) {
        // Prevent Dialog to close by "Enter" when the sanity check forbids it
        if (res == Gtk.ResponseType.OK && !ok_button.sensitive) {
            Signal.stop_emission_by_name(this, "response");
        }
    }

    private void format_add_option(string format_name) {
        format_options.add(format_name);
        ((Gtk.StringList)format_combo.model).append(format_name);
    }

    private void format_set_active_text(string text) {
        int selection_ticker = 0;

        foreach (string current_text in format_options) {
            if (current_text == text) {
                format_combo.set_selected(selection_ticker);
                return;
            }
            selection_ticker++;
        }

        critical("format_set_active_text: text '%s' isn't in combo box", text);
    }

    private PhotoFileFormat get_specified_format() {
        var index = format_combo.get_selected();
        if (index < NUM_SPECIAL_FORMATS)
            index = NUM_SPECIAL_FORMATS;

        index -= NUM_SPECIAL_FORMATS;
        PhotoFileFormat[] writeable_formats = PhotoFileFormat.get_image_writeable();
        return writeable_formats[index];
    }

    private string get_label_for_parameters(ExportFormatParameters params) {
        switch(params.mode) {
            case ExportFormatMode.UNMODIFIED:
                return UNMODIFIED_FORMAT_LABEL;

            case ExportFormatMode.CURRENT:
                return CURRENT_FORMAT_LABEL;

            case ExportFormatMode.SPECIFIED:
                return params.specified_format.get_properties().get_user_visible_name();

            default:
                error("get_label_for_parameters: unrecognized export format mode");
        }
    }

    // unlike other parameters, which should be persisted across dialog executions, the
    // format parameters must be set each time the dialog is executed -- this is why
    // it's passed qualified as ref and not as out
    public async ExportFormatParameters? execute(ExportFormatParameters parameters) {
        present();

        // if the export format mode isn't set to last (i.e., don't use the persisted settings),
        // reset the scale constraint to original size
        if (parameters.mode != ExportFormatMode.LAST) {
            current_constraint = parameters.constraint = ScaleConstraint.ORIGINAL;
            constraint_combo.set_selected(0);
        }

        if (parameters.mode == ExportFormatMode.LAST)
            parameters = current_parameters;
        else if (parameters.mode == ExportFormatMode.SPECIFIED && !parameters.specified_format.can_write())
            parameters.specified_format = PhotoFileFormat.get_system_default_format();

        format_set_active_text(get_label_for_parameters(parameters));
        on_format_changed();

        bool ok = false; //(run() == Gtk.ResponseType.OK);
        SourceFunc callback = execute.callback;
        response.connect((r) => {
            ok = r == Gtk.ResponseType.OK;
            callback();
        });
        yield;
        
        destroy();

        if (ok) {
            var index = constraint_combo.get_selected();
            parameters.constraint = CONSTRAINT_ARRAY[index];
            current_constraint = parameters.constraint;

            parameters.scale = int.parse(pixels_entry.get_text());
            if (parameters.constraint != ScaleConstraint.ORIGINAL)
                assert(parameters.scale > 0);
            current_scale = parameters.scale;

            parameters.export_metadata = export_metadata.sensitive ? export_metadata.active : false;

            if (((Gtk.StringObject)format_combo.get_selected_item()).string == UNMODIFIED_FORMAT_LABEL) {
                parameters.mode = current_parameters.mode = ExportFormatMode.UNMODIFIED;
            } else if (((Gtk.StringObject)format_combo.get_selected_item()).string == CURRENT_FORMAT_LABEL) {
                parameters.mode = current_parameters.mode = ExportFormatMode.CURRENT;
            } else {
                parameters.mode = current_parameters.mode = ExportFormatMode.SPECIFIED;
                parameters.specified_format = current_parameters.specified_format = get_specified_format();
                if (current_parameters.specified_format == PhotoFileFormat.JFIF)
                    parameters.quality = current_parameters.quality = QUALITY_ARRAY[(int)quality_combo.get_selected()];
            }

            //save current settings in config backend for reusing later
            Config.Facade config = Config.Facade.get_instance();
            config.set_export_export_format_mode(current_parameters.mode); //ExportFormatMode
            config.set_export_photo_file_format(current_parameters.specified_format); //PhotoFileFormat
            config.set_export_quality(current_parameters.quality); //quality
            config.set_export_export_metadata(current_parameters.export_metadata); //export metadata
            config.set_export_constraint(current_constraint); //constraint
            config.set_export_scale(current_scale); //scale
            return parameters;
        } else {
            parameters.scale = 0;
            parameters.constraint = ScaleConstraint.ORIGINAL;

            return null;
        }
    }

    private void add_label(string text, int x, int y, Gtk.Widget? widget = null) {
        Gtk.Label new_label = new Gtk.Label.with_mnemonic(text);
        new_label.halign = Gtk.Align.END;
        new_label.valign = Gtk.Align.CENTER;
        new_label.set_use_underline(true);

        if (widget != null)
            new_label.set_mnemonic_widget(widget);

        table.attach(new_label, x, y, 1, 1);
    }

    private void add_control(Gtk.Widget widget, int x, int y) {
        widget.halign = Gtk.Align.FILL;
        widget.valign = Gtk.Align.CENTER;
        widget.hexpand = true;
        widget.vexpand = true;

        table.attach(widget, x, y, 1, 1);
    }

    private void on_constraint_changed() {
        bool original = CONSTRAINT_ARRAY[(int)constraint_combo.get_selected()] == ScaleConstraint.ORIGINAL;
        bool jpeg = ((Gtk.StringObject)format_combo.get_selected_item()).string ==
            PhotoFileFormat.JFIF.get_properties().get_user_visible_name();
        pixels_entry.sensitive = !original;
        quality_combo.sensitive = !original && jpeg;
        if (original)
            ok_button.sensitive = true;
        else
            on_pixels_changed();
    }

    private void on_format_changed() {
        bool original = CONSTRAINT_ARRAY[constraint_combo.get_selected()] == ScaleConstraint.ORIGINAL;

        if (((Gtk.StringObject)format_combo.get_selected_item()).string == UNMODIFIED_FORMAT_LABEL) {
            // if the user wishes to export the media unmodified, then we just copy the original
            // files, so parameterize size, quality, etc. is impossible -- these are all
            // just as they are in the original file. In this case, we set the scale constraint to
            // original and lock out all the controls
            constraint_combo.set_selected(0); /* 0 == original size */
            constraint_combo.set_sensitive(false);
            quality_combo.set_sensitive(false);
            pixels_entry.sensitive = false;
            export_metadata.active = false;
            export_metadata.sensitive = false;
        } else if (((Gtk.StringObject)format_combo.get_selected_item()).string == CURRENT_FORMAT_LABEL) {
            // if the user wishes to export the media in its current format, we allow sizing but
            // not JPEG quality customization, because in a batch of many photos, it's not
            // guaranteed that all of them will be JPEGs or RAWs that get converted to JPEGs. Some
            // could be PNGs, and PNG has no notion of quality. So lock out the quality control.
            // If the user wants to set JPEG quality, he or she can explicitly specify the JPEG
            // format.
            constraint_combo.set_sensitive(true);
            quality_combo.set_sensitive(false);
            pixels_entry.sensitive = !original;
            export_metadata.sensitive = true;
        } else {
            // if the user has chosen a specific format, then allow JPEG quality customization if
            // the format is JPEG and the user is re-sizing the image, otherwise, disallow JPEG
            // quality customization; always allow scaling.
            constraint_combo.set_sensitive(true);
            bool jpeg = get_specified_format() == PhotoFileFormat.JFIF;
            quality_combo.sensitive = !original && jpeg;

            export_metadata.sensitive = get_specified_format().can_write_metadata();
            export_metadata.active = get_specified_format().can_write_metadata();
        }
    }

    private void on_activate() {
        response(Gtk.ResponseType.OK);
    }

    private void on_pixels_changed() {
        ok_button.sensitive = (pixels_entry.get_text_length() > 0) && (int.parse(pixels_entry.get_text()) > 0);
    }

    private void on_pixels_insert_text(string text, int length, ref int position) {
        // This is necessary because SignalHandler.block_by_func() is not properly bound
        if (in_insert)
            return;

        in_insert = true;

        if (length == -1)
            length = (int) text.length;

        // only permit numeric text
        string new_text = "";
        for (int ctr = 0; ctr < length; ctr++) {
            if (text[ctr].isdigit()) {
                new_text += ((char) text[ctr]).to_string();
            }
        }

        if (new_text.length > 0)
            pixels_entry.insert_text(new_text, (int) new_text.length, ref position);

        Signal.stop_emission_by_name(pixels_entry, "insert-text");

        in_insert = false;
    }
}
