/* Copyright (c) Citrix Systems Inc. 
 * All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met: 
 * 
 * *   Redistributions of source code must retain the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer. 
 * *   Redistributions in binary form must reproduce the above 
 *     copyright notice, this list of conditions and the 
 *     following disclaimer in the documentation and/or other 
 *     materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 * SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using XenAdmin.CustomFields;
using XenAPI;
using XenAdmin.XenSearch;


namespace XenAdmin.Controls.XenSearch
{
    public partial class GroupingControl : UserControl
    {
        private static readonly List<GroupingType> potentialGroups;
        private static readonly FolderGroupingType folderGroupingType;
        private static readonly List<CustomFieldGroupingType> customFields;
        private Searcher searcher;
        private NonReopeningContextMenuStrip contextMenuStrip;
        private Button lastButtonClicked;

        public static event EventHandler CustomFieldRemoved;
        private const int MAX_GROUPS = 5;
        private const int innerGutter = 6;

        static GroupingControl()
        {
            potentialGroups = new List<GroupingType>();
            XenModelObjectPropertyGroupingType<Pool> poolGroup =
                new XenModelObjectPropertyGroupingType<Pool>(ObjectTypes.AllExcFolders & ~ObjectTypes.Pool, // i.e., all except Pool
                PropertyNames.pool, null);

			var applianceGroup = new XenModelObjectPropertyGroupingType<VM_appliance>(ObjectTypes.VM, PropertyNames.appliance, poolGroup);
            
			XenModelObjectPropertyGroupingType<Host> hostGroup =
                new XenModelObjectPropertyGroupingType<Host>(ObjectTypes.AllExcFolders & ~ObjectTypes.Pool & ~ObjectTypes.Server,
				PropertyNames.host, applianceGroup);
        	
            potentialGroups.Add(poolGroup);
            potentialGroups.Add(hostGroup);
            potentialGroups.Add(new PropertyGroupingType<String>(ObjectTypes.VM, PropertyNames.os_name));
            potentialGroups.Add(new PropertyGroupingType<vm_power_state>(ObjectTypes.VM, PropertyNames.power_state));
            potentialGroups.Add(new PropertyGroupingType<VM.VirtualisationStatus>(ObjectTypes.VM, PropertyNames.virtualisation_status));
            potentialGroups.Add(new PropertyGroupingType<ObjectTypes>(ObjectTypes.AllExcFolders, PropertyNames.type));
            potentialGroups.Add(new XenModelObjectPropertyGroupingType<XenAPI.Network>(ObjectTypes.VM, PropertyNames.networks, poolGroup));
            XenModelObjectPropertyGroupingType<SR> srGroup =
                new XenModelObjectPropertyGroupingType<SR>(ObjectTypes.VM | ObjectTypes.VDI, PropertyNames.storage, poolGroup);
            potentialGroups.Add(srGroup);
            potentialGroups.Add(new XenModelObjectPropertyGroupingType<VDI>(ObjectTypes.VM, PropertyNames.disks, srGroup));
            potentialGroups.Add(new PropertyGroupingType<VM.HA_Restart_Priority>(ObjectTypes.VM, PropertyNames.ha_restart_priority));
			potentialGroups.Add(applianceGroup);
            potentialGroups.Add(new PropertyGroupingType<String>(ObjectTypes.AllExcFolders, PropertyNames.tags));
            potentialGroups.Add(new XenModelObjectPropertyGroupingType<VM>(
                ObjectTypes.AllExcFolders & ~ObjectTypes.Pool & ~ObjectTypes.Server & ~ObjectTypes.VM,
                PropertyNames.vm, hostGroup));
            potentialGroups.Add(new AllCustomFieldsGroupingType());

            folderGroupingType = new FolderGroupingType();
            customFields = new List<CustomFieldGroupingType>();
            OtherConfigAndTagsWatcher.OtherConfigChanged += OtherConfigWatcher_OtherConfigChanged;
            CustomFieldsManager.CustomFieldsChanged += OtherConfigWatcher_OtherConfigChanged;
        }

        private static void OtherConfigWatcher_OtherConfigChanged(object sender, EventArgs e)
        {
            List<CustomFieldDefinition> customFieldDefinitions = CustomFieldsManager.GetCustomFields();

            // Add new custom fields
            foreach (CustomFieldDefinition definition in customFieldDefinitions)
                if (!customFields.Exists(delegate(CustomFieldGroupingType customFieldGroupingType)
                {
                    return customFieldGroupingType.definition.Equals(definition);
                }))
                {
                    customFields.Add(new CustomFieldGroupingType(ObjectTypes.AllExcFolders, definition));
                }

            // Remove old ones
            foreach (CustomFieldGroupingType customFieldGroupingType in customFields.ToArray())
                if (!customFieldDefinitions.Exists(delegate(CustomFieldDefinition definition)
                {
                    return customFieldGroupingType.definition.Equals(definition);
                }))
                {
                    customFields.Remove(customFieldGroupingType);
                    OnCustomFieldRemoved(customFieldGroupingType);
                }
        }

        private static void OnCustomFieldRemoved(CustomFieldGroupingType groupingType)
        {
            if (CustomFieldRemoved != null)
                CustomFieldRemoved(groupingType, new EventArgs());
        }

        private readonly List<Button> groups = new List<Button>(); // Button.tag = GroupType

        public GroupingControl()
        {
            InitializeComponent();

            AddGroup(potentialGroups[0]);

            CustomFieldRemoved += new EventHandler(CustomFieldRemovalWatcher_CustomFieldRemoved); 
        }

        private void CustomFieldRemovalWatcher_CustomFieldRemoved(object sender, EventArgs e) 
        {
            CustomFieldGroupingType groupingType = sender as CustomFieldGroupingType;
            if (groupingType == null)
                return;
            foreach (Button button in groups.ToArray())
            {
                CustomFieldGroupingType groupType = button.Tag as CustomFieldGroupingType;
                if (groupType == null)
                    continue;
                if (groupType.Equals(groupingType))
                {
                    Remove(button);

                    Setup();
                }
            }
        }

        public event EventHandler GroupingChanged;
        protected void OnGroupChanged()
        {
            if (GroupingChanged != null)
                GroupingChanged(this, new EventArgs());
        }

        private void searcher_SearchForChanged()
        {
            RemoveUnwantedGroups();

            // If we're searching for folders, RemoveUnwantedGroups() has
            // removed everything. We then add in group by folder which is
            // mandatory.
            if (SearchingForFolders())
                AddGroup(folderGroupingType);
        }

        public Searcher Searcher
        {
            get { return searcher; }
            set
            {
                searcher = value;
                if (searcher != null)
                    searcher.SearchForChanged += searcher_SearchForChanged;
            }
        }

        public Grouping Grouping
        {
            get
            {
                Grouping group = null;

                Button[] buttons = groups.ToArray();

                for (int i = buttons.Length - 1; i >= 0; i--)
                {
                    Button button = buttons[i];

                    GroupingType groupType = button.Tag as GroupingType;
                    if (groupType == null)
                        return null;

                    group = groupType.GetGroup(group);
                }

                return group;
            }

            set
            {
                RemoveAll();

                Grouping grouping = value;

                while (grouping != null)
                {
                    FromGrouping(grouping);

                    grouping = grouping.subgrouping;
                }

                Setup();
            }
        }

        private bool SearchingForFolders()
        {
            return (searcher != null &&
                searcher.QueryScope != null &&
                searcher.QueryScope.WantType(ObjectTypes.Folder));
        }

        private void FromGrouping(Grouping grouping)
        {
            if (folderGroupingType.ForGrouping(grouping))
            {
                AddGroup(folderGroupingType);
                return;
            }

            foreach (GroupingType groupType in potentialGroups)
                if (groupType.ForGrouping(grouping))
                {
                    AddGroup(groupType);
                    return;
                }

            foreach (GroupingType groupType in customFields)
                if (groupType.ForGrouping(grouping))
                {
                    AddGroup(groupType);
                    return;
                }

            // If the custom field doesn't exist, we just create a new one
            CustomFieldGrouping customFieldGrouping = grouping as CustomFieldGrouping;
            if (customFieldGrouping == null)
                return;

            AddGroup(new CustomFieldGroupingType(ObjectTypes.AllExcFolders, customFieldGrouping.definition));
        }

        private void Setup()
        {
            SuspendLayout();

            bool folderSeen = false;
            int offset = 0;
            
            foreach (Button button in groups.ToArray())
            {
                // If the folder button is present, we must remove all other buttons.
                // But the folder button is always first, so it's sufficient just to
                // remove all later buttons.
                if (folderSeen)
                {
                    Remove(button);
                    continue;
                }

                if (button.Tag is FolderGroupingType)
                {
                    folderSeen = true;
                    // The folder button cannot be turned off if we're searching for folders
                    button.Enabled = !SearchingForFolders();
                }

                if (!dragging || draggedButton != button)
                {
                    button.Top = 3;
                    button.Left = offset;
                }

                offset += button.Width + innerGutter;
            }

            AddGroupButton.Left = offset;
            OnGroupChanged();

            AddGroupButton.Enabled = groups.Count < MAX_GROUPS && GetRemainingGroupTypes(null).Count > 0;

            ResumeLayout();
        }

        // Generate a list of valid groups for a button: disallowing already-used groups
        // and also ancestors of earlier buttons and descendants of later buttons.
        // "context" is the button to generate the list for.
        // Pass null for the Add Group button.
        private List<GroupingType> GetRemainingGroupTypes(Button context)
        {
            List<GroupingType> remainingGroupTypes = new List<GroupingType>(potentialGroups);
            foreach(GroupingType customField in customFields)
                remainingGroupTypes.Add(customField);

            // Remove group types which are not relevant to any of the things being searched for.
            foreach (GroupingType gt in remainingGroupTypes.ToArray())
            {
                if (!WantGroupingType(gt))
                    remainingGroupTypes.Remove(gt);
            }

            int posRelativeToContext = -1;  // -1 for before; 0 for context itself; +1 for after
            foreach (Button button in groups)
            {
                if (button == context)
                {
                    posRelativeToContext = 0;
                }
                else if (posRelativeToContext == 0)
                {
                    posRelativeToContext = 1;
                }

                GroupingType groupType = button.Tag as GroupingType;
                if(groupType == null)
                    continue;

                // Remove the button type itself.
                remainingGroupTypes.Remove(groupType);
                // Also if we are still to the left of context, also remove ancestor types of this button;
                // conversely, if we are to the right of context, remove descendant types of this button.
                // Also having Folder on another button precludes all other choices.
                foreach (GroupingType gt in remainingGroupTypes.ToArray())
                {
                    if (posRelativeToContext == -1 && groupType.IsDescendantOf(gt) ||
                        posRelativeToContext == 1 && gt.IsDescendantOf(groupType) ||
                        posRelativeToContext != 0 && groupType is FolderGroupingType)
                    {
                        remainingGroupTypes.Remove(gt);
                    }
                }
            }

            return remainingGroupTypes;
        }

        // Whether to show Folder on a button's drop-down menu
        // As before, pass context==null for the Add Group Button
        private bool ShowFolderOption(Button context)
        {
            // If this is the Add Group Button, we allow the option if there are no existing buttons
            if (context == null)
                return (groups.Count == 0);

            // For normal buttons, if this button already shows Folder, we don't allow it on the menu again
            GroupingType groupType = context.Tag as GroupingType;
            if (groupType is FolderGroupingType)
                return false;

            // Otheriwse we allow it only on the first button
            return (groups.Count != 0 && groups[0] == context);
        }

        private Button NewButton(GroupingType groupType)
        {
            Button button = new Button();
            button.AutoSize = true;
            button.AutoSizeMode = AutoSizeMode.GrowAndShrink;
            button.Text = groupType.ToString();
            button.UseVisualStyleBackColor = true;

            button.TextAlign = ContentAlignment.MiddleLeft;
            button.TextImageRelation = TextImageRelation.TextBeforeImage;
            button.Padding = new Padding(0, 0, 2, 0);
            button.Image = Properties.Resources.expanded_triangle;
            button.ImageAlign = ContentAlignment.MiddleRight;

            button.Tag = groupType;
            button.Click += new EventHandler(button_Click);
            button.MouseDown += new MouseEventHandler(button_MouseDown);
            button.MouseUp += new MouseEventHandler(button_MouseUp);
            button.MouseMove += new MouseEventHandler(button_MouseMove);

            return button;
        }

        private Button draggedButton = null;
        private Point clickPoint = Point.Empty;
        private int dragOffset = 0;
        private bool dragging = false;

        void button_MouseUp(object sender, MouseEventArgs e)
        {
            draggedButton = null;
            dragging = false;
        }

        void button_MouseDown(object sender, MouseEventArgs e)
        {
            Button button = sender as Button;
            if (button == null)
                return;

            draggedButton = button;
            clickPoint = button.PointToScreen(e.Location);
            dragOffset = e.X;
        }

        void button_MouseMove(object sender, MouseEventArgs e)
        {
            if (draggedButton == null)
                return;

            Point OnScreen = draggedButton.PointToScreen(e.Location);

            if (!dragging && Math.Abs(clickPoint.X - OnScreen.X) < 5)
                return;

            dragging = true;

            draggedButton.BringToFront();

            // Now we need to figure out where to move it,
            // both on the screen and in the internal array of buttons.
            // We need to take into account which buttons we have dragged it past,
            // and which buttons it's allowed to pass given the grouping hierarchy.
            // Note that if buttons are already out of order (legacy configuration),
            // we don't force them back in order, but they can't get any worse.

            GroupingType draggedGroupType = draggedButton.Tag as GroupingType;
            int draggedButtonLeft = draggedButton.Left;
            int draggedButtonRight = draggedButton.Left + draggedButton.Width;
            int draggedButtonIndex = groups.IndexOf(draggedButton);

            int offset = 0;
            int leftBarrier = -1;
            int swapWith = -1;
            int rightBarrier = -1;
            Button[] groupsArray = groups.ToArray();
            foreach (Button candidateButton in groupsArray)
            {
                GroupingType candidateGroupType = candidateButton.Tag as GroupingType;
                int candidateButtonMiddle = offset + (candidateButton.Width / 2);
                int candidateButtonIndex = groups.IndexOf(candidateButton);

                if (draggedGroupType != null && candidateGroupType != null)
                {
                    if (candidateButtonIndex < draggedButtonIndex)
                    {
                        if (draggedGroupType.IsDescendantOf(candidateGroupType))
                            leftBarrier = candidateButtonIndex;
                    }
                    else if (candidateButtonIndex > draggedButtonIndex)
                    {
                        if (candidateGroupType.IsDescendantOf(draggedGroupType))
                            rightBarrier = candidateButtonIndex;
                    }
                }


                if (swapWith == -1 &&
                    candidateButtonMiddle > draggedButtonLeft &&
                    candidateButtonMiddle < draggedButtonRight)
                {
                    swapWith = candidateButtonIndex;
                }

                offset += candidateButton.Width + innerGutter;
            }

            Point InControl = PointToClient(OnScreen);
            int potentialLocation = InControl.X - dragOffset;

            bool movedLeft = (swapWith >= 0 && swapWith < draggedButtonIndex);
            bool movedRight = (swapWith >= 0 && swapWith > draggedButtonIndex);
            bool movedNeither = !movedLeft && !movedRight;
            if (movedRight || movedNeither)
            {
                int maxRight = 0;
                if (rightBarrier >= 0)
                {
                    if (swapWith >= 0 && swapWith >= rightBarrier)
                        swapWith = rightBarrier - 1;
                    maxRight = groupsArray[rightBarrier].Left - draggedButton.Width - innerGutter;
                }
                else if (groupsArray.Length >= 2)
                    maxRight = groupsArray[groupsArray.Length - 2].Right + innerGutter;

                if (potentialLocation > maxRight)
                    potentialLocation = maxRight;
            }
            if (movedLeft || movedNeither)
            {
                int maxLeft = 0;
                if (leftBarrier >= 0)
                {
                    if (swapWith >= 0 && swapWith <= leftBarrier)
                        swapWith = leftBarrier + 1;
                    maxLeft = groupsArray[leftBarrier].Right + innerGutter;
                }

                if (potentialLocation < maxLeft)
                    potentialLocation = maxLeft;
            }

            draggedButton.Left = potentialLocation;
            if (swapWith >= 0 && swapWith != draggedButtonIndex)
            {
                groups.Remove(draggedButton);
                groups.Insert(swapWith, draggedButton);
                Setup();
            }
        }

        void AddItemToMenu(NonReopeningContextMenuStrip menu, string text, object tag, EventHandler clickHandler)
        {
            ToolStripMenuItem menuItem = new ToolStripMenuItem();
            menuItem.Text = text;
            menuItem.Tag = tag;
            menuItem.Click += clickHandler;
            menu.Items.Add(menuItem);
        }

        void button_Click(object sender, EventArgs e)
        {
            if (dragging)
                return;

            Button button = sender as Button;
            if (button == null)
                return;

            GroupingType groupType = button.Tag as GroupingType;
            if (groupType == null)
                return;

            if (button != lastButtonClicked)
                contextMenuStrip = new NonReopeningContextMenuStrip();  // new so that it can open immediately
            else if (!contextMenuStrip.CanOpen)
                return;
            else
                contextMenuStrip.Items.Clear();

            AddItemToMenu(contextMenuStrip, Messages.REMOVE_GROUPING, button,
                new EventHandler(removeGroupItem_Click));

            if (ShowFolderOption(button))
            {
                contextMenuStrip.Items.Add(new ToolStripSeparator());
                AddItemToMenu(contextMenuStrip, folderGroupingType.ToString(),
                    new KeyValuePair<Button, GroupingType>(button, folderGroupingType),
                    new EventHandler(changeGroupItem_Click));
            }

            List<GroupingType> remainingGroupTypes = GetRemainingGroupTypes(button);
            if (remainingGroupTypes.Count > 0)
            {
                contextMenuStrip.Items.Add(new ToolStripSeparator());
                foreach (GroupingType remainingGroupType in remainingGroupTypes)
                    AddItemToMenu(contextMenuStrip, remainingGroupType.ToString(),
                        new KeyValuePair<Button, GroupingType>(button, remainingGroupType),
                        new EventHandler(changeGroupItem_Click));
            }

            lastButtonClicked = button;
            contextMenuStrip.Show(this, new Point(button.Left, button.Bottom));

            //Setup();
        }

        void removeGroupItem_Click(object sender, EventArgs e)
        {
            ToolStripMenuItem menuitem = sender as ToolStripMenuItem;
            if (menuitem == null)
                return;

            Button button = menuitem.Tag as Button;
            if (button == null)
                return;

            Remove(button);

            Setup();
        }

        private void RemoveAll()
        {
            foreach (Button button in groups.ToArray())
                Remove(button);
        }

        private void Remove(Button button)
        {
            groups.Remove(button);
            Controls.Remove(button);
        }

        // Do we want this grouping type, based on the search-for?
        private bool WantGroupingType(GroupingType gt)
        {
            QueryScope scope = (searcher == null ? null : searcher.QueryScope);
            if (scope == null)
                return true;
            if (scope.WantType(ObjectTypes.Folder))
                return false;  // searching for folder forbids all grouping types (we add group by folder back in separately)
            return (scope.WantAnyOf(gt.AppliesTo));
        }

        private bool WantGroupingType(Button button)
        {
            GroupingType gt = button.Tag as GroupingType;
            return WantGroupingType(gt);
        }

        // Remove groups which don't match the current search-for
        private void RemoveUnwantedGroups()
        {
            foreach (Button button in groups.ToArray())
            {
                if (!WantGroupingType(button))
                    Remove(button);
            }
            Setup();
        }

        void changeGroupItem_Click(object sender, EventArgs e)
        {
            ToolStripMenuItem menuitem = sender as ToolStripMenuItem;
            if (menuitem == null)
                return;
             
            if(!(menuitem.Tag is KeyValuePair<Button, GroupingType>))
                return;

            KeyValuePair<Button, GroupingType> kvp = (KeyValuePair<Button, GroupingType>) menuitem.Tag;

            kvp.Key.Tag = kvp.Value;
            kvp.Key.Text = kvp.Value.ToString();

            Setup();
        }

        private void AddGroupButton_Click(object sender, EventArgs e)
        {
            if (groups.Count >= MAX_GROUPS)
                return;

            if (AddGroupButton != lastButtonClicked)
                contextMenuStrip = new NonReopeningContextMenuStrip();  // new so that it can open immediately
            else if (!contextMenuStrip.CanOpen)
                return;
            else
                contextMenuStrip.Items.Clear();

            if (ShowFolderOption(null))
            {
                AddItemToMenu(contextMenuStrip, folderGroupingType.ToString(), folderGroupingType,
                    new EventHandler(addGroupItem_Click));
                contextMenuStrip.Items.Add(new ToolStripSeparator());
            }

            foreach (GroupingType groupType in GetRemainingGroupTypes(null))
                AddItemToMenu(contextMenuStrip, groupType.ToString(), groupType,
                    new EventHandler(addGroupItem_Click));

            lastButtonClicked = AddGroupButton;
            contextMenuStrip.Show(this, new Point(AddGroupButton.Left, AddGroupButton.Bottom));
        }

        void addGroupItem_Click(object sender, EventArgs e)
        {
            ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
            if (menuItem == null)
                return;

            GroupingType groupType = menuItem.Tag as GroupingType;
            if (groupType == null)
                return;

            AddGroup(groupType);
        }

        void AddGroup(GroupingType groupType)
        {
            Button button = NewButton(groupType);

            groups.Add(button);
            Controls.Add(button);

            Setup();
        }

        public abstract class GroupingType
        {
            private readonly ObjectTypes appliesTo;

            protected GroupingType(ObjectTypes appliesTo)
            {
                this.appliesTo = appliesTo;
            }

            public ObjectTypes AppliesTo
            {
                get { return appliesTo; }
            }

            public abstract Grouping GetGroup(Grouping subgrouping);

            public abstract bool ForGrouping(Grouping grouping);

            public virtual bool IsDescendantOf(GroupingType gt)
            {
                return false;
            }
        }

        public class FolderGroupingType : GroupingType
        {
            public FolderGroupingType()
                : base(ObjectTypes.AllIncFolders)
            {
            }

            public override Grouping GetGroup(Grouping subgrouping)
            {
                return new FolderGrouping(subgrouping);
            }

            public override bool ForGrouping(Grouping grouping)
            {
                return grouping is FolderGrouping;
            }
            
            public override string ToString()
            {
                return Messages.FOLDER;
            }
        }

        public class PropertyGroupingType<T> : GroupingType
        {
            protected readonly PropertyNames property;
            protected readonly String i18n;

            public PropertyGroupingType(ObjectTypes appliesTo, PropertyNames property)
                : base(appliesTo)
            {
                this.property = property;
                this.i18n = PropertyAccessors.PropertyNames_i18n[property];
            }

            public override string ToString()
            {
                return i18n;
            }

            public override Grouping GetGroup(Grouping subgrouping)
            {
                return new PropertyGrouping<T>(property, subgrouping);
            }

            public override bool ForGrouping(Grouping grouping)
            {
                PropertyGrouping<T> propertyGrouping = grouping as PropertyGrouping<T>;

                if (propertyGrouping == null)
                    return false;

                return propertyGrouping.property == property;
            }
        }

        public class XenModelObjectPropertyGroupingType<T> 
            : PropertyGroupingType<T> where T : XenObject<T>
        {
            protected readonly GroupingType parent;  // the GroupingType next up in the tree: null for the top of the tree

            public XenModelObjectPropertyGroupingType(ObjectTypes appliesTo, PropertyNames property, GroupingType parent)
                : base(appliesTo, property)
            {
                this.parent = parent;
            }

            public override Grouping GetGroup(Grouping subgrouping)
            {
                return new XenModelObjectPropertyGrouping<T>(property, subgrouping);
            }

            public override bool IsDescendantOf(GroupingType gt)
            {
                return (gt == parent || (parent != null && parent.IsDescendantOf(gt)));
            }
        }

        public class CustomFieldGroupingType : GroupingType
        {
            public readonly CustomFieldDefinition definition;

            public CustomFieldGroupingType(ObjectTypes appliesTo, CustomFieldDefinition definition)
                : base(appliesTo)
            {
                this.definition = definition;
            }

            public override string ToString()
            {
                return definition.Name;
            }

            public override Grouping GetGroup(Grouping subgrouping)
            {
                return new CustomFieldGrouping(definition, subgrouping);
            }

            public override bool ForGrouping(Grouping grouping)
            {
                CustomFieldGrouping customFieldGrouping = grouping as CustomFieldGrouping;
                if (customFieldGrouping == null)
                    return false;

                return customFieldGrouping.definition.Equals(definition);
            }
        }

        public class AllCustomFieldsGroupingType : GroupingType
        {
            public AllCustomFieldsGroupingType()
                : base(ObjectTypes.AllExcFolders)
            {
            }

            public override Grouping GetGroup(Grouping subgrouping)
            {
                return new AllCustomFieldsGrouping(subgrouping);
            }

            public override bool ForGrouping(Grouping grouping)
            {
                return grouping is AllCustomFieldsGrouping;
            }
            
            public override string ToString()
            {
                return Messages.CUSTOM_FIELDS;
            }
        }
    }
}