using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Shapes;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Globalization;
using System.ComponentModel;
using C1.Util;
using C1.Silverlight.FlexGrid;

namespace C1.Silverlight.FlexGridExcel
{
    /// <summary>
    /// Custom cell factory that displays Excel-style row and column headers.
    /// </summary>
    public class ExcelCellFactory : CellFactory
    {
        // fields
        C1FlexGrid _flex;

        // static fields
        static Thickness _thickness = new Thickness(4, 0, 4, 0);
        static Thickness _thicknessOutlineBar = new Thickness(0, 0, 4, 0);
        static Thickness _thicknessEmpty = new Thickness(0);
        static BitmapImage _bmpCollapsed, _bmpExpandedAbove, _bmpExpandedBelow, _bmpConnector, _bmpTerminalAbove, _bmpTerminalBelow, _bmpNoChildren;
        static Brush _brBlack = new SolidColorBrush(Colors.Black);
        static Brush _brTransparent = new SolidColorBrush(Colors.Transparent);

        // images that may be present in each grid node
        enum NodeImage
        {
            None,
            Collapsed,
            Expanded,
            Connector,
            Terminal
        }

        /// <summary>
        /// Initializes a new instance of an <see cref="ExcelCellFactory"/>.
        /// </summary>
        /// <param name="flex"><see cref="C1FlexGrid"/> that owns this cell factory.</param>
        public ExcelCellFactory(C1FlexGrid flex)
        {
            _flex = flex;
        }
        /// <summary>
        /// Provides content for the cells in the top left panel of the grid.
        /// </summary>
        public override void CreateTopLeftContent(C1FlexGrid grid, Border bdr, CellRange range)
        {
            if (range.Row == 0 && range.Column == 0)
            {
                var poly = CreatePolygon(0, 10, 10, 10, 10, 0);
                poly.Fill = new SolidColorBrush(Color.FromArgb(0xe0, 0xf5, 0xf5, 0xf5));
                poly.VerticalAlignment = VerticalAlignment.Center;
                poly.HorizontalAlignment = HorizontalAlignment.Right;
                poly.Margin = new Thickness(2);
                bdr.Child = poly;
            }
        }
        /// <summary>
        /// Provides content for the row header cells.
        /// </summary>
        public override void CreateRowHeaderContent(C1FlexGrid grid, Border bdr, CellRange range)
        {
            // first column contains letters
            if (range.Column == 0)
            {
                var tb = new TextBlock();
                tb.HorizontalAlignment = HorizontalAlignment.Center;
                tb.VerticalAlignment = VerticalAlignment.Center;
                tb.Text = string.Format("{0}", range.Row + 1);
                bdr.Child = tb;
            }

            // apply style to show errors
            if (grid.ShowErrors && grid.ErrorStyle != null)
            {
                var r = grid.Rows[range.Row];
                var errors = r.GetErrors(null);
                if (!string.IsNullOrEmpty(errors))
                {
                    grid.ErrorStyle.Apply(bdr, SelectedState.None);
                    SetErrorTip(grid, bdr, errors);
                }
            }
        }
        /// <summary>
        /// Provides content for the column header cells.
        /// </summary>
        public override void CreateColumnHeaderContent(C1FlexGrid grid, Border bdr, CellRange rng)
        {
            // allow base class
            base.CreateColumnHeaderContent(grid, bdr, rng);

            // replace last TextBlock content with alpha column header
            TextBlock tb = null;
            foreach (var child in C1.Util.Util.GetChildrenOfType<TextBlock>(bdr))
            {
                tb = child;
            }
            if (tb != null)
            {
                tb.VerticalAlignment = VerticalAlignment.Center;
                tb.HorizontalAlignment = HorizontalAlignment.Center;
                tb.Text = GetAlphaColumnHeader(rng.Column);
            }

            // add custom sort symbols
            var xlg = grid as C1FlexGridExcel;
            if (xlg != null && grid.ShowSort && IsSortSymbolRow(grid, rng))
            {
                // get sort direction for this column
                var col = grid.Columns[rng.Column];
                foreach (var sd in xlg.ColumnSortDescriptions)
                {
                    if (sd.Column == col)
                    {
                        var icon = GetGlyphSort(sd.Direction, tb.Foreground);
                        SetBorderContent(bdr, bdr.Child, icon, 1);
                        break;
                    }
                }
            }
        }
        // creates a sort glyph
        static FrameworkElement GetGlyphSort(ListSortDirection dir, Brush brush)
        {
            var p = dir == ListSortDirection.Ascending
                ? CreatePolygon(0, 4, 8, 4, 4, 0)
                : CreatePolygon(0, 0, 8, 0, 4, 4);
            p.Fill = brush;
            return p;
        }
        // use a grid to combine text and icons in a single cell
        // (used for sort icons in column headers and collapse/expand icons in group rows)
        void SetBorderContent(Border bdr, UIElement e1, UIElement e2, int autoCol)
        {
            var g = new Grid();
            g.Background = _brTransparent; // make sure it's transparent
            g.ColumnDefinitions.Add(new ColumnDefinition());
            g.ColumnDefinitions.Add(new ColumnDefinition());
            g.ColumnDefinitions[autoCol].Width = new GridLength(1, GridUnitType.Auto);

            bdr.Child = null;
            g.Children.Add(e1);
            g.Children.Add(e2);
            e2.SetValue(Grid.ColumnProperty, 1);
            bdr.Child = g;
        }

        /// <summary>
        /// Provides content for the grid cells.
        /// </summary>
        public override void CreateCellContent(C1FlexGrid grid, Border bdr, CellRange range)
        {
            // get row and column
            var r = grid.Rows[range.Row];
            var c = grid.Columns[range.Column];
            var xlg = r.Grid as C1FlexGridExcel;
            int treeCol = GetOutlineColumn(grid);

            // special handling for custom column headers (no CheckBoxes, grouping symbols etc)
            if (r is HeaderRow)
            {
                bdr.Padding = _thickness;
                var tb = new TextBlock();
                tb.VerticalAlignment = VerticalAlignment.Center;
                tb.Text = grid[range.Row, range.Column] as string;
                bdr.Child = tb;
                return;
            }

            // calculate aggregates on flat lists
            // (the value getter will take care of this)
            if (xlg.GetFlatList() != null && c.GroupAggregate != Aggregate.None)
            {
                var value = r[c];
            }

            // regular row
            base.CreateCellContent(grid, bdr, range);

            // optional: remove drop-down from cell when not in edit mode
            // (less functionality, but more Excel-like...)
            //var g = bdr.Child as Grid;
            //if (g != null && g.Children.Count > 1)
            //{
            //    var bdrDropDown = g.Children[1] as Border;
            //    if (bdrDropDown != null && bdrDropDown.Child is Polygon)
            //    {
            //        g.Children.RemoveAt(1);
            //    }
            //}

            // honor gridline visibility 
            // (group rows always show horizontal lines by default)
            if (grid.GridLinesVisibility == GridLinesVisibility.None)
            {
                bdr.BorderThickness = _thicknessEmpty;
            }

            // special handling for BottomGroupRows
            var bgr = r as BottomGroupRow;
            if (bgr != null)
            {
                // honor bottom group row caption
                if (range.Column <= treeCol)
                {
                    if (bdr.Child is TextBlock)
                    {
                        var b = new Binding();
                        b.Source = bgr.GroupRow.Group;
                        b.Converter = grid.GroupHeaderConverter;
                        b.ConverterParameter = r;
                        var tb = bdr.Child as TextBlock;
                        tb.SetBinding(TextBlock.TextProperty, b);
                    }
                }
                else if (c.GroupAggregate == Aggregate.None && GetFlatList(xlg) != null)
                {
                    // remove content from bottom group rows with no data
                    bdr.Child = null;
                }
            }

            // create group and detail cells on the tree column
            var levels = GetOutlineLevels(grid);
            if (levels > 0 && range.Column <= treeCol)
            {
                CreateOutlineCellContent(grid, bdr, range, levels);
            }

            // hide aggregates on expanded group rows that have 
            // a corresponding BottomGrouRow
            SetContentVisibility(bdr, xlg, range, r, c, treeCol);
        }
        void SetContentVisibility(Border bdr, C1FlexGridExcel xlg, CellRange range, Row r, Column c, int treeCol)
        {
            var gr = r as GroupRow;
            if (gr != null && !gr.IsCollapsed && range.Column > treeCol && c.GroupAggregate != Aggregate.None)
            {
                if (xlg.GroupRowPosition == GroupRowPosition.AboveData && xlg.ShowBottomGroupRows)
                {
                    var flatList = xlg.GetFlatList();
                    if (flatList == null || flatList.GetGroupInformation(gr.DataItem).HasChildren)
                    {
                        bdr.Child.Visibility = Visibility.Collapsed;
                    }
                }
            }
        }
        /// <summary>
        /// Applies custom styles to the outline row.
        /// </summary>
        public override void ApplyCellStyles(C1FlexGrid grid, CellType cellType, CellRange range, Border bdr)
        {
            if (cellType == CellType.Cell)
            {
                // get grid, row, column
                var r = grid.Rows[range.Row];
                var c = grid.Columns[range.Column];
                var xlg = r.Grid as C1FlexGridExcel;

                if (r is HeaderRow)
                {
                    // format header row
                    if (c.CellStyle != null)
                    {
                        c.CellStyle.Apply(bdr, SelectedState.None);
                    }
                    if (xlg.HeaderRowStyle != null)
                    {
                        xlg.HeaderRowStyle.Apply(bdr, SelectedState.None);
                    }

                    var xc = c as ExcelColumn;
                    if (xc != null && xc.HeaderCellStyle != null)
                    {
                        xc.HeaderCellStyle.Apply(bdr, SelectedState.None);
                    }
                }
                else if (r is GroupRow && xlg.GroupRowStyle != null)
                {
                    // format group rows
                    // but don't apply group style to flat list items with no children
                    bool apply = true;
                    var flatList = GetFlatList(xlg);
                    if (flatList != null)
                    {
                        var gi = flatList.GetGroupInformation(r.DataItem);
                        if (!gi.HasChildren)
                        {
                            apply = false;
                        }
                    }
                    if (apply)
                    {
                        var selState = grid.GetSelectedState(range);
                        xlg.GroupRowStyle.Apply(bdr, selState);
                    }
                }
                else if (r is BottomGroupRow && xlg.BottomGroupRowStyle != null)
                {
                    // format bottom group rows
                    var selState = grid.GetSelectedState(range);
                    xlg.BottomGroupRowStyle.Apply(bdr, selState);
                }
            }
        }

        // creates an outline cell (with collapse/expand/branch graphics)
        void CreateOutlineCellContent(C1FlexGrid grid, Border bdr, CellRange range, int levels)
        {
            var row = grid.Rows[range.Row];
            var gr = row as GroupRow;
            if (gr != null)
            {
                // build grid for tree cell
                var img = gr.IsCollapsed ? NodeImage.Collapsed : NodeImage.Expanded;
                var g = CreateCellGrid(row, gr.Level, grid.TreeIndent, img);

                // copy data from original cell
                var p = bdr.Child as Panel;
                var tb = p.Children[1] as FrameworkElement;
                p.Children.Clear();
                tb.VerticalAlignment = VerticalAlignment.Center;
                tb.Margin = new Thickness(4, 0, 0, 0);
                tb.SetValue(Grid.ColumnProperty, gr.Level + 1);
                g.Children.Add(tb);

                // assign grid to cell
                bdr.Padding = _thickness;
                bdr.Margin = _thicknessEmpty;
                bdr.Child = g;
            }
            else
            {
                // add connecting lines to detail cells
                var g = CreateCellGrid(row, levels - 1, grid.TreeIndent, NodeImage.Connector);

                // copy data from original cell
                var tb = bdr.Child as FrameworkElement;
                bdr.Child = null;
                tb.VerticalAlignment = VerticalAlignment.Center;
                tb.SetValue(Grid.ColumnProperty, g.ColumnDefinitions.Count - 1);
                g.Children.Add(tb);

                // assign grid to cell
                bdr.Padding = _thickness;
                bdr.Margin = _thicknessEmpty;
                bdr.Child = g;
            }
        }

        // builds an outline button for expanding/collapsing the grid to a given level
        FrameworkElement BuildOutlineButton(C1FlexGrid grid, int level)
        {
            // create button border
            var bdr = new Border();
            bdr.BorderThickness = new Thickness(0, 0, 1, 0);
            bdr.BorderBrush = grid.HeaderGridLinesBrush;
            bdr.MouseLeftButtonDown += (s, e) =>
            {
                e.Handled = true;
                grid.CollapseGroupsToLevel(level);
            };
            bdr.MouseEnter += (s, e) =>
            {
                var tbc = bdr.Child as TextBlock;
                tbc.Foreground = grid.Foreground;
            };
            bdr.MouseLeave += (s, e) =>
            {
                var tbc = bdr.Child as TextBlock;
                tbc.Foreground = grid.HeaderGridLinesBrush;
            };

            // create button content
            var tb = new TextBlock();
            tb.Foreground = grid.HeaderGridLinesBrush;
            tb.Text = string.Format("{0}", level + 1);
            tb.VerticalAlignment = VerticalAlignment.Center;
            tb.HorizontalAlignment = HorizontalAlignment.Center;
            bdr.Child = tb;

            return bdr;
        }

        // gets the number of outline levels present in this grid
        int GetOutlineLevels(C1FlexGrid grid)
        {
            // get the grid that owns the row (we could be printing this)
            grid = grid.Columns[0].Grid;

            // special handling for IFlatList
            var flatList = GetFlatList(grid);
            if (flatList != null)
            {
                return flatList.GetMaxLevels();
            }

            // get the information from the grid that owns the column
            var view = grid.CollectionView;
            return view != null && grid.GroupRowPosition != GroupRowPosition.None
                ? view.GroupDescriptions.Count
                : 0;
        }

        // gets the index of the column that contains the outline
        int GetOutlineColumn(C1FlexGrid grid)
        {
            int col = -1;
            for (int c = 0; c < grid.Columns.Count; c++)
            {
                if (grid.Columns[c].IsVisible)
                {
                    col = c;
                    break;
                }
            }
            return col;
        }

        // creates a grid element with images to represent a grid node
        Grid CreateCellGrid(Row r, int level, double indent, NodeImage nodeImage)
        {
            // get group information
            var flatList = GetFlatList(r.Grid);
            if (flatList != null && r.DataItem != null)
            {
                var gi = flatList.GetGroupInformation(r.DataItem);
                if (!gi.HasChildren)
                {
                    nodeImage = NodeImage.None;
                }
            }

            // special treatment for bottom group rows
            var bgr = r as BottomGroupRow;
            if (bgr != null)
            {
                level = bgr.Level;
            }

            // create grid
            var g = new Grid();

            // add columns for images and content
            bool above = r.Grid.GroupRowPosition == GroupRowPosition.AboveData;

            // create elements for each level
            for (int i = 0; i < level + 2; i++)
            {
                // create grid column for this level
                var cd = new ColumnDefinition();
                g.ColumnDefinitions.Add(cd);

                // add images to the grid
                if (i <= level)
                {
                    // select image to display in this part of the cell
                    var ni = i == level ? nodeImage : NodeImage.Connector;
                    if (ni == NodeImage.Connector)
                    {
                        var index = r.Index;
                        var rows = r.Grid.Rows;

                        // find next group row to determine the image to display
                        int next = index;
                        if (above)
                        {
                            // look for next group below this one
                            for (next = index + 1; next < rows.Count; next++)
                            {
                                if (rows[next].IsVisible)
                                {
                                    var gr = rows[next] as GroupRow;
                                    if (gr != null && gr.Level <= i)
                                    {
                                        ni = NodeImage.Terminal;
                                    }
                                    break;
                                }
                            }
                            if (next == rows.Count)
                            {
                                ni = NodeImage.Terminal;
                            }

                            // special treatment for bottom group rows
                            if (bgr != null && i == level)
                            {
                                ni = NodeImage.Terminal;
                            }
                        }
                        else
                        {
                            // look for next group above this one
                            for (next = index - 1; next > -1; next--)
                            {
                                if (rows[next].IsVisible)
                                {
                                    var gr = rows[next] as GroupRow;
                                    if (gr != null && gr.Level <= i)
                                    {
                                        ni = NodeImage.Terminal;
                                    }
                                    break;
                                }
                            }
                            if (next <= 0)
                            {
                                ni = NodeImage.Terminal;
                            }
                        }
                    }

                    // add image
                    if (ni != NodeImage.None)
                    {
                        cd.Width = new GridLength(indent);
                        var img = GetNodeImage(r, ni);
                        img.SetValue(Grid.ColumnProperty, i);
                        g.Children.Add(img);
                    }
                    else
                    {
                        cd.Width = new GridLength(1, GridUnitType.Auto);
                    }
                }
            }
            return g;
        }

        // gets the IFlatList being used as the grid's items source
        IFlatList GetFlatList(C1FlexGrid grid)
        {
            return grid.GroupRowPosition == GroupRowPosition.AboveData
                ? grid.ItemsSource as IFlatList
                : null;
        }

        // gets an image to represents a node
        Image GetNodeImage(Row r, NodeImage nodeImage)
        {
            // load bitmaps (once)
            if (_bmpCollapsed == null)
            {
                _bmpCollapsed = LoadBitmap("Collapsed.png");
                _bmpExpandedAbove = LoadBitmap("ExpandedAbove.png");
                _bmpExpandedBelow = LoadBitmap("ExpandedBelow.png");
                _bmpConnector = LoadBitmap("Connector.png");
                _bmpTerminalAbove = LoadBitmap("TerminalAbove.png");
                _bmpTerminalBelow = LoadBitmap("TerminalBelow.png");
                _bmpNoChildren = LoadBitmap("NoChildren.png");
            }

            // create image element
            var img = new Image();
            img.Stretch = Stretch.None;
            img.VerticalAlignment = VerticalAlignment.Center;
            img.HorizontalAlignment = HorizontalAlignment.Center;
            img.Tag = r;

            // assign source
            bool above = r.Grid.GroupRowPosition == GroupRowPosition.AboveData;
            switch (nodeImage)
            {
                case NodeImage.Collapsed:
                    img.Source = HasVisibleChildren(r) ? _bmpCollapsed : _bmpNoChildren;
                    img.MouseLeftButtonDown += img_MouseLeftButtonDown;
                    break;
                case NodeImage.Expanded:
                    img.Source = HasVisibleChildren(r)
                        ? above ? _bmpExpandedAbove : _bmpExpandedBelow
                        : _bmpNoChildren;
                    img.MouseLeftButtonDown += img_MouseLeftButtonDown;
                    break;
                case NodeImage.Connector:
                    img.Source = _bmpConnector;
                    break;
                case NodeImage.Terminal:
                    img.Source = above ? _bmpTerminalAbove : _bmpTerminalBelow;
                    break;
            }

            // done
            return img;
        }
        bool HasVisibleChildren(Row r)
        {
            var retVal = false;
            var gr = r as GroupRow;
            if (gr != null && r.Grid != null)
            {
                var rg = gr.GetCellRange();
                var rows = r.Grid.Rows;
                for (int i = rg.TopRow; i <= rg.BottomRow; i++)
                {
                    var row = rows[i];
                    if (row != gr && !(row is HeaderRow) && row.Visible)
                    {
                        retVal = true;
                        break;
                    }
                }
            }
            return retVal;
        }

        // expands/collapses group rows when user clicks the node icon
        void img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var img = sender as Image;
            var groupRow = img.Tag as GroupRow;
            groupRow.IsCollapsed = !groupRow.IsCollapsed;
        }

        // loads a bitmap from an embedded resource
        BitmapImage LoadBitmap(string bmpName)
        {
            var bmp = new BitmapImage();
            bmp.SetSource(GetResourceStream(bmpName));
            return bmp;
        }

        // gets an embedded resource stream
        Stream GetResourceStream(string resName)
        {
            var asm = Assembly.GetExecutingAssembly();
            foreach (var rn in asm.GetManifestResourceNames())
            {
                if (rn.EndsWith(resName))
                {
                    return asm.GetManifestResourceStream(rn);
                }
            }
            return null;
        }

        // creates Excel style column headers ("A", "B", ... "AB", etc)
        string GetAlphaColumnHeader(int i)
        {
            return GetAlphaColumnHeader(i, string.Empty);
        }
        string GetAlphaColumnHeader(int i, string s)
        {
            var rem = i % 26;
            s = (char)((int)'A' + rem) + s;
            i = i / 26 - 1;
            return i < 0 ? s : GetAlphaColumnHeader(i, s);
        }

        // creates a polygon based on a list of points
        static Polygon CreatePolygon(params double[] values)
        {
            var p = new Polygon();
            p.Points = new PointCollection();
            for (int i = 0; i < values.Length - 1; i += 2)
            {
                p.Points.Add(new Point(values[i], values[i + 1]));
            }
            return p;
        }
    }
}
