// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;

namespace System.Windows.Forms.Layout
{
    internal partial class FlowLayout : LayoutEngine
    {
        internal static readonly FlowLayout Instance = new FlowLayout();

        private static readonly int s_wrapContentsProperty = PropertyStore.CreateKey();
        private static readonly int s_flowDirectionProperty = PropertyStore.CreateKey();

        private protected override bool LayoutCore(IArrangedElement container, LayoutEventArgs args)
        {
#if DEBUG
            if (CompModSwitches.FlowLayout.TraceInfo)
            {
                Debug.WriteLine("FlowLayout::Layout("
                    + "container=" + container.ToString() + ", "
                    + "displayRect=" + container.DisplayRectangle.ToString() + ", "
                    + "args=" + args.ToString() + ")");
            }

            Debug.Indent();
#endif

            // ScrollableControl will first try to get the layoutbounds from the derived control when
            // trying to figure out if ScrollBars should be added.
            CommonProperties.SetLayoutBounds(container, TryCalculatePreferredSize(container, container.DisplayRectangle, /* measureOnly = */ false));
#if DEBUG
            Debug.Unindent();
#endif
            return CommonProperties.GetAutoSize(container);
        }

        internal override Size GetPreferredSize(IArrangedElement container, Size proposedConstraints)
        {
#if DEBUG
            if (CompModSwitches.FlowLayout.TraceInfo)
            {
                Debug.WriteLine("FlowLayout::GetPreferredSize("
                    + "container=" + container.ToString() + ", "
                    + "proposedConstraints=" + proposedConstraints.ToString() + ")");
                Debug.Indent();
            }
#endif
            Rectangle measureBounds = new Rectangle(new Point(0, 0), proposedConstraints);
            Size prefSize = TryCalculatePreferredSize(container, measureBounds, /* measureOnly = */ true);

            if (prefSize.Width > proposedConstraints.Width || prefSize.Height > proposedConstraints.Height)
            {
                // Controls measured earlier than a control which couldn't be fit to constraints may
                // shift around with the new bounds. We need to make a 2nd pass through the
                // controls using these bounds which are gauranteed to fit.
                measureBounds.Size = prefSize;
                prefSize = TryCalculatePreferredSize(container, measureBounds, /* measureOnly = */ true);
            }

#if DEBUG
            if (CompModSwitches.FlowLayout.TraceInfo)
            {
                Debug.Unindent();
                Debug.WriteLine("GetPreferredSize returned " + prefSize);
            }
#endif
            return prefSize;
        }

        private static ContainerProxy CreateContainerProxy(IArrangedElement container, FlowDirection flowDirection)
        {
            switch (flowDirection)
            {
                case FlowDirection.RightToLeft:
                    return new RightToLeftProxy(container);
                case FlowDirection.TopDown:
                    return new TopDownProxy(container);
                case FlowDirection.BottomUp:
                    return new BottomUpProxy(container);
                case FlowDirection.LeftToRight:
                default:
                    return new ContainerProxy(container);
            }
        }

        /// <summary>
        ///  Both LayoutCore and GetPreferredSize forward to this method.
        ///  The measureOnly flag determines which behavior we get.
        /// </summary>
        private Size TryCalculatePreferredSize(IArrangedElement container, Rectangle displayRect, bool measureOnly)
        {
            FlowDirection flowDirection = GetFlowDirection(container);
            bool wrapContents = GetWrapContents(container);

            ContainerProxy containerProxy = CreateContainerProxy(container, flowDirection);
            containerProxy.DisplayRect = displayRect;

            // refetch as it's now adjusted for Vertical.
            displayRect = containerProxy.DisplayRect;

            ElementProxy elementProxy = containerProxy.ElementProxy;
            Size layoutSize = Size.Empty;

            if (!wrapContents)
            {
                // pretend that the container is infinitely wide to prevent wrapping.
                // DisplayRectangle.Right is Width + X - subtract X to prevent overflow.
                displayRect.Width = int.MaxValue - displayRect.X;
            }

            for (int i = 0; i < container.Children.Count;)
            {
                Size rowSize = Size.Empty;

                Rectangle measureBounds = new Rectangle(displayRect.X, displayRect.Y, displayRect.Width, displayRect.Height - layoutSize.Height);
                rowSize = MeasureRow(containerProxy, elementProxy, i, measureBounds, out int breakIndex);

                // if we are not wrapping contents, then the breakIndex (as set in MeasureRow)
                // should be equal to the count of child items in the container.
                Debug.Assert(wrapContents == true || breakIndex == container.Children.Count,
                    "We should not be trying to break the row if we are not wrapping contents.");

                if (!measureOnly)
                {
                    Rectangle rowBounds = new Rectangle(displayRect.X,
                        layoutSize.Height + displayRect.Y,
                        rowSize.Width,
                        rowSize.Height);
                    LayoutRow(containerProxy, elementProxy, startIndex: i, endIndex: breakIndex, rowBounds: rowBounds);
                }

                layoutSize.Width = Math.Max(layoutSize.Width, rowSize.Width);
                layoutSize.Height += rowSize.Height;
                i = breakIndex;
            }

            // Verify that our alignment is correct
            if (container.Children.Count != 0 && !measureOnly)
            {
                Debug_VerifyAlignment(container, flowDirection);
            }

            return LayoutUtils.FlipSizeIf(flowDirection == FlowDirection.TopDown || GetFlowDirection(container) == FlowDirection.BottomUp, layoutSize);
        }

        /// <summary>
        ///  Just forwards to TryCalculatePreferredSizeRow. This will layout elements from the start index to the end
        ///  index. RowBounds was computed by a call to measure row and is used for alignment/boxstretch.
        ///  See the ElementProxy class for an explaination of the elementProxy parameter.
        /// </summary>
        private void LayoutRow(ContainerProxy containerProxy, ElementProxy elementProxy, int startIndex, int endIndex, Rectangle rowBounds)
        {
            Size outSize = TryCalculatePreferredSizeRow(containerProxy, elementProxy, startIndex, endIndex, rowBounds, /* breakIndex = */ out int dummy, /* measureOnly = */ false);
            Debug.Assert(dummy == endIndex, "EndIndex / BreakIndex mismatch.");
        }

        /// <summary>
        ///  Just forwards to TryCalculatePreferredSizeRow. breakIndex is the index of the first control not to
        ///  fit in the displayRectangle. The returned Size is the size required to layout the
        ///  controls from startIndex up to but not including breakIndex. See the ElementProxy
        ///  class for an explaination of the elementProxy parameter.
        /// </summary>
        private Size MeasureRow(ContainerProxy containerProxy, ElementProxy elementProxy, int startIndex, Rectangle displayRectangle, out int breakIndex)
        {
            return TryCalculatePreferredSizeRow(containerProxy, elementProxy, startIndex, endIndex: containerProxy.Container.Children.Count, rowBounds: displayRectangle, breakIndex: out breakIndex, measureOnly: true);
        }

        /// <summary>
        ///  LayoutRow and MeasureRow both forward to this method. The measureOnly flag
        ///  determines which behavior we get.
        /// </summary>
        private Size TryCalculatePreferredSizeRow(ContainerProxy containerProxy, ElementProxy elementProxy, int startIndex, int endIndex, Rectangle rowBounds, out int breakIndex, bool measureOnly)
        {
            Debug.Assert(startIndex < endIndex, "Loop should be in forward Z-order.");
            Point location = rowBounds.Location;
            Size rowSize = Size.Empty;
            int laidOutItems = 0;
            breakIndex = startIndex;

            bool wrapContents = GetWrapContents(containerProxy.Container);
            bool breakOnNextItem = false;

            ArrangedElementCollection collection = containerProxy.Container.Children;
            for (int i = startIndex; i < endIndex; i++, breakIndex++)
            {
                elementProxy.Element = collection[i];
                if (!elementProxy.ParticipatesInLayout)
                {
                    continue;
                }

                // Figure out how much space this element is going to need (requiredSize)
                Size prefSize;
                if (elementProxy.AutoSize)
                {
                    Size elementConstraints = new Size(int.MaxValue, rowBounds.Height - elementProxy.Margin.Size.Height);
                    if (i == startIndex)
                    {
                        // If the element is the first in the row, attempt to pack it to the row width. (If its not 1st, it will wrap
                        // to the first on the next row if its too long and then be packed if needed by the next call to TryCalculatePreferredSizeRow).
                        elementConstraints.Width = rowBounds.Width - rowSize.Width - elementProxy.Margin.Size.Width;
                    }

                    // Make sure that subtracting the margins does not cause width/height to be <= 0, or we will
                    // size as if we had infinite space when in fact we are trying to be as small as possible.
                    elementConstraints = LayoutUtils.UnionSizes(new Size(1, 1), elementConstraints);
                    prefSize = elementProxy.GetPreferredSize(elementConstraints);
                }
                else
                {
                    // If autosizing is turned off, we just use the element's current size as its preferred size.
                    prefSize = elementProxy.SpecifiedSize;

                    // except if it is stretching - then ignore the affect of the height dimension.
                    if (elementProxy.Stretches)
                    {
                        prefSize.Height = 0;
                    }

                    // Enforce MinimumSize
                    if (prefSize.Height < elementProxy.MinimumSize.Height)
                    {
                        prefSize.Height = elementProxy.MinimumSize.Height;
                    }
                }

                Size requiredSize = prefSize + elementProxy.Margin.Size;

                // Position the element (if applicable).
                if (!measureOnly)
                {
                    // If measureOnly = false, rowBounds.Height = measured row hieght
                    // (otherwise its the remaining displayRect of the container)

                    Rectangle cellBounds = new Rectangle(location, new Size(requiredSize.Width, rowBounds.Height));

                    // We laid out the rows with the elementProxy's margins included.
                    // We now deflate the rect to get the actual elementProxy bounds.
                    cellBounds = LayoutUtils.DeflateRect(cellBounds, elementProxy.Margin);

                    AnchorStyles anchorStyles = elementProxy.AnchorStyles;
                    containerProxy.Bounds = LayoutUtils.AlignAndStretch(prefSize, cellBounds, anchorStyles);
                }

                // Keep track of how much space is being used in this row
                location.X += requiredSize.Width;
                if (laidOutItems > 0)
                {
                    // If control does not fit on this row, exclude it from row and stop now.
                    //   Exception: If row is empty, allow this control to fit on it. So controls
                    //   that exceed the maximum row width will end up occupying their own rows.
                    if (location.X > rowBounds.Right)
                    {
                        break;
                    }
                }

                // Control fits on this row, so update the row size.
                //   rowSize.Width != location.X because with a scrollable control
                //   we could have started with a location like -100.
                rowSize.Width = location.X - rowBounds.X;
                rowSize.Height = Math.Max(rowSize.Height, requiredSize.Height);

                // check for line breaks.
                if (wrapContents)
                {
                    if (breakOnNextItem)
                    {
                        break;
                    }
                    else if (i + 1 < endIndex && CommonProperties.GetFlowBreak(elementProxy.Element))
                    {
                        if (laidOutItems == 0)
                        {
                            breakOnNextItem = true;
                        }
                        else
                        {
                            breakIndex++;
                            break;
                        }
                    }
                }

                laidOutItems++;
            }

            return rowSize;
        }

        public static bool GetWrapContents(IArrangedElement container)
        {
            return container.Properties.GetInteger(s_wrapContentsProperty) == 0;
        }

        public static void SetWrapContents(IArrangedElement container, bool value)
        {
            container.Properties.SetInteger(s_wrapContentsProperty, value ? 0 : 1);
            LayoutTransaction.DoLayout(container, container, PropertyNames.WrapContents);
            Debug.Assert(GetWrapContents(container) == value, "GetWrapContents should return the same value as we set");
        }

        public static FlowDirection GetFlowDirection(IArrangedElement container)
        {
            return (FlowDirection)container.Properties.GetInteger(s_flowDirectionProperty);
        }

        public static void SetFlowDirection(IArrangedElement container, FlowDirection value)
        {
            SourceGenerated.EnumValidator.Validate(value);

            container.Properties.SetInteger(s_flowDirectionProperty, (int)value);
            LayoutTransaction.DoLayout(container, container, PropertyNames.FlowDirection);
            Debug.Assert(GetFlowDirection(container) == value, "GetFlowDirection should return the same value as we set");
        }

        [Conditional("DEBUG_VERIFY_ALIGNMENT")]
        private void Debug_VerifyAlignment(IArrangedElement container, FlowDirection flowDirection)
        {
#if DEBUG
            // We cannot apply any of these checks @ design-time since dragging new children into a FlowLayoutPanel
            // will attempt to set the children at the mouse position when the child was dropped - we rely on the controil
            // to reposition the children once added.
            if (container is Control flp && flp.Site != null && flp.Site.DesignMode)
            {
                return;
            }

            // Check to see if the first element is in its right place
            Padding margin = CommonProperties.GetMargin(container.Children[0]);
            switch (flowDirection)
            {
                case FlowDirection.LeftToRight:
                case FlowDirection.TopDown:
                    Debug.Assert(container.Children[0].Bounds.Y == margin.Top + container.DisplayRectangle.Y);
                    Debug.Assert(container.Children[0].Bounds.X == margin.Left + container.DisplayRectangle.X);
                    break;
                case FlowDirection.RightToLeft:
                    Debug.Assert(container.Children[0].Bounds.X == container.DisplayRectangle.X + container.DisplayRectangle.Width - container.Children[0].Bounds.Width - margin.Right);
                    Debug.Assert(container.Children[0].Bounds.Y == margin.Top + container.DisplayRectangle.Y);
                    break;
                case FlowDirection.BottomUp:
                    Debug.Assert(container.Children[0].Bounds.Y == container.DisplayRectangle.Y + container.DisplayRectangle.Height - container.Children[0].Bounds.Height - margin.Bottom);
                    Debug.Assert(container.Children[0].Bounds.X == margin.Left + container.DisplayRectangle.X);
                    break;
            }

            // Next check to see if everything is in bound
            ArrangedElementCollection collection = container.Children;
            for (int i = 1; i < collection.Count; i++)
            {
                switch (flowDirection)
                {
                    case FlowDirection.LeftToRight:
                    case FlowDirection.TopDown:
                        Debug.Assert(collection[i].Bounds.Y >= container.DisplayRectangle.Y);
                        Debug.Assert(collection[i].Bounds.X >= container.DisplayRectangle.X);
                        break;
                    case FlowDirection.RightToLeft:
                        Debug.Assert(collection[i].Bounds.Y >= container.DisplayRectangle.Y);
                        Debug.Assert(collection[i].Bounds.X + collection[i].Bounds.Width <= container.DisplayRectangle.X + container.DisplayRectangle.Width);
                        break;
                    case FlowDirection.BottomUp:
                        Debug.Assert(collection[i].Bounds.Y + collection[i].Bounds.Height <= container.DisplayRectangle.Y + container.DisplayRectangle.Height);
                        Debug.Assert(collection[i].Bounds.X >= container.DisplayRectangle.X);
                        break;
                }
            }
#endif
        }
    }
}
