using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using FastReport.Data;
using FastReport.Table;
using FastReport.Utils;

namespace FastReport.AdvMatrix
{
    /// <summary>
    /// Describes how the even style is applied to a matrix.
    /// </summary>
    public enum HeaderPriority
    {
        /// <summary>
        /// The even style is applied to matrix rows.
        /// </summary>
        Rows,

        /// <summary>
        /// The even style is applied to matrix columns.
        /// </summary>
        Columns
    }


    /// <summary>
    /// Represents the matrix object that is used to print cross-table.
    /// </summary>
    /// <remarks>
    /// The matrix consists of the following elements: columns, rows and data cells. Each element is
    /// represented by the <b>descriptor</b>. The <see cref="HeaderDescriptor"/> class represents
    /// columns and rows; data cells use dynamically created descriptors.
    /// The <see cref="Data"/> property holds two root descriptors - <b>Columns.Descriptor</b> and <b>Rows.Descriptor</b>.
    /// <para/>To create the matrix in a code, you should perform the following actions:
    /// <list type="bullet">
    ///   <item>
    ///     <description>create an instance of the <b>AdvMatrixObject</b> and add it to the report;</description>
    ///   </item>
    ///   <item>
    ///     <description>create descriptors for columns and rows and add it to the
    ///     root descriptor using the <b>matrix.Data.Columns.Descriptor</b> and <b>matrix.Data.Rows.Descriptor</b> respectively;</description>
    ///   </item>
    ///   <item>
    ///     <description>call the <see cref="BuildTemplate"/> method to create the matrix template
    ///     that will be used to create a result;</description>
    ///   </item>
    ///   <item>
    ///     <description>set the data cells content;</description>
    ///   </item>
    ///   <item>
    ///     <description>modify the matrix template (change captions, set the visual appearance).</description>
    ///   </item>
    /// </list>
    /// <para/>To connect the matrix to a datasource, use the <see cref="DataSource"/> property. If
    /// this property is not set, the result matrix will be empty. In this case you may use 
    /// the <see cref="ManualBuild"/> event handler to fill the matrix.
    /// </remarks>
    /// <example>This example demonstrates how to create a matrix in a code.
    /// <code>
    /// // create an instance of AdvMatrixObject
    /// AdvMatrixObject matrix = new AdvMatrixObject();
    /// matrix.Name = "Matrix1";
    /// // add it to the report title band of the first report page
    /// matrix.Parent = (Report.Pages[0] as ReportPage).ReportTitle;
    /// 
    /// // create two nested column descriptors and a total
    /// HeaderDescriptor column = new HeaderDescriptor("[MatrixDemo.Year]");
    /// matrix.Data.Columns.Descriptor.Add(column);
    /// HeaderDescriptor nestedColumn = new HeaderDescriptor("[MatrixDemo.Month]");
    /// column.Add(nestedColumn);
    /// HeaderDescriptor columnTotal = new HeaderDescriptor();
    /// columnTotal.DisplayText = "Total";
    /// matrix.Data.Columns.Descriptor.Add(columnTotal);
    /// 
    /// // create one row descriptor and a total
    /// HeaderDescriptor row = new HeaderDescriptor("[MatrixDemo.Name]");
    /// matrix.Data.Rows.Descriptor.Add(row);
    /// HeaderDescriptor rowTotal = new HeaderDescriptor();
    /// rowTotal.DisplayText = "Total";
    /// matrix.Data.Rows.Descriptor.Add(rowTotal);
    /// 
    /// // connect matrix to a datasource
    /// matrix.DataSource = Report.GetDataSource("MatrixDemo");
    /// 
    /// // create the matrix template
    /// matrix.BuildTemplate();
    /// 
    /// // change the style
    /// matrix.Style = "Gray";
    /// 
    /// // create data cells
    /// string cellText = "[Sum([MatrixDemo.Revenue])]";
    /// for (int i = matrix.Data.Rows.Size; i &lt; matrix.ColumnCount; i++)
    /// for (int j = matrix.Data.Columns.Size; j &lt; matrix.RowCount; j++)
    ///     matrix[i, j].Text = cellText;
    /// </code>
    /// </example>
    public partial class AdvMatrixObject : TableBase
    {
        #region Fields
        private DataSourceBase dataSource;
        private string style;
        private bool saveVisible;
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets a data source.
        /// </summary>
        /// <remarks>
        /// When you create the matrix in the designer by drag-drop data columns into it,
        /// this property will be set automatically. However you need to set it if you create 
        /// the matrix in code.
        /// </remarks>
        [Category("Data")]
        public DataSourceBase DataSource
        {
            get { return dataSource; }
            set
            {
                if (dataSource != value)
                {
                    if (dataSource != null)
                        dataSource.Disposed -= DataSource_Disposed;
                    if (value != null)
                        value.Disposed += DataSource_Disposed;
                }
                dataSource = value;
            }
        }

        /// <summary>
        /// Gets the row filter expression.
        /// </summary>
        /// <remarks>
        /// This property can contain any valid boolean expression. If the expression returns <b>false</b>,
        /// the corresponding data row will be skipped.
        /// </remarks>
        [Category("Data")]
        [Editor("FastReport.TypeEditors.ExpressionEditor, FastReport", typeof(UITypeEditor))]
        public string Filter { get; set; }

        /// <summary>
        /// Gets or sets a matrix style.
        /// </summary>
        [Category("Appearance")]
        [Editor("FastReport.TypeEditors.AdvMatrixStyleEditor, FastReport", typeof(UITypeEditor))]
        public new string Style
        {
            get { return style; }
            set
            {
                style = value;
                TemplateBuilder.UpdateStyle(this);
            }
        }

        /// <summary>
        /// Gets or sets even style priority for matrix cells.
        /// </summary>
        [Category("Behavior")]
        [DefaultValue(HeaderPriority.Rows)]
        public new HeaderPriority EvenStylePriority { get; set; }

        /// <summary>
        /// Gets or sets data row priority for matrix cells.
        /// </summary>
        [Category("Behavior")]
        [DefaultValue(HeaderPriority.Rows)]
        public HeaderPriority DataRowPriority { get; set; }

        /// <summary>
        /// Gets or sets a value indicating that empty matrix should be printed.
        /// </summary>
        [Category("Behavior")]
        [DefaultValue(true)]
        public bool PrintIfEmpty { get; set; }

        /// <summary>
        /// Gets or sets a value indicating that the matrix should reset its data on each report run.
        /// </summary>
        /// <remarks>
        /// Default value is false. In this case the matrix will use already prepared data when you refresh a report.
        /// If you set it to true the matrix data will be reset on each report refresh. This also resets all user interaction such as 
        /// interactive sort and collapse state.
        /// </remarks>
        [Category("Behavior")]
        [DefaultValue(false)]
        public bool ResetDataOnRun { get; set; }

        /// <summary>
        /// Gets or sets a script method name that will be used to handle the 
        /// <see cref="ManualBuild"/> event.
        /// </summary>
        /// <remarks>
        /// See the <see cref="ManualBuild"/> event for more details.
        /// </remarks>
        [Category("Build")]
        public string ManualBuildEvent { get; set; } 

        /// <summary>
        /// Gets or sets a script method name that will be used to handle the 
        /// <see cref="ModifyResult"/> event.
        /// </summary>
        /// <remarks>
        /// See the <see cref="ModifyResult"/> event for more details.
        /// </remarks>
        [Category("Build")]
        public string ModifyResultEvent { get; set; }

        /// <summary>
        /// Allows to fill the matrix in code.
        /// </summary>
        public event EventHandler ManualBuild;

        /// <summary>
        /// Allows to modify the prepared matrix elements such as cells, rows, columns.
        /// </summary>
        public event EventHandler ModifyResult;

        /// <summary>
        /// Gets the object that holds the collection of descriptors used to build a matrix.
        /// </summary>
        /// <remarks>
        /// See the <see cref="MatrixData"/> class for more details.
        /// </remarks>
        [Browsable(false)]
        public MatrixData Data { get; private set; }

        /// <summary>
        /// Gets or sets array of values that describes the currently printing column.
        /// </summary>
        /// <remarks>
        /// Use this property when report is running. It can be used to highlight matrix elements
        /// depending on values of the currently printing column. To do this:
        /// <list type="bullet">
        ///   <item>
        ///     <description>select the cell that you need to highlight;</description>
        ///   </item>
        ///   <item>
        ///     <description>click the "Highlight" button on the "Text" toolbar;</description>
        ///   </item>
        ///   <item>
        ///     <description>add a new highlight condition. Use the <b>Matrix.ColumnValues</b> to 
        ///     refer to the value you need to analyze. Note: these values are array of <b>dynamic</b>, 
        ///     so you don't need to cast it to actual type before making any comparisons. Example of highlight
        ///     condition: <c>Matrix1.ColumnValues[0] == 2000</c>.
        ///     </description>
        ///   </item>
        /// </list>
        /// </remarks>
        [Browsable(false)]
        public dynamic[] ColumnValues { get; set; }

        /// <summary>
        /// Gets or sets array of values that describes the currently printing row.
        /// </summary>
        /// <remarks>
        /// Use this property when report is running. It can be used to highlight matrix elements
        /// depending on values of the currently printing row. To do this:
        /// <list type="bullet">
        ///   <item>
        ///     <description>select the cell that you need to highlight;</description>
        ///   </item>
        ///   <item>
        ///     <description>click the "Highlight" button on the "Text" toolbar;</description>
        ///   </item>
        ///   <item>
        ///     <description>add a new highlight condition. Use the <b>Matrix.RowValues</b> to 
        ///     refer to the value you need to analyze. Note: these values are arrays of <b>dynamic</b>, 
        ///     so you don't need to cast it to actual type before making any comparisons. Example of highlight
        ///     condition: <c>Matrix1.RowValues[0] == "Andrew Fuller"</c>.
        ///     </description>
        ///   </item>
        /// </list>
        /// </remarks>
        [Browsable(false)]
        public dynamic[] RowValues { get; set; }

        /// <summary>
        /// Gets or sets the index of currently printing column.
        /// </summary>
        /// <remarks>
        /// This property may be used to print even columns with alternate color. To do this:
        /// <list type="bullet">
        ///   <item>
        ///     <description>select the cell that you need to highlight;</description>
        ///   </item>
        ///   <item>
        ///     <description>click the "Highlight" button on the "Text" toolbar;</description>
        ///   </item>
        ///   <item>
        ///     <description>add a new highlight condition that uses the <b>Matrix.ColumnIndex</b>,
        ///     for example: <c>Matrix1.ColumnIndex % 2 == 1</c>.
        ///     </description>
        ///   </item>
        /// </list>
        /// </remarks>
        [Browsable(false)]
        public int ColumnIndex { get; set; }

        /// <summary>
        /// Gets or sets the index of currently printing row.
        /// </summary>
        /// <remarks>
        /// This property may be used to print even rows with alternate color. To do this:
        /// <list type="bullet">
        ///   <item>
        ///     <description>select the cell that you need to highlight;</description>
        ///   </item>
        ///   <item>
        ///     <description>click the "Highlight" button on the "Text" toolbar;</description>
        ///   </item>
        ///   <item>
        ///     <description>add a new highlight condition that uses the <b>Matrix.RowIndex</b>,
        ///     for example: <c>Matrix1.RowIndex % 2 == 1</c>.
        ///     </description>
        ///   </item>
        /// </list>
        /// </remarks>
        [Browsable(false)]
        public int RowIndex { get; set; }

        /// <summary>
        /// Gets or sets the data row index of currently printing header.
        /// </summary>
        /// <remarks>
        /// Use this value if you want to display the header value with its data row number, e.g. "1. Andrew Fuller".
        /// To do this, set the header's DisplayExpression to something like this: Matrix1.RowNo + ". " + Value
        /// </remarks>
        [Browsable(false)]
        public int RowNo { get; set; }

        /// <summary>
        /// Gets or sets count of items in the currently printing header.
        /// </summary>
        [Browsable(false)]
        public int ItemCount { get; set; }

        internal Matrix.MatrixStyleSheet StyleSheet { get; private set; }

        private BandBase ParentBand
        {
            get
            {
                BandBase parentBand = this.Band;
                if (parentBand is ChildBand)
                    parentBand = (parentBand as ChildBand).GetTopParentBand;
                return parentBand;
            }
        }

        private DataBand FootersDataBand
        {
            get
            {
                DataBand dataBand = null;
                if (ParentBand is GroupFooterBand)
                    dataBand = ((ParentBand as GroupFooterBand).Parent as GroupHeaderBand).GroupDataBand;
                else if (ParentBand is DataFooterBand)
                    dataBand = ParentBand.Parent as DataBand;
                return dataBand;
            }
        }

        private bool IsOnFooter
        {
            get
            {
                DataBand dataBand = FootersDataBand;
                if (dataBand != null)
                {
                    return DataSource == dataBand.DataSource;
                }
                return false;
            }
        }
        #endregion

        #region Private Methods
        private void CreateResultTable()
        {
            SetResultTable(new TableResult());
            // assign properties from this object. Do not use Assign method: TableResult is incompatible with AdvMatrixObject.
            ResultTable.OriginalComponent = OriginalComponent;
            ResultTable.Alias = Alias;
            ResultTable.Border = Border.Clone();
            ResultTable.Fill = Fill.Clone();
            ResultTable.Bounds = Bounds;
            ResultTable.PrintOnParent = PrintOnParent;
            ResultTable.RepeatHeaders = RepeatHeaders;
            ResultTable.RepeatRowHeaders = RepeatRowHeaders;
            ResultTable.RepeatColumnHeaders = RepeatColumnHeaders;
            ResultTable.Layout = Layout;
            ResultTable.WrappedGap = WrappedGap;
            ResultTable.AdjustSpannedCellsWidth = AdjustSpannedCellsWidth;
            ResultTable.SetReport(Report);
            ResultTable.AfterData += ResultTable_AfterData;
        }

        private void DisposeResultTable()
        {
            ResultTable.Dispose();
            SetResultTable(null);
        }

        private void ResultTable_AfterData(object sender, EventArgs e)
        {
            OnModifyResult(e);
        }

        private void DataSource_Disposed(object sender, EventArgs e)
        {
            dataSource = null;
        }

        private void WireEvents(bool wire)
        {
            if (IsOnFooter)
            {
                DataBand dataBand = FootersDataBand;
                if (wire)
                    dataBand.BeforePrint += dataBand_BeforePrint;
                else
                    dataBand.BeforePrint -= dataBand_BeforePrint;
            }
        }

        private void dataBand_BeforePrint(object sender, EventArgs e)
        {
            bool firstRow = (sender as DataBand).IsFirstRow;
            if (firstRow)
                Data.Init();

            object match = true;
            if (!String.IsNullOrEmpty(Filter))
                match = Report.Calc(Filter);

            if (match is bool && (bool)match == true)
                Data.ProcessDataRow();
        }

        #endregion

        #region Protected Methods
        /// <inheritdoc/>
        protected override void DeserializeSubItems(FRReader reader)
        {
            if (String.Compare(reader.ItemName, "Columns", true) == 0)
                reader.Read(Data.Columns);
            else if (String.Compare(reader.ItemName, "Rows", true) == 0)
                reader.Read(Data.Rows);
            else
                base.DeserializeSubItems(reader);
        }
        #endregion

        #region Public Methods
        /// <inheritdoc/>
        public override void Assign(Base source)
        {
            BaseAssign(source);
            AdvMatrixObject src = source as AdvMatrixObject;
            DataSource = src.DataSource;
        }

        /// <inheritdoc/>
        public override void Serialize(FRWriter writer)
        {
            if (writer.SerializeTo != SerializeTo.SourcePages)
            {
                writer.Write(Data.Columns);
                writer.Write(Data.Rows);
            }

            base.Serialize(writer);
            AdvMatrixObject c = writer.DiffObject as AdvMatrixObject;
            
            if (DataSource != c.DataSource)
                writer.WriteRef("DataSource", DataSource);
            if (Filter != c.Filter)
                writer.WriteStr("Filter", Filter);
            if (Style != c.Style)
                writer.WriteStr("Style", Style);
            if (EvenStylePriority != c.EvenStylePriority)
                writer.WriteValue("EvenStylePriority", EvenStylePriority);
            if (DataRowPriority != c.DataRowPriority)
                writer.WriteValue("DataRowPriority", DataRowPriority);
            if (PrintIfEmpty != c.PrintIfEmpty)
                writer.WriteBool("PrintIfEmpty", PrintIfEmpty);
            if (ResetDataOnRun != c.ResetDataOnRun)
                writer.WriteBool("ResetDataOnRun", ResetDataOnRun);
            if (ManualBuildEvent != c.ManualBuildEvent)
                writer.WriteStr("ManualBuildEvent", ManualBuildEvent);
            if (ModifyResultEvent != c.ModifyResultEvent)
                writer.WriteStr("ModifyResultEvent", ModifyResultEvent);
        }

        /// <summary>
        /// Creates or updates the matrix template.
        /// </summary>
        /// <remarks>
        /// Call this method after you modify the matrix descriptors using the <see cref="Data"/> object's properties. 
        /// </remarks>
        public void BuildTemplate()
        {
            if (Data.Columns.Descriptor.Items.Count == 0)
                Data.Columns.Descriptor.Add();
            if (Data.Rows.Descriptor.Items.Count == 0)
                Data.Rows.Descriptor.Add();
            TemplateBuilder.BuildTemplate(this);
        }

        /// <summary>
        /// This method fires the <b>ManualBuild</b> event and the script code connected to the <b>ManualBuildEvent</b>.
        /// </summary>
        /// <param name="e">Event data.</param>
        public void OnManualBuild(EventArgs e)
        {
            if (ManualBuild != null)
                ManualBuild(this, e);
            InvokeEvent(ManualBuildEvent, e);
        }

        /// <summary>
        /// This method fires the <b>ModifyResult</b> event and the script code connected to the <b>ModifyResultEvent</b>.
        /// </summary>
        /// <param name="e">Event data.</param>
        public void OnModifyResult(EventArgs e)
        {
            if (ModifyResult != null)
                ModifyResult(this, e);
            InvokeEvent(ModifyResultEvent, e);
        }

        /// <summary>
        /// Toggles visible state of the column with specified index. For internal use only.
        /// </summary>
        /// <param name="index">Index of column.</param>
        /// <param name="collapseAll">If true collapse all items.</param>
        /// <param name="expandAll">If true expand all items.</param>
        public void ToggleColumnVisible(int index, bool collapseAll = false, bool expandAll = false)
        {
            Data.Columns.Data.ToggleVisible(index, collapseAll, expandAll);
        }

        /// <summary>
        /// Toggles visible state of the row with specified index. For internal use only.
        /// </summary>
        /// <param name="index">Index of row.</param>
        /// <param name="collapseAll">If true collapse all items.</param>
        /// <param name="expandAll">If true expand all items.</param>
        public void ToggleRowVisible(int index, bool collapseAll = false, bool expandAll = false)
        {
            Data.Rows.Data.ToggleVisible(index, collapseAll, expandAll);
        }

        /// <summary>
        /// Sort columns based on values of row with specified index. For internal use only.
        /// </summary>
        /// <param name="rowIndex">Index of row.</param>
        /// <param name="sort">The sort order.</param>
        public void SortColumnsByRow(int rowIndex, SortOrder sort)
        {
            Data.InteractiveSort.Row.Index = rowIndex;
            Data.InteractiveSort.Row.Sort = sort;
        }

        /// <summary>
        /// Sort rows based on values of column with specified index. For internal use only.
        /// </summary>
        /// <param name="columnIndex">Index of column.</param>
        /// <param name="sort">The sort order.</param>
        public void SortRowsByColumn(int columnIndex, SortOrder sort)
        {
            Data.InteractiveSort.Column.Index = columnIndex;
            Data.InteractiveSort.Column.Sort = sort;
        }
        #endregion

        #region Report Engine
        /// <inheritdoc/>
        public override void InitializeComponent()
        {
            base.InitializeComponent();
            WireEvents(true);
        }

        /// <inheritdoc/>
        public override void FinalizeComponent()
        {
            base.FinalizeComponent();
            WireEvents(false);
        }

        /// <inheritdoc/>
        public override string[] GetExpressions()
        {
            List<string> expressions = new List<string>();

            TemplateBuilder.UpdateDescriptors(this);

            expressions.AddRange(base.GetExpressions());
            expressions.AddRange(Data.GetExpressions());

            if (!String.IsNullOrEmpty(Filter))
                expressions.Add(Filter);
            
            return expressions.ToArray();
        }

        /// <inheritdoc/>
        public override void SaveState()
        {
            saveVisible = Visible;
            BandBase parent = Parent as BandBase;
            if (!Visible || (parent != null && !parent.Visible))
                return;

            // create the result table that will be rendered in the preview
            CreateResultTable();
            Visible = false;

            if (parent != null && !PrintOnParent)
            {
                parent.Height = Top;
                parent.CanGrow = false;
                parent.CanShrink = false;
                parent.AfterPrint += ResultTable.GeneratePages;
            }
        }

        /// <inheritdoc/>
        public override void GetData()
        {
            base.GetData();

            if (ResetDataOnRun)
                Data.Reset();

            if (!IsOnFooter && !Data.Completed)
            {
                // skip data processing if it was done before. It may happen if:
                // - second pass of two-pass report
                // - preview window needs to refresh a report due to user interaction
                Data.Init();
                Data.ProcessDataRows();
            }
        }

        /// <inheritdoc/>
        public override void OnAfterData(EventArgs e)
        {
            base.OnAfterData(e);
            Data.Done();

            if (PrintOnParent)
                ResultTable.AddToParent(Parent);
        }

        /// <inheritdoc/>
        public override void RestoreState()
        {
            BandBase parent = Parent as BandBase;
            if (!saveVisible || (parent != null && !parent.Visible))
                return;

            if (parent != null && !PrintOnParent)
                parent.AfterPrint -= ResultTable.GeneratePages;

            DisposeResultTable();
            Visible = saveVisible;
        }

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="AdvMatrixObject"/> class.
        /// </summary>
        public AdvMatrixObject()
        {
            BaseName = "Matrix";
            Data = new MatrixData(this);
            Filter = "";
            PrintIfEmpty = true;
            StyleSheet = new Matrix.MatrixStyleSheet();
            StyleSheet.Load(ResourceLoader.GetStream("cross.frss"));
            style = "";
            ManualBuildEvent = "";
            ModifyResultEvent = "";
        }
    }
}