/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include <algorithm>

#include "mozilla/gfx/2D.h"
#include "mozilla/Logging.h"

#include "gfxPlatform.h"
#include "nsArrayUtils.h"
#include "nsCOMPtr.h"
#include "nsClipboard.h"
#include "nsString.h"
#include "nsISupportsPrimitives.h"
#include "nsPrimitiveHelpers.h"
#include "nsIFile.h"
#include "nsStringStream.h"
#include "nsEscape.h"
#include "nsPrintfCString.h"
#include "nsObjCExceptions.h"
#include "imgIContainer.h"
#include "nsCocoaUtils.h"

using mozilla::gfx::DataSourceSurface;
using mozilla::gfx::SourceSurface;

mozilla::StaticRefPtr<nsITransferable> nsClipboard::sSelectionCache;
int32_t nsClipboard::sSelectionCacheChangeCount = 0;

nsClipboard::nsClipboard()
    : nsBaseClipboard(mozilla::dom::ClipboardCapabilities(
          false /* supportsSelectionClipboard */,
          true /* supportsFindClipboard */,
          true /* supportsSelectionCache */)) {}

nsClipboard::~nsClipboard() { ClearSelectionCache(); }

NS_IMPL_ISUPPORTS_INHERITED0(nsClipboard, nsBaseClipboard)

namespace {

// We separate this into its own function because after an @try, all local
// variables within that function get marked as volatile, and our C++ type
// system doesn't like volatile things.
static NSData* GetNSDataFromPasteboard(NSPasteboard* aPasteboard,
                                       NSString* aType) {
  NSData* data = nil;
  @try {
    data = [aPasteboard dataForType:aType];
  } @catch (NSException* e) {
    NS_WARNING(nsPrintfCString("Exception raised while getting data from the "
                               "pasteboard: \"%s - %s\"",
                               [[e name] UTF8String], [[e reason] UTF8String])
                   .get());
    (void)e;
  }
  return data;
}

static NSPasteboard* GetPasteboard(
    nsIClipboard::ClipboardType aWhichClipboard) {
  switch (aWhichClipboard) {
    case nsIClipboard::kGlobalClipboard:
      return [NSPasteboard generalPasteboard];
    case nsIClipboard::kFindClipboard:
      return [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
    default:
      return nil;
  }
}

}  // namespace

void nsClipboard::SetSelectionCache(nsITransferable* aTransferable) {
  sSelectionCacheChangeCount++;
  sSelectionCache = aTransferable;
}

void nsClipboard::ClearSelectionCache() { SetSelectionCache(nullptr); }

NS_IMETHODIMP
nsClipboard::SetNativeClipboardData(nsITransferable* aTransferable,
                                    ClipboardType aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_ASSERT(aTransferable);
  MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

  if (aWhichClipboard == kSelectionCache) {
    SetSelectionCache(aTransferable);
    return NS_OK;
  }

  NSDictionary* pasteboardOutputDict =
      PasteboardDictFromTransferable(aTransferable);
  if (!pasteboardOutputDict) return NS_ERROR_FAILURE;

  unsigned int outputCount = [pasteboardOutputDict count];
  NSArray* outputKeys = [pasteboardOutputDict allKeys];
  NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
  MOZ_ASSERT(cocoaPasteboard);
  if (aWhichClipboard == kFindClipboard) {
    NSString* stringType =
        [UTIHelper stringFromPboardType:NSPasteboardTypeString];
    [cocoaPasteboard declareTypes:[NSArray arrayWithObject:stringType]
                            owner:nil];
  } else {
    // Write everything else out to the general pasteboard.
    MOZ_ASSERT(aWhichClipboard == kGlobalClipboard);
    [cocoaPasteboard declareTypes:outputKeys owner:nil];
  }

  for (unsigned int i = 0; i < outputCount; i++) {
    NSString* currentKey = [outputKeys objectAtIndex:i];
    id currentValue = [pasteboardOutputDict valueForKey:currentKey];
    if (aWhichClipboard == kFindClipboard) {
      if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:
                                                     NSPasteboardTypeString]]) {
        [cocoaPasteboard setString:currentValue forType:currentKey];
      }
    } else {
      if ([currentKey isEqualToString:[UTIHelper stringFromPboardType:
                                                     NSPasteboardTypeString]] ||
          [currentKey
              isEqualToString:[UTIHelper
                                  stringFromPboardType:kPublicUrlPboardType]] ||
          [currentKey
              isEqualToString:
                  [UTIHelper stringFromPboardType:kPublicUrlNamePboardType]]) {
        [cocoaPasteboard setString:currentValue forType:currentKey];
      } else if ([currentKey
                     isEqualToString:
                         [UTIHelper
                             stringFromPboardType:kUrlsWithTitlesPboardType]]) {
        [cocoaPasteboard
            setPropertyList:[pasteboardOutputDict valueForKey:currentKey]
                    forType:currentKey];
      } else if ([currentKey
                     isEqualToString:[UTIHelper stringFromPboardType:
                                                    NSPasteboardTypeHTML]]) {
        [cocoaPasteboard
            setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
              forType:currentKey];
      } else if ([currentKey
                     isEqualToString:[UTIHelper stringFromPboardType:
                                                    kMozFileUrlsPboardType]]) {
        [cocoaPasteboard writeObjects:currentValue];
      } else if ([currentKey
                     isEqualToString:
                         [UTIHelper
                             stringFromPboardType:(NSString*)kUTTypeFileURL]]) {
        [cocoaPasteboard setString:currentValue forType:currentKey];
      } else if ([currentKey
                     isEqualToString:
                         [UTIHelper
                             stringFromPboardType:kPasteboardConcealedType]]) {
        // It's fine to set the data to null for this field - this field is an
        // addition to a value's other type and works like a flag.
        [cocoaPasteboard setData:NULL forType:currentKey];
      } else {
        [cocoaPasteboard setData:currentValue forType:currentKey];
      }
    }
  }

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

mozilla::Result<nsCOMPtr<nsISupports>, nsresult>
nsClipboard::GetDataFromPasteboard(const nsACString& aFlavor,
                                   NSPasteboard* aPasteboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  NSString* pboardType = nil;
  if (nsClipboard::IsStringType(aFlavor, &pboardType)) {
    NSString* pString = [aPasteboard stringForType:pboardType];
    if (!pString) {
      return nsCOMPtr<nsISupports>{};
    }

    NSData* stringData;
    bool isRTF = [pboardType
        isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeRTF]];
    if (isRTF) {
      stringData = [pString dataUsingEncoding:NSASCIIStringEncoding
                         allowLossyConversion:YES];
    } else {
      stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding
                         allowLossyConversion:YES];
    }
    unsigned int dataLength = [stringData length];
    void* clipboardDataPtr = malloc(dataLength);
    if (!clipboardDataPtr) {
      return mozilla::Err(NS_ERROR_OUT_OF_MEMORY);
    }
    [stringData getBytes:clipboardDataPtr length:dataLength];

    // The DOM only wants LF, so convert from MacOS line endings to DOM line
    // endings.
    int32_t signedDataLength = dataLength;
    nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &clipboardDataPtr,
                                                       &signedDataLength);
    dataLength = signedDataLength;

    // skip BOM (Byte Order Mark to distinguish little or big endian)
    char16_t* clipboardDataPtrNoBOM = (char16_t*)clipboardDataPtr;
    if ((dataLength > 2) && ((clipboardDataPtrNoBOM[0] == 0xFEFF) ||
                             (clipboardDataPtrNoBOM[0] == 0xFFFE))) {
      dataLength -= sizeof(char16_t);
      clipboardDataPtrNoBOM += 1;
    }

    nsCOMPtr<nsISupports> genericDataWrapper;
    nsPrimitiveHelpers::CreatePrimitiveForData(
        aFlavor, clipboardDataPtrNoBOM, dataLength,
        getter_AddRefs(genericDataWrapper));
    free(clipboardDataPtr);
    return std::move(genericDataWrapper);
  }

  if (aFlavor.EqualsLiteral(kFileMime)) {
    NSArray* items = [aPasteboard pasteboardItems];
    if (!items || [items count] <= 0) {
      return nsCOMPtr<nsISupports>{};
    }

    // XXX we don't support multiple clipboard item on DOM and XPCOM interface
    // for now, so we only get the data from the first pasteboard item.
    NSPasteboardItem* item = [items objectAtIndex:0];
    if (!item) {
      return nsCOMPtr<nsISupports>{};
    }

    return nsCOMPtr<nsISupports>(
        nsCocoaUtils::GetDataFromPasteboardItem(aFlavor, item));
  }

  if (aFlavor.EqualsLiteral(kCustomTypesMime)) {
    NSString* type = [aPasteboard
        availableTypeFromArray:
            [NSArray
                arrayWithObject:[UTIHelper stringFromPboardType:
                                               kMozCustomTypesPboardType]]];
    if (!type) {
      return nsCOMPtr<nsISupports>{};
    }

    NSData* pasteboardData = GetNSDataFromPasteboard(aPasteboard, type);
    if (!pasteboardData) {
      return nsCOMPtr<nsISupports>{};
    }

    unsigned int dataLength = [pasteboardData length];
    void* clipboardDataPtr = malloc(dataLength);
    if (!clipboardDataPtr) {
      return mozilla::Err(NS_ERROR_OUT_OF_MEMORY);
    }
    [pasteboardData getBytes:clipboardDataPtr length:dataLength];

    nsCOMPtr<nsISupports> genericDataWrapper;
    nsPrimitiveHelpers::CreatePrimitiveForData(
        aFlavor, clipboardDataPtr, dataLength,
        getter_AddRefs(genericDataWrapper));
    free(clipboardDataPtr);
    return std::move(genericDataWrapper);
  }

  if (aFlavor.EqualsLiteral(kJPEGImageMime) ||
      aFlavor.EqualsLiteral(kJPGImageMime) ||
      aFlavor.EqualsLiteral(kPNGImageMime) ||
      aFlavor.EqualsLiteral(kGIFImageMime)) {
    // Figure out if there's data on the pasteboard we can grab (sanity check)
    NSString* type = [aPasteboard
        availableTypeFromArray:
            [NSArray
                arrayWithObjects:[UTIHelper
                                     stringFromPboardType:(NSString*)
                                                              kUTTypeFileURL],
                                 [UTIHelper
                                     stringFromPboardType:NSPasteboardTypeTIFF],
                                 [UTIHelper
                                     stringFromPboardType:NSPasteboardTypePNG],
                                 nil]];
    if (!type) {
      return nsCOMPtr<nsISupports>{};
    }

    // Read data off the clipboard
    NSData* pasteboardData = GetNSDataFromPasteboard(aPasteboard, type);
    if (!pasteboardData) {
      return nsCOMPtr<nsISupports>{};
    }

    // Figure out what type we're converting to
    CFStringRef outputType = NULL;
    if (aFlavor.EqualsLiteral(kJPEGImageMime) ||
        aFlavor.EqualsLiteral(kJPGImageMime)) {
      outputType = CFSTR("public.jpeg");
    } else if (aFlavor.EqualsLiteral(kPNGImageMime)) {
      outputType = CFSTR("public.png");
    } else if (aFlavor.EqualsLiteral(kGIFImageMime)) {
      outputType = CFSTR("com.compuserve.gif");
    } else {
      return nsCOMPtr<nsISupports>{};
    }

    // Use ImageIO to interpret the data on the clipboard and transcode.
    // Note that ImageIO, like all CF APIs, allows NULLs to propagate freely
    // and safely in most cases (like ObjC). A notable exception is CFRelease.
    NSDictionary* options = [NSDictionary
        dictionaryWithObjectsAndKeys:(NSNumber*)kCFBooleanTrue,
                                     kCGImageSourceShouldAllowFloat, type,
                                     kCGImageSourceTypeIdentifierHint, nil];
    CGImageSourceRef source = nullptr;
    if (type == [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL]) {
      NSString* urlStr = [aPasteboard stringForType:type];
      NSURL* url = [NSURL URLWithString:urlStr];
      source =
          CGImageSourceCreateWithURL((CFURLRef)url, (CFDictionaryRef)options);
    } else {
      source = CGImageSourceCreateWithData((CFDataRef)pasteboardData,
                                           (CFDictionaryRef)options);
    }

    NSMutableData* encodedData = [NSMutableData data];
    CGImageDestinationRef dest = CGImageDestinationCreateWithData(
        (CFMutableDataRef)encodedData, outputType, 1, NULL);
    CGImageDestinationAddImageFromSource(dest, source, 0, NULL);

    nsCOMPtr<nsIInputStream> byteStream;
    if (CGImageDestinationFinalize(dest)) {
      // Put the converted data in a form Gecko can understand
      NS_NewByteInputStream(
          getter_AddRefs(byteStream),
          mozilla::Span((const char*)[encodedData bytes], [encodedData length]),
          NS_ASSIGNMENT_COPY);
    }

    if (dest) {
      CFRelease(dest);
    }
    if (source) {
      CFRelease(source);
    }

    return nsCOMPtr<nsISupports>(std::move(byteStream));
  }

  return nsCOMPtr<nsISupports>{};

  NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
}

mozilla::Result<nsCOMPtr<nsISupports>, nsresult>
nsClipboard::GetNativeClipboardData(const nsACString& aFlavor,
                                    ClipboardType aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

  if (kSelectionCache == aWhichClipboard) {
    if (!sSelectionCache) {
      return nsCOMPtr<nsISupports>{};
    }

    nsCOMPtr<nsISupports> dataSupports;
    if (NS_FAILED(sSelectionCache->GetTransferData(
            PromiseFlatCString(aFlavor).get(), getter_AddRefs(dataSupports)))) {
      return nsCOMPtr<nsISupports>{};
    }

    MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__,
                      PromiseFlatCString(aFlavor).get());
    return std::move(dataSupports);
  }

  NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
  if (!cocoaPasteboard) {
    return mozilla::Err(NS_ERROR_FAILURE);
  }

  return GetDataFromPasteboard(aFlavor, cocoaPasteboard);

  NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
}

// returns true if we have *any* of the passed in flavors available for pasting
mozilla::Result<bool, nsresult>
nsClipboard::HasNativeClipboardDataMatchingFlavors(
    const nsTArray<nsCString>& aFlavorList, ClipboardType aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

  if (kSelectionCache == aWhichClipboard) {
    nsTArray<nsCString> transferableFlavors;
    if (sSelectionCache &&
        NS_SUCCEEDED(sSelectionCache->FlavorsTransferableCanExport(
            transferableFlavors))) {
      if (MOZ_CLIPBOARD_LOG_ENABLED()) {
        MOZ_CLIPBOARD_LOG("    SelectionCache types (nums %zu)\n",
                          transferableFlavors.Length());
        for (const auto& transferableFlavor : transferableFlavors) {
          MOZ_CLIPBOARD_LOG("        MIME %s", transferableFlavor.get());
        }
      }

      for (const auto& transferableFlavor : transferableFlavors) {
        for (const auto& flavor : aFlavorList) {
          if (transferableFlavor.Equals(flavor)) {
            MOZ_CLIPBOARD_LOG("    has %s", flavor.get());
            return true;
          }
        }
      }
    }

    if (MOZ_CLIPBOARD_LOG_ENABLED()) {
      MOZ_CLIPBOARD_LOG("    no targets at clipboard (bad match)\n");
    }

    return false;
  }

  NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
  MOZ_ASSERT(cocoaPasteboard);
  if (MOZ_CLIPBOARD_LOG_ENABLED()) {
    NSArray* types = [cocoaPasteboard types];
    uint32_t count = [types count];
    MOZ_CLIPBOARD_LOG("    Pasteboard types (nums %d)\n", count);
    for (uint32_t i = 0; i < count; i++) {
      NSPasteboardType type = [types objectAtIndex:i];
      if (!type) {
        MOZ_CLIPBOARD_LOG("        failed to get MIME\n");
        continue;
      }
      MOZ_CLIPBOARD_LOG("        MIME %s\n", [type UTF8String]);
    }
  }

  for (auto& mimeType : aFlavorList) {
    NSString* pboardType = nil;
    if (nsClipboard::IsStringType(mimeType, &pboardType)) {
      NSString* availableType = [cocoaPasteboard
          availableTypeFromArray:[NSArray arrayWithObject:pboardType]];
      if (availableType && [availableType isEqualToString:pboardType]) {
        MOZ_CLIPBOARD_LOG("    has %s\n", mimeType.get());
        return true;
      }
    } else if (mimeType.EqualsLiteral(kCustomTypesMime)) {
      NSString* availableType = [cocoaPasteboard
          availableTypeFromArray:
              [NSArray
                  arrayWithObject:[UTIHelper stringFromPboardType:
                                                 kMozCustomTypesPboardType]]];
      if (availableType) {
        MOZ_CLIPBOARD_LOG("    has %s\n", mimeType.get());
        return true;
      }
    } else if (mimeType.EqualsLiteral(kJPEGImageMime) ||
               mimeType.EqualsLiteral(kJPGImageMime) ||
               mimeType.EqualsLiteral(kPNGImageMime) ||
               mimeType.EqualsLiteral(kGIFImageMime)) {
      NSString* availableType = [cocoaPasteboard
          availableTypeFromArray:
              [NSArray
                  arrayWithObjects:
                      [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
                      [UTIHelper stringFromPboardType:NSPasteboardTypePNG],
                      nil]];
      if (availableType) {
        MOZ_CLIPBOARD_LOG("    has %s\n", mimeType.get());
        return true;
      }
    } else if (mimeType.EqualsLiteral(kFileMime)) {
      NSArray* items = [cocoaPasteboard pasteboardItems];
      if (items && [items count] > 0) {
        // XXX we only check the first pasteboard item as we only get data from
        // first item in GetDataFromPasteboard for now.
        if (NSPasteboardItem* item = [items objectAtIndex:0]) {
          if ([item availableTypeFromArray:
                        [NSArray
                            arrayWithObjects:[UTIHelper
                                                 stringFromPboardType:
                                                     (NSString*)kUTTypeFileURL],
                                             nil]]) {
            MOZ_CLIPBOARD_LOG("    has %s\n", mimeType.get());
            return true;
          }
        }
      }
    }
  }

  if (MOZ_CLIPBOARD_LOG_ENABLED()) {
    MOZ_CLIPBOARD_LOG("    no targets at clipboard (bad match)\n");
  }

  return false;

  NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
}

// static
mozilla::Maybe<uint32_t> nsClipboard::FindIndexOfImageFlavor(
    const nsTArray<nsCString>& aMIMETypes) {
  for (uint32_t i = 0; i < aMIMETypes.Length(); ++i) {
    if (nsClipboard::IsImageType(aMIMETypes[i])) {
      return mozilla::Some(i);
    }
  }

  return mozilla::Nothing();
}

// This function converts anything that other applications might understand into
// the system format and puts it into a dictionary which it returns. static
NSDictionary* nsClipboard::PasteboardDictFromTransferable(
    nsITransferable* aTransferable) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  if (!aTransferable) {
    return nil;
  }

  NSMutableDictionary* pasteboardOutputDict = [NSMutableDictionary dictionary];

  nsTArray<nsCString> flavors;
  nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors);
  if (NS_FAILED(rv)) {
    return nil;
  }

  const mozilla::Maybe<uint32_t> imageFlavorIndex =
      nsClipboard::FindIndexOfImageFlavor(flavors);

  if (imageFlavorIndex) {
    // When right-clicking and "Copy Image" is clicked on macOS, some apps
    // expect the first flavor to be the image flavor. See bug 1689992. For
    // other apps, the order shouldn't matter.
    std::swap(*flavors.begin(), flavors[*imageFlavorIndex]);
  }
  for (uint32_t i = 0; i < flavors.Length(); i++) {
    nsCString& flavorStr = flavors[i];

    MOZ_CLIPBOARD_LOG("writing out clipboard data of type %s (%d)\n",
                      flavorStr.get(), i);

    NSString* pboardType = nil;
    if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
      nsCOMPtr<nsISupports> genericDataWrapper;
      rv = aTransferable->GetTransferData(flavorStr.get(),
                                          getter_AddRefs(genericDataWrapper));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsAutoString data;
      if (nsCOMPtr<nsISupportsString> text =
              do_QueryInterface(genericDataWrapper)) {
        text->GetData(data);
      }

      NSString* nativeString;
      if (!data.IsEmpty()) {
        nativeString = [NSString stringWithCharacters:(const unichar*)data.get()
                                               length:data.Length()];
      } else {
        nativeString = [NSString string];
      }

      // be nice to Carbon apps, normalize the receiver's contents using Form C.
      nativeString = [nativeString precomposedStringWithCanonicalMapping];
      if (nativeString) {
        [pasteboardOutputDict setObject:nativeString forKey:pboardType];
      }

      if (aTransferable->GetIsPrivateData()) {
        // In the case of password strings, we want to include the key for
        // concealed type. These will be flagged as private data.
        [pasteboardOutputDict
            setObject:nativeString
               forKey:[UTIHelper
                          stringFromPboardType:kPasteboardConcealedType]];
      }
    } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
      nsCOMPtr<nsISupports> genericDataWrapper;
      rv = aTransferable->GetTransferData(flavorStr.get(),
                                          getter_AddRefs(genericDataWrapper));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsAutoCString data;
      if (nsCOMPtr<nsISupportsCString> text =
              do_QueryInterface(genericDataWrapper)) {
        text->GetData(data);
      }

      if (!data.IsEmpty()) {
        NSData* nativeData = [NSData dataWithBytes:data.get()
                                            length:data.Length()];
        NSString* customType =
            [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
        if (!nativeData) {
          continue;
        }
        [pasteboardOutputDict setObject:nativeData forKey:customType];
      }
    } else if (nsClipboard::IsImageType(flavorStr)) {
      nsCOMPtr<nsISupports> transferSupports;
      rv = aTransferable->GetTransferData(flavorStr.get(),
                                          getter_AddRefs(transferSupports));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsCOMPtr<imgIContainer> image(do_QueryInterface(transferSupports));
      if (!image) {
        NS_WARNING("Image isn't an imgIContainer in transferable");
        continue;
      }

      RefPtr<SourceSurface> surface = image->GetFrame(
          imgIContainer::FRAME_CURRENT,
          imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
      if (!surface) {
        continue;
      }
      CGImageRef imageRef = NULL;
      rv = nsCocoaUtils::CreateCGImageFromSurface(surface, &imageRef);
      if (NS_FAILED(rv) || !imageRef) {
        continue;
      }

      // Convert the CGImageRef to TIFF and PNG data.
      CFMutableDataRef tiffData = CFDataCreateMutable(kCFAllocatorDefault, 0);
      CFMutableDataRef pngData = CFDataCreateMutable(kCFAllocatorDefault, 0);
      CGImageDestinationRef destRefTIFF = CGImageDestinationCreateWithData(
          tiffData, CFSTR("public.tiff"), 1, NULL);
      CGImageDestinationRef destRefPNG = CGImageDestinationCreateWithData(
          pngData, CFSTR("public.png"), 1, NULL);
      CGImageDestinationAddImage(destRefTIFF, imageRef, NULL);
      CGImageDestinationAddImage(destRefPNG, imageRef, NULL);
      const bool successfullyConvertedTIFF =
          CGImageDestinationFinalize(destRefTIFF);
      const bool successfullyConvertedPNG =
          CGImageDestinationFinalize(destRefPNG);

      CGImageRelease(imageRef);
      if (destRefTIFF) {
        CFRelease(destRefTIFF);
      }
      if (destRefPNG) {
        CFRelease(destRefPNG);
      }

      if (successfullyConvertedTIFF && tiffData) {
        NSString* tiffType =
            [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF];
        [pasteboardOutputDict setObject:(NSMutableData*)tiffData
                                 forKey:tiffType];
      }
      if (successfullyConvertedPNG && pngData) {
        NSString* pngType =
            [UTIHelper stringFromPboardType:NSPasteboardTypePNG];

        [pasteboardOutputDict setObject:(NSMutableData*)pngData forKey:pngType];
      }
      if (tiffData) {
        CFRelease(tiffData);
      }
      if (pngData) {
        CFRelease(pngData);
      }
    } else if (flavorStr.EqualsLiteral(kFileMime)) {
      nsCOMPtr<nsISupports> genericFile;
      rv = aTransferable->GetTransferData(flavorStr.get(),
                                          getter_AddRefs(genericFile));
      if (NS_FAILED(rv)) {
        continue;
      }

      nsCOMPtr<nsIFile> file(do_QueryInterface(genericFile));
      if (!file) {
        continue;
      }

      nsAutoString fileURI;
      rv = file->GetPath(fileURI);
      if (NS_FAILED(rv)) {
        continue;
      }

      NSString* str = nsCocoaUtils::ToNSString(fileURI);
      NSURL* url = [NSURL fileURLWithPath:str isDirectory:NO];
      if (!url || ![url absoluteString]) {
        continue;
      }
      NSString* fileUTType =
          [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
      [pasteboardOutputDict setObject:[url absoluteString] forKey:fileUTType];
    } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) {
      NSString* urlPromise = [UTIHelper
          stringFromPboardType:(NSString*)kPasteboardTypeFileURLPromise];
      NSString* urlPromiseContent = [UTIHelper
          stringFromPboardType:(NSString*)kPasteboardTypeFilePromiseContent];
      [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""]
                               forKey:urlPromise];
      [pasteboardOutputDict setObject:[NSArray arrayWithObject:@""]
                               forKey:urlPromiseContent];
      NSString* fileUTType =
          [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
      // Ideally, we would make use of `NSFilePromiseProvider` for file
      // promises. However, we cannot easily support this since
      // `NSFilePromiseProvider` expects to be the only data provider for the
      // drag session. This means that no other data types could be written out
      // to the drag pasteboard simultaneously without major rework. We work
      // around this by setting an empty string here for `kUTTypeFileURL` as our
      // signal to create the file once the drag completes, replicating the file
      // promise behavior.
      [pasteboardOutputDict setObject:@"" forKey:fileUTType];
    } else if (flavorStr.EqualsLiteral(kURLMime)) {
      nsCOMPtr<nsISupports> genericURL;
      rv = aTransferable->GetTransferData(flavorStr.get(),
                                          getter_AddRefs(genericURL));
      nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));

      nsAutoString url;
      urlObject->GetData(url);

      NSString* nativeTitle = nil;

      // A newline embedded in the URL means that the form is actually URL +
      // title. This embedding occurs in nsDragService::GetData.
      int32_t newlinePos = url.FindChar(char16_t('\n'));
      if (newlinePos >= 0) {
        url.Truncate(newlinePos);

        nsAutoString urlTitle;
        urlObject->GetData(urlTitle);
        urlTitle.Mid(urlTitle, newlinePos + 1,
                     urlTitle.Length() - (newlinePos + 1));

        nativeTitle =
            [NSString stringWithCharacters:reinterpret_cast<const unichar*>(
                                               urlTitle.get())
                                    length:urlTitle.Length()];
      }
      // The Finder doesn't like getting random binary data aka
      // Unicode, so change it into an escaped URL containing only
      // ASCII.
      nsAutoCString utf8Data = NS_ConvertUTF16toUTF8(url.get(), url.Length());
      nsAutoCString escData;
      NS_EscapeURL(utf8Data.get(), utf8Data.Length(),
                   esc_OnlyNonASCII | esc_AlwaysCopy, escData);

      NSString* nativeURL = [NSString stringWithUTF8String:escData.get()];
      NSString* publicUrl =
          [UTIHelper stringFromPboardType:kPublicUrlPboardType];
      if (!nativeURL) {
        continue;
      }
      [pasteboardOutputDict setObject:nativeURL forKey:publicUrl];
      if (nativeTitle) {
        NSArray* urlsAndTitles = @[ @[ nativeURL ], @[ nativeTitle ] ];
        NSString* urlName =
            [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
        NSString* urlsWithTitles =
            [UTIHelper stringFromPboardType:kUrlsWithTitlesPboardType];
        [pasteboardOutputDict setObject:nativeTitle forKey:urlName];
        [pasteboardOutputDict setObject:urlsAndTitles forKey:urlsWithTitles];
      }
    }
    // If it wasn't a type that we recognize as exportable we don't put it on
    // the system clipboard. We'll just access it from our cached transferable
    // when we need it.
  }

  return pasteboardOutputDict;

  NS_OBJC_END_TRY_BLOCK_RETURN(nil);
}

bool nsClipboard::IsStringType(const nsACString& aMIMEType,
                               NSString** aPboardType) {
  if (aMIMEType.EqualsLiteral(kTextMime)) {
    *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
    return true;
  } else if (aMIMEType.EqualsLiteral(kRTFMime)) {
    *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
    return true;
  } else if (aMIMEType.EqualsLiteral(kHTMLMime)) {
    *aPboardType = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
    return true;
  } else {
    return false;
  }
}

// static
bool nsClipboard::IsImageType(const nsACString& aMIMEType) {
  return aMIMEType.EqualsLiteral(kPNGImageMime) ||
         aMIMEType.EqualsLiteral(kJPEGImageMime) ||
         aMIMEType.EqualsLiteral(kJPGImageMime) ||
         aMIMEType.EqualsLiteral(kGIFImageMime) ||
         aMIMEType.EqualsLiteral(kNativeImageMime);
}

NSString* nsClipboard::WrapHtmlForSystemPasteboard(NSString* aString) {
  NSString* wrapped =
      [NSString stringWithFormat:@"<html>"
                                  "<head>"
                                  "<meta http-equiv=\"content-type\" "
                                  "content=\"text/html; charset=utf-8\">"
                                  "</head>"
                                  "<body>"
                                  "%@"
                                  "</body>"
                                  "</html>",
                                 aString];
  return wrapped;
}

nsresult nsClipboard::EmptyNativeClipboardData(ClipboardType aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

  if (kSelectionCache == aWhichClipboard) {
    ClearSelectionCache();
    return NS_OK;
  }

  if (NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard)) {
    [cocoaPasteboard clearContents];
  }

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

mozilla::Result<int32_t, nsresult>
nsClipboard::GetNativeClipboardSequenceNumber(ClipboardType aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

  if (kSelectionCache == aWhichClipboard) {
    return sSelectionCacheChangeCount;
  }

  NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
  if (!cocoaPasteboard) {
    return mozilla::Err(NS_ERROR_FAILURE);
  }

  return [cocoaPasteboard changeCount];

  NS_OBJC_END_TRY_BLOCK_RETURN(mozilla::Err(NS_ERROR_FAILURE));
}
