using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Reflection;
using System.Collections.Generic;
using System.Collections;
using System.Xml.Linq;
using System.Net.Mime;
using DomainModel;


namespace PageableTableApp.Controllers
{
    public class ItemsListController : Controller
    {
        /// <summary>
        /// Stores the repository for items.
        /// </summary>
        IPersonsRepository _itemsRepository;
        /// <summary>
        /// Stores a shortcut to _itemsRepository.Persons, e.g. a list of Person objects.
        /// </summary>
        IEnumerable<Object> _allItems;


        /// <summary>
        /// Initializes a new instance of the ItemsListController class.
        /// </summary>
        /// <param name="itemsRepository">The repository object which stores the Person objects.</param>
        public ItemsListController (IPersonsRepository itemsRepository)
        {
            _itemsRepository = itemsRepository;

            _allItems = itemsRepository.Persons.Select (x => x as Object);
        }


        /// <summary>
        /// This is a GET method, it gets executed for the following incoming URLs:
        /// /
        /// /4/1
        /// /4/1/id/asc
        /// /4/1/id/asc?filter=id:1,2,name:george,john
        /// </summary>
        /// <param name="pageSize">The number of rows the table includes.</param>
        /// <param name="page">The active page index (1-based).</param>
        /// <param name="sortBy">The name of the property by which the sorting is performed.</param>
        /// <param name="sortMode">The mode of the sort: ascending("asc") or descending("desc").</param>
        /// <param name="filter">This parameter corresponds to the Request.QueryString["filter"].</param>
        /// <returns>A ViewResult object that contains the list of items for the current page of the table.</returns>
        public ActionResult InitialList (int pageSize, int page, String sortBy, String sortMode, String filter)
        {
            // check if the sortBy value is a valid property name of the Person class
            if (sortBy != String.Empty && _allItems.Count () > 0)
            {
                var propertyInfo =
                    _allItems.First ().GetType ().GetProperties ().SingleOrDefault (x => x.Name.ToLower () == sortBy.ToLower ());

                if (propertyInfo != null)
                    sortBy = propertyInfo.Name;
                else
                    sortBy = null;
            }
            else
                sortBy = null;

            // sortMode value should be in lower case
            if (sortMode != String.Empty)
                sortMode = sortMode.ToLower ();
            else
                sortMode = null;

            // filter the items list by the query string
            // fData contains all the filter values
            var fData = FilterByQueryString (ref _allItems, filter);

            // check whether the current page is included in the new distribution of the pages
            if ((page - 1) * pageSize >= _allItems.Count ())
                // if it is then set the last page as active page
                page = (int)Math.Ceiling ((double)_allItems.Count () / pageSize);

            // build a list of even number of the available page sizes from 2 through 10
            ViewData ["pageSizes"] = BuildPageSizeDDL (pageSize);

            // store the FiltersData object
            ViewData ["filters"] = fData;

            // store the current page size
            ViewData ["pageSize"] = pageSize;
            // store the number of available pages
            ViewData ["pages"] = (int)Math.Ceiling ((double)_allItems.Count () / pageSize);
            // store the current page index(1-based)
            ViewData ["page"] = page;

            // it's not needed in this case because there should be no filter panel open
            ViewData ["showFilter"] = String.Empty;

            // store the property name by which the sorting is done
            ViewData ["sortBy"] = sortBy;
            // store the mode by which the sorting is done
            ViewData ["sortMode"] = sortMode;

            // store the total number of items
            ViewData ["numberOfRecords"] = _allItems.Count ();

            // sort the items
            _allItems = Sort (_allItems, sortBy, sortMode);

            // do the paging
            var items = _allItems.Skip (pageSize * (page - 1)).Take (pageSize);

            return View ("Index", items);
        }

        /// <summary>
        /// This is a POST method, it is executed both in AJAX and non-AJAX mode. It performs 
        /// the sorting, filtering and paging of the items.
        /// </summary>
        /// <param name="pageSize">The number of rows the table includes.</param>
        /// <param name="page">The active page index (1-based).</param>
        /// <param name="showFilter">In non-AJAX mode if instant filtering is on, it indicates the property name by which the 
        ///                          filtering is performed.</param>
        /// <param name="sortBy">The name of the property by which the sorting is performed.</param>
        /// <param name="sortMode">The mode of the sort: ascending("asc") or descending("desc").</param>
        /// <param name="fc">Contains all the HTML controls inside the FORM element.</param>
        /// <returns>In non-Ajax mode returns a ViewResult object that contains the list of items for the current page of the 
        ///          table, in Ajax mode returns the a JsonResult containing all the relevant data needed to update the client
        ///          table. </returns>
        [AcceptVerbs (HttpVerbs.Post)]
        public ActionResult MaintainList (int pageSize, int page, String showFilter, String sortBy, String sortMode, String scrollTop, FormCollection fc)
        {
            FiltersData fData;
            int j, k;


            // check if the sortBy value is a valid property name of the Person class
            if (sortBy != String.Empty && _allItems.Count () > 0)
            {
                var propertyInfo =
                    _allItems.First ().GetType ().GetProperties ().SingleOrDefault (x => x.Name.ToLower () == sortBy.ToLower ());

                if (propertyInfo != null)
                    sortBy = propertyInfo.Name;
                else
                    sortBy = null;
            }
            else
                sortBy = null;

            // sortMode value should be in lower case
            if (sortMode != String.Empty)
                sortMode = sortMode.ToLower ();
            else
                sortMode = null;

            // build the FiltersData object
            fData = new FiltersData (_allItems);

            // iterate through all the control values of the fc object
            for (var i = 0; i < fc.Count; ++i)
            {
                var key = fc.Keys [i];

                // if the name of the current control doesn't contain the "filter" text, then skip
                // this item
                if (!key.Contains ("filter"))
                    continue;

                // the key is of the following format: "filter_[property name]_[property value]"

                // the value is "true" or "false" based on the hidden input field
                var value = fc [i];

                if (value.ToLower ().Contains ("true"))
                    value = "true";
                else
                    value = "false";

                var isActive = bool.Parse (value);

                var propertyName = key.Substring (j = key.IndexOf ("_") + 1, (k = key.IndexOf ("_", j)) - j);
                var filterText = key.Substring (k + 1);

                // set the corresponding FilterData object's IsActive property to the respective value
                fData.Filters [propertyName].Single (x => x.FilterText == filterText).IsActive = isActive;
            }

            // filter the items list by the current fData object
            _allItems = Filter (_allItems, fData);

            // check whether the current page is included in the new distribution of the pages
            if ((page - 1) * pageSize >= _allItems.Count ())
                // if it is then set the last page as active page
                page = (int)Math.Ceiling ((double)_allItems.Count () / pageSize);

            // sort the items
            _allItems = Sort (_allItems, sortBy, sortMode);

            // do the paging
            var currentItems = _allItems.Skip (pageSize * (page - 1)).Take (pageSize);

            // if the request is not AJAX enabled 
            if (!Request.IsAjaxRequest ())
            {
                // build a list of even number of the available page sizes from 2 through 10
                ViewData ["pageSizes"] = BuildPageSizeDDL (pageSize);

                // store the FiltersData object
                ViewData ["filters"] = fData;

                // store the current page size
                ViewData ["pageSize"] = pageSize;
                // store the number of available pages
                ViewData ["pages"] = (int)Math.Ceiling ((double)_allItems.Count () / pageSize);
                // store the current page index(1-based)
                ViewData ["page"] = page;

                // store the property name by which the filtering is applied
                ViewData ["showFilter"] = showFilter;

                // store the property name by which the sorting is done
                ViewData ["sortBy"] = sortBy;
                // store the mode by which the sorting is done
                ViewData ["sortMode"] = sortMode;

                // store the amount by which the popup panel was scrolled
                ViewData ["scrollTop"] = scrollTop;

                // store the total number of items
                ViewData ["numberOfRecords"] = _allItems.Count ();

                return View ("Index", currentItems);
            }
            // if the request is AJAX enabled
            else
            {
                // instantiate an anonymous class and set up the relevant properties needed for the 
                // update of the client table
                var resultData =
                    new
                    {
                        Items = currentItems,
                        Pages = (int)Math.Ceiling ((double)_allItems.Count () / pageSize),
                        Page = page,
                        SortBy = sortBy,
                        SortMode = sortMode,
                        NumberOfRecords = _allItems.Count ()
                    };

                return new JsonResult { Data = resultData };
            }
        }


        /// <summary>
        /// Filters the items according to the given query string parameter.
        /// </summary>
        /// <param name="items">The list of items that has to be filtered.</param>
        /// <param name="queryString">The query string by which the filtering is applied.</param>
        /// <returns>A FiltersData object that corresponds to the query string.</returns>
        FiltersData FilterByQueryString (ref IEnumerable<Object> items, String queryString)
        {
            String filterText, filterBy;
            FiltersData fData;


            filterBy = null;
            fData = null;

            try
            {
                // build up a FiltersData object 
                fData = new FiltersData (items);

                // the query string should have the following format: "id:1,2,3,name:george,paul,ismarried:true,false"

                if (!String.IsNullOrEmpty (queryString))
                {
                    // parse the given text
                    for (int i = 0, j = 1; j < queryString.Length; ++j)
                        if (queryString [j] == ':')
                        {
                            filterBy = queryString.Substring (i, j - i);

                            i = j + 1;
                        }
                        else
                            if (queryString [j] == ',' || j == queryString.Length - 1)
                            {
                                if (queryString [j] == ',')
                                    filterText = queryString.Substring (i, j - i);
                                else
                                    filterText = queryString.Substring (i, j - i + 1);

                                i = j + 1;

                                fData.Filters.Single (x => String.Compare (x.Key, filterBy, true) == 0).Value.
                                    Single (x => String.Compare (x.FilterText, filterText, true) == 0).IsActive = true;
                            }

                    // do the filtering
                    items = Filter (items, fData);
                }
            }
            catch
            {
            }

            return fData;
        }

        /// <summary>
        /// Builds a list of SelectListItem objects that have values of even numbers from 2 to 10. One SelectListItem object
        /// whose value matches the one provided for the parameter of this method gets selected.
        /// </summary>
        /// <param name="selectedValue">The even number between 2 and 10 that should be selected.</param>
        /// <returns>A list of SelectedListItem objects.</returns>
        IEnumerable<SelectListItem> BuildPageSizeDDL (int selectedValue)
        {
            var items = new List<SelectListItem> ();

            for (var i = 1; i < 6; ++i)
                items.Add (
                    new SelectListItem
                    {
                        Text = i * 2 + "",
                        Value = i * 2 + "",
                        Selected = i * 2 == selectedValue
                    });

            return items;
        }

        /// <summary>
        /// Filters the items according to a FiltersData object.
        /// </summary>
        /// <param name="items">The list of items that should be filtered.</param>
        /// <param name="fData">The FiltersData object by which the filtering is performed.</param>
        /// <returns>A list of items that correspond to the filter expressions.</returns>
        IEnumerable<Object> Filter (IEnumerable<Object> items, FiltersData fData)
        {
            var filteredItems = new List<Object> ();

            // iterate through the list of items
            foreach (var item in items)
            {
                // we need to check whether this item should be included in the result of this filtering: if all filter values 
                // are inactive for a column then the item should be included in the result, otherwise if there is at least one 
                // filter active for a column then we select the current item only if its respective property is included in the 
                // list of active filters
                var include = true;

                // iterate through the properties of an item
                foreach (var pInfo in items.First ().GetType ().GetProperties ())
                {
                    // get the property value of the current item
                    var value = pInfo.GetValue (item, null).ToString ();

                    // if the current property value doesn't exist in the list of the active filter values and there is at least 
                    //one filter text active, then the current item doesn't belong to the result
                    if (!fData.Filters [pInfo.Name].Any (x => x.FilterText == value && x.IsActive) &&
                        fData.Filters [pInfo.Name].Count (x => x.IsActive) > 0)
                    {
                        include = false;

                        break;
                    }
                }

                if (include)
                    filteredItems.Add (item);
            }

            return filteredItems;
        }

        /// <summary>
        /// Sorts the items by the given property name in the given sort order.
        /// </summary>
        /// <param name="items">The list of items.</param>
        /// <param name="sortBy">The property name by the sorting is applied.</param>
        /// <param name="sortMode">The sort mode.</param>
        /// <returns>A list of sorted items.</returns>
        IEnumerable<Object> Sort (IEnumerable<Object> items, String sortBy, String sortMode)
        {
            var sortedItems = items;

            if (!String.IsNullOrEmpty (sortBy) && !String.IsNullOrEmpty (sortMode) && items.Count () > 0)
            {
                if (items.First ().GetType ().GetProperties ().Any (x => x.Name == sortBy) &&
                       new [] { "asc", "desc" }.Contains (sortMode))
                    if (sortMode == "asc")
                        sortedItems =
                            items.OrderBy (x =>
                            {
                                var pInfo = x.GetType ().GetProperties ().Single (y => y.Name == sortBy);

                                return pInfo.GetValue (x, null);
                            });
                    else
                        sortedItems =
                            items.OrderByDescending (x =>
                            {
                                var pInfo = x.GetType ().GetProperties ().Single (y => y.Name == sortBy);

                                return pInfo.GetValue (x, null);
                            });
            }

            return sortedItems;
        }
    }
}
