using System;
using System.Collections;
using System.Text.RegularExpressions;

using Gtk;
using Mono.Unix;
using Pango;
using Tomboy;

class CompareByKey : IComparer
{
    public int Compare(object o1, object o2)
    {
        DictionaryEntry e1 = (DictionaryEntry)o1;
        DictionaryEntry e2 = (DictionaryEntry)o2;
        return String.Compare((string)e1.Key, (string)e2.Key);
    }
}

class Tag : IComparable
{
    public static readonly Regex TaggingExpression =
        new Regex(@"\b[Tt]ags:\s*([^\n]*)", RegexOptions.IgnoreCase);

    private readonly ArrayList subtags;
    private readonly ArrayList notes;

    private readonly Note description;
    private readonly string title;

    private IList parentTags;

    public Tag(string title, Note description)
    {
        this.subtags = new ArrayList();
        this.notes = new ArrayList();
        this.description = description;
        this.title = title;
    }

    public string Title
    {
        get { return title; }
    }

    public Note Description
    {
        get { return description; }
    }

    public IList ParentTags
    {
        get
        {
            if (null == parentTags)
                parentTags = Extract(description);

            return parentTags;
        }
    }

    public IList SubTags
    {
        get { return subtags; }
    }

    public IList Notes
    {
        get { return notes; }

        set
        { 
            notes.Clear();
            notes.AddRange(value);
        }
    }

    public int CompareTo(object other)
    {
        return String.Compare(title, ((Tag)other).title);
    }

    public static IList Extract(Note note)
    {
        ArrayList tags = new ArrayList();

        if (null != note) {
            foreach(Match match in TaggingExpression.Matches(note.TextContent)) {
                if (match.Success) {
                    string[] tokens = match.Groups[1].Value.Split(',', ';');

                    for(int i = 0; i < tokens.Length; ++i)
                        tokens[i] = tokens[i].Trim();

                    tags.AddRange(tokens);
                }
            }
        }

        return tags;
    }
}

class TagViewDialog : Window
{
    private readonly TaggingModel model;
    private Label stats;

    public TagViewDialog(TaggingModel model) : base(WindowType.Toplevel)
    {
        this.model = model;

        InitializeComponent();
    }

    public void InitializeComponent()
    {
        AccelGroup accelGroup;
        CellRendererPixbuf iconRenderer;
        CellRendererText titleRenderer;
        TreeViewColumn titleColumn;
        TreeView treeView;
        ScrolledWindow scroller;
        HButtonBox buttonBox;
        Button closeButton;
        VBox vbox;

        accelGroup = new AccelGroup();
        iconRenderer = new CellRendererPixbuf();
        titleRenderer = new CellRendererText();
        titleColumn = new TreeViewColumn();
        treeView = new TreeView(model);
        scroller = new ScrolledWindow();
        closeButton = new Button(Stock.Close);

        stats = new Label();
        buttonBox = new HButtonBox();
        vbox = new VBox(false, 8);

        titleRenderer.Ellipsize = EllipsizeMode.End;

        titleColumn.PackStart(iconRenderer, false);
        titleColumn.AddAttribute(iconRenderer, "pixbuf", TaggingModel.IconColumn);
        titleColumn.PackStart(titleRenderer, true);
        titleColumn.AddAttribute(titleRenderer, "text", TaggingModel.TitleColumn);

        treeView.HeadersVisible = false;
        treeView.RowActivated += treeView_RowActivated;
        treeView.AppendColumn(titleColumn);

        scroller.HscrollbarPolicy = PolicyType.Never;
        scroller.VscrollbarPolicy = PolicyType.Automatic;
        scroller.ShadowType = ShadowType.In;
        scroller.Add(treeView);

        closeButton.AddAccelerator("activate", accelGroup, (uint)Gdk.Key.Escape, 0, AccelFlags.Visible);
        closeButton.Clicked += closeButton_Clicked;

        buttonBox.Layout = ButtonBoxStyle.Edge;
        buttonBox.Spacing = 8;
	buttonBox.PackStart(stats, true, true, 0);
	buttonBox.PackStart(closeButton, false, false, 0);

        vbox.BorderWidth = 6;
        vbox.PackStart(scroller, true, true, 0);
        vbox.PackStart(buttonBox, false, false, 0);
        vbox.ShowAll();

        DefaultSize = new Gdk.Size(500, 300);
        Title = Catalog.GetString("Tags");
        AddAccelGroup(accelGroup);
        Add(vbox);

        DeleteEvent += TagViewDialog_DeleteEvent;

        model.RowDeleted += model_RowDeleted;
        model.RowInserted += model_RowInserted;
    }

    private void UpdateStats()
    {
        int tagCount = model.TagCount;
        int noteCount = model.NoteCount;

        string tagCountText = Catalog.GetPluralString("{0} tag", "{0} tags", tagCount);
        string noteCountText = Catalog.GetPluralString("{0} note", "{0} notes", noteCount);

        stats.Text = String.Format(
            Catalog.GetString("Total: {0}, {1}"),
            String.Format(tagCountText, tagCount),
            String.Format(noteCountText, noteCount));
    }

    private void TagViewDialog_DeleteEvent(object sender, DeleteEventArgs e)
    {
        e.RetVal = true;
        closeButton_Clicked(null, null);
    }

    private void model_RowInserted(object sender, RowInsertedArgs e)
    {
        UpdateStats();
    }

    private void model_RowDeleted(object sender, RowDeletedArgs e)
    {
        UpdateStats();
    }

    private void closeButton_Clicked(object sender, EventArgs e)
    {
        Hide();
    }

    private void treeView_RowActivated(object sender, RowActivatedArgs e)
    {
        TreeIter iter;
        
        if (model.GetIter(out iter, e.Path)) {
            Note note = (Note)model.GetValue(iter, TaggingModel.NoteColumn);

            if (null == note) {
                Tag tag = (Tag)model.GetValue(iter, TaggingModel.TagColumn);

                if (null != tag && null != tag.Title)
                    note = model.Manager.Create(tag.Title);
            }

            if (null != note)
                note.Window.Show();
        }
    }
}

class TaggingModel : TreeStore
{
    public const int IconColumn = 0;
    public const int TitleColumn = 1;
    public const int NoteColumn = 2;
    public const int TagColumn = 3;

    private NoteManager manager;
    private int noteCount = 0;
    private int tagCount = 0;

    public TaggingModel(NoteManager manager)
        : base(typeof(Gdk.Pixbuf), typeof(string), typeof(Note), typeof(Tag))
    {
    }

    public NoteManager Manager
    {
        get { return manager; }

        set
        {
            if (value != manager) {
                if (null != manager) {
                    manager.NoteAdded -= manager_NoteAdded;
                    manager.NoteDeleted -= manager_NoteDeleted;
                    manager.NoteRenamed -= manager_NoteRenamed;
                }

                manager = value;

                if (null != manager) {
                    manager.NoteAdded += manager_NoteAdded;
                    manager.NoteDeleted += manager_NoteDeleted;
                    manager.NoteRenamed += manager_NoteRenamed;

                    UpdateNotes(manager.Notes);
                }
            }
        }
    }

    public int NoteCount
    {
        get { return noteCount; }
    }

    public int TagCount
    {
        get { return tagCount; }
    }

    public void UpdateNote(Note note)
    {
#if later
        DeleteNote(note);

        for(string tag in Tag.Extract(note)) {
        }
#else
        Manager = note.Manager; // TODO: Be more efficient
#endif
    }

    private void DeleteNote(Note note)
    {
        Manager = note.Manager; // TODO: Be more efficient
    }

    private void manager_NoteAdded(object sender, Note note)
    {
        UpdateNote(note);
    }

    private void manager_NoteDeleted(object sender, Note note)
    {
        DeleteNote(note);
    }

    private void manager_NoteRenamed(Note note, string oldTitle)
    {
        UpdateNote(note);
    }

    private void UpdateNotes(IList notes)
    {
        IDictionary unsortedTags = ExtractTags(notes);
        IDictionary rootTags = new Hashtable();
        IDictionary subTags = new Hashtable();
        Tag untagged = null;

        noteCount = notes.Count;
        tagCount = unsortedTags.Count;

        foreach(DictionaryEntry e in unsortedTags) {
            string title = (string)e.Key;

            if (title.Length > 0) {
                Tag tag = new Tag(title, manager.Find(title));
                tag.Notes = (IList)e.Value;

                if (0 == tag.ParentTags.Count) {
                    rootTags[tag.Title] = tag;
                } else {
                    subTags[tag.Title] = tag;
                }
            } else {
                untagged = new Tag(null, null);
                untagged.Notes = (IList)e.Value;
            }
        }
        
        foreach(Tag tag in subTags.Values) {
            IList parentTags = tag.ParentTags;

            if (null != parentTags) {
                foreach(string title in parentTags) {
                    Tag parent = (Tag)rootTags[title];

                    if (null == parent)
                        parent = (Tag)subTags[title];

                    parent.SubTags.Add(tag);
                }
            }
        }

        ArrayList sortedTags = new ArrayList(rootTags.Values);
        sortedTags.Sort();

        Clear();
        TreeIter root = TreeIter.Zero;

        foreach(Tag tag in sortedTags)
            MergeTag(root, tag, unsortedTags);

        if (null != untagged) {
            MergeTag(root, untagged, null);
            tagCount -= 1;
        }
    }

    private void MergeTag(TreeIter root, Tag tag, IDictionary tags)
    {
        if (null != tag.Notes && tag.Notes.Count > 0) {
            Gdk.Pixbuf noteIcon = GuiUtils.GetIcon("tomboy-note", 22);
            Gdk.Pixbuf tagIcon = GuiUtils.GetIcon(Stock.About, 22);

            string title = tag.Title;

            if (null == title)
                title = Catalog.GetString("Untagged");

            object[] values = {
                null == tag.Description ? tagIcon : noteIcon, 
                title, tag.Description, tag
            };

            if (root.Equals(TreeIter.Zero)) {
                root = AppendValues(values);
            } else {
                root = AppendValues(root, values);
            }

            foreach(Tag subtag in tag.SubTags)
                MergeTag(root, subtag, tags);

            foreach(Note note in tag.Notes) {
                if (null == tags || !tags.Contains(note.Title))
                    AppendValues(root, noteIcon, note.Title, note);
            }
        }
    }

    #region Extracting Tags from Notes

    private static IDictionary ExtractTags(IList notes)
    {
        Hashtable taggedNotes = new Hashtable();

        foreach(Note note in notes) {
            IList tags = Tag.Extract(note);

            if (tags.Count > 0) {
                foreach(string tag in tags)
                    PutNote(taggedNotes, tag, note);
            } else {
                PutNote(taggedNotes, "", note);
            }
        }

        return taggedNotes;
    }

    private static void PutNote(Hashtable tags, string tag, Note note)
    {
        IList notes = (IList)tags[tag];

        if (null == notes)
            tags[tag] = notes = new ArrayList();

        notes.Add(note);
    }

    #endregion
}

public class TaggingPlugin : NotePlugin 
{
    private TagViewDialog view = null;
    private TaggingModel model = null;

    protected override void Initialize()
    {
        model = new TaggingModel(Manager);

        ImageMenuItem viewTagsItem = new ImageMenuItem(Catalog.GetString ("View Tags"));
        viewTagsItem.Image = new Image(Stock.Index, IconSize.Menu);
        viewTagsItem.Activated += viewTagsItem_Clicked;
        viewTagsItem.Show();

        AddPluginMenuItem(viewTagsItem);
    }

    protected override void Shutdown()
    {
        try {
            if (null != model)
                model.Dispose();
        } finally {
            model = null; 
        }

        try {
            if (null != view)
                view.Destroy(); 
        } finally {
            view = null; 
        }
    }

    protected override void OnNoteOpened()
    {
        Buffer.InsertText += buffer_InsertText;
        Buffer.DeleteRange += buffer_DeleteRange;
    }

    private void buffer_InsertText (object sender, InsertTextArgs args)
    {
        if (null != model && null != Note)
            model.UpdateNote(Note);
    }

    private void buffer_DeleteRange (object sender, Gtk.DeleteRangeArgs args)
    {
        if (null != model && null != Note)
            model.UpdateNote(Note);
    }

    private void viewTagsItem_Clicked(object sender, EventArgs args)
    {
        if (null == view)
            view = new TagViewDialog(model);

        model.Manager = Manager;
        view.Show();
    }
}

// vim: sw=4 sta et
