/* 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 Tags.Branch : Sidebar.Branch {
    private Gee.HashMap<Tag, Tags.SidebarEntry> entry_map = new Gee.HashMap<Tag, Tags.SidebarEntry>();
    
    public Branch() {
        base (new Tags.Header(),
            Sidebar.Branch.Options.HIDE_IF_EMPTY
                | Sidebar.Branch.Options.AUTO_OPEN_ON_NEW_CHILD
                | Sidebar.Branch.Options.STARTUP_OPEN_GROUPING,
            comparator);
        
        // seed the branch with existing tags
        on_tags_added_removed(Tag.global.get_all(), null);
        
        // monitor collection for future events
        Tag.global.contents_altered.connect(on_tags_added_removed);
        Tag.global.items_altered.connect(on_tags_altered);
    }
    
    ~Branch() {
        Tag.global.contents_altered.disconnect(on_tags_added_removed);
        Tag.global.items_altered.disconnect(on_tags_altered);
    }
    
    public Tags.SidebarEntry? get_entry_for_tag(Tag tag) {
        return entry_map.get(tag);
    }
    
    public bool is_user_renameable() {
        return true;
    }
    
    private static int comparator(Sidebar.Entry a, Sidebar.Entry b) {
        if (a == b)
            return 0;
        
        return Tag.compare_names(((Tags.SidebarEntry) a).for_tag(),
            ((Tags.SidebarEntry) b).for_tag());
    }
    
    private void on_tags_added_removed(Gee.Iterable<DataObject>? added_raw, Gee.Iterable<DataObject>? removed) {
        // Store the tag whose page we'll eventually want to go to,
        // since this is lost when a tag is reparented (pruning a currently-
        // highlighted entry from the tree causes the highlight to go to the library,
        // and reparenting requires pruning the old location (along with adding the new one)).
        Tag? restore_point = null;
                
        if (added_raw != null) {
            // prepare a collection of tags guaranteed to be sorted; this is critical for
            // hierarchical tags since it ensures that parent tags must be encountered
            // before their children
            Gee.SortedSet<Tag> added = new Gee.TreeSet<Tag>(Tag.compare_names);
            foreach (DataObject object in added_raw) {
                Tag tag = (Tag) object;
                added.add(tag);
            }
                            
            foreach (Tag tag in added) {
                // ensure that all parent tags of this tag (if any) already have sidebar
                // entries
                Tag? parent_tag = tag.get_hierarchical_parent();
                while (parent_tag != null) {
                    if (!entry_map.has_key(parent_tag)) {
                        Tags.SidebarEntry parent_entry = new Tags.SidebarEntry(parent_tag);
                        entry_map.set(parent_tag, parent_entry);
                    }
                    
                    parent_tag = parent_tag.get_hierarchical_parent();
                    
                }
                
                Tags.SidebarEntry entry = new Tags.SidebarEntry(tag);
                entry_map.set(tag, entry);
                
                parent_tag = tag.get_hierarchical_parent();
                if (parent_tag != null) {
                    Tags.SidebarEntry parent_entry = entry_map.get(parent_tag);
                    graft(parent_entry, entry);
                } else {
                    graft(get_root(), entry);
                }
                
                // Save the most-recently-processed on tag.  During a reparenting,
                // this will be the only tag processed.
                restore_point = tag;
            }
        }
        
        if (removed != null) {
            foreach (DataObject object in removed) {
                Tag tag = (Tag) object;
                
                Tags.SidebarEntry? entry = entry_map.get(tag);
                assert(entry != null);
                
                bool is_removed = entry_map.unset(tag);
                assert(is_removed);
                
                prune(entry);
            }
        }
    }
    
    private void on_tags_altered(Gee.Map<DataObject, Alteration> altered) {
        foreach (DataObject object in altered.keys) {
            if (!altered.get(object).has_detail("metadata", "name"))
                continue;
            
            Tag tag = (Tag) object;
            Tags.SidebarEntry? entry = entry_map.get(tag);
            assert(entry != null);
            
            entry.sidebar_name_changed(tag.get_user_visible_name());
            entry.sidebar_tooltip_changed(tag.get_user_visible_name());
            reorder(entry);
        }
    }
}

public class Tags.Header : Sidebar.Header, Sidebar.InternalDropTargetEntry, 
    Sidebar.Contextable {
    private Gtk.PopoverMenu? context_menu = null;
    
    public Header() {
        base (_("Tags"), _("Organize and browse your photo’s tags"));
        this.context_menu = get_popover_menu_from_resource(Resources.get_ui("tag_sidebar_context.ui"), "popup-menu", null);
    }

    public bool internal_drop_received(Gee.Collection<MediaSource> media) {
        AddTagsDialog dialog = new AddTagsDialog();
        dialog.execute.begin((source, res) => {
            string[]? tags = dialog.execute.end(res);
            if (tags != null && tags.length > 0) {
                AppWindow.get_command_manager().execute(new AddTagsCommand(tags, media));
            }
        });

        return true;
    }

    public bool internal_drop_received_arbitrary(Sidebar.Entry entry) {
        if (!(entry is Tags.SidebarEntry)) {
            return false;
        }

        var tag_entry = (Tags.SidebarEntry) entry;
        var old_tag_path = tag_entry.for_tag().get_path();

        if (!Tag.global.exists(old_tag_path)) {
            warning ("Tag at path does not exist");
            return false;
        }

        // if this is already a top-level tag, do a short-circuit return
        if (HierarchicalTagUtilities.enumerate_path_components(old_tag_path).size < 2)
            return true;

        AppWindow.get_command_manager().execute(
            new ReparentTagCommand(Tag.for_path(old_tag_path), "/"));

            
        var new_path = "/" + HierarchicalTagUtilities.enumerate_path_components(old_tag_path).last();            
        var lw = (LibraryWindow)AppWindow.get_instance();
        lw.switch_to_tag(Tag.for_path(new_path));

        return true;
    }
    

    public Gtk.PopoverMenu? get_sidebar_context_menu() {
        return context_menu;
    }
}

public class Tags.SidebarEntry : Sidebar.Entry, Sidebar.SimplePageEntry, Sidebar.RenameableEntry,
    Sidebar.DestroyableEntry, Sidebar.InternalDropTargetEntry, Sidebar.ExpandableEntry
     {
    private string single_tag_icon = Resources.ICON_ONE_TAG;
    
    private Tag tag;
    
    public SidebarEntry(Tag tag) {
        this.tag = tag;
    }
    
    internal static void init() {
    }
    
    internal static void terminate() {
    }

    public override bool draggable() {
        return true;
    }
    
    public Tag for_tag() {
        return tag;
    }
    
    public override string get_sidebar_name() {
        return tag.get_user_visible_name();
    }
    
    public override string? get_sidebar_icon() {
        return single_tag_icon;
    }
    
    protected override Page create_page() {
        return new TagPage(tag);
    }
    
    public bool is_user_renameable() {
        return true;
    }
    
    public void rename(string new_name) {
        string? prepped = Tag.prep_tag_name(new_name);
        if (prepped == null)
            return;

        prepped = prepped.replace("/", "");
        
        if (prepped == tag.get_user_visible_name())
            return;
        
        if (prepped == "")
            return;
        
        AppWindow.get_command_manager().execute(new RenameTagCommand(tag, prepped));
    }
    
    public void destroy_source() {
        Dialogs.confirm_delete_tag.begin(tag, (source, res) => {
            if (Dialogs.confirm_delete_tag.end(res)) {
                AppWindow.get_command_manager().execute(new DeleteTagCommand(tag));
            }
        });
    }
    
    public bool internal_drop_received(Gee.Collection<MediaSource> media) {
        AppWindow.get_command_manager().execute(new TagUntagPhotosCommand(tag, media, media.size,
            true));
        
        return true;
    }


    public bool internal_drop_received_arbitrary(Sidebar.Entry entry) {
        if (!(entry is Tags.SidebarEntry)) {
            return false;
        }

        var tag_entry = (Tags.SidebarEntry) entry;
        var old_tag_path = tag_entry.for_tag().get_path();

        // This should be prevented by the drag & drop mechanics
        if (old_tag_path == tag.get_path()) {
            return true;
        }

        // if we're dragging onto one of our children, it's a no-op
        foreach (var parent_path in HierarchicalTagUtilities.enumerate_parent_paths(tag.get_path())) {
            if (parent_path == old_tag_path) {
                return true;
            }
        }

        if (!Tag.global.exists(old_tag_path)) {
            return false;
        }

        // if we're dragging onto our parent, it's a no-op
        Tag old_tag = Tag.for_path(old_tag_path);
        Tag old_tag_parent = old_tag.get_hierarchical_parent();
        if (old_tag_parent != null && old_tag_parent.get_path() == tag.get_path()) {
            return true;
        }
        
        
        AppWindow.get_command_manager().execute(
            new ReparentTagCommand(old_tag, tag.get_path()));
        var new_path = tag.get_path() + HierarchicalTagUtilities.enumerate_path_components(old_tag_path).last();
        var lw = (LibraryWindow)AppWindow.get_instance();
        lw.switch_to_tag(Tag.for_path(new_path));
        
        return true;
    }

    public bool expand_on_select() {
        return false;
    }
}

