/***************************************************
 * This code is Copyright 2008 Michael J. Ellison.  All rights reserved.
 * Usage and distribution of this code is governed by the
 * CodeProject Open License.  A copy of the license may be found
 * at:
 * http://www.codeproject.com/info/cpol10.aspx
 * **************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace UNLV.IAP.WebControls
{
    /// <summary>
    /// When bound to a datasource, computes an aggregation function 
    /// across all the items of the datasource
    /// </summary>
    /// <remarks>
    /// <para>
    /// The Aggregation control displays its computed value like a <see cref="Label" /> control
    /// assuming the DataSource has been bound and DataBind() called (or DataSourceID is assigned
    /// to a datasource control).
    /// </para>
    /// <para>
    /// Set the <see cref="Function"/> property to one of the
    /// <see cref="AggregationFunction"/> values to specify the aggregation operation
    /// to perform across the datasource.  If the datasource represents a list or collection
    /// of objects, or the datasource implements <see cref="IListSource" /> then
    /// specify the field to aggregate in the <see cref="DataField"/> property.
    /// Otherwise, if the datasource is a list or array of primitive-type values, or 
    /// if the <see cref="AggregationFunction">Count</see> function is employed,
    /// the <see cref="DataField" /> property may be left blank.
    /// </para>
    /// <para>
    /// For numeric functions such as <see cref="AggregationFunction">Sum</see> and
    /// <see cref="AggregationFunction">Avg</see>, field items that are not numeric
    /// are ignored.
    /// </para>
    /// </remarks>
    [ToolboxData("<{0}:Aggregation runat=server></{0}:Aggregation>")]
    [Designer(typeof(AggregationDesigner))]
    public class Aggregation : CompositeDataBoundControl
    {
        private const string kViewState_FunctionValue = "__!FunctionValue";

        private AggregationFunction _function = AggregationFunction.Count;
        private string _dataField = "";
        private string _formatString = "";
        private object _value = null;

        #region Properties

        /// <summary>
        /// Gets or sets the aggregation function to computed across the assigned
        /// <see cref="Aggregation.DataSource">DataSource</see>.
        /// </summary>
        public AggregationFunction Function 
        {
            get { return _function; }
            set { _function = value; }
        }


        /// <summary>
        /// Gets or sets the field within the <see cref="Aggregation.DataSource">DataSource</see> to aggregate.
        /// </summary>
        /// <remarks>
        /// If the <see cref="Function" /> is <see cref="AggregationFunction.Count">Count</see>
        /// or if the supplied datasource is a list or array of values (rather than objects)
        /// then DataField may be left blank.
        /// </remarks>
        public string DataField
        {
            get { return _dataField; }
            set { _dataField = value; }
        }


        /// <summary>
        /// Gets or sets the formatting string used to control how the aggregation is
        /// displayed.
        /// </summary>
        public string FormatString
        {
            get { return _formatString; }
            set { _formatString = value; }
        }


        /// <summary>
        /// Gets the computed aggregation value, once the control is bound to a datasource.
        /// If the control is not bound, this property returns <c>null</c>.
        /// </summary>
        public object Value
        {
            get 
            { 
                EnsureChildControls();  // ensure that we've evaluated the value
                return _value;
            } 
        }

        /// <summary>
        /// Identifies the &lt;span&gt; tag as the key for this control.
        /// </summary>
        protected override HtmlTextWriterTag TagKey
        {
            get
            {
                return HtmlTextWriterTag.Span;
            }
        }

        #endregion

        #region CompositeDataboundControl method overrides

        /// <summary>
        /// Creates child controls for this Aggregation control.
        /// </summary>
        /// <param name="dataSource">the datasource being bound to this Aggregation, or <c>null</c> if not in a databinding context</param>
        /// <param name="dataBinding"><c>true</c> if in a databinding context, <c>false</c> if not</param>
        /// <returns>1 if a value was computed, 0 if not</returns>
        protected override int CreateChildControls(System.Collections.IEnumerable dataSource, bool dataBinding)
        {
            _value = null;
            if (dataBinding)
            {
                _value = CreateChildControls_BindingScenario(dataSource);
                ViewState[kViewState_FunctionValue] = _value;
            }
            else
            {
                _value = ViewState[kViewState_FunctionValue];         
                CreateChildControls_PostbackScenario(_value);
            }

            if (_value == null) return 0; else return 1;
        }

        #endregion

        #region Methods supporting Postback scenarios for the databound control

        /// <summary>
        /// Creates child controls in a postback context
        /// </summary>
        /// <param name="value">the computed aggregation value</param>
        protected void CreateChildControls_PostbackScenario(object value)
        {
            if (value != null)
                this.Controls.Add(ValueToLiteralControl(value));
        }

        #endregion

        #region  Methods supporting DataBinding scnearios for the databound control

        /// <summary>
        /// Creates child controls in a databinding context, computing the aggregation
        /// </summary>
        /// <param name="dataSource">the datasource being bound</param>
        /// <returns>the aggregation value</returns>
        protected object CreateChildControls_BindingScenario(System.Collections.IEnumerable dataSource)
        {
            // return the aggregation value
            object value = null;

            if (dataSource != null)
            {
                // determine the aggregation according to the listed function   
                LiteralControl lc = new LiteralControl();

                switch (_function)
                {
                    case AggregationFunction.Count:
                        value = CountItems(dataSource); break;

                    case AggregationFunction.Sum:
                        value = SumValues(dataSource); break;

                    case AggregationFunction.First:
                        value = FirstValue(dataSource); break;

                    case AggregationFunction.Last:
                        value = LastValue(dataSource); break;

                    case AggregationFunction.Min:
                        value = MinValue(dataSource); break;

                    case AggregationFunction.Max:
                        value = MaxValue(dataSource); break;

                    case AggregationFunction.Avg:
                        double? avg = AvgValues(dataSource);
                        if (avg.HasValue) value = avg.Value; else value = null;
                        break;

                    case AggregationFunction.Median:
                        double? median = Median(dataSource);
                        if (median.HasValue) value = median.Value; else value = null;
                        break;

                    case AggregationFunction.Var:
                        double? var = Variance(dataSource);
                        if (var.HasValue) value = var.Value; else value = null;
                        break;

                    case AggregationFunction.StDev:
                        double? stdev = StandardDeviation(dataSource);
                        if (stdev.HasValue) value = stdev.Value; else value = null;
                        break;
                    
                    default:
                        value = null; break;                           
                }

                if (value != null)
                    this.Controls.Add(ValueToLiteralControl(value));
           
            }

            return value;
        }

        #endregion

        #region Helper methods

        /// <summary>
        /// Creates a <see cref="LiteralControl" /> object from the given value,
        /// applying the <see cref="FormatString" /> if specified.
        /// </summary>
        /// <param name="value">the value to display in the LiteralControl</param>
        /// <returns>a new LiteralControl displaying the formatted value</returns>
        protected LiteralControl ValueToLiteralControl(object value)
        {
            LiteralControl lc = new LiteralControl();
            lc.EnableViewState = false;  // we'll track our own viewstate

            if (string.IsNullOrEmpty(FormatString))
                lc.Text = value.ToString();
            else
                lc.Text = string.Format(FormatString, value);

            return lc;

        }

        /// <summary>
        /// Determines if the type of objects within the dataSource are primitives/strings,
        /// or something else.
        /// </summary>
        /// <param name="dataSource">the dataSource being bound</param>
        /// <returns><c>true</c> if the items within the dataSource are primitives or strings, <c>false</c> if not</returns>
        protected bool DataItemTypeIsPrimitiveOrString(IEnumerable dataSource)
        {
            // determine if a dataItem within dataSource represents a primitive type
            IEnumerator i = dataSource.GetEnumerator();
            if (i.MoveNext())
            {
                object o = i.Current;
                return ((o is string) || o.GetType().IsPrimitive);                
            }
            else
                return false;
        }


        #endregion

        #region Aggregation Functions

        /// <summary>
        /// Returns the number of items within the given dataSource
        /// </summary>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the number of items within the dataSource</returns>
        protected int CountItems(IEnumerable dataSource)
        {
            // return a count of the number of items in the dataSource
            IEnumerator i = dataSource.GetEnumerator();
            int count = 0;

            while (i.MoveNext())
            {
                count++;
            }

            return count;

        }

        /// <summary>
        /// Returns the sum of the values in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the sum of the items is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the total sum</returns>
        protected double SumValues(IEnumerable dataSource)
        {
            // return a sum of the values in DataField across the given dataSource
            IEnumerator i = dataSource.GetEnumerator();
            double d = 0;

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
                // if primitive, loop through the source and use the type as it is
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(i.Current);
                        d += val;
                    }
                    // don't process items that can't be converted to a numeric
                    catch { }

                }
            else
                // if not primitive, loop through the source and rely on DataField to get the property value
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(DataBinder.GetPropertyValue(i.Current, DataField));
                        d += val;
                    }
                    // don't process items that can't be converted to a numeric
                    catch { }
                        
                }

            return d;
        }


        /// <summary>
        /// Returns the value found in the field identified by 
        /// <see cref="DataField"/> in the first item in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the first item is then returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the first value found</returns>
        protected object FirstValue(IEnumerable dataSource)
        {
            // return the values in DataField for the first item in the given dataSource
            IEnumerator i = dataSource.GetEnumerator();

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                // if primitive, loop through the source and use the type as it is
                if (i.MoveNext())
                {
                    object val = i.Current;
                    return val;
                }
            }
            else
            {
                if (i.MoveNext())
                {
                    try
                    {
                        object val = DataBinder.GetPropertyValue(i.Current, DataField);
                        return val;
                    }
                    catch
                    {
                        return null;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Returns the value found in the field identified by 
        /// <see cref="DataField"/> in the last item in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the last value found is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the last value found</returns>
        protected object LastValue(IEnumerable dataSource)
        {
            // return the values in DataField for the first item in the given dataSource
            IEnumerator i = dataSource.GetEnumerator();
            object val = null;

            
            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                // if primitive, loop through the source and use the type as it is
                while (i.MoveNext())
                {
                    val = i.Current;
                }
            }
            else
            {

                while (i.MoveNext())
                {
                    try
                    {
                        val = DataBinder.GetPropertyValue(i.Current, DataField);
                    }
                    catch
                    {
                        // nothing
                    }
                }
            }

            return val;
        }

        /// <summary>
        /// Returns the lowest value found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the lowest-valued item is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the lowest value found</returns>
        protected object MinValue(IEnumerable dataSource)
        {
            // return the values in DataField for the first item in the given dataSource
            IEnumerator i = dataSource.GetEnumerator();
            object val = null;

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                while (i.MoveNext())
                {
                    try
                    {
                        object val2 = i.Current;
                        IComparable v2 = val2 as IComparable;
                        if (v2 != null)
                        {
                            if (val == null || v2.CompareTo(val) < 0) val = val2;
                        }
                    }
                    catch
                    {
                        // nothing
                    }
                }
            }
            else
            {
                while (i.MoveNext())
                {
                    try
                    {
                        object val2 = DataBinder.GetPropertyValue(i.Current, DataField);
                        IComparable v2 = val2 as IComparable;
                        if (v2 != null)
                        {
                            if (val == null || v2.CompareTo(val) < 0) val = val2;
                        }
                    }
                    catch
                    {
                        // nothing
                    }
                }
            }

            return val;
        }

        /// <summary>
        /// Returns the highest value found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the highest-valued item is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the highest value found</returns>
        protected object MaxValue(IEnumerable dataSource)
        {
            // return the values in DataField for the first item in the given dataSource
            IEnumerator i = dataSource.GetEnumerator();
            object val = null;

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                while (i.MoveNext())
                {
                    try
                    {
                        object val2 = i.Current;
                        IComparable v2 = val2 as IComparable;
                        if (v2 != null)
                        {
                            if (val == null || v2.CompareTo(val) > 0) val = val2;
                        }
                    }
                    catch
                    {
                        // nothing
                    }
                }
            }
            else
            {
                while (i.MoveNext())
                {
                    try
                    {
                        object val2 = DataBinder.GetPropertyValue(i.Current, DataField);
                        IComparable v2 = val2 as IComparable;
                        if (v2 != null)
                        {
                            if (val == null || v2.CompareTo(val) > 0) val = val2;
                        }
                    }
                    catch
                    {
                        // nothing
                    }
                }
            }

            return val;
        }

        /// <summary>
        /// Returns the average (statistical mean) of values found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the average of the items is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the average value</returns>
        protected double? AvgValues(IEnumerable dataSource)
        {
            // return an average of the values in DataField across the given dataSource
            IEnumerator i = dataSource.GetEnumerator();
            double d = 0;
            int count = 0;

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(i.Current);
                        d += val;
                        count++;
                    }
                    catch
                    {
                        // do nothing
                    }
                }
            }
            else
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(DataBinder.GetPropertyValue(i.Current, DataField));
                        d += val;
                        count++;
                    }
                    catch
                    {
                        // do nothing
                    }
                }
            }


            if (count == 0) return null; 
            else return (d / count);     

        }

        /// <summary>
        /// Returns the statistical variance of values found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the variance of the items is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the statistical variance</returns>
        protected double? Variance(IEnumerable dataSource)
        {
            // return the statistical variance of the values in DataField across the given dataSource

            // first, get the mean            
            double? avg = AvgValues(dataSource);
            if (!avg.HasValue) return null;

            double mean = avg.Value;

            // next, square the deviations of each number from the mean and add them up
            IEnumerator i = dataSource.GetEnumerator();
            double d = 0;
            int count = 0;

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(i.Current);
                        d += Math.Pow(val - mean, 2);
                        count++;
                    }
                    catch
                    {
                        // do nothing with items that can't be converted to numerics
                    }
                }
            }
            else
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(DataBinder.GetPropertyValue(i.Current, DataField));
                        d += Math.Pow(val - mean, 2);
                        count++;
                    }
                    catch
                    {
                        // do nothing with items that can't be converted to numerics
                    }
                }
            }

            if (count <= 1) return null;
            else
                // return the sum divided by one less than the number of items in the set
                return d / (count - 1);

        }

        /// <summary>
        /// Returns the standard deviation (the square root of the statistical variance)
        /// of values found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the standard deviation of the items 
        /// is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the standard deviation</returns>
        protected double? StandardDeviation(IEnumerable dataSource)
        {
            // return a standard deviation of the values in DataField across the given dataSource
            // this is the square root of the variance
            double? var = Variance(dataSource);

            if (var.HasValue)
                return Math.Sqrt(var.Value);
            else
                return null;

        }

        /// <summary>
        /// Returns the statistical median of values found in the field identified by 
        /// <see cref="DataField"/> across the items in the given datasource; that is,
        /// it returns the number that separates the higher half of the values from the 
        /// lower half.
        /// </summary>
        /// <remarks>
        /// If the given dataSource is a list or array of primitive-typed items,
        /// <see cref="DataField" /> may remain blank; the median of the items is returned.
        /// </remarks>
        /// <param name="dataSource">the dataSource bound to this Aggregation control</param>
        /// <returns>the median value</returns>
        protected double? Median(IEnumerable dataSource)
        {
            // return a statistical median for the given datasource (across DataField, if provided)
            IEnumerator i = dataSource.GetEnumerator();

            // first, we need a sorted set of values; create an array of items that
            // we'll sort
            List<double> list = new List<double>();

            bool isPrimOrString = DataItemTypeIsPrimitiveOrString(dataSource);
            if (isPrimOrString)
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(i.Current);
                        list.Add(val);
                    }
                    catch
                    {
                        // do nothing with items that can't be converted to numerics
                    }
                }
            }
            else
            {
                while (i.MoveNext())
                {
                    try
                    {
                        double val = Convert.ToDouble(DataBinder.GetPropertyValue(i.Current, DataField));
                        list.Add(val);
                    }
                    catch
                    {
                        // do nothing with items that can't be converted to numerics
                    }
                }
            }

            // if we ended up with no items, return null
            if (list.Count == 0) return null;

            // otherwise sort the list
            list.Sort();

            // if the list contains an even number of items, return the average of the middle two
            if (list.Count % 2 == 0)
            {
                int hiIndex = list.Count / 2;
                return (list[hiIndex - 1] + list[hiIndex]) / 2;
            }
            // otherwise if it is odd, just return the middle number
            else
            {
                int index = (list.Count - 1) / 2;
                return list[index];
            }

        }

        #endregion

    }
}
