// 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 Windows.Win32.Graphics.Gdi;

/// <summary>
///  <para>
///   Helper to scope getting a <see cref="HDC"/> from a <see cref="IHdcContext"/> object. Releases
///   the <see cref="HDC"/> when disposed, unlocking the parent <see cref="IHdcContext"/> object.
///  </para>
///  <para>
///   Also saves and restores the state of the HDC.
///  </para>
/// </summary>
/// <remarks>
///  <para>
///   Use in a <see langword="using" /> statement. If you must pass this around, always pass by+
///   <see langword="ref" /> to avoid duplicating the handle and risking a double release.
///  </para>
/// </remarks>
#if DEBUG
internal class DeviceContextHdcScope : DisposalTracking.Tracker, IDisposable
#else
internal readonly ref struct DeviceContextHdcScope
#endif
{
    public IHdcContext DeviceContext { get; }
    public HDC HDC { get; }

    private readonly int _savedHdcState;

    /// <summary>
    ///  Gets the <see cref="HDC"/> from the given <paramref name="deviceContext"/>.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   When a <see cref="T:System.Drawing.Graphics"/> object is created from a <see cref="Gdi.HDC"/> the clipping region and
    ///   the viewport origin are applied (<see cref="PInvokeCore.GetViewportExtEx(HDC, SIZE*)"/>). The clipping
    ///   region isn't reflected in <see cref="P:System.Drawing.Graphics.Clip"/>, which is combined with the HDC HRegion.
    ///  </para>
    ///  <para>
    ///   The Graphics object saves and restores DC state when performing operations that would modify the DC to
    ///   maintain the DC in its original or returned state after <see cref="M:System.Drawing.Graphics.ReleaseHdc()"/>.
    ///  </para>
    /// </remarks>
    /// <param name="applyGraphicsState">
    ///  Applies the origin transform and clipping region of the <paramref name="deviceContext"/> if it is an
    ///  object of type <see cref="T:System.Drawing.Graphics"/>. Otherwise this is a no-op.
    /// </param>
    /// <param name="saveHdcState">
    ///  When true, saves and restores the <see cref="HDC"/> state.
    /// </param>
    public DeviceContextHdcScope(
        IHdcContext deviceContext,
        bool applyGraphicsState = true,
        bool saveHdcState = false) : this(
            deviceContext,
            applyGraphicsState ? ApplyGraphicsProperties.All : ApplyGraphicsProperties.None,
            saveHdcState)
    {
    }

    /// <summary>
    ///  Prefer to use <see cref="DeviceContextHdcScope(IHdcContext, bool, bool)"/>.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   Ideally we'd not bifurcate what properties we apply unless we're absolutely sure we only want one.
    ///  </para>
    /// </remarks>
    public unsafe DeviceContextHdcScope(
        IHdcContext deviceContext,
        ApplyGraphicsProperties applyGraphicsState,
        bool saveHdcState = false)
    {
#if DEBUG
        if (deviceContext is null)
        {
            // As we're throwing in the constructor, `this` will never be passed back and as such .Dispose()
            // can't be called. We don't have anything to release at this point so there is no point in having
            // the finalizer run.
            DisposalTracking.SuppressFinalize(this!);
            throw new ArgumentNullException(nameof(deviceContext));
        }
#else
        ArgumentNullException.ThrowIfNull(deviceContext);
#endif

        DeviceContext = deviceContext;
        _savedHdcState = 0;

        HDC = default;

        IGraphicsHdcProvider? provider = deviceContext as IGraphicsHdcProvider;
        IGraphics? graphics = deviceContext as IGraphics;

        // There are three states of IDeviceContext that come into this class:
        //
        //  1. One that also implements IGraphicsHdcProvider
        //  2. One that is directly on Graphics
        //  3. All other IDeviceContext instances
        //
        // In the third case there is no Graphics to apply Properties from. In the second case we must query
        // the Graphics object itself for Properties (transform and clip). In the first case the
        // IGraphicsHdcProvider will let us know if we have an "unclean" Graphics object that we need to apply
        // Properties from.
        //
        // PaintEventArgs implements IGraphicsHdcProvider and uses it to let us know that either (1) a Graphics
        // object hasn't been created yet, OR (2) the Graphics object has never been given a transform or clip.

        bool needToApplyProperties = applyGraphicsState != ApplyGraphicsProperties.None;
        if (graphics is null && provider is null)
        {
            // We have an IDeviceContext (case 3 above), we can't apply properties because there is no
            // Graphics object available.
            needToApplyProperties = false;
        }
        else if (provider is not null && provider.IsGraphicsStateClean)
        {
            // We have IGraphicsHdcProvider and it is telling us we have no properties to apply (case 1 above)
            needToApplyProperties = false;
        }

        if (provider is not null)
        {
            // We have a provider, grab the underlying HDC if possible unless we know we've created and
            // modified a Graphics object around it.
            HDC = needToApplyProperties ? default : provider.GetHdc();

            if (HDC.IsNull)
            {
                graphics = provider.GetGraphics(createIfNeeded: true) ?? throw new InvalidOperationException();
                DeviceContext = graphics;
            }
        }

        if (!needToApplyProperties || graphics is null)
        {
            HDC = HDC.IsNull ? DeviceContext.GetHdc() : HDC;
            ValidateHDC();
            _savedHdcState = saveHdcState ? PInvokeCore.SaveDC(HDC) : 0;
            return;
        }

        // We have a Graphics object (either directly passed in or given to us by IGraphicsHdcProvider)
        // that needs properties applied.

        (HDC, _savedHdcState) = graphics.GetHdc(applyGraphicsState, saveHdcState);
    }

    public static implicit operator HDC(in DeviceContextHdcScope scope) => scope.HDC;
    public static implicit operator nint(in DeviceContextHdcScope scope) => scope.HDC;
    public static explicit operator WPARAM(in DeviceContextHdcScope scope) => (WPARAM)scope.HDC;

    [Conditional("DEBUG")]
    private void ValidateHDC()
    {
        if (HDC.IsNull)
        {
            // We don't want the disposal tracking to fire as it will take down unrelated tests.
#if DEBUG
            DisposalTracking.SuppressFinalize(this!);
#endif
            throw new InvalidOperationException("Null HDC");
        }

        OBJ_TYPE type = (OBJ_TYPE)PInvokeCore.GetObjectType(HDC);
        switch (type)
        {
            case OBJ_TYPE.OBJ_DC:
            case OBJ_TYPE.OBJ_MEMDC:
            case OBJ_TYPE.OBJ_METADC:
            case OBJ_TYPE.OBJ_ENHMETADC:
                break;
            default:
#if DEBUG
                DisposalTracking.SuppressFinalize(this!);
#endif
                throw new InvalidOperationException($"Invalid handle ({type})");
        }
    }

    public void Dispose()
    {
        if (_savedHdcState != 0)
        {
            PInvokeCore.RestoreDC(HDC, _savedHdcState);
        }

        // Note that Graphics keeps track of the HDC it passes back, so we don't need to pass it back in
        if (DeviceContext is not IGraphicsHdcProvider)
        {
            DeviceContext?.ReleaseHdc();
        }

#if DEBUG
        DisposalTracking.SuppressFinalize(this!);
#endif
    }
}
