/* -*- 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 "mozilla/Unused.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::LogLevel;
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* GetDataFromPasteboard(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());
    mozilla::Unused << e;
  }
  return data;
}

static NSPasteboard* GetPasteboard(int32_t 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,
                                    nsIClipboardOwner* aOwner,
                                    int32_t 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);
}

nsresult nsClipboard::TransferableFromPasteboard(
    nsITransferable* aTransferable, NSPasteboard* cocoaPasteboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  // get flavor list that includes all acceptable flavors (including ones
  // obtained through conversion)
  nsTArray<nsCString> flavors;
  nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
  if (NS_FAILED(rv)) return NS_ERROR_FAILURE;

  for (uint32_t i = 0; i < flavors.Length(); i++) {
    nsCString& flavorStr = flavors[i];

    // printf("looking for clipboard data of type %s\n", flavorStr.get());

    NSString* pboardType = nil;
    if (nsClipboard::IsStringType(flavorStr, &pboardType)) {
      NSString* pString = [cocoaPasteboard stringForType:pboardType];
      if (!pString) {
        continue;
      }

      NSData* stringData;
      bool isRTF = [pboardType
          isEqualToString:[UTIHelper stringFromPboardType:NSPasteboardTypeRTF]];
      if (isRTF) {
        stringData = [pString dataUsingEncoding:NSASCIIStringEncoding];
      } else {
        stringData = [pString dataUsingEncoding:NSUnicodeStringEncoding];
      }
      unsigned int dataLength = [stringData length];
      void* clipboardDataPtr = malloc(dataLength);
      if (!clipboardDataPtr) {
        return 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(
          flavorStr, clipboardDataPtrNoBOM, dataLength,
          getter_AddRefs(genericDataWrapper));
      aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
      free(clipboardDataPtr);
      break;
    } else if (flavorStr.EqualsLiteral(kFileMime)) {
      NSArray* items = [cocoaPasteboard pasteboardItems];
      if (!items || [items count] <= 0) {
        continue;
      }

      // 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) {
        continue;
      }

      nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable,
                                                             flavorStr, item);
    } else if (flavorStr.EqualsLiteral(kCustomTypesMime)) {
      NSString* type = [cocoaPasteboard
          availableTypeFromArray:
              [NSArray
                  arrayWithObject:[UTIHelper stringFromPboardType:
                                                 kMozCustomTypesPboardType]]];
      if (!type) {
        continue;
      }

      NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
      if (!pasteboardData) {
        continue;
      }

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

      nsCOMPtr<nsISupports> genericDataWrapper;
      nsPrimitiveHelpers::CreatePrimitiveForData(
          flavorStr, clipboardDataPtr, dataLength,
          getter_AddRefs(genericDataWrapper));

      aTransferable->SetTransferData(flavorStr.get(), genericDataWrapper);
      free(clipboardDataPtr);
    } else if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
               flavorStr.EqualsLiteral(kJPGImageMime) ||
               flavorStr.EqualsLiteral(kPNGImageMime) ||
               flavorStr.EqualsLiteral(kGIFImageMime)) {
      // Figure out if there's data on the pasteboard we can grab (sanity check)
      NSString* type = [cocoaPasteboard
          availableTypeFromArray:
              [NSArray
                  arrayWithObjects:
                      [UTIHelper
                          stringFromPboardType:(NSString*)kUTTypeFileURL],
                      [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF],
                      [UTIHelper stringFromPboardType:NSPasteboardTypePNG],
                      nil]];
      if (!type) continue;

      // Read data off the clipboard
      NSData* pasteboardData = GetDataFromPasteboard(cocoaPasteboard, type);
      if (!pasteboardData) continue;

      // Figure out what type we're converting to
      CFStringRef outputType = NULL;
      if (flavorStr.EqualsLiteral(kJPEGImageMime) ||
          flavorStr.EqualsLiteral(kJPGImageMime))
        outputType = CFSTR("public.jpeg");
      else if (flavorStr.EqualsLiteral(kPNGImageMime))
        outputType = CFSTR("public.png");
      else if (flavorStr.EqualsLiteral(kGIFImageMime))
        outputType = CFSTR("com.compuserve.gif");
      else
        continue;

      // 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 = [cocoaPasteboard 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);
      bool successfullyConverted = CGImageDestinationFinalize(dest);

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

        aTransferable->SetTransferData(flavorStr.get(), byteStream);
      }

      if (dest) CFRelease(dest);
      if (source) CFRelease(source);

      if (successfullyConverted) {
        // XXX Maybe try to fill in more types? Is there a point?
        break;
      } else {
        continue;
      }
    }
  }

  return NS_OK;

  NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
}

NS_IMETHODIMP
nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable,
                                    int32_t aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

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

  if (kSelectionCache == aWhichClipboard) {
    if (!sSelectionCache) {
      return NS_OK;
    }

    // get flavor list that includes all acceptable flavors (including ones
    // obtained through conversion)
    nsTArray<nsCString> flavors;
    nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
    if (NS_FAILED(rv)) {
      return NS_ERROR_FAILURE;
    }

    for (const auto& flavor : flavors) {
      nsCOMPtr<nsISupports> dataSupports;
      rv = sSelectionCache->GetTransferData(flavor.get(),
                                            getter_AddRefs(dataSupports));
      if (NS_SUCCEEDED(rv)) {
        CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__, flavor.get());
        aTransferable->SetTransferData(flavor.get(), dataSupports);
        // XXX Maybe try to fill in more types? Is there a point?
        break;
      }
    }
    return NS_OK;
  }

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

  return TransferableFromPasteboard(aTransferable, cocoaPasteboard);

  NS_OBJC_END_TRY_BLOCK_RETURN(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, int32_t aWhichClipboard) {
  NS_OBJC_BEGIN_TRY_BLOCK_RETURN;

  MOZ_DIAGNOSTIC_ASSERT(
      nsIClipboard::IsClipboardTypeSupported(aWhichClipboard));

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

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

    if (CLIPBOARD_LOG_ENABLED()) {
      CLIPBOARD_LOG("    no targets at clipboard (bad match)\n");
    }

    return false;
  }

  NSPasteboard* cocoaPasteboard = GetPasteboard(aWhichClipboard);
  MOZ_ASSERT(cocoaPasteboard);
  if (CLIPBOARD_LOG_ENABLED()) {
    NSArray* types = [cocoaPasteboard types];
    uint32_t count = [types count];
    CLIPBOARD_LOG("    Pasteboard types (nums %d)\n", count);
    for (uint32_t i = 0; i < count; i++) {
      NSPasteboardType type = [types objectAtIndex:i];
      if (!type) {
        CLIPBOARD_LOG("        failed to get MIME\n");
        continue;
      }
      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]) {
        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) {
        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) {
        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 TransferableFromPasteboard for now.
        if (NSPasteboardItem* item = [items objectAtIndex:0]) {
          if (NSString *availableType = [item
                  availableTypeFromArray:
                      [NSArray
                          arrayWithObjects:[UTIHelper
                                               stringFromPboardType:
                                                   (NSString*)kUTTypeFileURL],
                                           nil]]) {
            CLIPBOARD_LOG("    has %s\n", mimeType.get());
            return true;
          }
        }
      }
    }
  }

  if (CLIPBOARD_LOG_ENABLED()) {
    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];

    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 data.
      CFMutableDataRef tiffData = CFDataCreateMutable(kCFAllocatorDefault, 0);
      CGImageDestinationRef destRef = CGImageDestinationCreateWithData(
          tiffData, CFSTR("public.tiff"), 1, NULL);
      CGImageDestinationAddImage(destRef, imageRef, NULL);
      bool successfullyConverted = CGImageDestinationFinalize(destRef);

      CGImageRelease(imageRef);
      if (destRef) {
        CFRelease(destRef);
      }

      if (!successfullyConverted || !tiffData) {
        if (tiffData) {
          CFRelease(tiffData);
        }
        continue;
      }

      NSString* tiffType =
          [UTIHelper stringFromPboardType:NSPasteboardTypeTIFF];
      [pasteboardOutputDict setObject:(NSMutableData*)tiffData forKey:tiffType];
      CFRelease(tiffData);
    } 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];
    } 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 nsCString& 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(int32_t 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(int32_t 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));
}
