// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Drawing;

namespace System.Windows.Forms.Layout;

internal static 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 don't have one
                _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 don't 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 = 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 don't, 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;
        }
    }
}
