using System.Globalization;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using DataObject = System.Windows.Forms.DataObject;
using DataFormats = System.Windows.Forms.DataFormats;
using FormsClipboard = System.Windows.Forms.Clipboard;

namespace Olive.Services;

internal sealed class ClipboardService
{
  public static async Task CopyGifBytesAsync(byte[] gifBytes, CancellationToken cancellationToken)
  {
    ArgumentNullException.ThrowIfNull(gifBytes);
    if (gifBytes.Length == 0) throw new InvalidOperationException("GIF data is empty.");

    var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
    var thread = new Thread(() => CopyOnStaThread(gifBytes, completion))
    {
      Name = "Olive clipboard STA"
    };

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();

    cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
    await completion.Task.ConfigureAwait(false);
  }

  private static void CopyOnStaThread(byte[] gifBytes, TaskCompletionSource completion)
  {
    try
    {
      var clipboardHtml = BuildClipboardHtml(gifBytes);

      const int retryCount = 5;
      for (var attempt = 1; attempt <= retryCount; attempt++)
        try
        {
          var data = new DataObject();
          data.SetData("image/gif", false, GifStream(gifBytes));
          data.SetData("GIF", false, GifStream(gifBytes));
          data.SetData(DataFormats.Html, false, clipboardHtml);
          data.SetData("text/html", false, clipboardHtml);
          TryAddBitmapPreview(data, gifBytes);
          FormsClipboard.SetDataObject(data, true, 10, 100);
          completion.TrySetResult();
          return;
        }
        catch (ExternalException) when (attempt < retryCount)
        {
          Thread.Sleep(120);
        }

      completion.TrySetException(new InvalidOperationException("The clipboard is temporarily unavailable."));
    }
    catch (Exception ex)
    {
      completion.TrySetException(ex);
    }
  }

  private static MemoryStream GifStream(byte[] gifBytes)
  {
    return new MemoryStream(gifBytes, false);
  }

  private static void TryAddBitmapPreview(DataObject data, byte[] gifBytes)
  {
    try
    {
      using var stream = new MemoryStream(gifBytes, false);
      using var image = Image.FromStream(stream);
      data.SetData(DataFormats.Bitmap, true, new Bitmap(image));
    }
    catch (Exception ex) when (ex is ArgumentException or ExternalException or OutOfMemoryException)
    {
    }
  }

  private static string BuildClipboardHtml(byte[] gifBytes)
  {
    var dataUri = "data:image/gif;base64," + Convert.ToBase64String(gifBytes);
    var fragment = $"<img src=\"{dataUri}\" alt=\"GIF\">";
    const string markerPrefix =
      "Version:1.0\r\nStartHTML:{0:0000000000}\r\nEndHTML:{1:0000000000}\r\nStartFragment:{2:0000000000}\r\nEndFragment:{3:0000000000}\r\n";
    var prefix = string.Format(CultureInfo.InvariantCulture, markerPrefix, 0, 0, 0, 0);
    const string beforeFragment = "<!DOCTYPE html><html><body><!--StartFragment-->";
    const string afterFragment = "<!--EndFragment--></body></html>";
    var html = beforeFragment + fragment + afterFragment;

    var startHtml = Encoding.UTF8.GetByteCount(prefix);
    var startFragment = startHtml + Encoding.UTF8.GetByteCount(beforeFragment);
    var endFragment = startFragment + Encoding.UTF8.GetByteCount(fragment);
    var endHtml = startHtml + Encoding.UTF8.GetByteCount(html);

    var header = string.Format(CultureInfo.InvariantCulture, markerPrefix, startHtml, endHtml, startFragment,
      endFragment);
    return header + html;
  }
}
