using System;
using System.Collections.Generic;
using System.Text;


namespace System.Drawing.Html
{
    internal class CssTable
    {
        #region Subclasses

        /// <summary>
        /// Used to make space on vertical cell combination
        /// </summary>
        public class SpacingBox
            : CssBox
        {
            public readonly CssBox ExtendedBox;

            public SpacingBox(CssBox tableBox, ref CssBox extendedBox, int startRow)
                : base(tableBox, new HtmlTag("<none colspan=" + extendedBox.GetAttribute("colspan", "1") + ">"))
            {
                ExtendedBox = extendedBox;
                Display = CssConstants.None;

                _startRow = startRow;
                _endRow = startRow + int.Parse(extendedBox.GetAttribute("rowspan", "1")) - 1;
            }

            #region Props

            private int _startRow;
            /// <summary>
            /// Gets the index of the row where box starts
            /// </summary>
            public int StartRow
            {
                get { return _startRow; }
            }

            private int _endRow;

            /// <summary>
            /// Gets the index of the row where box ends
            /// </summary>
            public int EndRow
            {
                get { return _endRow; }
            }


            #endregion
        }

        #endregion

        #region Fields

        private CssBox _tableBox;
        private int _rowCount;
        private int _columnCount;
        private List<CssBox> _bodyrows;
        private CssBox _caption;
        private List<CssBox> _columns;
        private CssBox _headerBox;
        private CssBox _footerBox;
        private List<CssBox> _allRows;
        private float[] _columnWidths;
        private bool _widthSpecified;
        private float[] _columnMinWidths;

        #endregion

        #region Ctor

        private CssTable()
        {
            _bodyrows = new List<CssBox>();
            _columns = new List<CssBox>();
            _allRows = new List<CssBox>();
        }

        public CssTable(CssBox tableBox, Graphics g)
            :this()
        {
            if (!(tableBox.Display == CssConstants.Table || tableBox.Display == CssConstants.InlineTable))
                throw new ArgumentException("Box is not a table", "tableBox");

            _tableBox = tableBox;

            MeasureWords(tableBox, g);

            Analyze(g);
        }

        #endregion

        #region Props

        /// <summary>
        /// Gets if the user specified a width for the table
        /// </summary>
        public bool WidthSpecified
        {
            get { return _widthSpecified; }
        }

        /// <summary>
        /// Hosts a list of all rows in the table, including those on the TFOOT, THEAD and TBODY
        /// </summary>
        public List<CssBox> AllRows
        {
            get { return _allRows; }
        }

        /// <summary>
        /// Gets the box that represents the caption of this table, if any.
        /// WARNING: May be null
        /// </summary>
        public CssBox Caption
        {
            get { return _caption; }
        }

        /// <summary>
        /// Gets the column count of this table
        /// </summary>
        public int ColumnCount
        {
            get { return _columnCount; }
        }

        /// <summary>
        /// Gets the minimum width of each column
        /// </summary>
        public float[] ColumnMinWidths
        {
            get 
            {
                if (_columnMinWidths == null)
                {
                    _columnMinWidths = new float[ColumnWidths.Length];

                    foreach (CssBox row in AllRows)
                    {
                        foreach (CssBox cell in row.Boxes)
                        {
                            int colspan = GetColSpan(cell);
                            int col = GetCellRealColumnIndex(row, cell);
                            int affectcol = col + colspan - 1;
                            float spannedwidth = GetSpannedMinWidth(row, cell, col, colspan) + (colspan - 1) * HorizontalSpacing;

                            _columnMinWidths[affectcol] = Math.Max(_columnMinWidths[affectcol], cell.GetMinimumWidth() - spannedwidth);

                        }
                    }

                }

                return _columnMinWidths; 
            }
        }

        /// <summary>
        /// Gets the declared Columns on the TABLE tag
        /// </summary>
        public List<CssBox> Columns
        {
            get { return _columns; }
        }

        /// <summary>
        /// Gets an array indicating the withs of each column.
        /// This must have the same count than <see cref="Columns"/>
        /// </summary>
        public float[] ColumnWidths
        {
            get { return _columnWidths; }
        }

        /// <summary>
        /// Gets the boxes that represents the table-row Boxes of the table, 
        /// including those inside of the TBODY tags
        /// </summary>
        public List<CssBox> BodyRows
        {
            get { return _bodyrows; }
        }

        /// <summary>
        /// Gets the table-footer-group Box
        /// WARNING: May be null
        /// </summary>
        public CssBox FooterBox
        {
            get { return _footerBox; }
        }

        /// <summary>
        /// Gets the table-header-group Box
        /// WARNING: May be null
        /// </summary>
        public CssBox HeaderBox
        {
            get { return _headerBox; }
        }

        /// <summary>
        /// Gets the actual horizontal spacing of the table
        /// </summary>
        public float HorizontalSpacing
        {
            get 
            {
                if (TableBox.BorderCollapse == CssConstants.Collapse)
                {
                    return -1f;
                }

                return TableBox.ActualBorderSpacingHorizontal;
            }
        }

        /// <summary>
        /// Gets the actual vertical spacing of the table
        /// </summary>
        public float VerticalSpacing
        {
            get
            {
                if (TableBox.BorderCollapse == CssConstants.Collapse)
                {
                    return -1f;
                }

                return TableBox.ActualBorderSpacingVertical;
            }
        }

        /// <summary>
        /// Gets the row count of this table, including the rows inside the table-row-group,
        /// table-row-heaer and table-row-footer Boxes
        /// </summary>
        public int RowCount
        {
            get { return _rowCount; }
        }

        /// <summary>
        /// Gets the original table box
        /// </summary>
        public CssBox TableBox
        {
            get { return _tableBox; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Analyzes the Table and assigns values to this CssTable object.
        /// To be called from the constructor
        /// </summary>
        private void Analyze(Graphics g)
        {
            float availSpace = GetAvailableWidth();
            float availCellSpace = float.NaN; //Will be set later

            #region Assign box kinds
            foreach (CssBox b in TableBox.Boxes)
            {
                b.RemoveAnonymousSpaces();
                switch (b.Display)
                {
                    case CssConstants.TableCaption:
                        _caption = b;
                        break;
                    case CssConstants.TableColumn:
                        for (int i = 0; i < GetSpan(b); i++)
                        {
                            Columns.Add(CreateColumn(b));
                        }
                        break;
                    case CssConstants.TableColumnGroup:
                        if (b.Boxes.Count == 0)
                        {
                            int gspan = GetSpan(b);
                            for (int i = 0; i < gspan; i++)
                            {
                                Columns.Add(CreateColumn(b));
                            }
                        }
                        else
                        {
                            foreach (CssBox bb in b.Boxes)
                            {
                                int bbspan = GetSpan(bb);
                                for (int i = 0; i < bbspan; i++)
                                {
                                    Columns.Add(CreateColumn(bb));
                                }
                            }
                        }
                        break;
                    case CssConstants.TableFooterGroup:
                        if (FooterBox != null)
                            BodyRows.Add(b);
                        else
                            _footerBox = b;
                        break;
                    case CssConstants.TableHeaderGroup:
                        if (HeaderBox != null)
                            BodyRows.Add(b);
                        else
                            _headerBox = b;
                        break;
                    case CssConstants.TableRow:
                        BodyRows.Add(b);
                        break;
                    case CssConstants.TableRowGroup:
                        foreach (CssBox bb in b.Boxes)
                            if (b.Display == CssConstants.TableRow)
                                BodyRows.Add(b);
                        break;
                    default:
                        break;
                }
            } 
            #endregion

            #region Gather AllRows

            if (HeaderBox != null) _allRows.AddRange(HeaderBox.Boxes);
            _allRows.AddRange(BodyRows);
            if (FooterBox != null) _allRows.AddRange(FooterBox.Boxes);

            #endregion

            #region Insert EmptyBoxes for vertical cell spanning

            if (!TableBox.TableFixed)
            {
                int currow = 0;
                int curcol = 0;
                List<CssBox> rows = BodyRows;

                foreach (CssBox row in rows)
                {
                    row.RemoveAnonymousSpaces();
                    curcol = 0;
                    for(int k = 0; k < row.Boxes.Count ; k++)
                    {

                        CssBox cell = row.Boxes[k];
                        int rowspan = GetRowSpan(cell);
                        int realcol = GetCellRealColumnIndex(row, cell); //Real column of the cell

                        for (int i = currow + 1; i < currow + rowspan; i++)
                        {
                            int colcount = 0;
                            for (int j = 0; j <= rows[i].Boxes.Count; j++)
                            {
                                if (colcount == realcol)
                                {
                                    rows[i].Boxes.Insert(colcount, new SpacingBox(TableBox, ref cell, currow));
                                    break;
                                }
                                colcount++;
                                realcol -= GetColSpan(rows[i].Boxes[j]) - 1;
                            }

                        } // End for (int i = currow + 1; i < currow + rowspan; i++)
                        curcol++;
                    } /// End foreach (Box cell in row.Boxes)
                    currow++;
                } /// End foreach (Box row in rows)

                TableBox.TableFixed = true;

            } /// End if (!TableBox.TableFixed)

            #endregion

            #region Determine Row and Column Count, and ColumnWidths

            //Rows
            _rowCount = BodyRows.Count +
                (HeaderBox != null ? HeaderBox.Boxes.Count : 0) +
                (FooterBox != null ? FooterBox.Boxes.Count : 0);

            //Columns
            if (Columns.Count > 0)
                _columnCount = Columns.Count;
            else
                foreach (CssBox b in AllRows) //Check trhough rows
                    _columnCount = Math.Max(_columnCount, b.Boxes.Count);

            //Initialize column widths array
            _columnWidths = new float[_columnCount];

            //Fill them with NaNs
            for (int i = 0; i < _columnWidths.Length; i++)
                _columnWidths[i] = float.NaN;

            availCellSpace = GetAvailableCellWidth();

            if (Columns.Count > 0)
            {
                #region Fill ColumnWidths array by scanning column widths

                for (int i = 0; i < Columns.Count; i++)
                {
                    CssLength len = new CssLength(Columns[i].Width); //Get specified width

                    if (len.Number > 0) //If some width specified
                    {
                        if (len.IsPercentage)//Get width as a percentage
                        {
                            ColumnWidths[i] = CssValue.ParseNumber(Columns[i].Width, availCellSpace);
                        }
                        else if (len.Unit == CssLength.CssUnit.Pixels || len.Unit == CssLength.CssUnit.None)
                        {
                            ColumnWidths[i] = len.Number; //Get width as an absolute-pixel value
                        }
                    }
                }

                #endregion
            }
            else
            {
                #region Fill ColumnWidths array by scanning width in table-cell definitions
                foreach (CssBox row in AllRows)
                {
                    //Check for column width in table-cell definitions
                    for (int i = 0; i < _columnCount; i++)
                    {
                        if (float.IsNaN(ColumnWidths[i]) &&                 //Check if no width specified for column
                            i < row.Boxes.Count &&                          //And there's a box to check
                            row.Boxes[i].Display == CssConstants.TableCell)//And the box is a table-cell
                        {
                            CssLength len = new CssLength(row.Boxes[i].Width); //Get specified width
                            
                            if (len.Number > 0) //If some width specified
                            {
                                int colspan = GetColSpan(row.Boxes[i]);
                                float flen = 0f;
                                if (len.IsPercentage)//Get width as a percentage
                                {
                                    flen = CssValue.ParseNumber(row.Boxes[i].Width, availCellSpace);
                                }
                                else if (len.Unit == CssLength.CssUnit.Pixels || len.Unit == CssLength.CssUnit.None)
                                {
                                    flen = len.Number; //Get width as an absolute-pixel value
                                }
                                flen /= Convert.ToSingle(colspan);

                                for (int j = i; j < i + colspan; j++)
                                {
                                    ColumnWidths[j] = flen;
                                }
                            }
                        }
                    }
                }
                #endregion
            }

            #endregion

            #region Determine missing Column widths

            if (WidthSpecified) //If a width was specified,
            {
                //Assign NaNs equally with space left after gathering not-NaNs
                int numberOfNans = 0;
                float occupedSpace = 0f;

                //Calculate number of NaNs and occuped space
                for (int i = 0; i < ColumnWidths.Length; i++)
                    if (float.IsNaN(ColumnWidths[i]))
                        numberOfNans++;
                    else
                        occupedSpace += ColumnWidths[i];

                //Determine width that will be assigned to un asigned widths
                float nanWidth = (availCellSpace - occupedSpace) / Convert.ToSingle(numberOfNans);

                for (int i = 0; i < ColumnWidths.Length; i++)
                    if (float.IsNaN(ColumnWidths[i]))
                        ColumnWidths[i] = nanWidth;
            }
            else
            {
                //Assign NaNs using full width
                float[] _maxFullWidths = new float[ColumnWidths.Length];

                //Get the maximum full length of NaN boxes
                foreach (CssBox row in AllRows)
                {
                    for (int i = 0; i < row.Boxes.Count; i++)
                    {
                        int col = GetCellRealColumnIndex(row, row.Boxes[i]);

                        if (float.IsNaN(ColumnWidths[col]) &&
                            i < row.Boxes.Count &&
                            GetColSpan(row.Boxes[i]) == 1)
                        {
                            _maxFullWidths[col] = Math.Max(_maxFullWidths[col], row.Boxes[i].GetFullWidth(g));
                        }
                    }
                }

                for (int i = 0; i < ColumnWidths.Length; i++)
                    if (float.IsNaN(ColumnWidths[i]))
                        ColumnWidths[i] = _maxFullWidths[i];
            }

            #endregion

            #region Reduce widths if necessary

            int curCol = 0;
            float reduceAmount = 1f;

            //While table width is larger than it should, and width is reductable
            while (GetWidthSum() > GetAvailableWidth() && CanReduceWidth())
            {
                while (!CanReduceWidth(curCol)) curCol++;

                ColumnWidths[curCol] -= reduceAmount;

                curCol++;

                if (curCol >= ColumnWidths.Length) curCol = 0;
            }

            #endregion

            #region Check for minimum sizes (increment widths if necessary)

            foreach (CssBox row in AllRows)
            {
                foreach (CssBox cell in row.Boxes)
                {
                    int colspan = GetColSpan(cell);
                    int col = GetCellRealColumnIndex(row, cell);
                    int affectcol = col + colspan - 1;
                    
                    if (ColumnWidths[col] < ColumnMinWidths[col])
                    {
                        float diff = ColumnMinWidths[col] - ColumnWidths[col];
                        ColumnWidths[affectcol] = ColumnMinWidths[affectcol];

                        if (col < ColumnWidths.Length - 1)
                        {
                            ColumnWidths[col + 1] -= diff;
                        }
                    }
                }
            }

            #endregion

            #region Set table padding

            TableBox.Padding = "0"; //Ensure there's no padding

            #endregion

            #region Layout cells

            //Actually layout cells!
            float startx = TableBox.ClientLeft + HorizontalSpacing;
            float starty = TableBox.ClientTop + VerticalSpacing;
            float curx = startx;
            float cury = starty;
            float maxRight = startx;
            float maxBottom = 0f;
            int currentrow = 0;

            foreach (CssBox row in AllRows)
            {
                if (row is CssAnonymousSpaceBlockBox || row is CssAnonymousSpaceBox) continue;

                curx = startx;
                curCol = 0;

                foreach (CssBox cell in row.Boxes)
                {
                    if (curCol >= ColumnWidths.Length) break;

                    int rowspan = GetRowSpan(cell);
                    float width = GetCellWidth(GetCellRealColumnIndex(row, cell), cell);

                    cell.Location = new PointF(curx, cury);
                    cell.Size = new SizeF(width, 0f);
                    cell.MeasureBounds(g); //That will automatically set the bottom of the cell

                    //Alter max bottom only if row is cell's row + cell's rowspan - 1
                    SpacingBox sb = cell as SpacingBox;
                    if (sb != null)
                    {
                        if (sb.EndRow == currentrow)
                        {
                            maxBottom = Math.Max(maxBottom, sb.ExtendedBox.ActualBottom);
                        }
                    }
                    else if(rowspan == 1)
                    {
                        maxBottom = Math.Max(maxBottom, cell.ActualBottom);
                    }
                    maxRight = Math.Max(maxRight, cell.ActualRight);
                    curCol++;
                    curx = cell.ActualRight + HorizontalSpacing;
                }

                foreach (CssBox cell in row.Boxes)
                {
                    SpacingBox spacer = cell as SpacingBox;

                    if (spacer == null && GetRowSpan(cell) == 1)
                    {
                        cell.ActualBottom = maxBottom;
                        CssLayoutEngine.ApplyCellVerticalAlignment(g, cell);
                    }
                    else if(spacer != null && spacer.EndRow == currentrow)
                    {
                        spacer.ExtendedBox.ActualBottom = maxBottom;
                        CssLayoutEngine.ApplyCellVerticalAlignment(g, spacer.ExtendedBox);
                    }
                }

                cury = maxBottom + VerticalSpacing;
                currentrow++;
            }

            TableBox.ActualRight = maxRight + HorizontalSpacing + TableBox.ActualBorderRightWidth;
            TableBox.ActualBottom = maxBottom + VerticalSpacing + TableBox.ActualBorderBottomWidth;

            #endregion
        }

        /// <summary>
        /// Gets the spanned width of a cell
        /// (With of all columns it spans minus one)
        /// </summary>
        /// <param name="row"></param>
        /// <param name="cell"></param>
        /// <param name="realcolindex"></param>
        /// <param name="colspan"></param>
        /// <returns></returns>
        private float GetSpannedMinWidth(CssBox row, CssBox cell, int realcolindex, int colspan)
        {
            float w = 0f;

            for (int i = realcolindex; i < row.Boxes.Count || i < realcolindex + colspan - 1; i++)
            {
                w += ColumnMinWidths[i];
            }

            return w;
        }

        /// <summary>
        /// Gets the cell column index checking its position and other cells colspans
        /// </summary>
        /// <param name="row"></param>
        /// <param name="cell"></param>
        /// <returns></returns>
        private int GetCellRealColumnIndex(CssBox row, CssBox cell)
        {
            int i = 0;

            foreach (CssBox b in row.Boxes)
            {
                if (b.Equals(cell)) break;
                i += GetColSpan(b);
            }

            return i;
        }

        /// <summary>
        /// Gets the cells width, taking colspan and being in the specified column
        /// </summary>
        /// <param name="column"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private float GetCellWidth(int column, CssBox b)
        {
            float colspan = Convert.ToSingle(GetColSpan(b));
            float sum = 0f;

            for (int i = column; i < column + colspan; i++)
            {
                if (column >= ColumnWidths.Length) break;
                if (ColumnWidths.Length <= i) break;
                sum += ColumnWidths[i]; 
            }

            sum += (colspan - 1) * HorizontalSpacing;

            return sum; ;// -b.ActualBorderLeftWidth - b.ActualBorderRightWidth - b.ActualPaddingRight - b.ActualPaddingLeft;
        }

        /// <summary>
        /// Gets the colspan of the specified box
        /// </summary>
        /// <param name="b"></param>
        private int GetColSpan(CssBox b)
        {
            string att = b.GetAttribute("colspan", "1");
            int colspan;

            if (!int.TryParse(att, out colspan))
            {
                return 1;
            }

            return colspan;
        }

        /// <summary>
        /// Gets the rowspan of the specified box
        /// </summary>
        /// <param name="b"></param>
        private int GetRowSpan(CssBox b)
        {
            string att = b.GetAttribute("rowspan", "1");
            int rowspan;

            if (!int.TryParse(att, out rowspan))
            {
                return 1;
            }

            return rowspan;
        }

        /// <summary>
        /// Recursively measures the specified box
        /// </summary>
        /// <param name="b"></param>
        /// <param name="g"></param>
        private void Measure(CssBox b, Graphics g)
        {
            if (b == null) return;

            foreach (CssBox bb in b.Boxes)
            {
                bb.MeasureBounds(g);
                Measure(bb, g);
            }
        }

        /// <summary>
        /// Recursively measures words inside the box
        /// </summary>
        /// <param name="b"></param>
        /// <param name="g"></param>
        private void MeasureWords(CssBox b, Graphics g)
        {
            if (b == null) return;

            foreach (CssBox bb in b.Boxes)
            {
                bb.MeasureWordsSize(g);
                MeasureWords(bb, g);
            }
        }

        /// <summary>
        /// Gets the number of reductable columns
        /// </summary>
        /// <returns></returns>
        private int GetReductableColumns()
        {
            int response = 0;

            for (int i = 0; i < ColumnWidths.Length; i++)
                if (CanReduceWidth(i))
                    response++;

            return response;
        }

        /// <summary>
        /// Tells if the columns widths can be reduced,
        /// by checking the minimum widths of all cells
        /// </summary>
        /// <returns></returns>
        private bool CanReduceWidth()
        {
            for (int i = 0; i < ColumnWidths.Length; i++)
            {
                if (CanReduceWidth(i))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Tells if the specified column can be reduced,
        /// by checking its minimum width
        /// </summary>
        /// <param name="columnIndex"></param>
        /// <returns></returns>
        private bool CanReduceWidth(int columnIndex)
        {
            if (ColumnWidths.Length >= columnIndex || ColumnMinWidths.Length >= columnIndex) return false;
            return ColumnWidths[columnIndex] > ColumnMinWidths[columnIndex];
        }

        /// <summary>
        /// Gets the available width for the whole table.
        /// It also sets the value of <see cref="WidthSpecified"/>
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// The table's width can be larger than the result of this method, because of the minimum 
        /// size that individual boxes.
        /// </remarks>
        private float GetAvailableWidth()
        {
            CssLength tblen = new CssLength(TableBox.Width);

            if (tblen.Number > 0)
            {
                _widthSpecified = true;

                if (tblen.IsPercentage)
                {
                    return CssValue.ParseNumber(tblen.Length, TableBox.ParentBox.AvailableWidth);
                }
                else
                {
                    return tblen.Number;
                }
            }
            else
            {
                return TableBox.ParentBox.AvailableWidth;
            }
        }

        /// <summary>
        /// Gets the width available for cells
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// It takes away the cell-spacing from <see cref="GetAvailableWidth()"/>
        /// </remarks>
        private float GetAvailableCellWidth()
        {
            return GetAvailableWidth() - 
                HorizontalSpacing * (ColumnCount + 1) -
                TableBox.ActualBorderLeftWidth - TableBox.ActualBorderRightWidth;
        }

        /// <summary>
        /// Gets the current sum of column widths
        /// </summary>
        /// <returns></returns>
        private float GetWidthSum()
        {
            float f = 0f;

            for (int i = 0; i < ColumnWidths.Length; i++)
                if (float.IsNaN(ColumnWidths[i]))
                    throw new Exception("CssTable Algorithm error: There's a NaN in column widths");
                else
                    f += ColumnWidths[i];
            
            //Take cell-spacing
            f += HorizontalSpacing * (ColumnWidths.Length + 1);

            //Take table borders
            f += TableBox.ActualBorderLeftWidth + TableBox.ActualBorderRightWidth;

            return f;
        }

        /// <summary>
        /// Gets the span attribute of the tag of the specified box
        /// </summary>
        /// <param name="b"></param>
        private int GetSpan(CssBox b)
        {
            float f = CssValue.ParseNumber(b.GetAttribute("span"), 1);

            return Math.Max(1, Convert.ToInt32(f));
        }

        /// <summary>
        /// Creates the column with the specified width
        /// </summary>
        /// <param name="width"></param>
        /// <returns></returns>
        private CssBox CreateColumn(CssBox modelBox)
        {
            return modelBox;
            //Box b = new Box(null, new HtmlTag(string.Format("<COL style=\"width:{0}\" >", width)));
            //b.Width = width;
            //return b;
        }

        #endregion
    }
}

