// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System.Windows.Media;

namespace System.Windows.Controls.Primitives
{
    /// <summary>
    /// TabPanel is a Panel designed to handle the intricacies of laying out the tab buttons in a TabControl.  Specically, it handles:
    /// Serving as an ItemsHost for TabItems within a TabControl 
    /// Determining correct sizing and positioning for TabItems 
    /// Handling the logic associated with MultiRow scenarios, namely: 
    /// Calculating row breaks in a collection of TabItems 
    /// Laying out TabItems in multiple rows based on those breaks 
    /// Performing specific layout for a selected item to indicate selection, namely: 
    /// Bringing the selected tab to the front, or, in other words, making the selected tab appear to be in front of other tabs. 
    /// Increasing the size pre-layout size of a selected item (note that this is not a transform, but rather an increase in the size allotted to the element in which to perform layout). 
    /// Bringing the selected tab to the front 
    /// Exposing attached properties that allow TabItems to be styled based on their placement within the TabPanel. 
    /// </summary>
    public class TabPanel : Panel
    {
        #region Constructor

        /// <summary>
        /// The default constructor for the TabPanel
        /// </summary>
        public TabPanel()
        {
        }

        #endregion Constructor

        #region Measure/Arrange

        /// <summary>
        /// Updates DesiredSize of the TabPanel.  Called by parent UIElement.  This is the first pass of layout.
        /// </summary>
        /// <param name="availableSize">AvailableSize size is an "upper limit" that TabPanel should not exceed.</param>
        /// <returns>TabPanel' desired size.</returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            Size contentSize = new Size();
            Dock tabAlignment = TabAlignment;

            _numRows = 1;
            _rowHeight = 0;

            // For top and bottom placement the panel flow its children to calculate the number of rows and
            // desired vertical size
            if (tabAlignment == Dock.Top || tabAlignment == Dock.Bottom)
            {
                int numInCurrentRow = 0;
                double currentRowWidth = 0;
                double maxRowWidth = 0;
                foreach (UIElement child in Children)
                {
                    // Helper measures child, and deals with Min, Max, and base Width & Height properties.
                    // Helper returns the size a child needs to take up (DesiredSize or property specified size).
                    child.Measure(availableSize);

                    if (child.Visibility == Visibility.Collapsed)
                    {
                        continue;
                    }

                    Size childSize = GetDesiredSizeWithoutMargin(child);

                    if (_rowHeight < childSize.Height)
                    {
                        _rowHeight = childSize.Height;
                    }
                    if (currentRowWidth + childSize.Width > availableSize.Width && numInCurrentRow > 0)
                    { // If child does not fit in the current row - create a new row
                        if (maxRowWidth < currentRowWidth)
                        {
                            maxRowWidth = currentRowWidth;
                        }
                        currentRowWidth = childSize.Width;
                        numInCurrentRow = 1;
                        _numRows++;
                    }
                    else
                    {
                        currentRowWidth += childSize.Width;
                        numInCurrentRow++;
                    }
                }

                if (maxRowWidth < currentRowWidth)
                {
                    maxRowWidth = currentRowWidth;
                }
                contentSize.Height = _rowHeight * _numRows;

                // If we don't have constraint or content width is smaller than constraint width then size to content
                if (double.IsInfinity(contentSize.Width) || double.IsNaN(contentSize.Width) || maxRowWidth < availableSize.Width)
                {
                    contentSize.Width = maxRowWidth;
                }
                else
                {
                    contentSize.Width = availableSize.Width;
                }
            }
            else if (tabAlignment == Dock.Left || tabAlignment == Dock.Right)
            {
                foreach (UIElement child in Children)
                {
                    if (child.Visibility == Visibility.Collapsed)
                    {
                        continue;
                    }

                    // Helper measures child, and deals with Min, Max, and base Width & Height properties.
                    // Helper returns the size a child needs to take up (DesiredSize or property specified size).
                    child.Measure(availableSize);

                    Size childSize = GetDesiredSizeWithoutMargin(child);

                    if (contentSize.Width < childSize.Width)
                    {
                        contentSize.Width = childSize.Width;
                    }

                    contentSize.Height += childSize.Height;
                }
            }

            // Returns our minimum size & sets DesiredSize.
            return contentSize;
        }

        /// <summary>
        /// TabPanel arranges each of its children.
        /// </summary>
        /// <param name="finalSize">Size that TabPanel will assume to position children.</param>
        protected override Size ArrangeOverride(Size finalSize)
        {
            Dock tabAlignment = TabAlignment;
            if (tabAlignment == Dock.Top || tabAlignment == Dock.Bottom)
            {
                ArrangeHorizontal(finalSize);
            }
            else if (tabAlignment == Dock.Left || tabAlignment == Dock.Right)
            {
                ArrangeVertical(finalSize);
            }
            return finalSize;
        }

        private void ArrangeHorizontal(Size arrangeSize)
        {
            Dock tabAlignment = TabAlignment;
            bool isMultiRow = _numRows > 1;
            int activeRow = 0;
            int[] solution = new int[0];
            Size childOffset = new Size(0,0);
            double[] headerSize = GetHeadersSize();

            // If we have multirows, then calculate the best header distribution
            if (isMultiRow)
            {
                solution = CalculateHeaderDistribution(arrangeSize.Width, headerSize);
                activeRow = GetActiveRow(solution);

                // TabPanel starts to layout children depend on activeRow which should be always on bottom (top)
                // The first row should start from Y = (_numRows - 1 - activeRow) * _rowHeight
                if (tabAlignment == Dock.Top)
                {
                    childOffset.Height = (_numRows - 1 - activeRow) * _rowHeight;
                }
                if (tabAlignment == Dock.Bottom && activeRow != 0)
                {
                    childOffset.Height = (_numRows - activeRow) * _rowHeight;
                }
            }

            int childIndex = 0;
            int separatorIndex = 0;
            foreach (UIElement child in Children)
            {
                Thickness margin = (Thickness)child.GetValue(MarginProperty);
                double leftOffset = margin.Left;
                double rightOffset = margin.Right;
                double topOffset = margin.Top;
                double bottomOffset = margin.Bottom;

                bool lastHeaderInRow = isMultiRow && (separatorIndex < solution.Length && solution[separatorIndex] == childIndex || childIndex == Children.Count - 1);

                // Length left, top, right, bottom;
                Size cellSize = new Size(headerSize[childIndex], _rowHeight);

                // Align the last header in the row; If headers are not aligned directional navigation would not work correctly
                if (lastHeaderInRow)
                {
                    cellSize.Width = arrangeSize.Width - childOffset.Width;
                }

                // Set ZIndex
                TabItem tabItem = child as TabItem;
                if (tabItem != null)
                {
                    if (tabItem.IsSelected)
                    {
                        tabItem.SetValue(Canvas.ZIndexProperty, 1);
                    }
                    else
                    {
                        tabItem.SetValue(Canvas.ZIndexProperty, 0);
                    }
                }

                child.Arrange(new Rect(childOffset.Width, childOffset.Height, cellSize.Width, cellSize.Height));

                Size childSize = cellSize;
                childSize.Height = Math.Max(0d, childSize.Height - topOffset - bottomOffset);
                childSize.Width = Math.Max(0d, childSize.Width - leftOffset - rightOffset);

                // Calculate the offset for the next child
                childOffset.Width += cellSize.Width;
                if (lastHeaderInRow)
                {
                    if ((separatorIndex == activeRow && tabAlignment == Dock.Top) ||
                        (separatorIndex == activeRow - 1 && tabAlignment == Dock.Bottom))
                    {
                        childOffset.Height = 0d;
                    }
                    else
                    {
                        childOffset.Height += _rowHeight;
                    }

                    childOffset.Width = 0d;
                    separatorIndex++;
                }

                childIndex++;
            }
        }

        private void ArrangeVertical(Size arrangeSize)
        {
            double childOffsetY = 0d;
            foreach (UIElement child in Children)
            {
                if (child.Visibility != Visibility.Collapsed)
                {
                    // Set ZIndex
                    TabItem tabItem = child as TabItem;
                    if (tabItem != null)
                    {
                        if (tabItem.IsSelected)
                        {
                            tabItem.SetValue(Canvas.ZIndexProperty, 1);
                        }
                        else
                        {
                            tabItem.SetValue(Canvas.ZIndexProperty, 0);
                        }
                    }

                    Size childSize = GetDesiredSizeWithoutMargin(child);
                    child.Arrange(new Rect(0, childOffsetY, arrangeSize.Width, childSize.Height));

                    // Calculate the offset for the next child
                    childOffsetY += childSize.Height;
                }
            }
        }

        #endregion Measure/Arrange

        #region HelperFunctions

        private Dock TabAlignment
        {
            get { return TabControlParent != null ? TabControlParent.TabStripPlacement : Dock.Top; }
        }

        internal static Size GetDesiredSizeWithoutMargin(UIElement element)
        {
            // We have a hard coded margin value on each side for the
            // selected item. To account for this additional size,
            // we add this size to the calculations taking place.
            double selectedMarginSize = 0;
            TabItem tabItem = element as TabItem;
            if (tabItem != null && tabItem.IsSelected)
            {
                Panel panel = tabItem.GetTemplate(tabItem.IsSelected, tabItem.TabStripPlacement) as Panel;
                FrameworkElement fe = (panel != null && panel.Children.Count > 0) ? panel.Children[0] as FrameworkElement : null;
                if (fe != null)
                {
                    selectedMarginSize += (Math.Abs(fe.Margin.Left + fe.Margin.Right));
                }
            }

            Thickness margin = (Thickness)element.GetValue(MarginProperty);
            Size desiredSizeWithoutMargin = new Size();
            desiredSizeWithoutMargin.Height = Math.Max(0d, element.DesiredSize.Height - margin.Top - margin.Bottom);
            desiredSizeWithoutMargin.Width = Math.Max(0d, element.DesiredSize.Width - margin.Left - margin.Right + selectedMarginSize);
            return desiredSizeWithoutMargin;
        }

        // Returns the row which contain the child with IsSelected==true
        private int GetActiveRow(int[] solution)
        {
            int activeRow = 0;
            int childIndex = 0;
            if (solution.Length > 0)
            {
                foreach (UIElement child in Children)
                {
                    bool isActiveTab = (bool)child.GetValue(TabItem.IsSelectedProperty);

                    if (isActiveTab)
                    {
                        return activeRow;
                    }

                    if (activeRow < solution.Length && solution[activeRow] == childIndex)
                    {
                        activeRow++;
                    }

                    childIndex++;
                }
            }

            // If the is no selected element and aligment is Top  - then the active row is the last row 
            if (TabAlignment == Dock.Top)
            {
                activeRow = _numRows - 1;
            }

            return activeRow;
        }

        /*   TabPanel layout calculation:
         
        After measure call we have:
        rowWidthLimit - width of the TabPanel
        Header[0..n-1]  - headers
        headerWidth[0..n-1] - header width
         
        Calculated values:
        numSeparators                       - number of separators between numSeparators+1 rows
        rowWidth[0..numSeparators]           - row width
        rowHeaderCount[0..numSeparators]    - Row Count = number of headers on that row
        rowAverageGap[0..numSeparators]     - Average Gap for the row i = (rowWidth - rowWidth[i])/rowHeaderCount[i]
        currentSolution[0..numSeparators-1] - separator currentSolution[i]=x means Header[x] and h[x+1] are separated with new line
        bestSolution[0..numSeparators-1]    - keep the last Best Solution
        bestSolutionRowAverageGap           - keep the last Best Solution Average Gap

        Between all separators distribution the best solution have minimum Average Gap - 
        this is the amount of pixels added to the header (to justify) in the row

        How does it work:
        First we flow the headers to calculate the number of necessary rows (numSeparators+1).
        That means we need to insert numSeparators separators between n headers (numSeparators<n always).
        For each current state rowAverageGap[1..numSeparators+1] are calculated for each row.
        Current state rowAverageGap = MAX (rowAverageGap[1..numSeparators+1]).
        Our goal is to find the solution with MIN (rowAverageGap).
        On each iteration step we move a header from a previous row to the row with maximum rowAverageGap.
        We countinue the itterations only if we move to better solution, i.e. rowAverageGap is smaller.
        Maximum iteration steps are less the number of headers.

        */
        // Input: Row width and width of all headers
        // Output: int array which size is the number of separators and contains each separator position
        private int[] CalculateHeaderDistribution(double rowWidthLimit, double[] headerWidth)
        {
            double bestSolutionMaxRowAverageGap = 0;
            int numHeaders = headerWidth.Length;

            int numSeparators = _numRows - 1;
            double currentRowWidth = 0;
            int numberOfHeadersInCurrentRow = 0;
            double currentAverageGap = 0;
            int[] currentSolution = new int[numSeparators];
            int[] bestSolution = new int[numSeparators];
            int[] rowHeaderCount = new int[_numRows];
            double[] rowWidth = new double[_numRows];
            double[] rowAverageGap = new double[_numRows];
            double[] bestSolutionRowAverageGap = new double[_numRows];

            // Initialize the current state; Do the initial flow of the headers
            int currentRowIndex = 0;

            for (int index = 0; index < numHeaders; index++)
            {
                if (currentRowWidth + headerWidth[index] > rowWidthLimit && numberOfHeadersInCurrentRow > 0)
                { // if we cannot add next header - flow to next row
                    // Store current row before we go to the next
                    rowWidth[currentRowIndex] = currentRowWidth; // Store the current row width
                    rowHeaderCount[currentRowIndex] = numberOfHeadersInCurrentRow; // For each row we store the number os headers inside
                    currentAverageGap = Math.Max(0d, (rowWidthLimit - currentRowWidth) / numberOfHeadersInCurrentRow); // The amout of width that should be added to justify the header
                    rowAverageGap[currentRowIndex] = currentAverageGap;
                    currentSolution[currentRowIndex] = index - 1; // Separator points to the last header in the row
                    if (bestSolutionMaxRowAverageGap < currentAverageGap) // Remember the maximum of all currentAverageGap
                    {
                        bestSolutionMaxRowAverageGap = currentAverageGap;
                    }

                    // Iterate to next row
                    currentRowIndex++;
                    currentRowWidth = headerWidth[index]; // Accumulate header widths on the same row
                    numberOfHeadersInCurrentRow = 1;
                }
                else
                {
                    currentRowWidth += headerWidth[index]; // Accumulate header widths on the same row
                    // Increase the number of headers only if they are not collapsed (width=0)
                    if (headerWidth[index] != 0)
                    {
                        numberOfHeadersInCurrentRow++;
                    }
                }
            }

            // If everything fits in 1 row then exit (no separators needed)
            if (currentRowIndex == 0)
            {
                return new int[0];
            }

            // Add the last row
            rowWidth[currentRowIndex] = currentRowWidth;
            rowHeaderCount[currentRowIndex] = numberOfHeadersInCurrentRow;
            currentAverageGap = (rowWidthLimit - currentRowWidth) / numberOfHeadersInCurrentRow;
            rowAverageGap[currentRowIndex] = currentAverageGap;
            if (bestSolutionMaxRowAverageGap < currentAverageGap)
            {
                bestSolutionMaxRowAverageGap = currentAverageGap;
            }

            currentSolution.CopyTo(bestSolution, 0); // Remember the first solution as initial bestSolution
            rowAverageGap.CopyTo(bestSolutionRowAverageGap, 0); // bestSolutionRowAverageGap is used in ArrangeOverride to calculate header sizes

            // Search for the best solution
            // The exit condition if when we cannot move header to the next row 
            while (true)
            {
                // Find the row with maximum AverageGap
                int worstRowIndex = 0; // Keep the row index with maximum AverageGap
                double maxAG = 0;

                for (int i = 0; i < _numRows; i++) // for all rows
                {
                    if (maxAG < rowAverageGap[i])
                    {
                        maxAG = rowAverageGap[i];
                        worstRowIndex = i;
                    }
                }

                // If we are on the first row - cannot move from previous
                if (worstRowIndex == 0)
                {
                    break;
                }

                // From the row with maximum AverageGap we try to move a header from previous row
                int moveToRow = worstRowIndex;
                int moveFromRow = moveToRow - 1;
                int moveHeader = currentSolution[moveFromRow];
                double movedHeaderWidth = headerWidth[moveHeader];

                rowWidth[moveToRow] += movedHeaderWidth;

                // If the moved header cannot fit - exit. We have the best solution already.
                if (rowWidth[moveToRow] > rowWidthLimit)
                {
                    break;
                }

                // If header is moved successfully to the worst row
                // we update the arrays keeping the row state
                currentSolution[moveFromRow]--;
                rowHeaderCount[moveToRow]++;
                rowWidth[moveFromRow] -= movedHeaderWidth;
                rowHeaderCount[moveFromRow]--;
                rowAverageGap[moveFromRow] = (rowWidthLimit - rowWidth[moveFromRow]) / rowHeaderCount[moveFromRow];
                rowAverageGap[moveToRow] = (rowWidthLimit - rowWidth[moveToRow]) / rowHeaderCount[moveToRow];

                // EvaluateSolution:
                // If the current solution is better than bestSolution - keep it in bestSolution
                maxAG = 0;
                for (int i = 0; i < _numRows; i++) // for all rows
                {
                    if (maxAG < rowAverageGap[i])
                    {
                        maxAG = rowAverageGap[i];
                    }
                }

                if (maxAG < bestSolutionMaxRowAverageGap)
                {
                    bestSolutionMaxRowAverageGap = maxAG;
                    currentSolution.CopyTo(bestSolution, 0);
                    rowAverageGap.CopyTo(bestSolutionRowAverageGap, 0);
                }
            }

            // Each header size should be increased so headers in the row stretch to fit the row
            currentRowIndex = 0;
            for (int index = 0; index < numHeaders; index++)
            {
                headerWidth[index] += bestSolutionRowAverageGap[currentRowIndex];
                if (currentRowIndex < numSeparators && bestSolution[currentRowIndex] == index)
                {
                    currentRowIndex++;
                }
            }
            // Use the best solution bestSolution[0..numSeparators-1] to layout
            return bestSolution;
        }

        internal double[] GetHeadersSize()
        {
            double[] headerSize = new double[Children.Count];
            int childIndex = 0;
            foreach (UIElement child in Children)
            {
                Size childSize = GetDesiredSizeWithoutMargin(child);
                headerSize[childIndex] = child.Visibility == Visibility.Collapsed ? 0 : childSize.Width;
                childIndex++;
            }
            return headerSize;
        }

        #endregion HelperFunctions

        #region TabControlParent

        /// <summary>
        /// This will step up the UI tree to find the TabControl
        /// that contains this TabPanel.
        /// </summary>
        internal TabControl TabControlParent
        {
            get
            {
                FrameworkElement fe = this as FrameworkElement;
                while (fe != null)
                {
                    TabControl tc = fe as TabControl;
                    if (tc != null)
                    {
                        return tc;
                    }
                    fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
                }
                return null;
            }
        }

        #endregion TabControlParent
        
        #region Member Variables

        /// <summary>
        /// Number of row calculated in measure and used in arrange
        /// </summary>
        internal int _numRows = 1;

        /// <summary>
        /// Maximum of all headers height
        /// </summary>
        internal double _rowHeight;

        #endregion Member Variables
    }
}
