using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Services.Client;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Data;
using C1.Silverlight.DataGrid;
using C1.Silverlight.DataGrid.Data;

namespace C1DataGrid_WCF_DS
{
    /// <summary>
    /// WCFCollectionViewBase contains the basic logic to interact with an Ado.Net data servce and allows
    /// The user to implement the ICollectionView interface without worrying about communication with the 
    /// service or the translation of the ICollectionView features like sorting and filtering into Ado.Net
    /// queries.
    /// </summary>
    /// <typeparam name="T">The main of the datacontext associated to thisWCFCollectionViewBase.</typeparam>
    public abstract class WCFCollectionViewBase<T> : BaseCollectionView, ICollectionViewFilterExtension,
        ICollectionViewSortExtension, ICollectionViewGroupExtension
        where T : class
    {
        /// <summary>
        /// The DataGrid that owns (bind its item source to) this instance of the WCFCollectionViewBase.
        /// </summary>
        private C1DataGrid Owner { get; set; }

        /// <summary>
        /// The query used to load the data from the Ado.Net DataService.
        /// </summary>
        private IQueryable<T> LoadQuery { get; set; }

        /// <summary>
        /// The number of requests to defer Refresh()
        /// </summary>
        private int _deferLevel;

        /// <summary>
        /// Initializes a new instance of the WCFCollectionViewBase class.
        /// </summary>
        /// <param name="loadQuery">The query used to get the data from the WCF service.</param>
        /// <param name="owner">The C1DataGrid that owns this ICollectionView.</param>
        public WCFCollectionViewBase(IQueryable<T> loadQuery, C1DataGrid owner)
            : base()
        {
            Owner = owner;
            LoadQuery = loadQuery;
            CollectionView = new PagedCollectionView(new List<T>());
        }

        /// <summary>
        /// The implementation of the ICollectionViewFilterExtension interface. It allows the user to 
        /// gain control of the information passed by the grid when filtering the data.
        /// When C1FilterDescriptions are set, the underlying data must be filtered according to the
        /// given value.
        /// When the owner asks for the C1FilterDescriptions the current applied filters must be returned
        /// so that, the owner C1DataGrid can show the filter icons and values on the filtered columns.
        /// </summary>
        private DataGridColumnValue<DataGridFilterState>[] _filterDescriptions = new DataGridColumnValue<DataGridFilterState>[0];
        public DataGridColumnValue<DataGridFilterState>[] C1FilterDescriptions
        {
            get
            {
                return _filterDescriptions;
            }
            set
            {
                using (DeferRefresh())
                {
                    _filterDescriptions = value;
                }
            }
        }

        /// <summary>
        /// The implementation of the ICollectionViewSortExtension interface. It allows the user to 
        /// gain control of the information passed by the grid when sorting the data.
        /// When C1SortDescriptions are set, the underlying data must be sorted according to the
        /// given value.
        /// When the owner asks for the C1SortDescriptions the current applied sorting must be returned
        /// so that, the owner C1DataGrid can show the sort icons and values on the sorted columns.
        /// </summary>
        private bool _groupSortPriority;
        private DataGridColumnValue<DataGridSortDirection>[] _sortDescriptions = new DataGridColumnValue<DataGridSortDirection>[0];
        public DataGridColumnValue<DataGridSortDirection>[] C1SortDescriptions
        {
            get
            {
                return _sortDescriptions;
            }
            set
            {
                using (DeferRefresh())
                {
                    _groupSortPriority = false;
                    _sortDescriptions = value;
                }
            }
        }

        /// <summary>
        /// The implementation of the ICollectionViewGroupExtension interface. It allows the user to 
        /// gain control of the information passed by the grid when grouping the data.
        /// When C1GroupDescriptions are set, the underlying data must be grouped according to the
        /// given value.
        /// When the owner asks for the C1GroupDescriptions the current applied groups must be returned
        /// so that, the owner C1GroupDescriptions can show the group headers and update the grouping panel.
        /// </summary>
        private DataGridColumnValue<DataGridSortDirection>[] _groupDescriptions = new DataGridColumnValue<DataGridSortDirection>[0];
        public DataGridColumnValue<DataGridSortDirection>[] C1GroupDescriptions
        {
            get
            {
                return _groupDescriptions;
            }
            set
            {
                using (DeferRefresh())
                {
                    _groupSortPriority = true;
                    _groupDescriptions = value;
                }
            }
        }

        /// <summary>
        /// Recreates the view.
        /// </summary>
        public override void Refresh()
        {
            IQueryable<T> refreshQuery = FilterQuery(C1FilterDescriptions, LoadQuery);
            _sortDescriptions = SynchronizeGroupSort(_groupDescriptions, _sortDescriptions);
            refreshQuery = SortQuery(_sortDescriptions, refreshQuery);
            InvokeService(refreshQuery);
        }

        /// <summary>
        /// Creates a filtered query applying the specified filterDescriptions to the query passed as parameter.
        /// </summary>
        /// <param name="filterDescriptions">The filter descriptions to apply to the original query.</param>
        /// <param name="query">The query to add the filters to.</param>
        /// <returns>The query after applying the filters.</returns>
        private IQueryable<T> FilterQuery(DataGridColumnValue<DataGridFilterState>[] filterDescriptions, IQueryable<T> query)
        {
            IQueryable<T> filteredQuery = query;
            foreach (DataGridColumnValue<DataGridFilterState> filter in filterDescriptions)
            {
                FilterDescription filterDescription = new FilterDescription(filter.Column.FilterMemberPath, filter.Value);
                filteredQuery = filteredQuery.Where(filterDescription.GetExpression<T>());
            }
            return filteredQuery;
        }

        /// <summary>
        /// Creates a sorted query applying the specified sortDescriptions to the query passed as parameter.
        /// </summary>
        /// <param name="sortDescriptions">The sort descriptions to apply to the original query./param>
        /// <param name="query">The query to add the filters to.</param>
        /// <returns>The query after applying the sorting.</returns>
        private IQueryable<T> SortQuery(DataGridColumnValue<DataGridSortDirection>[] sortDescriptions, IQueryable<T> query)
        {
            IQueryable<T> sortedQuery = query;
            foreach (DataGridColumnValue<DataGridSortDirection> sortDescription in sortDescriptions)
            {
                sortedQuery = InvokeSort(sortDescription.Column.SortMemberPath, new object[] { sortedQuery, sortDescription });
            }
            return sortedQuery;
        }


        /// <summary>
        /// Creates a sorted query applying the specified the sort description associated to a single proeprty.
        /// </summary>
        /// <param name="propertyPath">The property path.</param>
        /// <param name="parameters">The query to sort and the sort description to apply.</param>
        /// <returns></returns>
        private DataServiceQuery<T> InvokeSort(string propertyPath, object[] parameters)
        {
            string methodName = "SingleSort";
            Type propertyType = GetPropertyType<T>(propertyPath);
            MethodInfo info = this.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance).MakeGenericMethod(new Type[] { propertyType });
            return info.Invoke(this, parameters) as DataServiceQuery<T>;
        }

        /// <summary>
        /// Gets the type of the property.
        /// </summary>
        /// <typeparam name="T">The type that owns the property</typeparam>
        /// <param name="propertyPath">The path of the property to return.</param>
        /// <returns></returns>
        private Type GetPropertyType<T>(string propertyPath)
        {
            string[] properties = propertyPath.Split('.');
            Type propertyType = typeof(T);
            for (int i = 0; i < properties.Length; i++)
            {
                propertyType = propertyType.GetProperty(properties[i]).PropertyType;
            }
            return propertyType;
        }

        /// <summary>
        /// Sorts a query by a given property.
        /// </summary>
        /// <typeparam name="PropertyType">The type of the roperty type.</typeparam>
        /// <param name="sortedQuery">The sorted query.</param>
        /// <param name="sortInfo">The sort info.</param>
        /// <returns>The sorted query</returns>
        public DataServiceQuery<T> SingleSort<PropertyType>(DataServiceQuery<T> sortedQuery, DataGridColumnValue<DataGridSortDirection> sortInfo)
        {
            if (sortInfo.Value == DataGridSortDirection.Ascending)
            {
                if (!IsSortedQuery(sortedQuery))
                {
                    sortedQuery = sortedQuery.OrderBy(GetKeySelector<PropertyType>(sortInfo.Column.SortMemberPath)) as DataServiceQuery<T>;
                }
                else
                {
                    sortedQuery = ((IOrderedQueryable<T>)sortedQuery).ThenBy(GetKeySelector<PropertyType>(sortInfo.Column.SortMemberPath)) as DataServiceQuery<T>; ;
                }
            }
            else
            {
                if (!IsSortedQuery(sortedQuery))
                {
                    sortedQuery = sortedQuery.OrderByDescending(GetKeySelector<PropertyType>(sortInfo.Column.SortMemberPath)) as DataServiceQuery<T>;
                }
                else
                {
                    sortedQuery = ((IOrderedQueryable<T>)sortedQuery).ThenByDescending(GetKeySelector<PropertyType>(sortInfo.Column.SortMemberPath)) as DataServiceQuery<T>; ;
                }
            }
            return sortedQuery;
        }

        /// <summary>
        /// Translates the group specification used by C1DataGrid into the sort specification used by MS ICollectionView.
        /// </summary>
        /// <param name="collectionView">The collection view.</param>
        /// <param name="groupDescriptions">The group descriptions.</param>
        private void ToMSGroupDescriptions(ICollectionView collectionView, DataGridColumnValue<DataGridSortDirection>[] groupDescriptions)
        {
            if (collectionView != null)
            {
                collectionView.GroupDescriptions.Clear();
                if (groupDescriptions != null)
                {
                    foreach (DataGridColumnValue<DataGridSortDirection> gd in groupDescriptions)
                    {
                        if (gd.Column.GroupConverter == null)
                        {
                            collectionView.GroupDescriptions.Add(new System.Windows.Data.PropertyGroupDescription(gd.Column.SortMemberPath));
                        }
                        else
                        {
                            collectionView.GroupDescriptions.Add(new System.Windows.Data.PropertyGroupDescription(gd.Column.SortMemberPath, gd.Column.GroupConverter));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Columns can be sorted through the column headers or through the group headers.
        /// This method synchronizes the sorted columns no matter where it was sorted (group or column).
        /// </summary>
        /// <param name="groupDescriptions">The group descriptions.</param>
        /// <param name="sortDescriptions">The sort descriptions.</param>
        /// <returns></returns>
        private DataGridColumnValue<DataGridSortDirection>[] SynchronizeGroupSort(DataGridColumnValue<DataGridSortDirection>[] groupDescriptions, DataGridColumnValue<DataGridSortDirection>[] sortDescriptions)
        {
            List<DataGridColumnValue<DataGridSortDirection>> synchronizedSort = new List<DataGridColumnValue<DataGridSortDirection>>();
            // Get sorting for grouped columns from sort descriptions or from the group definition
            for (int i = 0; i < groupDescriptions.Length; i++)
            {
                DataGridColumnValue<DataGridSortDirection> groupSortInfo = sortDescriptions.FirstOrDefault(sd => sd.Column.SortMemberPath.Equals(groupDescriptions[i].Column.SortMemberPath));
                if ((groupSortInfo == null) || _groupSortPriority)
                {
                    groupSortInfo = new DataGridColumnValue<DataGridSortDirection>(groupDescriptions[i].Column, groupDescriptions[i].Value);
                }
                _groupDescriptions[i] = new DataGridColumnValue<DataGridSortDirection>(groupSortInfo.Column, groupSortInfo.Value);
                synchronizedSort.Add(groupSortInfo);
            }
            // Add the sort definitions for non grouped columns
            foreach (DataGridColumnValue<DataGridSortDirection> sortInfo in sortDescriptions)
            {
                if (synchronizedSort.FirstOrDefault(s => s.Column.SortMemberPath.Equals(sortInfo.Column.SortMemberPath)) == null)
                {
                    synchronizedSort.Add(sortInfo);
                }
            }
            return synchronizedSort.ToArray();
        }

        /// <summary>
        /// Determines whether the query is sorted.
        /// </summary>
        /// <param name="query">The query.</param>
        /// <returns>true if query is sorted; otherwise, false.</returns>
        private bool IsSortedQuery(IQueryable<T> query)
        {
            string sortMethod = "ORDERBY";
            return ((DataServiceQuery)query).RequestUri.ToString().ToUpper().Contains(sortMethod);
        }

        /// <summary>
        /// Returns an expression representing a function that retrieves the value of a propery.
        /// </summary>
        /// <typeparam name="TResult">The type of the result.</typeparam>
        /// <param name="propertyPath">The property path.</param>
        /// <returns>The value of the property</returns>
        private Expression<Func<T, TResult>> GetKeySelector<TResult>(string propertyPath)
        {
            ParameterExpression param = Expression.Parameter(typeof(T), "e");
            string[] properties = propertyPath.Split('.');
            Expression propertyAccess = param;
            for (int i = 0; i < properties.Length; i++)
            {
                propertyAccess = Expression.Property(propertyAccess, properties[i]);
            }
            return Expression.Lambda<Func<T, TResult>>(propertyAccess, param);
        }


        /// <summary>
        /// Executes the query against the WCF Service.
        /// </summary>
        /// <param name="query">The query to execute.</param>
        private void InvokeService(IQueryable<T> query)
        {
            DataServiceQuery<T> dataServiceQuery = (DataServiceQuery<T>)query;
            dataServiceQuery.BeginExecute(EndInvokeService, query);
        }

        /// <summary>
        /// Ends the invoke service.
        /// </summary>
        /// <param name="result">The result of the invokation to the WCF service.</param>
        private void EndInvokeService(IAsyncResult result)
        {
            DataServiceQuery<T> dataServiceQuery = result.AsyncState as DataServiceQuery<T>;
            IEnumerable<T> data = dataServiceQuery.EndExecute(result).ToList();

            //Synchronize received data
            PagedCollectionView updatedCollectionView = new PagedCollectionView(data);
            ToMSGroupDescriptions(updatedCollectionView, _groupDescriptions);
            CollectionView = updatedCollectionView;
        }

        /// <summary>
        /// Enters a defer cycle that you can use to merge changes to the view and delay
        /// automatic refresh.
        /// </summary>
        /// <returns>
        /// The typical usage is to create a using scope with an implementation of this
        /// method and then include multiple view-changing calls within the scope. The
        /// implementation should delay automatic refresh until after the using scope
        /// exits.
        /// </returns>
        public override IDisposable DeferRefresh()
        {
            if (this.IsAddingNew || this.IsEditingItem)
            {
                throw new InvalidOperationException();
            }

            ++this._deferLevel;
            return new DeferHelper(this);
        }

        /// <summary>
        /// Subtracts from the deferLevel counter and calls Refresh() if there are no other defers
        /// </summary>
        private void EndDefer()
        {
            --this._deferLevel;

            if (this._deferLevel == 0)
            {
                this.Refresh();
            }
        }

        /// <summary>
        /// Used to keep track of Defer calls on the PagedCollectionView, which
        /// will prevent the user from calling Refresh() on the view. In order
        /// to allow refreshes again, the user will have to call IDisposable.Dispose,
        /// to end the Defer operation.
        /// </summary>
        private class DeferHelper : IDisposable
        {
            /// <summary>
            /// Private reference to the CollectionView that created this DeferHelper
            /// </summary>
            private WCFCollectionViewBase<T> collectionView;

            /// <summary>
            /// Initializes a new instance of the DeferHelper class
            /// </summary>
            /// <param name="collectionView">CollectionView that created this DeferHelper</param>
            public DeferHelper(WCFCollectionViewBase<T> collectionView)
            {
                this.collectionView = collectionView;
            }

            /// <summary>
            /// Cleanup method called when done using this class
            /// </summary>
            public void Dispose()
            {
                if (this.collectionView != null)
                {
                    this.collectionView.EndDefer();
                    this.collectionView = null;
                }
                GC.SuppressFinalize(this);
            }
        }
    }
}
