// 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.

using System.Drawing;

namespace System.Windows.Forms.Layout
{
    internal partial class LayoutUtils
    {
        ///  MeasureTextCache
        ///  3000 character strings take 9 seconds to load the form
        public sealed class MeasureTextCache
        {
            // the number of preferred sizes to store
            private const int MaxCacheSize = 6;

            // the next place in the ring buffer to store a preferred size
            private int _nextCacheEntry = -1;

            // MRU of size MaxCacheSize
            private PreferredSizeCache[]? _sizeCacheList;
            private Size _unconstrainedPreferredSize = s_invalidSize;

            ///  GetTextSize
            ///  Given constraints, format flags a font and text, determine the size of the string
            ///  employs an MRU of the last several constraints passed in via a ring-buffer of size MaxCacheSize.
            ///  Assumes Text and TextFormatFlags are the same, if either were to change, a call to
            ///  InvalidateCache should be made
            public Size GetTextSize(string? text, Font? font, Size proposedConstraints, TextFormatFlags flags)
            {
                if (!TextRequiresWordBreak(text, font, proposedConstraints, flags))
                {
                    // Text fits within proposed width

                    // IF we're here, this means we've got text that can fit into the proposedConstraints
                    // without wrapping.  We've determined this because our

                    // as a side effect of calling TextRequiresWordBreak,
                    // unconstrainedPreferredSize is set.
                    return _unconstrainedPreferredSize;
                }
                else
                {
                    // Text does NOT fit within proposed width - requires WordBreak

                    // IF we're here, this means that the wrapping width is smaller
                    // than our max width.  For example: we measure the text with infinite
                    // bounding box and we determine the width to fit all the characters
                    // to be 200 px wide.  We would come here only for proposed widths less
                    // than 200 px.

                    // Create our ring buffer if we dont have one
                    if (_sizeCacheList is null)
                    {
                        _sizeCacheList = new PreferredSizeCache[MaxCacheSize];
                    }

                    // check the existing constraints from previous calls
                    foreach (PreferredSizeCache sizeCache in _sizeCacheList)
                    {
                        if (sizeCache.ConstrainingSize == proposedConstraints)
                        {
                            return sizeCache.PreferredSize;
                        }
                        else if ((sizeCache.ConstrainingSize.Width == proposedConstraints.Width)
                                  && (sizeCache.PreferredSize.Height <= proposedConstraints.Height))
                        {
                            // Caching a common case where the width matches perfectly, and the stored preferred height
                            // is smaller or equal to the constraining size.
                            //        prefSize = GetPreferredSize(w,Int32.MaxValue);
                            //        prefSize = GetPreferredSize(w,prefSize.Height);

                            return sizeCache.PreferredSize;
                        }

                        //
                    }

                    // if we've gotten here, it means we dont have a cache entry, therefore
                    // we should add a new one in the next available slot.
                    Size prefSize = TextRenderer.MeasureText(text, font, proposedConstraints, flags);
                    _nextCacheEntry = (_nextCacheEntry + 1) % MaxCacheSize;
                    _sizeCacheList[_nextCacheEntry] = new PreferredSizeCache(proposedConstraints, prefSize);

                    return prefSize;
                }
            }

            ///  InvalidateCache
            ///  Clears out the cached values, should be called whenever Text, Font or a TextFormatFlag has changed
            public void InvalidateCache()
            {
                _unconstrainedPreferredSize = LayoutUtils.s_invalidSize;
                _sizeCacheList = null;
            }

            ///  TextRequiresWordBreak
            ///  If you give the text all the space in the world it wants, then there should be no reason
            ///  for it to break on a word.  So we find out what the unconstrained size is (Int32.MaxValue, Int32.MaxValue)
            ///  for a string - eg. 35, 13.  If the size passed in has a larger width than 35, then we know that
            ///  the WordBreak flag is not necessary.
            public bool TextRequiresWordBreak(string? text, Font? font, Size size, TextFormatFlags flags)
            {
                // if the unconstrained size of the string is larger than the proposed width
                // we need the word break flag, otherwise we dont, its a perf hit to use it.
                return GetUnconstrainedSize(text, font, flags).Width > size.Width;
            }

            ///  GetUnconstrainedSize
            ///  Gets the unconstrained (Int32.MaxValue, Int32.MaxValue) size for a piece of text
            private Size GetUnconstrainedSize(string? text, Font? font, TextFormatFlags flags)
            {
                if (_unconstrainedPreferredSize == s_invalidSize)
                {
                    // we also investigated setting the SingleLine flag, however this did not yield as much benefit as the word break
                    // and had possibility of causing internationalization issues.

                    flags &= ~TextFormatFlags.WordBreak; // rip out the wordbreak flag
                    _unconstrainedPreferredSize = TextRenderer.MeasureText(text, font, s_maxSize, flags);
                }

                return _unconstrainedPreferredSize;
            }
        }
    }
}
