using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace InvExplorer
{
    /// <summary>
    /// TextBox with an auto-search dropdown
    /// </summary>
    public class SearchBox : TextBox
    {
        //------------------------------------------------------------------------------
        #region ** fields

        Popup       _popup;
        ListBox     _listBox;
        bool        _ignoreTextBoxChange, _ignoreListBoxChange;
        Storyboard  _sbUpdateListBoxContent;
        object      _selectedItem;

        #endregion

        //------------------------------------------------------------------------------
        #region ** ctor

        /// <summary>
        /// Initializes a new instance of a <see cref="SearchBox"/>.
        /// </summary>
        public SearchBox()
        {
            // initialize properties
            MaxDropDownItems = 8;

            // initialize ListBox
            _listBox = new ListBox();
            _listBox.IsTabStop = false;

            // put list in a popup
            _popup = new Popup();
            _popup.Child = _listBox;

            // hook up event handlers
            TextChanged += SearchBox_TextChanged;
            _listBox.SelectionChanged += _listBox_SelectionChanged;

            // storyboard to update the list on a delay while the user types
            _sbUpdateListBoxContent = new Storyboard();
            _sbUpdateListBoxContent.Duration = new Duration(TimeSpan.FromMilliseconds(200));
            _sbUpdateListBoxContent.Completed += UpdateListBoxContent;

            // close popup when root visual is resized and when we lose focus
            Loaded += (s, e) =>
            {
                var root = Application.Current.RootVisual as FrameworkElement;
                if (root != null)
                {
                    root.SizeChanged += (ss, ee) => { IsDropDownOpen = false; };
                    LostFocus += (ss, ee) => { IsDropDownOpen = false; };
                }
            };
        }

        #endregion

        //------------------------------------------------------------------------------
        #region ** object model

        /// <summary>
        /// Gets or sets the collection of items to be selected from.
        /// </summary>
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="ItemsSource"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register(
                "ItemsSource", typeof(IEnumerable), typeof(SearchBox),
                new PropertyMetadata(null));
        /// <summary>
        /// Gets or sets the currently selected item.
        /// </summary>
        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="SelectedItem"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register(
                "SelectedItem", typeof(object), typeof(SearchBox),
                new PropertyMetadata(OnPropertyChanged));
        /// <summary>
        /// Gets or sets the name of the property displayed when the user selects an item.
        /// </summary>
        public string DisplayMemberPath { get; set; }
        /// <summary>
        /// Gets or sets a comma-delimited list of item properties to use in the search.
        /// </summary>
        public string SearchMemberPaths { get; set; }
        /// <summary>
        /// Gets or sets the maximum number of items to show in the dropdown.
        /// </summary>
        public int MaxDropDownItems { get; set; }
        public object SelectedItemXX
        {
            get { return _selectedItem; }
            set
            {
                // hide popup
                IsDropDownOpen = false;

                if (value != _selectedItem)
                {
                    // update TextBox content
                    _ignoreTextBoxChange = true;
                    string text = string.Empty;
                    if (value != null)
                    {
                        var pi = !string.IsNullOrEmpty(DisplayMemberPath)
                            ? value.GetType().GetProperty(DisplayMemberPath)
                            : null;
                        text = pi != null
                            ? string.Format("{0}", pi.GetValue(value, null))
                            : value.ToString();
                    }
                    Text = text;
                    SelectAll();

                    // save new value and fire event
                    _selectedItem = value;
                    OnItemSelected(EventArgs.Empty);
                }
            }
        }
        /// <summary>
        /// Gets or sets a value that indicates whether the dropdown is currently open.
        /// </summary>
        public bool IsDropDownOpen
        {
            get { return _popup.IsOpen; }
            set { _popup.IsOpen = value; }
        }
        /// <summary>
        /// Occurs when the user makes a selection.
        /// </summary>
        public event EventHandler ItemSelected;
        /// <summary>
        /// Raises the <see cref="ItemSelected"/> event.
        /// </summary>
        /// <param name="e"><see cref="EventArgs"/> that contains the event data.</param>
        protected void OnItemSelected(EventArgs e)
        {
            if (ItemSelected != null)
                ItemSelected(this, e);
        }

        #endregion

        //------------------------------------------------------------------------------
        #region ** overrides/event handlers

        // change selection with up/down keys, commit with Enter
        protected override void OnKeyDown(KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Up:
                case Key.Down:
                    var itemCount = _listBox.Items.Count;
                    if (IsDropDownOpen && itemCount > 0)
                    {
                        _ignoreListBoxChange = true;
                        _listBox.SelectedIndex = (e.Key == Key.Down)
                            ? (_listBox.SelectedIndex + 1) % itemCount
                            : (_listBox.SelectedIndex + itemCount - 1) % itemCount;
                        e.Handled = true;
                    }
                    break;

                case Key.Enter:
                    CommitSelection();
                    e.Handled = true;
                    break;

                case Key.Escape:
                    IsDropDownOpen = false;
                    e.Handled = true;
                    break;
            }
            base.OnKeyDown(e);
        }

        // select content when getting focus
        protected override void OnGotFocus(RoutedEventArgs e)
        {
            SelectAll();
            base.OnGotFocus(e);
        }

        // commit selection when losing focus
        protected override void OnLostFocus(RoutedEventArgs e)
        {
            var fe = FocusManager.GetFocusedElement();
            if (!IsDropDownOpen || (fe != this && fe != _listBox))
            {
                CommitSelection();
            }
            base.OnLostFocus(e);
        }

        // update dropdown content when the text changes
        void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (!_ignoreTextBoxChange)
            {
                UpdateListBoxContent();
            }
            _ignoreTextBoxChange = false;
        }

        // commit ListBox changes when a new item is selected with the mouse
        void _listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (!_ignoreListBoxChange)
            {
                CommitSelection();
                Focus();
            }
            _ignoreListBoxChange = false;
        }

        #endregion

        //------------------------------------------------------------------------------
        #region ** private stuff

        // handle property change notifications
        static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var searchBox = d as SearchBox;
            searchBox.OnPropertyChanged(e);
        }
        protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == SelectedItemProperty)
            {
                // close drop-down
                IsDropDownOpen = false;

                // update TextBox content
                string text = string.Empty;
                if (SelectedItem != null)
                {
                    var pi = !string.IsNullOrEmpty(DisplayMemberPath)
                        ? SelectedItem.GetType().GetProperty(DisplayMemberPath)
                        : null;
                    text = pi != null
                        ? string.Format("{0}", pi.GetValue(SelectedItem, null))
                        : SelectedItem.ToString();
                }
                _ignoreTextBoxChange = true;
                Text = text;
                SelectAll();

                // and fire event
                OnItemSelected(EventArgs.Empty);
            }
        }

        // commit selection (close dropdown, copy selected item to TextBox)
        void CommitSelection()
        {
            var lbi = (ListBoxItem)_listBox.SelectedItem;
            SelectedItem = lbi != null
                ? lbi.Tag
                : null;
        }

        // update ListBox content to match current items/text
        void UpdateListBoxContent()
        {
            _sbUpdateListBoxContent.Stop();
            _sbUpdateListBoxContent.Seek(TimeSpan.Zero);
            _sbUpdateListBoxContent.Begin();
        }
        void UpdateListBoxContent(object sender, EventArgs e)
        {
            // not while an item is selected!
            if (IsDropDownOpen && _listBox.SelectedItem != null)
            {
                return;
            }

            // build list of items to show
            var terms = Text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            var items = BuildDisplayList(terms);

            // update ListBox content
            _ignoreListBoxChange = true;
            _listBox.Items.Clear();
            foreach (var item in items)
            {
                var lbi = BuildListBoxItem(item.Key, item.Value, terms);
                _listBox.Items.Add(lbi);
            }
            _ignoreListBoxChange = false;

            // update ListBox properties
            _listBox.Padding = this.Padding;
            _listBox.MinWidth = this.RenderSize.Width;
            var root = Application.Current.RootVisual;
            if (root != null)
            {
                _listBox.MaxHeight = root.DesiredSize.Height / 2;
            }

            // update popup position with new content
            //_listBox.UpdateLayout();
            UpdatePopupPosition();
        }

        // update the position of the drop-down
        void UpdatePopupPosition()
        {
            // show/hide popup
            IsDropDownOpen = _listBox.Items.Count > 0;
            if (IsDropDownOpen)
            {
                // get popup location
                var root = Application.Current.RootVisual;
                var loc = this.TransformToVisual(root).Transform(new Point(0, 0));
                _listBox.UpdateLayout();

                // adjust horizontal offset
                if (loc.X + _listBox.ActualWidth > root.DesiredSize.Width)
                {
                    loc.X = Math.Max(0, root.DesiredSize.Width - _listBox.ActualWidth);
                }

                // adjust vertical offset
                loc.Y += this.ActualHeight;
                if (loc.Y + _listBox.ActualHeight > root.DesiredSize.Height)
                {
                    var y = loc.Y - this.ActualHeight - _listBox.ActualHeight;
                    if (y > 0)
                    {
                        loc.Y = y;
                    }
                }

                // show popup
                _popup.VerticalOffset = loc.Y;
                _popup.HorizontalOffset = loc.X;
            }
        }

        // build list of items that contain the text in the textbox
        Dictionary<object, string> BuildDisplayList(string[] terms)
        {
            // list of properties to search for
            List<PropertyInfo> _props = null;

            // array of properties to include in the search
            var searchMembersArray = SearchMemberPaths != null
                ? SearchMemberPaths.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
                : new string[0];

            // get the list of items to search for
            var list = new Dictionary<object, string>();
            if (Text.Length > 2 && 
                ItemsSource != null && 
                searchMembersArray.Length > 0 && 
                terms.Length > 0)
            {
                // to build string that represents each item
                var sb = new StringBuilder();

                // add items that contain the search term
                foreach (var item in ItemsSource)
                {
                    // build list of PropertyInfo objects (once)
                    if (_props == null)
                    {
                        _props = new List<PropertyInfo>();
                        foreach (var propName in searchMembersArray)
                        {
                            var pi = item.GetType().GetProperty(propName);
                            if (pi == null)
                            {
                                throw new Exception("Search Property '" + propName + "' not found.");
                            }
                            _props.Add(pi);
                        }
                    }

                    // build string that represents each item
                    sb.Clear();
                    foreach (var pi in _props)
                    {
                        if (sb.Length > 0)
                        {
                            sb.Append('\t');
                        }
                        sb.Append(pi.GetValue(item, null));
                    }
                    var itemText = sb.ToString();

                    // check whether this item includes all search terms
                    bool include = true;
                    foreach (var term in terms)
                    {
                        if (itemText.IndexOf(term, StringComparison.OrdinalIgnoreCase) < 0)
                        {
                            include = false;
                            break;
                        }
                    }

                    // if it does, add item to list
                    if (include)
                    {
                        list[item] = itemText;
                        if (list.Count >= MaxDropDownItems)
                        {
                            break;
                        }
                    }
                }
            }

            // done
            return list;
        }

        // build ListBoxItem that corresponds to a given item
        ListBoxItem BuildListBoxItem(object key, string value, string[] terms)
        {
            var lbi = new ListBoxItem();
            lbi.Tag = key;
            lbi.Content = BuildItemContent(value, terms);
            return lbi;
        }
        TextBlock BuildItemContent(string value, string[] terms)
        {
            var tb = new TextBlock();
            tb.Text = string.Empty;
            tb.FontSize = this.FontSize;
            tb.FontFamily = this.FontFamily;

            for (int start = 0; ; )
            {
                // look for first match
                var index = -1;
                var boldItem = string.Empty;
                for (int i = 0; i < terms.Length; i++)
                {
                    int ti = value.IndexOf(terms[i], start, StringComparison.InvariantCultureIgnoreCase);
                    if (ti > -1)
                    {
                        if (index < 0 || ti < index)
                        {
                            boldItem = terms[i];
                            index = ti;
                        }
                    }
                }

                // match not found, add remainder and be done
                if (index < 0)
                {
                    CreateRun(tb, false, value.Substring(start));
                    break;
                }

                // match found: add regular and bold parts
                if (index > start)
                {
                    CreateRun(tb, false, value.Substring(start, index - start));
                }
                CreateRun(tb, true, value.Substring(index, boldItem.Length));
                start = index + boldItem.Length;
            }

            // done
            return tb;
        }
        void CreateRun(TextBlock tb, bool highlight, string text)
        {
            Run run = new Run();
            run.Text = text;
            run.FontFamily = tb.FontFamily;
            run.FontSize = tb.FontSize;
            run.FontWeight = highlight ? FontWeights.Bold : FontWeights.Normal;
            tb.Inlines.Add(run);
        }

        #endregion
    }
}
