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

/// <summary>
///  DCMapping is used to change the mapping and clip region of the specified device context to the given
///  bounds. When the DCMapping is disposed, the original mapping and clip rectangle are restored.
///
///  Example:
///
///  using(DCMapping mapping = new DCMapping(hDC, new Rectangle(10,10, 50, 50)
///  {
///      // inside here the hDC's mapping of (0,0) is inset by (10,10) and
///      // all painting is clipped at (0,0) - (50,50)
///  }
///
///  PERF: DCMapping is a structure so that it will allocate on the stack rather than in GC managed memory. This
///  way disposing the object does not force a GC. Since DCMapping objects aren't likely to be passed between
///  functions rather used and disposed in the same one, this reduces overhead.
/// </summary>
internal readonly struct DCMapping : IDisposable
{
    private readonly HDC _hdc;
    private readonly int _savedState;

    public unsafe DCMapping(HDC hdc, Rectangle bounds)
    {
        ArgumentNullException.ThrowIfNull(hdc);

        _hdc = hdc;
        _savedState = PInvokeCore.SaveDC(hdc);

        // Retrieve the x-coordinates and y-coordinates of the viewport origin for the specified device context.
        Point viewportOrg = default;
        bool success = PInvokeCore.GetViewportOrgEx(hdc, &viewportOrg);
        Debug.Assert(success, "GetViewportOrgEx() failed.");

        // Create a new rectangular clipping region based off of the bounds specified, shifted over by the x & y specified in the viewport origin.
        RegionScope clippingRegion = new(
            viewportOrg.X + bounds.Left,
            viewportOrg.Y + bounds.Top,
            viewportOrg.X + bounds.Right,
            viewportOrg.Y + bounds.Bottom);
        Debug.Assert(!clippingRegion.IsNull, "CreateRectRgn() failed.");

        try
        {
            RegionScope originalRegion = new(hdc);

            // Shift the viewpoint origin by coordinates specified in "bounds".
            success = PInvoke.SetViewportOrgEx(
                hdc,
                viewportOrg.X + bounds.Left,
                viewportOrg.Y + bounds.Top,
                lppt: null);
            Debug.Assert(success, "SetViewportOrgEx() failed.");

            GDI_REGION_TYPE originalRegionType;
            if (!originalRegion.IsNull)
            {
                // Get the original clipping region so we can determine its type (we'll check later if we've restored the region back properly.)
                RECT originalClipRect = default;
                originalRegionType = PInvoke.GetRgnBox(originalRegion, &originalClipRect);
                Debug.Assert(
                    originalRegionType != GDI_REGION_TYPE.RGN_ERROR,
                    "ERROR returned from SelectClipRgn while selecting the original clipping region..");

                if (originalRegionType == GDI_REGION_TYPE.SIMPLEREGION)
                {
                    // Find the intersection of our clipping region and the current clipping region (our parent's)

                    GDI_REGION_TYPE combineResult = PInvokeCore.CombineRgn(
                        clippingRegion,
                        clippingRegion,
                        originalRegion,
                        RGN_COMBINE_MODE.RGN_AND);

                    Debug.Assert(
                        combineResult is GDI_REGION_TYPE.SIMPLEREGION or GDI_REGION_TYPE.NULLREGION,
                        "SIMPLEREGION or NULLREGION expected.");
                }
            }
            else
            {
                // If there was no clipping region, then the result is a simple region.
                originalRegionType = GDI_REGION_TYPE.SIMPLEREGION;
            }

            // Select the new clipping region; make sure it's a SIMPLEREGION or NULLREGION
            GDI_REGION_TYPE selectResult = PInvokeCore.SelectClipRgn(hdc, clippingRegion);
            Debug.Assert(
                selectResult is GDI_REGION_TYPE.SIMPLEREGION or GDI_REGION_TYPE.NULLREGION,
                "SIMPLEREGION or NULLLREGION expected.");
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
        }
    }

    public void Dispose()
    {
        if (!_hdc.IsNull)
        {
            PInvokeCore.RestoreDC(_hdc, _savedState);
        }
    }
}
