// 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;
using System.Numerics;

namespace System.Windows.Forms;

/// <summary>
///  Collection of screen device contexts.
/// </summary>
/// <remarks>
///  <para>
///   This caching counts on consumers not leaving the HDC in a dirty state. There is a significant overhead to
///   saving and restoring the state which would make this cache much less impactful. If we can't have confidence
///   the DC state is being restored the best option may be to simply create a brand new screen DC every time we
///   need one.
///  </para>
///  <para>
///   Creating a screen DC from scratch and deleting it takes about 11us. Saving and restoring takes about half
///   that time. Renting an existing DC from the cache and returning it is on the order of 20_ns_. If we were
///   forced to save state we'd be taking about 9us renting and returning cached DCs- which would take around
///   20 rentals to break even with simply creating and tossing away a brand new DC.
///  </para>
/// </remarks>
internal sealed partial class ScreenDcCache : IDisposable
{
    private readonly IntPtr[] _itemsCache;

    /// <summary>
    ///  Create a cache with space for the specified number of HDCs.
    /// </summary>
    public ScreenDcCache(int cacheSpace = 5)
    {
        Debug.Assert(cacheSpace >= 0);

        _itemsCache = new IntPtr[cacheSpace];
    }

    /// <summary>
    ///  Get a DC from the cache or create one if none are available.
    /// </summary>
    public ScreenDcScope Acquire()
    {
        IntPtr item;

        for (int i = 0; i < _itemsCache.Length; i++)
        {
            item = Interlocked.Exchange(ref _itemsCache[i], IntPtr.Zero);
            if (item != IntPtr.Zero)
            {
                return new ScreenDcScope(this, (HDC)item);
            }
        }

        // Didn't find anything in the cache, create a new HDC
        return new ScreenDcScope(this, PInvokeCore.CreateCompatibleDC(default));
    }

    /// <summary>
    ///  Release an item back to the cache, disposing if no room is available.
    /// </summary>
    private void Release(HDC hdc)
    {
        ArgumentValidation.ThrowIfNull(hdc);
        ValidateHdc(hdc);

        IntPtr temp = (IntPtr)hdc;

        for (int i = 0; i < _itemsCache.Length; i++)
        {
            // Flip with the array until we get back an empty slot
            temp = Interlocked.Exchange(ref _itemsCache[i], temp);
            if (temp == IntPtr.Zero)
            {
                return;
            }
        }

        // Too many to store, delete the last item we swapped.
        PInvokeCore.DeleteDC((HDC)temp);
    }

    ~ScreenDcCache() => Dispose();

    public void Dispose()
    {
        for (int i = 0; i < _itemsCache.Length; i++)
        {
            IntPtr hdc = _itemsCache[i];
            if (hdc != IntPtr.Zero)
            {
                PInvokeCore.DeleteDC((HDC)hdc);
            }
        }
    }

    [Conditional("DEBUG")]
    private static unsafe void ValidateHdc(HDC hdc)
    {
        // A few sanity checks against the HDC to see if it was left in a dirty state

        HRGN hrgn = PInvoke.CreateRectRgn(0, 0, 0, 0);
        Debug.Assert(PInvoke.GetClipRgn(hdc, hrgn) == 0, "Should not have a clipping region");
        PInvokeCore.DeleteObject(hrgn);

        Point point;
        PInvokeCore.GetViewportOrgEx(hdc, &point);
        Debug.Assert(point.IsEmpty, "Viewport origin shouldn't be shifted");
        Debug.Assert(PInvoke.GetMapMode(hdc) == HDC_MAP_MODE.MM_TEXT);
        Debug.Assert(PInvoke.GetROP2(hdc) == R2_MODE.R2_COPYPEN);
        Debug.Assert(PInvoke.GetBkMode(hdc) == BACKGROUND_MODE.OPAQUE);

        Matrix3x2 matrix = default;
        Debug.Assert(PInvoke.GetWorldTransform(hdc, (XFORM*)(void*)&matrix));
        Debug.Assert(matrix.IsIdentity);
    }
}
