﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.OLE.Interop;

namespace Microsoft.VisualStudio.LanguageServices.Utilities
{
    internal static class ClipboardHelpers
    {
        private const ushort CF_TEXT = 1;            // winuser.h
        private const ushort CF_UNICODETEXT = 13;    // winuser.h

        private static readonly FORMATETC[] TextFormats = new[]
        {
            CreateFormatEtc(CF_UNICODETEXT),
            CreateFormatEtc(CF_TEXT),
        };

        #region Native Methods
        [DllImport("ole32.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
        private static extern int OleGetClipboard(out IDataObject dataObject);

        [DllImport("ole32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern void ReleaseStgMedium(ref STGMEDIUM medium);

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GlobalLock(HandleRef handle);

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool GlobalUnlock(HandleRef handle);
        #endregion

        /// <summary>
        /// For cases where the clipboard data is retrieved in a performance critical path this 
        /// should be used to avoid the overhead that WPF/WinForms adds for the clipboard APIs.
        /// If performance is less of a concern (for example responding directly to a user paste
        /// command instead of VS activation) then it is recommended to use paste command handlers
        /// or the clipboard API directly. 
        /// </summary>
        public static string? GetTextNoRetry()
        {
            if (OleGetClipboard(out var dataObject) != VSConstants.S_OK || dataObject is null)
            {
                return null;
            }

            foreach (var format in TextFormats)
            {
                if (dataObject.QueryGetData(new[] { format }) == VSConstants.S_OK)
                {
                    return GetData(dataObject, format);
                }
            }

            return null;
        }

        // 
        // The rest of the methods are derived from the WPF implementation of getting
        // data from an OLE IDataObject. We use our own logic because there are built in 
        // mechanisms within WPF that result in retries on getting clipboard data which
        // is undesirable for paths that check the clipboard data in a hot path. 
        // See https://github.com/dotnet/wpf/blob/212f376fbca58bf2970964610c426ee05e633872/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs
        // for information on how dataobject is used in WPF. This code only supports
        // a subset of the functionality for now because it is limited to text.
        //
        private static string? GetData(IDataObject dataObject, FORMATETC format)
        {
            var af = new FORMATETC[1];
            af[0] = format;
            var sm = new STGMEDIUM[1];
            dataObject.GetData(af, sm);
            var medium = sm[0];

            try
            {
                if (medium.tymed != 1) // TYMED_HGLOBAL
                {
                    return null;
                }

                return ReadStringFromHandle(medium.unionmember, format.cfFormat == CF_UNICODETEXT);
            }
            finally
            {
                ReleaseStgMedium(ref medium);
            }
        }

        private static FORMATETC CreateFormatEtc(ushort format)
            => new FORMATETC
            {
                cfFormat = format,
                ptd = IntPtr.Zero,
                dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
                lindex = -1,
                tymed = (uint)TYMED.TYMED_HGLOBAL
            };

        private static unsafe string? ReadStringFromHandle(IntPtr handle, bool unicode)
        {
            string? stringData = null;
            IntPtr ptr;
            object handleRefObj = new();

            ptr = Win32GlobalLock(new HandleRef(handleRefObj, handle));
            try
            {
                if (unicode)
                {
                    stringData = new string((char*)ptr);
                }
                else
                {
                    stringData = new string((sbyte*)ptr);
                }
            }
            finally
            {
                Win32GlobalUnlock(new HandleRef(handleRefObj, handle));
            }

            return stringData;
        }

        private static IntPtr Win32GlobalLock(HandleRef handle)
        {
            var win32Pointer = GlobalLock(handle);
            var win32Error = Marshal.GetLastWin32Error();
            if (win32Pointer == IntPtr.Zero)
            {
                throw new Win32Exception(win32Error);
            }

            return win32Pointer;
        }

        private static void Win32GlobalUnlock(HandleRef handle)
        {
            var win32Return = GlobalUnlock(handle);
            var win32Error = Marshal.GetLastWin32Error();
            if (!win32Return && win32Error != 0)
            {
                throw new Win32Exception(win32Error);
            }
        }
    }
}
