using C1.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Shapes;

namespace Fortune1000
{
    public partial class FilterControl : UserControl
    {
        //---------------------------------------------------------------------------------------
        #region ** fields

        C1CollectionView _view;
        FilterConditionCollection _conditions;
        DelayedAction _updateHistogram;
        DataTemplate _barTemplate;

        #endregion

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

        public FilterControl()
        {
            InitializeComponent();

            // initialize conditions
            _conditions = new FilterConditionCollection();
            _conditions.CollectionChanged += _conditions_CollectionChanged;

            // initialize conditions list
            _lbConditions.ItemsSource = _conditions;
            _lbConditions.SelectionChanged += _lbConditions_SelectionChanged;
            Loaded += (s, e) => { _lbConditions.Foreground = this.Foreground; };

            // update histogram selection when size changes
            SizeChanged += (s, e) => { UpdateHistogramPosition(false); };

            // select filter values by dragging histogram with the mouse
            var dragHelper = new C1DragHelper(_bdrHistogram);
            dragHelper.DragStarted += dragHelper_DragStarted;
            dragHelper.DragDelta += dragHelper_DragDelta;
            dragHelper.DragCompleted += dragHelper_DragCompleted;
            _bdrHistogram.SizeChanged += (s, e) =>
                {
                    var rg = new RectangleGeometry();
                    rg.Rect = new Rect(new Point(0, 0), _bdrHistogram.RenderSize);
                    _bdrHistogram.Clip = rg;
                };

            // delayed action to update histogram on changes
            _updateHistogram = new DelayedAction(UpdateHistogram, 100);

            _barTemplate = (DataTemplate)Resources["_histogramBarTemplate"];
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (finalSize.Height > finalSize.Width)
            {
                // portrait
                SetGridPosition(_filterConditions, 0, 0, 1, 2);
                SetGridPosition(_bdrHistogram, 1, 0, 1, 2);
            }
            else
            {
                // landscape
                SetGridPosition(_filterConditions, 0, 0, 2, 1);
                SetGridPosition(_bdrHistogram, 0, 1, 2, 1);
            }
            return base.ArrangeOverride(finalSize);
        }
        static void SetGridPosition(FrameworkElement e, int r, int c, int rs, int cs)
        {
            e.SetValue(Grid.RowProperty, r);
            e.SetValue(Grid.ColumnProperty, c);
            e.SetValue(Grid.RowSpanProperty, rs);
            e.SetValue(Grid.ColumnSpanProperty, cs);
        }

        #endregion

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

        int _selDown, _indexDown;

        void dragHelper_DragStarted(object sender, C1DragStartedEventArgs e)
        {
            _indexDown = GetItemIndex(e);
            _selDown = SelectedCondition.SelectedIndex;
        }

        void dragHelper_DragDelta(object sender, C1DragDeltaEventArgs e)
        {
            var indexNow = GetItemIndex(e);
            var selNow = _selDown + (_indexDown - indexNow);
            if (selNow != _selDown)
            {
                SelectedCondition.SelectedIndex = selNow;
                UpdateHistogramPosition(true);
            }
        }

        void dragHelper_DragCompleted(object sender, C1DragCompletedEventArgs e)
        {
            // play click sound and refresh view
            if (SelectedCondition.SelectedIndex != _selDown)
            {
                if (ClickSound != null)
                {
                    ClickSound.Position = TimeSpan.Zero;
                    ClickSound.Play();
                }
                if (_view != null)
                {
                    _view.Refresh();
                }
            }
        }

        // converts mouse position to relative histogram index
        int GetItemIndex(C1InputEventArgs e)
        {
            var pt = e.GetPosition(this);
            var bw = _histogram.ColumnDefinitions[0].ActualWidth;
            return (int)(pt.X / bw + .5);
        }

        // update histogram when there are changes in the view or in the conditions
        void _view_CollectionChanged(IObservableVector<object> sender, IVectorChangedEventArgs e)
        {
            // update count
            var cnt = 0;
            foreach (var item in _view)
            {
                cnt++;
            }
            _tbCount.Text = string.Format("{0:n0} item{1}",
                cnt, cnt == 1 ? string.Empty : "s");

            // update histogram
            _updateHistogram.Invoke();
        }
        void _lbConditions_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            _updateHistogram.Invoke();
        }
        void _conditions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (SelectedCondition == null && Conditions.Count > 0)
            {
                _lbConditions.SelectedIndex = 0;
            }
            _updateHistogram.Invoke();
        }

        #endregion

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

        /// <summary>
        /// Gets or sets the <see cref="FilterCondition"/> that is currently selected.
        /// </summary>
        public FilterCondition SelectedCondition
        {
            get { return _lbConditions != null ? _lbConditions.SelectedValue as FilterCondition : null; }
            set { if (_lbConditions != null) _lbConditions.SelectedValue = value; }
        }
        /// <summary>
        /// Gets or sets the <see cref="C1CollectionView"/> being filtered by this control.
        /// </summary>
        public C1CollectionView CollectionView
        {
            get { return GetValue(CollectionViewProperty) as C1CollectionView; }
            set { SetValue(CollectionViewProperty, value); }
        }
        public static readonly DependencyProperty CollectionViewProperty =
            DependencyProperty.Register(
                "CollectionView", typeof(C1CollectionView), typeof(FilterControl),
                new PropertyMetadata(null, FilterControl.OnCollectionViewPropertyChanged));
        static void OnCollectionViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var fc = d as FilterControl;
            fc.OnCollectionViewPropertyChanged(e);
        }
        void OnCollectionViewPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            // disconnect old
            if (_view != null)
            {
                _view.VectorChanged -= _view_CollectionChanged;
                _view.Filter = null;
            }

            // update
            _view = e.NewValue as C1CollectionView;

            // connect new
            if (_view != null)
            {
                _view.VectorChanged += _view_CollectionChanged;
                _view.Filter = Filter;
                _updateHistogram.Invoke();
            }
        }
        /// <summary>
        /// Gets the list of <see cref="FilterCondition"/> objects being applied to the 
        /// <see cref="CollectionView"/>.
        /// </summary>
        public FilterConditionCollection Conditions
        {
            get { return _conditions; }
        }
        /// <summary>
        /// Gets or sets a MediaElement that plays a sound when the selection changes.
        /// </summary>
        public MediaElement ClickSound
        {
            get { return (MediaElement)GetValue(ClickSoundProperty); }
            set { SetValue(ClickSoundProperty, value); }
        }
        public static readonly DependencyProperty ClickSoundProperty =
            DependencyProperty.Register(
                "ClickSound", typeof(MediaElement), typeof(FilterControl),
                new PropertyMetadata(null));

        #endregion

        //---------------------------------------------------------------------------------------
        #region ** implementation

        // update the bars in the histogram to reflect the frequency of values in the 
        // data source
        void UpdateHistogram()
        {
            // get current condition
            var c = SelectedCondition;

            // no selection/data? hide and leave
            if (c == null || _view == null)
            {
                _bdrHistogram.Opacity = 0;
                return;
            }

            // update condition state
            c.Update(this);

            // get max value for scaling
            int max = 0;
            foreach (var kv in c.Values)
            {
                max = Math.Max(max, kv.Value);
            }

            // clear old histogram (except for 'all' element)
            var g = _histogram;
            var all = g.Children[0];
            var cd = g.ColumnDefinitions[0];
            g.Children.Clear();
            g.ColumnDefinitions.Clear();
            g.ColumnDefinitions.Add(cd);
            g.Children.Add(all);

            // build new histogram
            foreach (var kv in c.Values)
            {
                // add column
                cd = new ColumnDefinition();
                cd.Width = g.ColumnDefinitions[0].Width;
                g.ColumnDefinitions.Add(cd);

                // create new histogram item, add to grid
                var item = _barTemplate.LoadContent() as Grid;
                item.SetValue(Grid.ColumnProperty, g.ColumnDefinitions.Count - 1);
                g.Children.Add(item);
#if !WINDOWS_PHONE
                var tip = string.Format("{0}: {1} items", kv.Key, kv.Value);
                ToolTipService.SetToolTip(item, tip);
#endif
                // compute histogram bar size
                var transform = new ScaleTransform();
                transform.ScaleY = kv.Value / (double)max * .8 + .1;
                item.Children[0].RenderTransform = transform;
            }

            // ready, make sure it's visible
            _bdrHistogram.Opacity = 1;
            UpdateHistogramPosition(true);
        }

        // update the position of the histogram to reflect the user selection
        void UpdateHistogramPosition(bool updateBrushes)
        {
            // get selected condition
            var c = SelectedCondition;
            if (c != null)
            {
                var middle = _bdrHistogram.ActualWidth / 2;
                var barWidth = _histogram.ColumnDefinitions[0].ActualWidth;
                var pos = middle - (c.SelectedIndex + 1.5) * barWidth;
                _histogram.Margin = new Thickness(pos, 0, 0, 0);
            }

            // apply selected brush to selected item
            if (updateBrushes)
            {
                // get selected index
                var selIndex = SelectedCondition != null
                    ? SelectedCondition.SelectedIndex
                    : -1;

                // enumerate histogram children
                for (int i = 0; i < _histogram.Children.Count; i++)
                {
                    // select brush
                    var brush = i == selIndex + 1
                        ? (Brush)Resources["_brAll"]
                        : (Brush)Resources["_brBar"];

                    // get inner element and apply brush
                    var g = _histogram.Children[i] as Grid;
                    var e = g.Children[0];
                    if (e is TextBlock)
                    {
                        ((TextBlock)e).Foreground = brush;
                    }
                    else
                    {
                        ((Rectangle)e).Fill = brush;
                    }
                }
            }
        }

        // filter predicate
        bool Filter(object value)
        {
            foreach (var c in _conditions)
            {
                if (!c.Filter(value))
                {
                    return false;
                }
            }
            return true;
        }

        #endregion

    }
    /// <summary>
    /// Represents a filter condition being applied to the source <see cref="C1CollectionView"/>.
    /// </summary>
    public class FilterCondition : INotifyPropertyChanged
    {
        string _header;
        string _propName;
        object _propValue;
        PropertyInfo _propInfo;
        Dictionary<object, int> _dct = new Dictionary<object, int>();
        IValueConverter _converter;
        object _converterParameter;

        /// <summary>
        /// Gets or sets the name of the property that this condition applies to.
        /// </summary>
        public string PropertyName
        {
            get { return _propName; }
            set
            {
                if (value != PropertyName)
                {
                    _propName = value;
                    _propInfo = null;
                    OnPropertyChanged("PropertyName");
                }
            }
        }
        /// <summary>
        /// Gets or sets the string displayed in the filter list.
        /// </summary>
        public string Header
        {
            get { return !string.IsNullOrEmpty(_header) ? _header : PropertyName; }
            set
            {
                if (value != Header)
                {
                    _header = value;
                    OnPropertyChanged("Header");
                }
            }
        }
        /// <summary>
        /// Gets or sets the value being filtered on (null means 'all values').
        /// </summary>
        public object PropertyValue
        {
            get { return _propValue; }
            set
            {
                if (value != PropertyValue)
                {
                    _propValue = value;
                    OnPropertyChanged("PropertyValue");
                }
            }
        }
        /// <summary>
        /// Gets or sets the <see cref="IValueConverter"/> used to format
        /// values for grouping and display.
        /// </summary>
        public IValueConverter Converter
        {
            get { return _converter; }
            set
            {
                if (value != Converter)
                {
                    _converter = value;
                    OnPropertyChanged("Converter");
                }
            }
        }
        /// <summary>
        /// Gets or sets the object used as a converter parameter when formatting
        /// values for grouping and display.
        /// </summary>
        public object ConverterParameter
        {
            get { return _converterParameter; }
            set
            {
                if (value != ConverterParameter)
                {
                    _converterParameter = value;
                    OnPropertyChanged("ConverterParameter");
                }
            }
        }
        /// <summary>
        /// Gets the display value for the current property value.
        /// </summary>
        public object DisplayValue
        {
            get { return PropertyValue == null ? "ALL" : PropertyValue; }
        }
        /// <summary>
        /// Gets a <see cref="PropertyInfo"/> object that contains information about the
        /// property being filtered on.
        /// </summary>
        public PropertyInfo PropertyInfo
        {
            get { return _propInfo; }
        }
        /// <summary>
        /// Gets a histogram with values and their counts within the collection.
        /// </summary>
        public Dictionary<object, int> Values
        {
            get { return _dct; }
        }
        /// <summary>
        /// Gets or sets the index of the value that is currently selected.
        /// </summary>
        public int SelectedIndex
        {
            get
            {
                int current = 0;
                if (PropertyValue != null)
                {
                    foreach (var kv in _dct)
                    {
                        if (kv.Key.Equals(PropertyValue))
                        {
                            return current;
                        }
                        current++;
                    }
                }
                return -1;
            }
            set
            {
                if (value < 0)
                {
                    PropertyValue = null;
                }
                else
                {
                    int current = 0;
                    foreach (var kv in _dct)
                    {
                        if (current == value)
                        {
                            PropertyValue = kv.Key;
                            break;
                        }
                        current++;
                    }
                }
            }
        }
        /// <summary>
        /// Fires when the value of a property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        // apply this filter to an item
        internal bool Filter(object item)
        {
            if (PropertyValue != null)
            {
                var itemValue = GetFilterValue(item);
                return object.Equals(PropertyValue, itemValue);
            }
            return true;
        }

        // get the converted filter value from the item
        object GetFilterValue(object item)
        {
            if (_propInfo == null)
            {
                _propInfo = item.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName);
            }
            var itemValue = _propInfo.GetValue(item, null);
            if (_converter != null)
            {
                //itemValue = _converter.Convert(itemValue, typeof(object), _converterParameter, CultureInfo.CurrentCulture);
            }
            return itemValue;
        }

        // apply all conditions except this to an item
        bool ApplyOtherConditions(object item, FilterConditionCollection conditions)
        {
            foreach (var c in conditions)
            {
                if (c != this && !c.Filter(item))
                {
                    return false;
                }
            }
            return true;
        }

        // update the value histogram
        internal void Update(FilterControl filterControl)
        {
            // get sorted, unfiltered data to use for building histogram
            var view = filterControl.CollectionView;
            if (view != null)
            {
                var newView = new C1CollectionView();
                newView.SourceCollection = view.SourceCollection;
                using (newView.DeferRefresh())
                {
                    // keep original sort
                    foreach (var sd in view.SortDescriptions)
                    {
                        newView.SortDescriptions.Add(sd);
                    }

                    // sort by property being filtered on
                    if (filterControl.SelectedCondition != null)
                    {
                        var propName = filterControl.SelectedCondition.PropertyName;
                        var sd = new SortDescription(propName, ListSortDirection.Ascending);
                        newView.SortDescriptions.Insert(0, sd);
                    }
                }
                view = newView;
            }

            // update histogram data
            _dct.Clear();
            _propInfo = null;
            foreach (var item in view)
            {
                if (ApplyOtherConditions(item, filterControl.Conditions))
                {
                    var key = GetFilterValue(item);
                    int count;
                    _dct[key] = _dct.TryGetValue(key, out count) ? count + 1 : 1;
                }
            }
        }
    }
    /// <summary>
    /// Collection of filter conditions.
    /// </summary>
    public class FilterConditionCollection : ObservableCollection<FilterCondition>
    {
    }
    /// <summary>
    /// Performs an action after a certain delay.
    /// </summary>
    public class DelayedAction
    {
        Storyboard _sb = new Storyboard();
        public DelayedAction(Action action, int delay)
        {
            _sb.Duration = new Duration(TimeSpan.FromMilliseconds(delay));
            _sb.Completed += (s, e) => { action(); };
        }
        public void Invoke()
        {
            _sb.Stop();
            _sb.Begin();
        }
    }
}
