// A ComboBox Control With Grouping
// Bradley Smith - 2010/06/24 (updated 2015/04/14)

using System;
using System.Collections;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel;
using System.Windows.Forms.VisualStyles;
using BufferedPainting;
using SharpBoot;

/// <summary>
/// Represents a Windows combo box control that, when bound to a data source, is capable of 
/// displaying items in groups/categories.
/// </summary>
[DesignerCategory("")]
public class GroupedComboBox : ComboBox, IComparer {

	private BindingSource _bindingSource;		                // used for change detection and grouping
	private Font _groupFont;					                // for painting
	private string _groupMember;				                // name of group-by property
	private PropertyDescriptor _groupProperty;	                // used to get group-by values
	private ArrayList _internalItems;			                // internal sorted collection of items
    private BindingSource _internalSource;                      // binds sorted collection to the combobox
	private TextFormatFlags _textFormatFlags;	                // used in measuring/painting
    private BufferedPainter<ComboBoxState> _bufferedPainter;    // provides buffered paint animations
    private bool _isNotDroppedDown;
	private IComparer _sortComparer;

	/// <summary>
	/// Gets or sets the data source for this GroupedComboBox.
	/// </summary>
	[DefaultValue("")]
	[RefreshProperties(RefreshProperties.Repaint)]
	[AttributeProvider(typeof(IListSource))]
	public new object DataSource {
		get {
			// binding source should be transparent to the user
			return (_bindingSource != null) ? _bindingSource.DataSource : null;
		}
		set {
            _internalSource = null;

			if (value != null) {
				// wrap the object in a binding source and listen for changes
                _bindingSource = new BindingSource(value, String.Empty);
				_bindingSource.ListChanged += new ListChangedEventHandler(mBindingSource_ListChanged);
				SyncInternalItems();
			}
			else {
				// remove binding
				base.DataSource = _bindingSource = null;
			}
		}
	}
	/// <summary>
	/// Gets a value indicating whether the drawing of elements in the list will be handled by user code. 
	/// </summary>
	[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
	public new DrawMode DrawMode {
		get {
			return base.DrawMode;
		}
	}
	/// <summary>
	/// Gets or sets the property to use when grouping items in the list.
	/// </summary>
	[DefaultValue("")]
	public string GroupMember {
		get { return _groupMember; }
		set {
			_groupMember = value;
			if (_bindingSource != null) SyncInternalItems();
		}
	}
	/// <summary>
	/// Gets or sets an implementation of the <see cref="IComparer"/> interface 
	/// that sorts the items in the control. It will be applied separately to 
	/// the group headings. The default value is <see cref="Comparer.Default"/>.
	/// </summary>
	[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
	public IComparer SortComparer {
		get {
			return _sortComparer;
		}
		set {
			if (value == null) throw new ArgumentNullException("value");
			if (value == this) throw new ArgumentException("The owning control cannot be used as a comparer.", "value");

			if (_sortComparer != value) {				
				_sortComparer = value;
				if (_bindingSource != null) SyncInternalItems();
			}
		}
	}

	/// <summary>
	/// Initialises a new instance of the GroupedComboBox class.
	/// </summary>
	public GroupedComboBox() {
		base.DrawMode = DrawMode.OwnerDrawVariable;
		_groupMember = String.Empty;
		_internalItems = new ArrayList();
		_textFormatFlags = TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | TextFormatFlags.SingleLine | TextFormatFlags.VerticalCenter;
		_sortComparer = Comparer.Default;

	    if (Program.IsWin)
	    {
	        _bufferedPainter = new BufferedPainter<ComboBoxState>(this);
	        _bufferedPainter.DefaultState = ComboBoxState.Normal;
	        _bufferedPainter.PaintVisualState += new EventHandler<BufferedPaintEventArgs<ComboBoxState>>(_bufferedPainter_PaintVisualState);
	        _bufferedPainter.AddTransition(ComboBoxState.Normal, ComboBoxState.Hot, 250);
	        _bufferedPainter.AddTransition(ComboBoxState.Hot, ComboBoxState.Normal, 350);
	        _bufferedPainter.AddTransition(ComboBoxState.Pressed, ComboBoxState.Normal, 350);
	    }

        ToggleStyle();
	}

    /// <summary>
    /// Releases the resources used by the control.
    /// </summary>
    /// <param name="disposing"></param>
    protected override void Dispose(bool disposing) {
        if (_bindingSource != null) _bindingSource.Dispose();
        if (_internalSource != null) _internalSource.Dispose();
        
        base.Dispose(disposing);
    }

    /// <summary>
    /// Recreates the control's handle when the DropDownStyle property changes.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnDropDownStyleChanged(EventArgs e) {
        base.OnDropDownStyleChanged(e);
        ToggleStyle();
    }

    /// <summary>
    /// Redraws the control when the dropdown portion is displayed.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnDropDown(EventArgs e) {
        base.OnDropDown(e);
        _isNotDroppedDown = false;
        if (_bufferedPainter.Enabled) Invalidate();
    }

    /// <summary>
    /// Redraws the control when the dropdown portion closes.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnDropDownClosed(EventArgs e) {
        base.OnDropDownClosed(e);
        _isNotDroppedDown = true;
        if (_bufferedPainter.Enabled) Invalidate();
    }

    /// <summary>
    /// Repaints the control when it receives input focus.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnGotFocus(EventArgs e) {
        base.OnGotFocus(e);
        if (_bufferedPainter.Enabled) Invalidate();
    }

    /// <summary>
    /// Repaints the control when it loses input focus.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnLostFocus(EventArgs e) {
        base.OnLostFocus(e);
        if (_bufferedPainter.Enabled) Invalidate();
    }

    /// <summary>
    /// Paints the control without a background (when using buffered painting).
    /// </summary>
    /// <param name="pevent"></param>
    protected override void OnPaintBackground(PaintEventArgs pevent) {
        _bufferedPainter.State = GetRenderState();
    }

    /// <summary>
    /// Redraws the control when the selected item changes.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnSelectedItemChanged(EventArgs e) {
        base.OnSelectedItemChanged(e);
        if (_bufferedPainter.Enabled) Invalidate();
    }

	/// <summary>
	/// Explicit interface implementation for the IComparer.Compare method. Performs a two-tier comparison 
	/// on two list items so that the list can be sorted by group, then by display value.
	/// </summary>
	/// <param name="x"></param>
	/// <param name="y"></param>
	/// <returns></returns>
	int IComparer.Compare(object x, object y) {
		// compare the display values (and return the result if there is no grouping)
		int secondLevelSort = _sortComparer.Compare(GetItemText(x), GetItemText(y));
		if (_groupProperty == null) return secondLevelSort;

		// compare the group values - if equal, return the earlier comparison
		int firstLevelSort = _sortComparer.Compare(
			Convert.ToString(_groupProperty.GetValue(x)),
			Convert.ToString(_groupProperty.GetValue(y))		
		);

		if (firstLevelSort == 0)
			return secondLevelSort;
		else
			return firstLevelSort;
	}

    /// <summary>
    /// Converts a ComboBoxState into its equivalent PushButtonState value.
    /// </summary>
    /// <param name="combo"></param>
    /// <returns></returns>
    static PushButtonState GetPushButtonState(ComboBoxState combo) {
        switch (combo) {
            case ComboBoxState.Disabled:
                return PushButtonState.Disabled;
            case ComboBoxState.Hot:
                return PushButtonState.Hot;
            case ComboBoxState.Pressed:
                return PushButtonState.Pressed;
            default:
                return PushButtonState.Normal;
        }
    }

    /// <summary>
    /// Determines the state in which to render the control (when using buffered painting).
    /// </summary>
    /// <returns></returns>
    ComboBoxState GetRenderState() {
        if (!Enabled) {
            return ComboBoxState.Disabled;
        }
        else if (DroppedDown && !_isNotDroppedDown) {
            return ComboBoxState.Pressed;
        }
        else if (ClientRectangle.Contains(PointToClient(Cursor.Position))) {
            if (((Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left) && !_isNotDroppedDown) {
                return ComboBoxState.Pressed;
            }
            else {
                return ComboBoxState.Hot;
            }
        }
        else {
            return ComboBoxState.Normal;
        }
    }

	/// <summary>
	/// Determines whether the list item at the specified index is the start of a new group. In all 
	/// cases, populates the string respresentation of the group that the item belongs to.
	/// </summary>
	/// <param name="index"></param>
	/// <param name="groupText"></param>
	/// <returns></returns>
	private bool IsGroupStart(int index, out string groupText) {
		bool isGroupStart = false;
		groupText = String.Empty;

		if ((_groupProperty != null) && (index >= 0) && (index < Items.Count)) {
			// get the group value using the property descriptor
			groupText = Convert.ToString(_groupProperty.GetValue(Items[index]));

			// this item is the start of a group if it is the first item with a group -or- if
			// the previous item has a different group
			if ((index == 0) && (groupText != String.Empty)) {
				isGroupStart = true;
			}
			else if ((index - 1) >= 0) {
				string previousGroupText = Convert.ToString(_groupProperty.GetValue(Items[index - 1]));
				if (previousGroupText != groupText) isGroupStart = true;
			}
		}

		return isGroupStart;
	}

	/// <summary>
	/// Re-synchronises the internal sorted collection when the data source changes.
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e"></param>
	private void mBindingSource_ListChanged(object sender, ListChangedEventArgs e) {
		SyncInternalItems();
	}

	/// <summary>
	/// When the control font changes, updates the font used to render group names.
	/// </summary>
	/// <param name="e"></param>
	protected override void OnFontChanged(EventArgs e) {
		base.OnFontChanged(e);
		_groupFont = new Font(Font, FontStyle.Bold);
	}

	/// <summary>
	/// When the parent control changes, updates the font used to render group names.
	/// </summary>
	/// <param name="e"></param>
	protected override void OnParentChanged(EventArgs e) {
		base.OnParentChanged(e);
		_groupFont = new Font(Font, FontStyle.Bold);
	}

	/// <summary>
	/// Performs custom painting for a list item.
	/// </summary>
	/// <param name="e"></param>
	protected override void OnDrawItem(DrawItemEventArgs e) {
		base.OnDrawItem(e);

		if ((e.Index >= 0) && (e.Index < Items.Count)) {
			// get noteworthy states
			bool comboBoxEdit = (e.State & DrawItemState.ComboBoxEdit) == DrawItemState.ComboBoxEdit;
			bool selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
			bool noAccelerator = (e.State & DrawItemState.NoAccelerator) == DrawItemState.NoAccelerator;
			bool disabled = (e.State & DrawItemState.Disabled) == DrawItemState.Disabled;
			bool focus = (e.State & DrawItemState.Focus) == DrawItemState.Focus;

			// determine grouping
			string groupText;
			bool isGroupStart = IsGroupStart(e.Index, out groupText) && !comboBoxEdit;
			bool hasGroup = (groupText != String.Empty) && !comboBoxEdit;

			// the item text will appear in a different colour, depending on its state
			Color textColor;
			if (disabled)
				textColor = SystemColors.GrayText;
			else if (!comboBoxEdit && selected)
				textColor = SystemColors.HighlightText;
			else
				textColor = ForeColor;

			// items will be indented if they belong to a group
			Rectangle itemBounds = Rectangle.FromLTRB(
				e.Bounds.X + (hasGroup ? 12 : 0), 
				e.Bounds.Y + (isGroupStart ? (e.Bounds.Height / 2) : 0), 
				e.Bounds.Right, 
				e.Bounds.Bottom
			);
			Rectangle groupBounds = new Rectangle(
				e.Bounds.X, 
				e.Bounds.Y, 
				e.Bounds.Width, 
				e.Bounds.Height / 2
			);

			if (isGroupStart && selected) {
				// ensure that the group header is never highlighted
				e.Graphics.FillRectangle(SystemBrushes.Highlight, e.Bounds);
				e.Graphics.FillRectangle(new SolidBrush(BackColor), groupBounds);
			}
            else if (disabled) {
                // disabled appearance
                e.Graphics.FillRectangle(Brushes.WhiteSmoke, e.Bounds);
            }
            else if (!comboBoxEdit) {
                // use the default background-painting logic
                e.DrawBackground();
            }

			// render group header text
			if (isGroupStart) TextRenderer.DrawText(
				e.Graphics, 
				groupText, 
				_groupFont, 
				groupBounds, 
				ForeColor, 
				_textFormatFlags
			);

			// render item text
			TextRenderer.DrawText(
				e.Graphics, 
				GetItemText(Items[e.Index]), 
				Font, 
				itemBounds, 
				textColor, 
				_textFormatFlags
			);

			// paint the focus rectangle if required
			if (focus && !noAccelerator) {
				if (isGroupStart && selected) {
					// don't draw the focus rectangle around the group header
					ControlPaint.DrawFocusRectangle(e.Graphics, Rectangle.FromLTRB(groupBounds.X, itemBounds.Y, itemBounds.Right, itemBounds.Bottom));
				}
				else {
					// use default focus rectangle painting logic
					e.DrawFocusRectangle();
				}
			}
		}
	}

	/// <summary>
	/// Determines the size of a list item.
	/// </summary>
	/// <param name="e"></param>
	protected override void OnMeasureItem(MeasureItemEventArgs e) {
		base.OnMeasureItem(e);

		e.ItemHeight = Font.Height;

		string groupText;
		if (IsGroupStart(e.Index, out groupText)) {
			// the first item in each group will be twice as tall in order to accommodate the group header
			e.ItemHeight *= 2;
			e.ItemWidth = Math.Max(
				e.ItemWidth, 
				TextRenderer.MeasureText(
					e.Graphics, 
					groupText, 
					_groupFont, 
					new Size(e.ItemWidth, e.ItemHeight), 
					_textFormatFlags
				).Width
			);
		}
	}

	/// <summary>
	/// Rebuilds the internal sorted collection.
	/// </summary>
	private void SyncInternalItems() {
		// locate the property descriptor that corresponds to the value of GroupMember
		_groupProperty = null;
		foreach (PropertyDescriptor descriptor in _bindingSource.GetItemProperties(null)) {
			if (descriptor.Name.Equals(_groupMember)) {
				_groupProperty = descriptor;
				break;
			}
		}

		// rebuild the collection and sort using custom logic
		_internalItems.Clear();
		foreach (object item in _bindingSource) _internalItems.Add(item);
		_internalItems.Sort(this);

		// bind the underlying ComboBox to the sorted collection
        if (_internalSource == null) {
            _internalSource = new BindingSource(_internalItems, String.Empty);
            base.DataSource = _internalSource;
        }
        else {
            _internalSource.ResetBindings(false);
        }
	}

    /// <summary>
    /// Changes the control style to allow user-painting in DropDownList mode (when using buffered painting).
    /// </summary>
    protected void ToggleStyle() {
        if (Program.IsWin && _bufferedPainter != null && _bufferedPainter.BufferedPaintSupported && (DropDownStyle == ComboBoxStyle.DropDownList)) {
            _bufferedPainter.Enabled = true;
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.SupportsTransparentBackColor, true);
        }
        else {
            if(Program.IsWin && _bufferedPainter != null) _bufferedPainter.Enabled = false;
            SetStyle(ControlStyles.UserPaint, false);
            SetStyle(ControlStyles.AllPaintingInWmPaint, false);
            SetStyle(ControlStyles.SupportsTransparentBackColor, false);
        }

        if (IsHandleCreated) RecreateHandle();
    }

	/// <summary>
	/// Draws a combo box in the Windows Vista (and newer) style.
	/// </summary>
	/// <param name="graphics"></param>
	/// <param name="bounds"></param>
	/// <param name="state"></param>
	internal static void DrawComboBox(Graphics graphics, Rectangle bounds, ComboBoxState state) {
		Rectangle comboBounds = bounds;
		comboBounds.Inflate(1, 1);
		ButtonRenderer.DrawButton(graphics, comboBounds, GetPushButtonState(state));

		Rectangle buttonBounds = new Rectangle(
			bounds.Left + (bounds.Width - 17),
			bounds.Top,
			17,
			bounds.Height - (state != ComboBoxState.Pressed ? 1 : 0)
		);

		Rectangle buttonClip = buttonBounds;
		buttonClip.Inflate(-2, -2);

		using (Region oldClip = graphics.Clip.Clone()) {
			graphics.SetClip(buttonClip, System.Drawing.Drawing2D.CombineMode.Intersect);
			ComboBoxRenderer.DrawDropDownButton(graphics, buttonBounds, state);
			graphics.SetClip(oldClip, System.Drawing.Drawing2D.CombineMode.Replace);
		}
	}

    /// <summary>
    /// Paints the control (using the Buffered Paint API).
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void _bufferedPainter_PaintVisualState(object sender, BufferedPaintEventArgs<ComboBoxState> e) {
		VisualStyleRenderer r = new VisualStyleRenderer(VisualStyleElement.Button.PushButton.Normal);
		r.DrawParentBackground(e.Graphics, ClientRectangle, this);
		
		DrawComboBox(e.Graphics, ClientRectangle, e.State);

		Rectangle itemBounds = new Rectangle(0, 0, Width - 21, Height);
		itemBounds.Inflate(-1, -3);
		itemBounds.Offset(2, 0);

        // draw the item in the editable portion
        DrawItemState state = DrawItemState.ComboBoxEdit;
        if (Focused && ShowFocusCues && !DroppedDown) state |= DrawItemState.Focus;
        if (!Enabled) state |= DrawItemState.Disabled;
        OnDrawItem(new DrawItemEventArgs(e.Graphics, Font, itemBounds, SelectedIndex, state));
    }
}