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

// This dialog displays a boolean search configuration.
[GtkTemplate (ui = "/org/gnome/Shotwell/ui/saved_search_dialog.ui")]
public class SavedSearchDialog : Gtk.Dialog {
    
    // Contains a search row, with a type selector and remove button.
    private class SearchRowContainer {
        public signal void remove(SearchRowContainer this_row);
        public signal void changed(SearchRowContainer this_row);
        
        private Gtk.DropDown type_combo;
        private Gtk.Box box;
        private Gtk.Button remove_button;
        private SearchCondition.SearchType[] search_types;
        private Gee.HashMap<SearchCondition.SearchType, int> search_types_index;
        
        private SearchRow? my_row = null;
        
        public SearchRowContainer() {
            setup_gui();
            set_type(SearchCondition.SearchType.ANY_TEXT);
        }
        
        public SearchRowContainer.edit_existing(SearchCondition sc) {
            setup_gui();
            set_type(sc.search_type);
            set_type_combo_box(sc.search_type);
            my_row.populate(sc);
        }
        
        // Creates the GUI for this row.
        private void setup_gui() {
            search_types = SearchCondition.SearchType.as_array();
            search_types_index = new Gee.HashMap<SearchCondition.SearchType, int>();
            SearchCondition.SearchType.sort_array(ref search_types);
            
            var model = new Gtk.StringList(null);
            for (int i = 0; i < search_types.length; i++) {
                SearchCondition.SearchType st = search_types[i];
                search_types_index.set(st, i);
                model.append(st.display_text());
            }
            type_combo = new Gtk.DropDown(model, null);
            set_type_combo_box(SearchCondition.SearchType.ANY_TEXT); // Sets default.
            type_combo.notify["selected-item"].connect(on_type_changed);
            
            remove_button = new Gtk.Button.from_icon_name("window-close-symbolic");
            remove_button.clicked.connect(on_removed);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
            box.prepend(type_combo);
            box.append(remove_button);
            box.margin_top = 2;
            box.margin_bottom = 2;
            box.margin_start = 8;
            box.margin_end = 8;
            box.set_visible(true);
        }
        
        private void on_type_changed() {
            set_type(get_search_type());
            changed(this);
        }
        
        private void set_type_combo_box(SearchCondition.SearchType st) {
            type_combo.set_selected(search_types_index.get(st));
        }
        
        private void set_type(SearchCondition.SearchType type) {
            if (my_row != null)
                box.remove(my_row.get_widget());
            
            switch (type) {
                case SearchCondition.SearchType.ANY_TEXT:
                case SearchCondition.SearchType.EVENT_NAME:
                case SearchCondition.SearchType.FILE_NAME:
                case SearchCondition.SearchType.FACE:
                case SearchCondition.SearchType.TAG:
                case SearchCondition.SearchType.COMMENT:
                case SearchCondition.SearchType.TITLE:
                    my_row = new SearchRowText(this);
                    break;
                    
                case SearchCondition.SearchType.MEDIA_TYPE:
                    my_row = new SearchRowMediaType(this);
                    break;
                    
                case SearchCondition.SearchType.FLAG_STATE:
                    my_row = new SearchRowFlagged(this);
                    break;
                    
                case SearchCondition.SearchType.MODIFIED_STATE:
                    my_row = new SearchRowModified(this);
                    break;
                
                case SearchCondition.SearchType.RATING:
                    my_row = new SearchRowRating(this);
                    break;
                
                case SearchCondition.SearchType.DATE:
                    my_row = new SearchRowDate(this);
                    break;
                
                default:
                    assert_not_reached ();
            }
            
            box.append (my_row.get_widget());
        }
        
        public SearchCondition.SearchType get_search_type() {
            return search_types[type_combo.get_selected()];
        }
        
        private void on_removed() {
            remove(this);
        }
        
        public void allow_removal(bool allow) {
            remove_button.sensitive = allow;
        }
        
        public Gtk.Widget get_widget() {
            return box;
        }
        
        public SearchCondition get_search_condition() {
            return my_row.get_search_condition();
        }
        
        public bool is_complete() {
            return my_row.is_complete();
        }
    }
    
    // Represents a row-type.
    private abstract class SearchRow {
        // Returns the GUI widget for this row. 
        public abstract Gtk.Widget get_widget();
        
        // Returns the search condition for this row.
        public abstract SearchCondition get_search_condition();
        
        // Fills out the fields in this row based on an existing search condition (for edit mode.)
        public abstract void populate(SearchCondition sc);
        
        // Returns true if the row is valid and complete.
        public abstract bool is_complete();
    }
    
    private class SearchRowText : SearchRow {
        private Gtk.Box box;
        private Gtk.DropDown text_context;
        private Gtk.Entry entry;
        
        private SearchRowContainer parent;
        
        public SearchRowText(SearchRowContainer parent) {
            this.parent = parent;
            
            // Ordering must correspond with SearchConditionText.Context
            text_context = new Gtk.DropDown.from_strings({
                _("contains"), _("is exactly"), _("starts with"),
                _("ends with"), _("does not contain"), _("is not set"),
                _("is set")});
            text_context.set_selected(0);
            text_context.notify["selected-item"].connect(on_changed);
            
            entry = new Gtk.Entry();
            entry.set_width_chars(25);
            entry.set_activates_default(true);
            entry.changed.connect(on_changed);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(text_context);
            box.append(entry);
            box.set_visible(true);
        }
        
        ~SearchRowText() {
            text_context.notify["selected-item"].disconnect(on_changed);
            entry.changed.disconnect(on_changed);
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }
        
        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType type = parent.get_search_type();
            string text = entry.get_text();
            SearchConditionText.Context context = get_text_context();
            SearchConditionText c = new SearchConditionText(type, text, context);
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionText? text = sc as SearchConditionText;
            assert(text != null);
            text_context.set_selected(text.context);
            entry.set_text(text.text);
            on_changed();
        }
        
        public override bool is_complete() {
            return entry.text.chomp() != "" ||
                get_text_context() == SearchConditionText.Context.IS_NOT_SET ||
                get_text_context() == SearchConditionText.Context.IS_SET;
        }
        
        private SearchConditionText.Context get_text_context() {
            return (SearchConditionText.Context) text_context.get_selected();
        }
        
        private void on_changed() {
            entry.set_visible(!is_complete());

            parent.changed(parent);
        }
    }
    
    private class SearchRowMediaType : SearchRow {
        private Gtk.Box box;
        private Gtk.DropDown media_context;
        private Gtk.DropDown media_type;
        
        private SearchRowContainer parent;
        
        public SearchRowMediaType(SearchRowContainer parent) {
            this.parent = parent;
            
            // Ordering must correspond with SearchConditionMediaType.Context
            media_context = new Gtk.DropDown.from_strings({
                _("is"), _("is not")
            });
            media_context.notify["selected-item"].connect(on_changed);
            media_context.set_selected(0);
            
            // Ordering must correspond with SearchConditionMediaType.MediaType
            media_type = new Gtk.DropDown.from_strings({
            _("any photo"),
            _("a raw photo"),
            _("a video")});
            media_type.notify["selected-item"].connect(on_changed);
            media_type.set_selected(0);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(media_context);
            box.append(media_type);
            box.set_visible(true);
        }
        
        ~SearchRowMediaType() {
            media_context.notify["selected-item"].disconnect(on_changed);
            media_type.notify["selected-item"].disconnect(on_changed);
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }
        
        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType search_type = parent.get_search_type();
            SearchConditionMediaType.Context context = (SearchConditionMediaType.Context) media_context.get_selected();
            SearchConditionMediaType.MediaType type = (SearchConditionMediaType.MediaType) media_type.get_selected();
            SearchConditionMediaType c = new SearchConditionMediaType(search_type, context, type);
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionMediaType? media = sc as SearchConditionMediaType;
            assert(media != null);
            media_context.set_selected(media.context);
            media_type.set_selected(media.media_type);
        }
        
        public override bool is_complete() {
            return true;
        }
        
        private void on_changed() {
            parent.changed(parent);
        }
    }
    
    private class SearchRowModified : SearchRow {
        private Gtk.Box box;
        private Gtk.DropDown modified_context;
        private Gtk.DropDown modified_state;
        
        private SearchRowContainer parent;
        
        public SearchRowModified(SearchRowContainer parent) {
            this.parent = parent;

            modified_context = new Gtk.DropDown.from_strings({
                _("has"),
                _("has no")
            });
            modified_context.notify["selected-item"].connect(on_changed);
            modified_context.set_selected(0);
            
            modified_state = new Gtk.DropDown.from_strings({
                _("modifications"),
                _("internal modifications"),
                _("external modifications")
            });
            modified_state.notify["selected-item"].connect(on_changed);
            modified_state.set_selected(0);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(modified_context);
            box.append(modified_state);
            box.set_visible(true);
        }
        
        ~SearchRowModified() {
            modified_state.notify["selected-item"].disconnect(on_changed);
            modified_context.notify["selected-item"].disconnect(on_changed);
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }

        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType search_type = parent.get_search_type();
            SearchConditionModified.Context context = (SearchConditionModified.Context) modified_context.get_selected();
            SearchConditionModified.State state = (SearchConditionModified.State) modified_state.get_selected();
            SearchConditionModified c = new SearchConditionModified(search_type, context, state);
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionModified? scm = sc as SearchConditionModified;
            assert(scm != null);
            modified_state.set_selected(scm.state);
            modified_context.set_selected(scm.context);
        }
        
        public override bool is_complete() {
            return true;
        }
        
        private void on_changed() {
            parent.changed(parent);
        }
    }
    
    private class SearchRowFlagged : SearchRow {
        private Gtk.Box box;
        private Gtk.DropDown flagged_state;
        
        private SearchRowContainer parent;
        
        public SearchRowFlagged(SearchRowContainer parent) {
            this.parent = parent;
            
            // Ordering must correspond with SearchConditionFlagged.State
            flagged_state = new Gtk.DropDown.from_strings({
                _("flagged"),
                _("not flagged")
            });
            flagged_state.notify["selected-item"].connect(on_changed);
            flagged_state.set_selected(0);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(new Gtk.Label(_("is")));
            box.append(flagged_state);
            box.set_visible(true);
        }
        
        ~SearchRowFlagged() {
            flagged_state.notify["selected-item"].disconnect(on_changed);
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }
        
        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType search_type = parent.get_search_type();
            SearchConditionFlagged.State state = (SearchConditionFlagged.State) flagged_state.get_selected();
            SearchConditionFlagged c = new SearchConditionFlagged(search_type, state);
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionFlagged? f = sc as SearchConditionFlagged;
            assert(f != null);
            flagged_state.set_selected(f.state);
        }
        
        public override bool is_complete() {
            return true;
        }
        
        private void on_changed() {
            parent.changed(parent);
        }
    }
    
    private class SearchRowRating : SearchRow {
        private Gtk.Box box;
        private Gtk.DropDown rating;
        private Gtk.DropDown context;
        
        private SearchRowContainer parent;
        
        public SearchRowRating(SearchRowContainer parent) {
            this.parent = parent;
            
            // Ordering must correspond with Rating
            rating = new Gtk.DropDown.from_strings(
                {
                    Resources.rating_combo_box(Rating.REJECTED),
                    Resources.rating_combo_box(Rating.UNRATED),
                    Resources.rating_combo_box(Rating.ONE),
                    Resources.rating_combo_box(Rating.TWO),
                    Resources.rating_combo_box(Rating.THREE),
                    Resources.rating_combo_box(Rating.FOUR),
                    Resources.rating_combo_box(Rating.FIVE),
                }
            );
            rating.notify["selected-item"].connect(on_changed);
            rating.set_selected(0);
            
            context = new Gtk.DropDown.from_strings({
                _("and higher"),
                _("only"),
                _("and lower"),
            });
            context.notify["selected-item"].connect(on_changed);
            context.set_selected(0);
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(new Gtk.Label(_("is")));
            box.append(rating);
            box.append(context);
            box.set_visible(true);
        }
        
        ~SearchRowRating() {
            rating.notify["selected-item"].disconnect(on_changed);
            context.notify["selected-item"].disconnect(on_changed);
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }
        
        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType search_type = parent.get_search_type();
            Rating search_rating = (Rating) rating.get_selected() + Rating.REJECTED;
            SearchConditionRating.Context search_context = (SearchConditionRating.Context) context.get_selected();
            SearchConditionRating c = new SearchConditionRating(search_type, search_rating, search_context);
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionRating? r = sc as SearchConditionRating;
            assert(r != null);
            context.set_selected(r.context);
            rating.set_selected(r.rating - Rating.REJECTED);
        }
        
        public override bool is_complete() {
            return true;
        }
        
        private void on_changed() {
            parent.changed(parent);
        }
    }
    
    private class SearchRowDate : SearchRow {
        private const string DATE_FORMAT = "%x";
        private Gtk.Box box;
        private Gtk.DropDown context;
        private Gtk.MenuButton label_one;
        private Gtk.MenuButton label_two;
        private Gtk.Calendar cal_one;
        private Gtk.Calendar cal_two;
        private Gtk.Label and;
        
        private SearchRowContainer parent;
        
        public SearchRowDate(SearchRowContainer parent) {
            this.parent = parent;
            
            // Ordering must correspond with Context
            context = new Gtk.DropDown.from_strings({
                _("is exactly"),
                _("is after"),
                _("is before"),
                _("is between"),
                _("is not set"),
            });
            context.notify["selected-item"].connect(on_changed);
            context.set_selected(0);
            
            cal_one = new Gtk.Calendar();
            cal_two = new Gtk.Calendar();
            
            label_one = new Gtk.MenuButton();
            var popover = new Gtk.Popover();
            label_one.set_popover(popover);
            popover.set_child(cal_one);
            cal_one.day_selected.connect(this.update_date_labels);

            label_two = new Gtk.MenuButton();
            popover = new Gtk.Popover();
            label_two.set_popover(popover);
            popover.set_child(cal_two);
            cal_two.day_selected.connect(this.update_date_labels);
            
            and = new Gtk.Label(_("and"));
            
            box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 3);
            box.append(context);
            box.append(label_one);
            box.append(and);
            box.append(label_two);
            
            box.set_visible(true);
            update_date_labels();
        }
        
        ~SearchRowDate() {
            context.notify["selected-item"].disconnect(on_changed);
        }
        
        private void update_date_labels() {
            SearchConditionDate.Context c = (SearchConditionDate.Context) context.get_selected();
            
            // Only show "and" and 2nd date label for between mode.
            if (c == SearchConditionDate.Context.BETWEEN) {
                label_one.set_visible(true);
                and.set_visible(true);
                label_two.set_visible(true);
            } else if (c == SearchConditionDate.Context.IS_NOT_SET) {
                label_one.set_visible(false);
                and.set_visible(false);
                label_two.set_visible(false);
            } else {
                label_one.set_visible(true);
                and.set_visible(false);
                label_two.set_visible(false);
            }
            
            // Set label text to date.
            label_one.label = get_date_one().format(DATE_FORMAT);
            label_two.label = get_date_two().format(DATE_FORMAT);;
        }
        
        public override Gtk.Widget get_widget() {
            return box;
        }
        
        private DateTime get_date_one() {
            return new DateTime(Application.timezone, cal_one.year, cal_one.month + 1, cal_one.day, 0, 0, 0.0);
        }
        
        private DateTime get_date_two() {
            return new DateTime(Application.timezone, cal_two.year, cal_two.month + 1, cal_two.day, 0, 0, 0.0);
        }
        
        private void set_date_one(DateTime date) {
            cal_one.day   = date.get_day_of_month();
            cal_one.month = date.get_month() - 1;
            cal_one.year  = date.get_year();
        }
        
        private void set_date_two(DateTime date) {
            cal_two.day   = date.get_day_of_month();
            cal_two.month = date.get_month() - 1;
            cal_two.year  = date.get_year();
        }
        
        public override SearchCondition get_search_condition() {
            SearchCondition.SearchType search_type = parent.get_search_type();
            SearchConditionDate.Context search_context = (SearchConditionDate.Context) context.get_selected();
            SearchConditionDate c = new SearchConditionDate(search_type, search_context, get_date_one(),
                get_date_two());
            return c;
        }
        
        public override void populate(SearchCondition sc) {
            SearchConditionDate? cond = sc as SearchConditionDate;
            assert(cond != null);
            context.set_selected(cond.context);
            set_date_one(cond.date_one);
            set_date_two(cond.date_two);
            update_date_labels();
        }
        
        public override bool is_complete() {
            return true;
        }
        
        private void on_changed() {
            parent.changed(parent);
            update_date_labels();
        }
        
    }
    
    [GtkChild]
    private unowned Gtk.Button add_criteria;
    [GtkChild]
    private unowned Gtk.DropDown operator;
    [GtkChild]
    private unowned Gtk.Entry search_title;
    [GtkChild]
    private unowned Gtk.ListBox row_listbox;

    private Gee.ArrayList<SearchRowContainer> row_list = new Gee.ArrayList<SearchRowContainer>();
    private bool edit_mode = false;
    private SavedSearch? previous_search = null;
    private bool valid = false;
    private ulong notify_id = 0;
    
    public SavedSearchDialog() {
        Object (use_header_bar : Resources.use_header_bar());

        setup_dialog();
        
        // Default name.
        search_title.set_text(SavedSearchTable.get_instance().generate_unique_name());
        search_title.select_region(0, -1); // select all
        
        // Default is text search.
        add_text_search();
        row_list.get(0).allow_removal(false);

        present();
        set_valid(false);
        response.connect (this.destroy);
    }
    
    public SavedSearchDialog.edit_existing(SavedSearch saved_search) {
        Object (use_header_bar : Resources.use_header_bar());

        previous_search = saved_search;
        edit_mode = true;
        setup_dialog();
        
        present();
        
        // Load existing search into dialog.
        operator.set_selected((SearchOperator) saved_search.get_operator());
        search_title.set_text(saved_search.get_name());
        foreach (SearchCondition sc in saved_search.get_conditions()) {
            add_row(new SearchRowContainer.edit_existing(sc));
        }
        
        if (row_list.size == 1)
            row_list.get(0).allow_removal(false);
        
        set_valid(true);
        response.connect (this.destroy);
    }
    
    // Builds the dialog UI.  Doesn't add buttons to the dialog or call dialog.show().
    private void setup_dialog() {
        set_transient_for(AppWindow.get_instance());
        response.connect(on_response);

        add_criteria.clicked.connect(on_add_criteria);
        
        search_title.changed.connect(on_title_changed);
        if (Resources.use_header_bar() == 1) {
            var box = (Gtk.Box)search_title.get_parent();
            box.remove(search_title);
            ((Gtk.Box)box.get_parent()).remove(box);
            ((Gtk.HeaderBar) get_header_bar()).set_title_widget(search_title);
        }
    }
    
    double upper;
    // Adds a row of search criteria.
    private void on_add_criteria() {
        this.upper = row_listbox.get_adjustment().upper;
        this.notify_id = row_listbox.get_adjustment().notify["upper"].connect(on_scroll);
        add_text_search();
        // Wait for upper to change. Then scroll to it, disconnect afterwards
        // Otherwise the ListBox will randomly scroll to the bottom
    }
    
    private void add_text_search() {
        SearchRowContainer text = new SearchRowContainer();
        add_row(text);
    }
    
    // Appends a row of search criteria to the list and table.
    private void add_row(SearchRowContainer row) {
        if (row_list.size == 1)
            row_list.get(0).allow_removal(true);
        row_listbox.insert(row.get_widget(), row_list.size);
        row_list.add(row);
        row.remove.connect(on_remove_row);
        row.changed.connect(on_row_changed);
        set_valid(row.is_complete());

    }

    private void on_scroll() {
        var adj = row_listbox.get_adjustment();
        if (adj.upper < this.upper) {
            return;
        }

        if (this.notify_id != 0) {
            adj.disconnect(this.notify_id);
            this.notify_id = 0;
        }

        adj.value = adj.upper;
    }
    
    // Removes a row of search criteria.
    private void on_remove_row(SearchRowContainer row) {
        row.remove.disconnect(on_remove_row);
        row.changed.disconnect(on_row_changed);
        row_listbox.remove(row.get_widget().get_parent());
        row_list.remove(row);
        if (row_list.size == 1)
            row_list.get(0).allow_removal(false);
        set_valid(true); // try setting to "true" since we removed a row
    }

    private void on_response(int response_id) {
        if (response_id == Gtk.ResponseType.OK) {
            if (SavedSearchTable.get_instance().exists(search_title.get_text()) && 
                !(edit_mode && previous_search.get_name() == search_title.get_text())) {
                AppWindow.error_message(Resources.rename_search_exists_message(search_title.get_text()));
                return;
            }
            
            if (edit_mode) {
                // Remove previous search.
                SavedSearchTable.get_instance().remove(previous_search);
            }
            
            // Build the condition list from the search rows, and add our new saved search to the table.
            Gee.ArrayList<SearchCondition> conditions = new Gee.ArrayList<SearchCondition>();
            foreach (SearchRowContainer c in row_list) {
                conditions.add(c.get_search_condition());
            }
            
            // Create the object.  It will be added to the DB and SearchTable automatically.
            SearchOperator search_operator = (SearchOperator)operator.get_selected();
            SavedSearchTable.get_instance().create(search_title.get_text(), search_operator, conditions);
        }
    }
    
    private void on_row_changed(SearchRowContainer row) {
        set_valid(row.is_complete());
    }
    
    private void on_title_changed() {
        set_valid(is_title_valid());
    }
    
    private bool is_title_valid() {
        if (edit_mode && previous_search != null && 
            previous_search.get_name() == search_title.get_text())
            return true; // Title hasn't changed.
        if (search_title.get_text().chomp() == "")
            return false;
        if (SavedSearchTable.get_instance().exists(search_title.get_text()))
            return false;
        return true;
    }
    
    // Call this with your new value for validity whenever a row or the title changes.
    private void set_valid(bool v) {
        if (!v) {
            valid = false;
        } else if (v != valid) {
            if (is_title_valid()) {
                // Go through rows to check validity.
                int valid_rows = 0;
                foreach (SearchRowContainer c in row_list) {
                    if (c.is_complete())
                        valid_rows++;
                }
                valid = (valid_rows == row_list.size);
            } else {
                valid = false; // title was invalid
            }
        }
        
        set_response_sensitive(Gtk.ResponseType.OK, valid);
    }
}
