using System;
using System.Net;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Globalization;
using C1.Silverlight.FlexGrid;

namespace C1.Silverlight.FlexGridExcel
{
    /// <summary>
    /// Helper class used to print the C1FlexGrid control using the browser and
    /// javascript (bypassing the Silverlight printing mechanism)
    /// </summary>
    public class FlexHtmlPrinter
    {
        //------------------------------------------------------------------------------------------------
        #region ** fields

        C1FlexGrid _flex;

        #endregion

        //------------------------------------------------------------------------------------------------
        #region ** ctor

        /// <summary>
        /// Initializes a new instance of a <see cref="FlexHtmlPrinter"/>.
        /// </summary>
        /// <param name="flex"></param>
        public FlexHtmlPrinter(C1FlexGrid flex)
        {
            _flex = flex;
        }

        #endregion

        //------------------------------------------------------------------------------------------------
        #region ** object model

        /// <summary>
        /// Prints the grid directly to the browser, bypassing the Silverlight printing mechanism.
        /// </summary>
        /// <param name="title"></param>
        /// <param name="pageSize"></param>
        /// <param name="preview"></param>
        public void Print(string title, Size pageSize, bool preview)
        {
            Print(title, pageSize, SaveOptions.SaveHeaders | SaveOptions.VisibleOnly, preview);
        }
        /// <summary>
        /// Prints the grid directly to the browser, bypassing the Silverlight printing mechanism.
        /// </summary>
        /// <param name="title"></param>
        /// <param name="pageSize"></param>
        /// <param name="options"></param>
        /// <param name="preview"></param>
        public void Print(string title, Size pageSize, SaveOptions options, bool preview)
        {
            // hide header row before printing (we will print the fixed row instead)
            var hdr = _flex.Rows[0] as HeaderRow;
            if (hdr != null)
            {
                hdr.Visible = false;
            }

            // get the html
            var html = GetHtml(pageSize, options);

            // restore row visibility
            if (hdr != null)
            {
                hdr.Visible = true;
            }

            // and go print
            Print("Preview", html, preview);
        }

        #endregion

        //------------------------------------------------------------------------------------------------
        #region ** javascript

        static void Print(string previewWinTitle, string html, bool preview)
        {
            var options = new HtmlPopupWindowOptions();
            options.Width = 800;
            options.Height = 600;
            options.Status = false;
            options.Directories = false;
            options.Location = false;
            options.Menubar = false;
            options.Toolbar = false;
            Print(previewWinTitle, html, preview, options);
        }
        static void Print(string previewWinTitle, string html, bool preview, HtmlPopupWindowOptions options)
        {
            if (Application.Current.Host.Source.Scheme == "file")
            {
                throw new InvalidOperationException("In order to Print the application should be executed from a web application");
            }

            if (!Application.Current.Host.Settings.EnableHTMLAccess)
            {
                throw new InvalidOperationException("In order to Print the Silverlight plug-in must have HTMLAccess='true'");
            }

            // clean html
            html = html.Replace("\"", "'").Replace("\r", string.Empty).Replace("\n", string.Empty);

            // write javascript code
            var code = new StringBuilder();
            code.AppendFormat(@"var tmpWin = window.open('', '{0}', " +
                "'width={1},height={2},scrollbars={3},resizable={4},location={5},toolbar={6}," +
                "top={7},left={8},menubar={9},directories={10},status={11}');",
                previewWinTitle,
                options.Width,
                options.Height,
                options.Scrollbars ? 1 : 0,
                options.Resizeable ? 1 : 0,
                options.Location ? 1 : 0,
                options.Toolbar ? 1 : 0,
                options.Top,
                options.Left,
                options.Menubar ? 1 : 0,
                options.Directories ? 1 : 0,
                options.Status ? 1 : 0);
            code.AppendFormat(
                "if (tmpWin != null) " +
                "{{" +
                "  tmpWin.blur();" +
                "  tmpWin.document.write(\"{0}\");" +
                "  tmpWin.document.close();" +
                "  {1}" + // tmpWin.print();
                "  tmpWin.close();" +
                "}}",
                html,
                preview ? "tmpWin.print();" : string.Empty);

            // execute javascript code
            HtmlPage.Window.Eval(code.ToString());
        }

        #endregion

        //------------------------------------------------------------------------------------------------
        #region ** generate paged HTML

        /// <summary>
        /// Creates an HTML string with page breaks suitable for printing via javascript.
        /// </summary>
        /// <param name="pageSize"></param>
        /// <param name="options"></param>
        /// <returns></returns>
        string GetHtml(Size pageSize, SaveOptions options)
        {
            using (var stream = new MemoryStream())
            {
                var sw = new StreamWriter(stream);
                GetHtml(sw, pageSize, options);
                var html = Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Position);
                return html;
            }
        }
        /// <summary>
        /// Creates an HTML string with page breaks suitable for printing via javascript.
        /// </summary>
        /// <param name="sw"><see cref="StreamWriter"/> where the output will be created.</param>
        /// <param name="pageSize">Page size, in 96 dpi pixels.</param>
        /// <param name="options">Options used when creating the output.</param>
        void GetHtml(StreamWriter sw, Size pageSize, SaveOptions options)
        {
            // start document
            sw.WriteLine("<html>\r\n<head>\r\n<style type='text/css'>");
            sw.WriteLine(GetCss(options));
            sw.WriteLine("</style>\r\n<body>");

            // parse options
            var saveColHdr = (options & SaveOptions.SaveColumnHeaders) != 0;
            var saveRowHdr = (options & SaveOptions.SaveRowHeaders) != 0;

            // render each range
            foreach (var range in GetPageBreaks(pageSize))
            {
                // print page header
                //if (!string.IsNullOrEmpty(HeaderLeft) || !string.IsNullOrEmpty(HeaderCenter) || !string.IsNullOrEmpty(HeaderRight))
                //{
                //    sw.WriteLine(PageHeaderTemplate, HeaderLeft, HeaderCenter, HeaderRight);
                //}

                // start this table
                sw.WriteLine("<table>");

                // column headers
                if (saveColHdr)
                {
                    sw.WriteLine("<thead>");
                    for (int r = 0; r < _flex.ColumnHeaders.Rows.Count; r++)
                    {
                        // save top left cells
                        var sb = new StringBuilder();
                        if (saveRowHdr)
                        {
                            var rng = new CellRange(r, 0, r, _flex.TopLeftCells.Columns.Count - 1);
                            SaveRowHtml(sb, _flex.TopLeftCells, rng, r, options, "th");
                        }

                        // save column headers
                        SaveRowHtml(sb, _flex.ColumnHeaders, range, r, options, "th");

                        // save row
                        if (sb.Length > 0)
                        {
                            sw.WriteLine("<tr>");
                            sw.Write(sb.ToString());
                            sw.WriteLine("</tr>");
                        }
                    }
                    sw.WriteLine("</thead>");
                }

                // cells
                sw.WriteLine("<tbody>");
                for (int r = range.TopRow; r <= range.BottomRow; r++)
                {
                    // save row headers
                    var sb = new StringBuilder();
                    if (saveRowHdr)
                    {
                        var rng = new CellRange(r, 0, r, _flex.RowHeaders.Columns.Count - 1);
                        SaveRowHtml(sb, _flex.RowHeaders, rng, r, options, "td");
                    }

                    // save row cells
                    SaveRowHtml(sb, _flex.Cells, range, r, options, "td");

                    // save row
                    if (sb.Length > 0)
                    {
                        sw.WriteLine("<tr>");
                        sw.Write(sb.ToString());
                        sw.WriteLine("</tr>");
                    }
                }
                sw.WriteLine("</tbody>");

                // end this table
                sw.WriteLine("</table>");
            }

            // end table, body, document
            sw.WriteLine("</body>\r\n</html>");
            sw.Flush();
        }
        /// <summary>
        /// Saves a portion of a row to an html file.
        /// </summary>
        void SaveRowHtml(StringBuilder sb, GridPanel p, CellRange range, int row, SaveOptions options, string tdth)
        {
            // parse options
            var formatted = (options & SaveOptions.Formatted) != 0;
            var visCols = (options & SaveOptions.VisibleColumns) != 0;
            var visRows = (options & SaveOptions.VisibleRows) != 0;

            // skip invisible rows
            if (visRows && !p.Rows[row].Visible)
            {
                return;
            }

            // save columns
            for (int col = range.LeftColumn; col < range.RightColumn; col++)
            {
                // skip invisible columns
                if (visCols && !p.Columns[col].Visible)
                {
                    continue;
                }

                // get cell content
                var cellString = GetCellText(p, row, col, formatted);
                cellString = HtmlEncode(cellString);

                // get style name
                var styleName = p.CellType == CellType.ColumnHeader || p.CellType == CellType.Cell
                    ? string.Format("c{0}", col)
                    : string.Format("h{0}", col);

                // calculate column span
                var colSpan = string.Empty;
                if (_flex.MergeManager != null)
                {
                    var rng = _flex.MergeManager.GetMergedRange(_flex, p.CellType, new CellRange(row, col));
                    rng.Column2 = Math.Min(rng.Column2, range.Column2);
                    if (rng.ColumnSpan > 1)
                    {
                        var width = 0.0;
                        for (int c = rng.Column; c <= rng.Column2; c++)
                        {
                            width += _flex.Columns[c].ActualWidth;
                        }
                        colSpan = string.Format(" colspan={0} style='width:{1:0}px'", rng.ColumnSpan, width);
                        col += rng.ColumnSpan - 1;
                    }
                }

                // add cell
                sb.AppendFormat("<{0} class={1}{2}>{3}</{0}>\r\n", tdth, styleName, colSpan, cellString);
            }
        }
        /// <summary>
        /// Build the CSS style sheet used for printing the grid.
        /// </summary>
        /// <param name="options"></param>
        /// <returns></returns>
        string GetCss(SaveOptions options)
        {
            var sb = new StringBuilder();

            // table style
            // NOTE: page-break-after seems to work OK; page-break-before does NOT!
            sb.AppendFormat("\ttable\t{{ font-family:{0}; font-size:{1:0.##}pt; page-break-after:always; }}\r\n",
                _flex.FontFamily,
                _flex.FontSize / 96.0 * 72.0);

            // table headers
            sb.Append("\tth\t{ background-color:lightgray; }\r\n");

            // row header styles
            if ((options & SaveOptions.SaveRowHeaders) != 0)
            {
                for (int c = 0; c < _flex.RowHeaders.Columns.Count; c++)
                {
                    var col = _flex.RowHeaders.Columns[c];
                    if (col.ActualWidth > 0)
                    {
                        sb.AppendFormat("\t.h{0}\t{{ {1}background-color:lightgray; }}\r\n", c, GetColumnStyle(col));
                    }
                }
            }

            // scrollable column styles
            for (int c = 0; c < _flex.Columns.Count; c++)
            {
                var col = _flex.Columns[c];
                if (col.ActualWidth > 0)
                {
                    sb.AppendFormat("\t.c{0}\t{{ {1} }}\r\n", c, GetColumnStyle(col));
                }
            }

            // done
            return sb.ToString();
        }
        /// <summary>
        /// Encode strings so they are valid HTML.
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        static string HtmlEncode(string s)
        {
            if (string.IsNullOrEmpty(s))
            {
                return string.Empty;
            }
            s = s.Replace("&", "&amp;");
            s = s.Replace("<", "&lt;");
            s = s.Replace(">", "&gt;");
            return s;
        }
        /// <summary>
        /// Gets the CSS style used to render a given column.
        /// </summary>
        /// <param name="c"></param>
        string GetColumnStyle(Column c)
        {
            var sb = new StringBuilder();
            sb.AppendFormat("width:{0}px; ", (int)c.ActualWidth);
            if (c.HorizontalAlignment != null)
            {
                switch (c.HorizontalAlignment.Value)
                {
                    case HorizontalAlignment.Center:
                        sb.Append("text-align:center; ");
                        break;
                    case HorizontalAlignment.Right:
                        sb.Append("text-align:right; ");
                        break;
                }
            }
            return sb.ToString();
        }
        /// <summary>
        /// Gets the content of a cell as formatted HTML.
        /// </summary>
        /// <param name="p"></param>
        /// <param name="row"></param>
        /// <param name="col"></param>
        /// <param name="formatted"></param>
        /// <returns></returns>
        string GetCellText(GridPanel p, int row, int col, bool formatted)
        {
            // get raw value
            var value = p[row, col];

            // handle group rows
            if (value == null && p.Rows[row] is GroupRow && p.CellType == CellType.Cell)
            {
                if (p.Columns[col].Left == 0)
                {
                    var gr = p.Rows[row] as GroupRow;
                    var converter = _flex.GroupHeaderConverter;
                    return (string)converter.Convert(null, typeof(string), gr, CultureInfo.CurrentUICulture);
                }
            }

            // handle column headers
            if (value == null && p.CellType == CellType.ColumnHeader && row == 0)
            {
                var c = p.Columns[col];
                var hdr = c.Header;
                value = string.IsNullOrEmpty(hdr) ? c.BoundPropertyName : hdr;
            }

            // apply formatting if required
            if (formatted && value is IFormattable)
            {
                var fmt = p.Columns[col].Format;
                if (string.IsNullOrEmpty(fmt) && p.Columns[col].Binding != null)
                {
                    fmt = p.Columns[col].Binding.StringFormat;
                }
                if (!string.IsNullOrEmpty(fmt))
                {
                    value = ((IFormattable)value).ToString(fmt, CultureInfo.CurrentUICulture);
                }
            }

            // return value as string
            return value == null ? string.Empty : value.ToString();
        }
        /// <summary>
        /// Gets a list of page ranges to use when printing the grid.
        /// </summary>
        List<CellRange> GetPageBreaks(Size pageSize)
        {
            // get page size to calculate breaks
            var pageWidth = pageSize.Width;
            var pageHeight = pageSize.Height;

            // calculate size of fixed/frozen areas
            var widFixed = 0.0;
            for (int c = 0; c < _flex.RowHeaders.Columns.Count; c++)
            {
                widFixed += _flex.RowHeaders.Columns[c].ActualWidth;
            }
            var heiFixed = 0.0;
            for (int r = 0; r < _flex.ColumnHeaders.Rows.Count; r++)
            {
                heiFixed += _flex.ColumnHeaders.Rows[r].ActualHeight;
            }

            // fill row break array
            var rowBreaks = new List<int>();
            var totalHeight = heiFixed;
            for (var r = 0; r < _flex.Rows.Count; r++)
            {
                totalHeight += _flex.Rows[r].ActualHeight;
                if (totalHeight > pageHeight)
                {
                    rowBreaks.Add(r - 1);
                    totalHeight = heiFixed + _flex.Rows[r].ActualHeight;
                }
            }
            if (rowBreaks.Count == 0 || rowBreaks[rowBreaks.Count - 1] != _flex.Rows.Count - 1)
            {
                rowBreaks.Add(_flex.Rows.Count - 1);
            }

            // fill row break array
            var colBreaks = new List<int>();
            double totalWidth = widFixed;
            for (int c = 0; c < _flex.Columns.Count; c++)
            {
                totalWidth += _flex.Columns[c].ActualWidth;
                if (totalWidth > pageWidth)
                {
                    colBreaks.Add(c - 1);
                    totalWidth = widFixed + _flex.Columns[c].ActualWidth;
                }
            }
            if (colBreaks.Count == 0 || colBreaks[colBreaks.Count - 1] != _flex.Columns.Count - 1)
            {
                colBreaks.Add(_flex.Columns.Count - 1);
            }

            // translate breaks into ranges
            var pageRanges = new List<CellRange>();
            for (int r = 0; r < rowBreaks.Count; r++)
            {
                for (int c = 0; c < colBreaks.Count; c++)
                {
                    var rng = new CellRange();
                    rng.Row = r == 0 ? 0 : rowBreaks[r - 1];
                    rng.Column = c == 0 ? 0 : colBreaks[c - 1];
                    rng.Row2 = rowBreaks[r];
                    rng.Column2 = colBreaks[c];
                    pageRanges.Add(rng);
                }
            }
            return pageRanges;
        }
        #endregion
    }
}
