/* 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 Sidebar.Tree : Gtk.TreeView {
    public const int ICON_SIZE = 16;
    
    // Only one ExternalDropHandler can be registered with the Tree; it's responsible for completing
    // the "drag-data-received" signal properly.
//    public delegate void ExternalDropHandler(Gdk.DragContext context, Sidebar.Entry? entry,
//        Gtk.SelectionData data, uint info, uint time);
    
    private class EntryWrapper : Object {
        public Sidebar.Entry entry;
        public Gtk.TreeRowReference row;
        
        public EntryWrapper(Gtk.TreeModel model, Sidebar.Entry entry, Gtk.TreePath path) {
            this.entry = entry;
            this.row = new Gtk.TreeRowReference(model, path);
        }
        
        public Gtk.TreePath get_path() {
            return row.get_path();
        }
        
        public Gtk.TreeIter get_iter() {
            Gtk.TreeIter iter;
            bool valid = row.get_model().get_iter(out iter, get_path());
            assert(valid);
            
            return iter;
        }

        public bool draggable() {
            return entry.draggable();
        }
    }
    
    private class RootWrapper : EntryWrapper {
        public int root_position;
        
        public RootWrapper(Gtk.TreeModel model, Sidebar.Entry entry, Gtk.TreePath path, int root_position) {
            base (model, entry, path);
            
            this.root_position = root_position;
        }
    }
    
    private enum Columns {
        NAME,
        TOOLTIP,
        WRAPPER,
        ICON,
        N_COLUMNS
    }
    
    private class TreeStore : Gtk.TreeStore, Gtk.TreeDragDest, Gtk.TreeDragSource {
        public TreeStore() {
            set_column_types({typeof(string), typeof(string?), typeof(EntryWrapper), typeof(Icon?)});
        }
        public bool row_draggable(Gtk.TreePath path) {
            var wrapper = get_wrapper_at_path(path);
            var draggable = wrapper.entry.draggable();
            return draggable;
        }

        public Gdk.ContentProvider? drag_data_get(Gtk.TreePath path) {
            var wrapper =  get_wrapper_at_path(path);
            var provider = new Gdk.ContentProvider.for_value(wrapper);
            return provider;
        }

        public bool row_drop_possible(Gtk.TreePath dest, GLib.Value value) {
            return true;
        }

        public bool drag_data_received (Gtk.TreePath dest, GLib.Value value) {
            return false;
        }

        public EntryWrapper? get_wrapper_at_path(Gtk.TreePath path) {
            Gtk.TreeIter iter;
            if (!get_iter(out iter, path)) {
                message("No entry found in sidebar at %s", path.to_string());
            
                return null;
            }
        
            return get_wrapper_at_iter(iter);
        }

        public EntryWrapper? get_wrapper_at_iter(Gtk.TreeIter iter) {
            Value val;
            get_value(iter, Columns.WRAPPER, out val);
            
            EntryWrapper? wrapper = (EntryWrapper?) val;
            if (wrapper == null)
                message("No entry found in sidebar at %s", get_path(iter).to_string());
            
            return wrapper;
        }        
    }

    private TreeStore store = new TreeStore();
    
    private Gtk.Builder builder = new Gtk.Builder ();
    private Gtk.CellRendererText text_renderer;
    private Gtk.Entry? text_entry = null;
    private Gee.HashMap<Sidebar.Entry, EntryWrapper> entry_map =
        new Gee.HashMap<Sidebar.Entry, EntryWrapper>();
    private Gee.HashMap<Sidebar.Branch, int> branches = new Gee.HashMap<Sidebar.Branch, int>();
    private int editing_disabled = 0;
    private bool mask_entry_selected_signal = false;
    private weak EntryWrapper? selected_wrapper = null;
    private Gtk.PopoverMenu? default_context_menu = null;
    private bool expander_called_manually = false;
    private int expander_special_count = 0;
    private bool is_internal_drag_in_progress = false;
    private Sidebar.Entry? internal_drag_source_entry = null;
    private Gtk.TreeRowReference? old_path_ref = null;
    private Gee.ArrayList<unowned Branch> expand_to_child = new Gee.ArrayList<unowned Branch>();
    private Gee.ArrayList<unowned Branch> expand_to_element = new Gee.ArrayList<unowned Branch>();
    
    public signal void entry_selected(Sidebar.SelectableEntry selectable);
    
    public signal void selected_entry_removed(Sidebar.SelectableEntry removed);
    
    public signal void branch_added(Sidebar.Branch branch);
    
    public signal void branch_removed(Sidebar.Branch branch);
    
    public signal void branch_shown(Sidebar.Branch branch, bool shown);
    
    public signal void page_created(Sidebar.PageRepresentative entry, Page page);
    
    public signal void destroying_page(Sidebar.PageRepresentative entry, Page page);
    
    public Tree() {

        Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn();
        text_column.set_expand(true);
        Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
        text_column.pack_start (icon_renderer, false);
        text_column.add_attribute(icon_renderer, "gicon", Columns.ICON);
        text_column.set_cell_data_func(icon_renderer, icon_renderer_function);
        text_column.set_spacing(6);
        text_renderer = new Gtk.CellRendererText();
        text_renderer.ellipsize = Pango.EllipsizeMode.END;
        text_renderer.editing_canceled.connect(on_editing_canceled);
        text_renderer.editing_started.connect(on_editing_started);
        text_column.pack_start(text_renderer, true);
        text_column.add_attribute(text_renderer, "markup", Columns.NAME);
        append_column(text_column);
        
        Gtk.CellRendererText invisitext = new Gtk.CellRendererText();
        Gtk.TreeViewColumn page_holder = new Gtk.TreeViewColumn();
        page_holder.pack_start(invisitext, true);
        page_holder.visible = false;
        append_column(page_holder);
        
        set_headers_visible(false);
        set_enable_search(false);
        set_show_expanders(true);
        set_enable_tree_lines(false);
        set_grid_lines(Gtk.TreeViewGridLines.NONE);
        set_tooltip_column(Columns.TOOLTIP);
        
        Gtk.TreeSelection selection = get_selection();
        selection.set_mode(Gtk.SelectionMode.BROWSE);
        selection.set_select_function(on_selection);
        
        test_expand_row.connect(on_toggle_row);
        test_collapse_row.connect(on_toggle_row);
        
        // It Would Be Nice if the target entries and actions were gleaned by querying each 
        // Sidebar.Entry as it was added, but that's a tad too complicated for our needs
        // currently
        var formats = new Gdk.ContentFormats.for_gtype(typeof(EntryWrapper));
        enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, formats, Gdk.DragAction.COPY);

        // FIXME: This is an ugly hack to force the TreeView to handle the hightlighting when
        // doing an internal drag/drop, the actual drop is done by the drop target
        var builder = new Gdk.ContentFormatsBuilder();
        builder.add_gtype(typeof(EntryWrapper));
        builder.add_gtype(typeof(DragAndDropHandler.MediaListWrapper));
        enable_model_drag_dest(builder.to_formats(), Gdk.DragAction.COPY);
        var target = new Gtk.DropTarget(Type.NONE, Gdk.DragAction.COPY);
        target.set_gtypes({typeof(EntryWrapper), typeof(DragAndDropHandler.MediaListWrapper)});
        target.drop.connect(on_drop);
        add_controller(target);

        var click = new Gtk.GestureClick();
        click.set_name ("Sidebar Tree click source");
        click.set_button (0); // Listen to all buttons
        //click.set_exclusive (true); // TODO: Need to be true or false?
        click.pressed.connect (on_click_pressed);
        add_controller(click);

        var key = new Gtk.EventControllerKey();
        key.key_pressed.connect(key_press_event);
        add_controller(key);

        setup_default_context_menu();
    }
    
    ~Tree() {
        text_renderer.editing_canceled.disconnect(on_editing_canceled);
        text_renderer.editing_started.disconnect(on_editing_started);
    }

    public void finish() {
        set_model(store);
        foreach (var branch in expand_to_child) {
            expand_to_first_child(branch.get_root());
        }
        expand_to_child.clear();
        foreach (var branch in expand_to_element) {
            expand_to_entry(branch.get_root());
        }
        expand_to_element.clear();
    }
    
    public void icon_renderer_function(Gtk.CellLayout layout, Gtk.CellRenderer renderer, Gtk.TreeModel model, Gtk.TreeIter iter) {
        EntryWrapper? wrapper = get_wrapper_at_iter(iter);
        if (wrapper == null) {
            return;
        }
        renderer.visible = !(wrapper.entry is Sidebar.Header);
    }

    private const GLib.ActionEntry[] entries = {
        { "tag.new", on_new_tag },
        { "search.new", on_new_search }
    };

    private void setup_default_context_menu() {
        // FIXME: This will throw a critical, which is an issue in GTK4 TreeView
        // We should move away to ListView + TreeListModel anyway.
        try {
            this.builder.add_from_resource(Resources.get_ui("sidebar_default_context.ui"));
            this.default_context_menu = get_popover_menu_from_builder(builder, "popup-menu", this);
            var group = new GLib.SimpleActionGroup ();
            group.add_action_entries (entries, this);
            this.insert_action_group ("sidebar", group);
        } catch (Error error) {
            AppWindow.error_message("Error loading UI resource: %s".printf(
                error.message));
            Application.get_instance().panic();
        }
    }
    
    private bool has_wrapper(Sidebar.Entry entry) {
        return entry_map.has_key(entry);
    }
    
    private EntryWrapper? get_wrapper(Sidebar.Entry entry) {
        EntryWrapper? wrapper = entry_map.get(entry);
        if (wrapper == null)
            warning("Entry %s not found in sidebar", entry.to_string());
        
        return wrapper;
    }
    
    private EntryWrapper? get_wrapper_at_iter(Gtk.TreeIter iter) {
        return store.get_wrapper_at_iter(iter);
    }
    
    private EntryWrapper? get_wrapper_at_path(Gtk.TreePath path) {
        return store.get_wrapper_at_path(path);
    }
    
    // Note that this method will result in the "entry-selected" signal to fire if mask_signal
    // is set to false.
    public bool place_cursor(Sidebar.Entry entry, bool mask_signal) {
        if (!expand_to_entry(entry))
            return false;
        
        EntryWrapper? wrapper = get_wrapper(entry);
        if (wrapper == null)
            return false;
        
        get_selection().select_path(wrapper.get_path());
        
        mask_entry_selected_signal = mask_signal;
        set_cursor(wrapper.get_path(), null, false);
        mask_entry_selected_signal = false;
        
        return scroll_to_entry(entry);
    }
    
    public bool is_selected(Sidebar.Entry entry) {
        EntryWrapper? wrapper = get_wrapper(entry);
        
        // Even though get_selection() does not report its return type as nullable, it can be null
        // if the window has been destroyed.
        Gtk.TreeSelection selection = get_selection();
        if (selection == null)
            return false;
        
        return (wrapper != null) ? selection.path_is_selected(wrapper.get_path()) : false;
    }
    
    public bool is_any_selected() {
        return get_selection().count_selected_rows() != 0;
    }

    private Gtk.TreePath? get_selected_path() {
        Gtk.TreeModel model;
        Gtk.TreeSelection? selection = get_selection();
        if (selection == null){
            return null;
        }
        GLib.List<Gtk.TreePath> rows = selection.get_selected_rows(out model);
        assert(rows.length() == 0 || rows.length() == 1);

        return rows.length() != 0 ? rows.nth_data(0) : null;
    }

    private string get_name_for_entry(Sidebar.Entry entry) {
        string name = guarded_markup_escape_text(entry.get_sidebar_name());
        
        Sidebar.EmphasizableEntry? emphasizable_entry = entry as Sidebar.EmphasizableEntry;
        if (emphasizable_entry != null && emphasizable_entry.is_emphasized())
            name = "<b>%s</b>".printf(name);
        
        return name;
    }
    
    public virtual bool accept_cursor_changed() {
        return true;
    }
    
    public override void cursor_changed() {
        Gtk.TreePath? path = get_selected_path();
        if (path == null) {
            if (base.cursor_changed != null)
                base.cursor_changed();
            return;
        }
        
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        
        if (selected_wrapper != wrapper) {
            EntryWrapper old_wrapper = selected_wrapper;
            selected_wrapper = wrapper;
            
            if (editing_disabled == 0 && wrapper != null && wrapper.entry is Sidebar.RenameableEntry)
                text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable();
            
            if (wrapper != null && !mask_entry_selected_signal) {
                Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry;
                if (selectable != null) {
                    if (accept_cursor_changed()) {
                        entry_selected(selectable);
                    } else {
                        place_cursor(old_wrapper.entry, true);
                    }
                }
            }
        }
        
        if (base.cursor_changed != null)
            base.cursor_changed();
    }
    
    public void disable_editing() {
        if (editing_disabled++ == 0)
            text_renderer.editable = false;
    }
    
    public void enable_editing() {
        Gtk.TreePath? path = get_selected_path();
        if (path != null && editing_disabled > 0 && --editing_disabled == 0) {
            EntryWrapper? wrapper = get_wrapper_at_path(path);
            if (wrapper != null && (wrapper.entry is Sidebar.RenameableEntry))
                text_renderer.editable = ((Sidebar.RenameableEntry) wrapper.entry).
                    is_user_renameable();
        }
    }
    
    public void toggle_branch_expansion(Gtk.TreePath path, bool expand_all) {
        expander_called_manually = true;
        if (is_row_expanded(path))
            collapse_row(path);
        else
            expand_row(path, expand_all);
    }
    
    public bool expand_to_entry(Sidebar.Entry entry) {
        expander_called_manually = true;
        EntryWrapper? wrapper = get_wrapper(entry);
        if (wrapper == null)
            return false;
        
        expand_to_path(wrapper.get_path());
        
        return true;
    }
    
    public void expand_to_first_child(Sidebar.Entry entry) {
        expander_called_manually = true;
        EntryWrapper? wrapper = get_wrapper(entry);
        if (wrapper == null)
            return;
        
        Gtk.TreePath path = wrapper.get_path();
        
        Gtk.TreeIter iter;
        while (store.get_iter(out iter, path)) {
            if (!store.iter_has_child(iter))
                break;
            
            path.down();
        }
        
        expand_to_path(path);
    }
    
    public void graft(Sidebar.Branch branch, int position) requires (position >= 0) {
        assert(!branches.has_key(branch));
        
        branches.set(branch, position);
        
        if (branch.get_show_branch()) {
            associate_branch(branch);
            
            if (branch.is_startup_expand_to_first_child()) {
                expand_to_child.add(branch);
                
            }

            if (branch.is_startup_open_grouping()) {
                expand_to_element.add(branch);
            }
        }
        
        branch.entry_added.connect(on_branch_entry_added);
        branch.entry_removed.connect(on_branch_entry_removed);
        branch.entry_moved.connect(on_branch_entry_moved);
        branch.entry_reparented.connect(on_branch_entry_reparented);
        branch.children_reordered.connect(on_branch_children_reordered);
        branch.show_branch.connect(on_show_branch);
        
        branch_added(branch);
    }
    
    // This is used to associate a known branch with the TreeView.
    private void associate_branch(Sidebar.Branch branch) {
        assert(branches.has_key(branch));
        
        int position = branches.get(branch);
        
        Gtk.TreeIter? insertion_iter = null;
        
        // search current roots for insertion point
        Gtk.TreeIter iter;
        bool found = store.get_iter_first(out iter);
        while (found) {
            RootWrapper? root_wrapper = get_wrapper_at_iter(iter) as RootWrapper;
            assert(root_wrapper != null);
            
            if (position < root_wrapper.root_position) {
                store.insert_before(out insertion_iter, null, iter);
                
                break;
            }
            
            found = store.iter_next(ref iter);
        }
        
        // if not found, append
        if (insertion_iter == null)
            store.append(out insertion_iter, null);
        
        associate_wrapper(insertion_iter,
            new RootWrapper(store, branch.get_root(), store.get_path(insertion_iter), position));
        
        // mirror the branch's initial contents from below the root down, let the signals handle
        // future work
        associate_children(branch, branch.get_root(), insertion_iter);
    }
    
    private void associate_children(Sidebar.Branch branch, Sidebar.Entry parent,
        Gtk.TreeIter parent_iter) {
        Gee.List<Sidebar.Entry>? children = branch.get_children(parent);
        if (children == null)
            return;
        
        foreach (Sidebar.Entry child in children) {
            Gtk.TreeIter append_iter;
            store.append(out append_iter, parent_iter);
            
            associate_entry(append_iter, child);
            associate_children(branch, child, append_iter);
        }
    }
    
    private void associate_entry(Gtk.TreeIter assoc_iter, Sidebar.Entry entry) {
        associate_wrapper(assoc_iter, new EntryWrapper(store, entry, store.get_path(assoc_iter)));
    }
    
    private void associate_wrapper(Gtk.TreeIter assoc_iter, EntryWrapper wrapper) {
        Sidebar.Entry entry = wrapper.entry;
        
        assert(!entry_map.has_key(entry));
        entry_map.set(entry, wrapper);
        
        store.set(assoc_iter, Columns.NAME, get_name_for_entry(entry));
        store.set(assoc_iter, Columns.TOOLTIP, guarded_markup_escape_text(entry.get_sidebar_tooltip()));
        store.set(assoc_iter, Columns.WRAPPER, wrapper);
        load_entry_icons(assoc_iter);
        
        entry.sidebar_tooltip_changed.connect(on_sidebar_tooltip_changed);
        entry.sidebar_icon_changed.connect(on_sidebar_icon_changed);
        
        Sidebar.PageRepresentative? pageable = entry as Sidebar.PageRepresentative;
        if (pageable != null) {
            pageable.page_created.connect(on_sidebar_page_created);
            pageable.destroying_page.connect(on_sidebar_destroying_page);
        }

        Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
        if (emphasizable != null)
            emphasizable.is_emphasized_changed.connect(on_is_emphasized_changed);
        
        Sidebar.RenameableEntry? renameable = entry as Sidebar.RenameableEntry;
        if (renameable != null)
            renameable.sidebar_name_changed.connect(on_sidebar_name_changed);
                
        entry.grafted(this);
    }
    
    private EntryWrapper reparent_wrapper(Gtk.TreeIter new_iter, EntryWrapper current_wrapper) {
        Sidebar.Entry entry = current_wrapper.entry;
        
        bool removed = entry_map.unset(entry);
        assert(removed);
        
        EntryWrapper new_wrapper = new EntryWrapper(store, entry, store.get_path(new_iter));
        entry_map.set(entry, new_wrapper);
        
        store.set(new_iter, Columns.NAME, get_name_for_entry(entry));
        store.set(new_iter, Columns.TOOLTIP, guarded_markup_escape_text(entry.get_sidebar_tooltip()));
        store.set(new_iter, Columns.WRAPPER, new_wrapper);
        load_entry_icons(new_iter);
        
        return new_wrapper;
    }
    
    public void prune(Sidebar.Branch branch) {
        assert(branches.has_key(branch));
        
        if (has_wrapper(branch.get_root()))
            disassociate_branch(branch);
        
        branch.entry_added.disconnect(on_branch_entry_added);
        branch.entry_removed.disconnect(on_branch_entry_removed);
        branch.entry_moved.disconnect(on_branch_entry_moved);
        branch.entry_reparented.disconnect(on_branch_entry_reparented);
        branch.children_reordered.disconnect(on_branch_children_reordered);
        branch.show_branch.disconnect(on_show_branch);
        
        bool removed = branches.unset(branch);
        assert(removed);
        
        branch_removed(branch);
    }
    
    private void disassociate_branch(Sidebar.Branch branch) {
        RootWrapper? root_wrapper = get_wrapper(branch.get_root()) as RootWrapper;
        assert(root_wrapper != null);
        
        disassociate_wrapper_and_signal(root_wrapper, false);
    }
    
    // A wrapper for disassociate_wrapper() (?!?) that fires the "selected-entry-removed" signal if
    // condition exists
    private void disassociate_wrapper_and_signal(EntryWrapper wrapper, bool only_children) {
        bool selected = is_selected(wrapper.entry);
        
        disassociate_wrapper(wrapper, only_children);
        
        if (selected) {
            Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry;
            assert(selectable != null);
            
            selected_entry_removed(selectable);
        }
    }
    
    private void disassociate_wrapper(EntryWrapper wrapper, bool only_children) {
        Gee.ArrayList<EntryWrapper> children = new Gee.ArrayList<EntryWrapper>();
        
        Gtk.TreeIter child_iter;
        bool found = store.iter_children(out child_iter, wrapper.get_iter());
        while (found) {
            EntryWrapper? child_wrapper = get_wrapper_at_iter(child_iter);
            assert(child_wrapper != null);
            
            children.add(child_wrapper);
            
            found = store.iter_next(ref child_iter);
        }
        
        foreach (EntryWrapper child_wrapper in children)
            disassociate_wrapper(child_wrapper, false);
        
        if (only_children)
            return;
        
        Gtk.TreeIter iter = wrapper.get_iter();
        store.remove(ref iter);
        
        if (selected_wrapper == wrapper)
            selected_wrapper = null;
        
        Sidebar.Entry entry = wrapper.entry;
        entry.pruned(this);

        entry.sidebar_tooltip_changed.disconnect(on_sidebar_tooltip_changed);
        entry.sidebar_icon_changed.disconnect(on_sidebar_icon_changed);
        
        Sidebar.PageRepresentative? pageable = entry as Sidebar.PageRepresentative;
        if (pageable != null) {
            pageable.page_created.disconnect(on_sidebar_page_created);
            pageable.destroying_page.disconnect(on_sidebar_destroying_page);
        }
        
        Sidebar.RenameableEntry? renameable = entry as Sidebar.RenameableEntry;
        if (renameable != null)
            renameable.sidebar_name_changed.disconnect(on_sidebar_name_changed);
        
        Sidebar.EmphasizableEntry? emphasizable = entry as Sidebar.EmphasizableEntry;
        if (emphasizable != null)
            emphasizable.is_emphasized_changed.disconnect(on_is_emphasized_changed);
        
        bool removed = entry_map.unset(entry);
        assert(removed);
    }
    
    private void on_branch_entry_added(Sidebar.Branch branch, Sidebar.Entry entry) {
        Sidebar.Entry? parent = branch.get_parent(entry);
        assert(parent != null);
        
        EntryWrapper? parent_wrapper = get_wrapper(parent);
        assert(parent_wrapper != null);
        
        Gtk.TreeIter insertion_iter;
        Sidebar.Entry? next = branch.get_next_sibling(entry);
        if (next != null) {
            EntryWrapper next_wrapper = get_wrapper(next);
            
            // insert before the next sibling in this branch level
            store.insert_before(out insertion_iter, parent_wrapper.get_iter(), next_wrapper.get_iter());
        } else {
            // append to the bottom of this branch level
            store.append(out insertion_iter, parent_wrapper.get_iter());
        }
        
        associate_entry(insertion_iter, entry);
        associate_children(branch, entry, insertion_iter);
        
        if (branch.is_auto_open_on_new_child())
            expand_to_entry(entry);
    }
    
    private void on_branch_entry_removed(Sidebar.Branch branch, Sidebar.Entry entry) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        assert(!(wrapper is RootWrapper));
        
        disassociate_wrapper_and_signal(wrapper, false);
    }
    
    private void on_branch_entry_moved(Sidebar.Branch branch, Sidebar.Entry entry) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        assert(!(wrapper is RootWrapper));
        
        // null means entry is now at the top of the sibling list
        Gtk.TreeIter? prev_iter = null;
        Sidebar.Entry? prev = branch.get_previous_sibling(entry);
        if (prev != null) {
            EntryWrapper? prev_wrapper = get_wrapper(prev);
            assert(prev_wrapper != null);
            
            prev_iter = prev_wrapper.get_iter();
        }
        
        Gtk.TreeIter entry_iter = wrapper.get_iter();
        store.move_after(ref entry_iter, prev_iter);
    }
    
    private void on_branch_entry_reparented(Sidebar.Branch branch, Sidebar.Entry entry,
        Sidebar.Entry old_parent) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        assert(!(wrapper is RootWrapper));
        
        bool selected = (get_current_path().compare(wrapper.get_path()) == 0);
        
        // remove from current position in tree
        Gtk.TreeIter iter = wrapper.get_iter();
        store.remove(ref iter);
        
        Sidebar.Entry? parent = branch.get_parent(entry);
        assert(parent != null);
        
        EntryWrapper? parent_wrapper = get_wrapper(parent);
        assert(parent_wrapper != null);
        
        // null means entry is now at the top of the sibling list
        Gtk.TreeIter? prev_iter = null;
        Sidebar.Entry? prev = branch.get_previous_sibling(entry);
        if (prev != null) {
            EntryWrapper? prev_wrapper = get_wrapper(prev);
            assert(prev_wrapper != null);
            
            prev_iter = prev_wrapper.get_iter();
        }
        
        Gtk.TreeIter new_iter;
        store.insert_after(out new_iter, parent_wrapper.get_iter(), prev_iter);
        
        EntryWrapper new_wrapper = reparent_wrapper(new_iter, wrapper);
        
        if (selected) {
            expand_to_entry(new_wrapper.entry);
            place_cursor(new_wrapper.entry, false);
        }
    }
    
    private void on_branch_children_reordered(Sidebar.Branch branch, Sidebar.Entry entry) {
        Gee.List<Sidebar.Entry>? children = branch.get_children(entry);
        if (children == null)
            return;
        
        // This works by moving the entries to the bottom of the tree's list in the order they
        // are presented in the Sidebar.Branch list.
        foreach (Sidebar.Entry child in children) {
            EntryWrapper? child_wrapper = get_wrapper(child);
            assert(child_wrapper != null);
            
            Gtk.TreeIter child_iter = child_wrapper.get_iter();
            store.move_before(ref child_iter, null);
        }
    }
    
    private void on_show_branch(Sidebar.Branch branch, bool shown) {
        if (shown)
            associate_branch(branch);
        else
            disassociate_branch(branch);
        
        branch_shown(branch, shown);
    }
    
    private void on_sidebar_tooltip_changed(Sidebar.Entry entry, string? tooltip) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        
        store.set(wrapper.get_iter(), Columns.TOOLTIP, guarded_markup_escape_text(tooltip));
    }
    
    private void on_sidebar_icon_changed(Sidebar.Entry entry, string? icon_name) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        Icon? icon = null;

        try {
            if (icon_name != null) {
                icon = Icon.new_for_string (icon_name);
            }
        } catch (Error e) { }

        store.set(wrapper.get_iter(), Columns.ICON, icon);
    }

    private void rename_entry(Sidebar.Entry entry) {
        EntryWrapper? wrapper = get_wrapper(entry);
        assert(wrapper != null);
        
        store.set(wrapper.get_iter(), Columns.NAME, get_name_for_entry(entry));
    }
    
    private void on_sidebar_name_changed(Sidebar.Entry entry, string name) {
        rename_entry(entry);
    }
    
    private void on_sidebar_page_created(Sidebar.PageRepresentative entry, Page page) {
        page_created(entry, page);
    }
    
    private void on_is_emphasized_changed(Sidebar.EmphasizableEntry entry, bool is_emphasized) {
        rename_entry(entry);
    }
    
    private void on_sidebar_destroying_page(Sidebar.PageRepresentative entry, Page page) {
        destroying_page(entry, page);
    }
    
    private void load_entry_icons(Gtk.TreeIter iter) {
        EntryWrapper? wrapper = get_wrapper_at_iter(iter);
        Icon? icon = null;
        if (wrapper == null)
            return;

        try {
            string? name = wrapper.entry.get_sidebar_icon();
            if (name != null) {
                icon = Icon.new_for_string (name);
            }
        } catch (Error e) { }


        store.set(iter, Columns.ICON, icon);
    }
    
    private bool on_selection(Gtk.TreeSelection selection, Gtk.TreeModel model, Gtk.TreePath path,
        bool path_currently_selected) {
        // only allow selection if a page is selectable
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        
        return (wrapper != null) ? (wrapper.entry is Sidebar.SelectableEntry) : false;
    }
    
    private Gtk.TreePath? get_current_path() {
        Gtk.TreeModel model;
        GLib.List<Gtk.TreePath> rows = get_selection().get_selected_rows(out model);
        assert(rows.length() == 0 || rows.length() == 1);
        
        return rows.length() != 0 ? rows.nth_data(0) : null;
    }
    
    private bool popup_context_menu(Gtk.TreePath path, double x, double y) {
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        if (wrapper == null)
            return false;
        
        Sidebar.Contextable? contextable = wrapper.entry as Sidebar.Contextable;
        if (contextable == null)
            return false;
        
        // First select the sidebar item so that its context menu will be available.        
        Sidebar.SelectableEntry? selectable = wrapper.entry as Sidebar.SelectableEntry;
        if (selectable != null)
            entry_selected(selectable);

        var context_menu = contextable.get_sidebar_context_menu();
        if (context_menu == null)
            return false;

        var parent = context_menu.get_parent();
        if (parent != null && parent != this)
            context_menu.unparent();
        else
            parent = null;

        context_menu.set_parent (this);

        context_menu.set_pointing_to ({(int)x, (int)y, 1, 1});
        context_menu.position = Gtk.PositionType.BOTTOM;
        context_menu.halign = Gtk.Align.START;
        context_menu.popup();

        // Fixme: Awful. Probably better to hand out the model, not the menu
        ulong id = 0;
        id = context_menu.closed.connect(() => {
            if (parent != null) {
                context_menu.unparent();
                context_menu.set_parent(parent);
            }
            GLib.SignalHandler.disconnect(context_menu, id);
        });

        return true;
    }
    
    private bool popup_default_context_menu(double x, double y) {
        default_context_menu.set_pointing_to ({(int)x, (int)y, 1, 1});
        default_context_menu.popup();
        return true;
    }
    
    public bool on_toggle_row(Gtk.TreeIter iter, Gtk.TreePath path) {
        // Determine whether to allow the row to toggle
        EntryWrapper? wrapper = get_wrapper_at_iter(iter);
        if (wrapper == null) {
            return false; // don't affect things
        }
        
        // Most of the time, only allow manual toggles
        bool should_allow_toggle = expander_called_manually;
        
        // Cancel out the manual flag
        expander_called_manually = false;
        
        // If we are an expanded parent entry with content
        if (is_row_expanded(path) && store.iter_has_child(iter) && wrapper.entry is Sidebar.SelectableEntry) {
            // We are taking a special action
            expander_special_count++;
            if (expander_special_count == 1) {
                // Workaround that prevents arrows from double-toggling
                return true;
            } else {
                // Toggle only if non-manual, as opposed to the usual behavior
                should_allow_toggle = !should_allow_toggle;
            }
        } else {
            // Reset the special behavior count
            expander_special_count = 0;
        }
        
        if (should_allow_toggle) {
            return false;
        }
        // Prevent branch expansion toggle
        return true;
    }
    
    public void on_click_pressed(Gtk.GestureClick gesture, int press, double x, double y) {
        Gtk.TreePath? path;
        get_path_at_pos((int)x, (int)y, out path, null, null, null);

        var sequence = gesture.get_current_sequence();
        var event = gesture.get_last_event (sequence);

        if (path == null && !event.triggers_context_menu())
            return;

        if (event.triggers_context_menu()) {
            if (path != null)
                popup_context_menu(path, x, y);
            else
                popup_default_context_menu(x, y);

            return;
        }

        if (gesture.get_current_button() == 1) {
            if (path == null) {
                old_path_ref = null;
                return;
            }
        }
        EntryWrapper? wrapper = get_wrapper_at_path(path);
            
        if (wrapper == null) {
            old_path_ref = null;
            return;
        }
        
        // Enable single click to toggle tree entries (bug 4985)
        if (wrapper.entry is Sidebar.ExpandableEntry
            || wrapper.entry is Sidebar.InternalDropTargetEntry) {
            // all labels are InternalDropTargetEntries
            toggle_branch_expansion(path, false);
        }
        
        // Is this a click on an already-highlighted tree item?
        if ((old_path_ref != null) && (old_path_ref.get_path() != null)
            && (old_path_ref.get_path().compare(path) == 0)) {
            // yes, don't allow single-click editing, but 
            // pass the event on for dragging.
            text_renderer.editable = false;
            return;
        }
        
        // Got click on different tree item, make sure it is editable
        // if it needs to be.
        if (wrapper.entry is Sidebar.RenameableEntry &&
            ((Sidebar.RenameableEntry) wrapper.entry).is_user_renameable()) {
            text_renderer.editable = true;
        }
        
        // Remember what tree item is highlighted for next time.
        old_path_ref = new Gtk.TreeRowReference(store, path);
    }
    
    public bool is_keypress_interpreted(Gtk.EventControllerKey event, uint keyval, uint keycode, Gdk.ModifierType modifiers) {
        switch (Gdk.keyval_name(keyval)) {
            case "F2":
            case "Delete":
            case "Return":
            case "KP_Enter":
                return true;
            
            default:
                return false;
        }
    }

    public bool key_press_event(Gtk.EventControllerKey event, uint keyval, uint keycode, Gdk.ModifierType modifiers) {
        switch (Gdk.keyval_name(keyval)) {
            case "Return":
            case "KP_Enter":
                Gtk.TreePath? path = get_current_path();
                if (path != null)
                    toggle_branch_expansion(path, false);
                
                return true;
            
            case "F2":
                return rename_in_place();
            
            case "Delete":
                Gtk.TreePath? path = get_current_path();
                
                return (path != null) ? destroy_path(path) : false;
        }
        
        return false;
    }
    
    public bool rename_entry_in_place(Sidebar.Entry entry) {
        if (!expand_to_entry(entry))
            return false;
        
        if (!place_cursor(entry, false))
            return false;
        
        return rename_in_place();
    }
    
    private bool rename_in_place() {
        Gtk.TreePath? cursor_path;
        Gtk.TreeViewColumn? cursor_column;
        get_cursor(out cursor_path, out cursor_column);
        
        if (can_rename_path(cursor_path)) {
            set_cursor_on_cell (cursor_path, cursor_column, text_renderer, true);
            
            return true;
        }
        
        return false;
    }
    
    public bool scroll_to_entry(Sidebar.Entry entry) {
        EntryWrapper? wrapper = get_wrapper(entry);
        if (wrapper == null)
            return false;
        
        scroll_to_cell(wrapper.get_path(), null, false, 0, 0);
        
        return true;
    }
    
    private bool on_drop(Gtk.DropTarget target, Value value, double x, double y) {
        
        Gtk.TreePath path;
        Gtk.TreeViewDropPosition pos;
        if (!get_dest_row_at_pos((int)x, (int)y, out path, out pos)) {
            return false;
        }
        
        // Note that a drop outside a sidebar entry is legal if an external drop.
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        
        // An internal drop only applies to DropTargetEntry's
        if (wrapper == null) {
            return false;
        }
        
        if (!(wrapper.entry is Sidebar.InternalDropTargetEntry)) {
            return false;
        }

        var targetable = (Sidebar.InternalDropTargetEntry)wrapper.entry;

        if (value.holds(typeof(EntryWrapper))) {
            var source_wrapper = (EntryWrapper)value.get_object();
            return targetable.internal_drop_received_arbitrary(source_wrapper.entry);
        }
        
        if (value.holds(typeof(DragAndDropHandler.MediaListWrapper))) {
            var media = ((DragAndDropHandler.MediaListWrapper)value.get_object()).unwrap();
            return targetable.internal_drop_received(media);
        }
        
        return false;
    }

    // Returns true if path is renameable, and selects the path as well.
    private bool can_rename_path(Gtk.TreePath path) {
        if (editing_disabled > 0)
            return false;
        
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        if (wrapper == null)
            return false;
        
        Sidebar.RenameableEntry? renameable = wrapper.entry as Sidebar.RenameableEntry;
        if (renameable == null)
            return false;
        
        if (wrapper.entry is Sidebar.Header)
            return false;
        
        get_selection().select_path(path);
        
        return true;
    }

    private bool destroy_path(Gtk.TreePath path) {
        EntryWrapper? wrapper = get_wrapper_at_path(path);
        if (wrapper == null)
            return false;
        
        Sidebar.DestroyableEntry? destroyable = wrapper.entry as Sidebar.DestroyableEntry;
        if (destroyable == null)
            return false;
        
        destroyable.destroy_source();
        
        return true;
    }

    private void on_editing_started(Gtk.CellEditable editable, string path) {
        if (editable is Gtk.Entry) {
            text_entry = (Gtk.Entry) editable;
            text_entry.editing_done.connect(on_editing_done);
            //text_entry.focus_out_event.connect(on_editing_focus_out);
            text_entry.editable = true;
        }
    }
    
    private void on_editing_canceled() {
        text_entry.editable = false;
        
        text_entry.editing_done.disconnect(on_editing_done);
    }
    
    private void on_editing_done() {
        text_entry.editable = false;
        
        EntryWrapper? wrapper = get_wrapper_at_path(get_current_path());
        if (wrapper != null) {
            Sidebar.RenameableEntry? renameable = wrapper.entry as Sidebar.RenameableEntry;
            if (renameable != null)
                renameable.rename(text_entry.get_text());
        }
        
        text_entry.editing_done.disconnect(on_editing_done);
    }
    
    private void on_new_search() {
        new SavedSearchDialog().present();
    }

    private void on_new_tag() {
        NewRootTagCommand creation_command = new NewRootTagCommand();
        AppWindow.get_command_manager().execute(creation_command);
        LibraryWindow.get_app().rename_tag_in_sidebar(creation_command.get_created_tag());
    }    
}

