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

using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;

namespace System.Drawing;

/// <summary>
///  The BufferedGraphicsContext class can be used to perform standard double buffer rendering techniques.
/// </summary>
public sealed unsafe class BufferedGraphicsContext : IDisposable
{
    private Size _maximumBuffer;
    private Size _bufferSize = Size.Empty;
    private Size _virtualSize;
    private Point _targetLoc;
    private HDC _compatDC;
    private HBITMAP _dib;
    private HBITMAP _oldBitmap;
    private Graphics? _compatGraphics;
    private BufferedGraphics? _buffer;
    private int _busy;
    private bool _invalidateWhenFree;

    private const int BufferFree = 0; // The graphics buffer is free to use.
    private const int BufferBusyPainting = 1; // The graphics buffer is busy being created/painting.
    private const int BufferBusyDisposing = 2; // The graphics buffer is busy disposing.

    /// <summary>
    ///  Basic constructor.
    /// </summary>
    public BufferedGraphicsContext()
    {
        // By default, the size of our max buffer will be 3 x standard button size.
        _maximumBuffer.Width = 75 * 3;
        _maximumBuffer.Height = 32 * 3;
    }

    /// <summary>
    ///  Allows you to set the maximum width and height of the buffer that will be retained in memory.
    ///  You can allocate a buffer of any size, however any request for a buffer that would have a total
    ///  memory footprint larger that the maximum size will be allocated temporarily and then discarded
    ///  with the BufferedGraphics is released.
    /// </summary>
    public Size MaximumBuffer
    {
        get => _maximumBuffer;
        set
        {
            if (value.Width <= 0 || value.Height <= 0)
            {
                throw new ArgumentException(SR.Format(SR.InvalidArgumentValue, nameof(MaximumBuffer), value), nameof(value));
            }

            // If we've been asked to decrease the size of the maximum buffer,
            // then invalidate the older & larger buffer.
            if (value.Width * value.Height < _maximumBuffer.Width * _maximumBuffer.Height)
            {
                Invalidate();
            }

            _maximumBuffer = value;
        }
    }

    ~BufferedGraphicsContext() => Dispose(false);

    /// <summary>
    ///  Returns a BufferedGraphics that is matched for the specified target Graphics object.
    /// </summary>
    public BufferedGraphics Allocate(Graphics targetGraphics, Rectangle targetRectangle)
    {
        if (ShouldUseTempManager(targetRectangle))
        {
            return AllocBufferInTempManager(targetGraphics, HDC.Null, targetRectangle);
        }

        return AllocBuffer(targetGraphics, HDC.Null, targetRectangle);
    }

    /// <summary>
    ///  Returns a BufferedGraphics that is matched for the specified target HDC object.
    /// </summary>
    public BufferedGraphics Allocate(IntPtr targetDC, Rectangle targetRectangle)
    {
        if (ShouldUseTempManager(targetRectangle))
        {
            return AllocBufferInTempManager(null, (HDC)targetDC, targetRectangle);
        }

        return AllocBuffer(null, (HDC)targetDC, targetRectangle);
    }

    /// <summary>
    ///  Returns a BufferedGraphics that is matched for the specified target HDC object.
    /// </summary>
    private BufferedGraphics AllocBuffer(Graphics? targetGraphics, HDC targetDC, Rectangle targetRectangle)
    {
        int oldBusy = Interlocked.CompareExchange(ref _busy, BufferBusyPainting, BufferFree);

        // In the case were we have contention on the buffer - i.e. two threads
        // trying to use the buffer at the same time, we just create a temp
        // buffer manager and have the buffer dispose of it when it is done.
        if (oldBusy != BufferFree)
        {
            return AllocBufferInTempManager(targetGraphics, targetDC, targetRectangle);
        }

        Graphics surface;
        _targetLoc = new Point(targetRectangle.X, targetRectangle.Y);

        try
        {
            if (targetGraphics is not null)
            {
                IntPtr destDc = targetGraphics.GetHdc();
                try
                {
                    surface = CreateBuffer((HDC)destDc, targetRectangle.Width, targetRectangle.Height);
                }
                finally
                {
                    targetGraphics.ReleaseHdcInternal(destDc);
                }
            }
            else
            {
                surface = CreateBuffer(targetDC, targetRectangle.Width, targetRectangle.Height);
            }

            _buffer = new BufferedGraphics(surface, this, targetGraphics, targetDC, _targetLoc, _virtualSize);
        }
        catch
        {
            // Free the buffer so it can be disposed.
            _busy = BufferFree;
            throw;
        }

        return _buffer;
    }

    /// <summary>
    ///  Returns a BufferedGraphics that is matched for the specified target HDC object.
    /// </summary>
    private static BufferedGraphics AllocBufferInTempManager(Graphics? targetGraphics, HDC targetDC, Rectangle targetRectangle)
    {
        BufferedGraphicsContext? tempContext = null;
        BufferedGraphics? tempBuffer = null;

        try
        {
            tempContext = new BufferedGraphicsContext();
            tempBuffer = tempContext.AllocBuffer(targetGraphics, targetDC, targetRectangle);
            tempBuffer.DisposeContext = true;
        }
        finally
        {
            if (tempContext is not null && tempBuffer is not { DisposeContext: true })
            {
                tempContext.Dispose();
            }
        }

        return tempBuffer;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    ///  This routine allows us to control the point were we start using throw away
    ///  managers for painting. Since the buffer manager stays around (by default)
    ///  for the life of the app, we don't want to consume too much memory
    ///  in the buffer. However, re-allocating the buffer for small things (like
    ///  buttons, labels, etc) will hit us on runtime performance.
    /// </summary>
    private bool ShouldUseTempManager(Rectangle targetBounds)
    {
        return (targetBounds.Width * targetBounds.Height) > (MaximumBuffer.Width * MaximumBuffer.Height);
    }

    /// <summary>
    ///  Fills in the fields of a BITMAPINFO so that we can create a bitmap
    ///  that matches the format of the display.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is done by creating a compatible bitmap and calling GetDIBits
    ///   to return the color masks. This is done with two calls. The first
    ///   call passes in biBitCount = 0 to GetDIBits which will fill in the
    ///   base BITMAPINFOHEADER data. The second call to GetDIBits (passing
    ///   in the BITMAPINFO filled in by the first call) will return the color
    ///   table or bitmasks, as appropriate.
    ///  </para>
    /// </remarks>
    /// <returns>True if successful, false otherwise.</returns>
    private bool FillBitmapInfo(HDC hdc, HPALETTE hpalette, BITMAPINFO* bitmapInfo)
    {
        // Create a dummy bitmap from which we can query color format info about the device surface.
        using CreateBitmapScope hbm = new(hdc, 1, 1);

        if (hbm.IsNull)
        {
#pragma warning disable CA2201 // Do not raise reserved exception types - for compat we'll leave this as is, should probably be InvalidOperationException
            throw new OutOfMemoryException(SR.GraphicsBufferQueryFail);
#pragma warning restore CA2201
        }

        bitmapInfo->bmiHeader.biSize = (uint)sizeof(BITMAPINFOHEADER);

        // Call first time to fill in BITMAPINFO header.
        PInvoke.GetDIBits(
            hdc,
            hbm,
            0,
            0,
            null,
            bitmapInfo,
            DIB_USAGE.DIB_RGB_COLORS);

        if (bitmapInfo->bmiHeader.biBitCount <= 8)
        {
            return FillColorTable(hdc, hpalette, bitmapInfo);
        }
        else
        {
            if (bitmapInfo->bmiHeader.biCompression == (uint)BI_COMPRESSION.BI_BITFIELDS)
            {
                // Call a second time to get the color masks.
                PInvoke.GetDIBits(
                    hdc,
                    hbm,
                    0,
                    (uint)bitmapInfo->bmiHeader.biHeight,
                    null,
                    bitmapInfo,
                    DIB_USAGE.DIB_RGB_COLORS);
            }
        }

        return true;
    }

    /// <summary>
    ///  Initialize the color table of the <see cref="BITMAPINFO"/>. Colors
    ///  are set to the current system palette.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   Note: call only valid for displays of 8bpp or less.
    ///  </para>
    /// </remarks>
    /// <returns>True is successful, false otherwise.</returns>
    private bool FillColorTable(HDC hdc, HPALETTE hpalette, BITMAPINFO* bitmapInfo)
    {
        int colorCount = 1 << bitmapInfo->bmiHeader.biBitCount;
        if (colorCount > 256)
        {
            return false;
        }

        PALETTEENTRY* paletteEntries = stackalloc PALETTEENTRY[colorCount];
        Span<RGBQUAD> rgbQuads = new((RGBQUAD*)&bitmapInfo->bmiColors, colorCount);

        // Note: we don't support 4bpp displays.
        uint entries;

        if (hpalette.IsNull)
        {
            HPALETTE halftonePalette = (HPALETTE)Graphics.GetHalftonePalette();
            entries = PInvokeCore.GetPaletteEntries(halftonePalette, 0, (uint)colorCount, paletteEntries);
        }
        else
        {
            entries = PInvokeCore.GetPaletteEntries(hpalette, 0, (uint)colorCount, paletteEntries);
        }

        if (entries == 0)
        {
            return false;
        }

        for (int i = 0; i < colorCount; i++)
        {
            // Unfortunately, the structs' fields are not in the same order.
            rgbQuads[i].rgbRed = paletteEntries[i].peRed;
            rgbQuads[i].rgbGreen = paletteEntries[i].peGreen;
            rgbQuads[i].rgbBlue = paletteEntries[i].peBlue;
            rgbQuads[i].rgbReserved = 0;
        }

        return true;
    }

    /// <summary>
    ///  Returns a Graphics object representing a buffer.
    /// </summary>
    private Graphics CreateBuffer(HDC src, int width, int height)
    {
        // Create the compat DC.
        _busy = BufferBusyDisposing;
        DisposeDC();
        _busy = BufferBusyPainting;
        _compatDC = PInvokeCore.CreateCompatibleDC(src);

        // Recreate the bitmap if necessary.
        if (width > _bufferSize.Width || height > _bufferSize.Height)
        {
            int optWidth = Math.Max(width, _bufferSize.Width);
            int optHeight = Math.Max(height, _bufferSize.Height);

            _busy = BufferBusyDisposing;
            DisposeBitmap();
            _busy = BufferBusyPainting;

            _dib = CreateCompatibleDIB(src, HPALETTE.Null, optWidth, optHeight);
            _bufferSize = new Size(optWidth, optHeight);
        }

        // Select the bitmap.
        _oldBitmap = (HBITMAP)PInvokeCore.SelectObject(_compatDC, _dib);

        // Create compat graphics.
        _compatGraphics = Graphics.FromHdcInternal(_compatDC);
        _compatGraphics.TranslateTransform(-_targetLoc.X, -_targetLoc.Y);
        _virtualSize = new Size(width, height);

        GC.KeepAlive(this);
        return _compatGraphics;
    }

    /// <summary>
    ///  Create a DIB section with an optimal format w.r.t. the specified hdc.
    ///
    ///  If DIB &lt;= 8bpp, then the DIB color table is initialized based on the
    ///  specified palette. If the palette handle is NULL, then the system
    ///  palette is used.
    ///
    ///  Note: The hdc must be a direct DC (not an info or memory DC).
    ///
    ///  Note: On palettized displays, if the system palette changes the
    ///  UpdateDIBColorTable function should be called to maintain
    ///  the identity palette mapping between the DIB and the display.
    /// </summary>
    /// <returns>A valid bitmap handle if successful, IntPtr.Zero otherwise.</returns>
    private HBITMAP CreateCompatibleDIB(HDC hdc, HPALETTE hpalette, int ulWidth, int ulHeight)
    {
        if (hdc.IsNull)
        {
            throw new ArgumentNullException(nameof(hdc));
        }

        HBITMAP hbitmap = HBITMAP.Null;

        // Reserve enough space for all RGBQUADS
        byte* buffer = stackalloc byte[sizeof(BITMAPINFO) + 256 * sizeof(RGBQUAD)];
        BITMAPINFO* bitmapInfo = (BITMAPINFO*)buffer;

        // Validate hdc.
        OBJ_TYPE objType = (OBJ_TYPE)PInvokeCore.GetObjectType(hdc);
        switch (objType)
        {
            case OBJ_TYPE.OBJ_DC:
            case OBJ_TYPE.OBJ_METADC:
            case OBJ_TYPE.OBJ_MEMDC:
            case OBJ_TYPE.OBJ_ENHMETADC:
                break;
            default:
                throw new ArgumentException(SR.DCTypeInvalid);
        }

        if (FillBitmapInfo(hdc, hpalette, bitmapInfo))
        {
            // Change bitmap size to match specified dimensions.
            bitmapInfo->bmiHeader.biWidth = ulWidth;
            bitmapInfo->bmiHeader.biHeight = ulHeight;
            if (bitmapInfo->bmiHeader.biCompression == (uint)BI_COMPRESSION.BI_RGB)
            {
                bitmapInfo->bmiHeader.biSizeImage = 0;
            }
            else
            {
                if (bitmapInfo->bmiHeader.biBitCount == 16)
                {
                    bitmapInfo->bmiHeader.biSizeImage = (uint)(ulWidth * ulHeight * 2);
                }
                else if (bitmapInfo->bmiHeader.biBitCount == 32)
                {
                    bitmapInfo->bmiHeader.biSizeImage = (uint)(ulWidth * ulHeight * 4);
                }
                else
                {
                    bitmapInfo->bmiHeader.biSizeImage = 0;
                }
            }

            bitmapInfo->bmiHeader.biClrUsed = 0;
            bitmapInfo->bmiHeader.biClrImportant = 0;

            // Create the DIB section. Let Win32 allocate the memory and return a pointer to the bitmap surface.

            void* pvBits = null;
            hbitmap = PInvokeCore.CreateDIBSection(hdc, bitmapInfo, DIB_USAGE.DIB_RGB_COLORS, &pvBits, HANDLE.Null, 0);
            if (hbitmap.IsNull)
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
        }

        return hbitmap;
    }

    /// <summary>
    ///  Disposes the DC, but leaves the bitmap alone.
    /// </summary>
    private void DisposeDC()
    {
        if (!_oldBitmap.IsNull && !_compatDC.IsNull)
        {
            PInvokeCore.SelectObject(_compatDC, _oldBitmap);
            _oldBitmap = HBITMAP.Null;
        }

        if (!_compatDC.IsNull)
        {
            PInvokeCore.DeleteDC(_compatDC);
            _compatDC = HDC.Null;
        }

        GC.KeepAlive(this);
    }

    /// <summary>
    ///  Disposes the bitmap, will ASSERT if bitmap is being used (checks oldbitmap). if ASSERTed, call DisposeDC() first.
    /// </summary>
    private void DisposeBitmap()
    {
        if (!_dib.IsNull)
        {
            Debug.Assert(_oldBitmap.IsNull);

            PInvokeCore.DeleteObject(_dib);
            _dib = HBITMAP.Null;
        }

        GC.KeepAlive(this);
    }

    /// <summary>
    ///  Disposes of the Graphics buffer.
    /// </summary>
    private void Dispose(bool disposing)
    {
        int oldBusy = Interlocked.CompareExchange(ref _busy, BufferBusyDisposing, BufferFree);

        if (disposing)
        {
            if (oldBusy == BufferBusyPainting)
            {
                throw new InvalidOperationException(SR.GraphicsBufferCurrentlyBusy);
            }

            _compatGraphics?.Dispose();
            _compatGraphics = null;
        }

        DisposeDC();
        DisposeBitmap();

        _buffer?.Dispose();
        _buffer = null;

        _bufferSize = Size.Empty;
        _virtualSize = Size.Empty;

        _busy = BufferFree;
    }

    /// <summary>
    ///  Invalidates the cached graphics buffer.
    /// </summary>
    public void Invalidate()
    {
        int oldBusy = Interlocked.CompareExchange(ref _busy, BufferBusyDisposing, BufferFree);

        // If we're not busy with our buffer, lets clean it up now
        if (oldBusy == BufferFree)
        {
            Dispose();
            _busy = BufferFree;
        }
        else
        {
            // This will indicate to free the buffer as soon as it becomes non-busy.
            _invalidateWhenFree = true;
        }
    }

    /// <summary>
    ///  Returns a Graphics object representing a buffer.
    /// </summary>
    internal void ReleaseBuffer()
    {
        _buffer = null;
        if (_invalidateWhenFree)
        {
            // Clears everything including the bitmap.
            _busy = BufferBusyDisposing;
            Dispose();
        }
        else
        {
            // Otherwise, just dispose the DC. A new one will be created next time.
            _busy = BufferBusyDisposing;

            // Only clears out the DC.
            DisposeDC();
        }

        _busy = BufferFree;
    }
}
