/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "mozilla/dom/ClipboardItem.h"

#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Record.h"
#include "nsComponentManagerUtils.h"
#include "nsIClipboard.h"
#include "nsIInputStream.h"
#include "nsISupportsPrimitives.h"
#include "nsNetUtil.h"
#include "nsServiceManagerUtils.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION(ClipboardItem::ItemEntry, mGlobal, mData,
                         mPendingGetTypeRequests)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ClipboardItem::ItemEntry)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(ClipboardItem::ItemEntry)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ClipboardItem::ItemEntry)

void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx,
                                                JS::Handle<JS::Value> aValue,
                                                ErrorResult& aRv) {
  MOZ_ASSERT(!mLoadingPromise.Exists());
  mIsLoadingData = false;
  OwningStringOrBlob clipboardData;
  if (!clipboardData.Init(aCx, aValue)) {
    JS_ClearPendingException(aCx);
    RejectPendingPromises(NS_ERROR_DOM_DATA_ERR);
    return;
  }

  MaybeResolvePendingPromises(std::move(clipboardData));
}

void ClipboardItem::ItemEntry::RejectedCallback(JSContext* aCx,
                                                JS::Handle<JS::Value> aValue,
                                                ErrorResult& aRv) {
  MOZ_ASSERT(!mLoadingPromise.Exists());
  mIsLoadingData = false;
  RejectPendingPromises(NS_ERROR_DOM_DATA_ERR);
}

RefPtr<ClipboardItem::ItemEntry::GetDataPromise>
ClipboardItem::ItemEntry::GetData() {
  // Data is still being loaded, either from the system clipboard or the data
  // promise provided to the ClipboardItem constructor.
  if (mIsLoadingData) {
    MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(),
               "Data should be uninitialized");
    MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result");

    MozPromiseHolder<GetDataPromise> holder;
    RefPtr<GetDataPromise> promise = holder.Ensure(__func__);
    mPendingGetDataRequests.AppendElement(std::move(holder));
    return promise.forget();
  }

  if (NS_FAILED(mLoadResult.value())) {
    MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(),
               "Data should be uninitialized");
    // We are not able to load data, so reject the promise directly.
    return GetDataPromise::CreateAndReject(mLoadResult.value(), __func__);
  }

  MOZ_ASSERT(mData.IsString() || mData.IsBlob(), "Data should be initialized");
  OwningStringOrBlob data(mData);
  return GetDataPromise::CreateAndResolve(std::move(data), __func__);
}

void ClipboardItem::ItemEntry::LoadDataFromSystemClipboard(
    nsITransferable& aTransferable) {
  // XXX maybe we could consider adding a method to check whether the union
  // object is uninitialized or initialized.
  MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(),
                        "Data should be uninitialized.");
  MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result");
  MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(),
                        "Should not be in the process of loading data");

  nsresult rv;
  nsCOMPtr<nsIClipboard> clipboard(
      do_GetService("@mozilla.org/widget/clipboard;1", &rv));
  if (NS_FAILED(rv)) {
    return;
  }

  mIsLoadingData = true;
  nsCOMPtr<nsITransferable> trans(&aTransferable);
  clipboard->AsyncGetData(trans, nsIClipboard::kGlobalClipboard)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          /* resolved */
          [self = RefPtr{this}, trans]() {
            self->mIsLoadingData = false;
            self->mLoadingPromise.Complete();

            nsCOMPtr<nsISupports> data;
            nsresult rv = trans->GetTransferData(
                NS_ConvertUTF16toUTF8(self->Type()).get(),
                getter_AddRefs(data));
            if (NS_WARN_IF(NS_FAILED(rv))) {
              self->RejectPendingPromises(rv);
              return;
            }

            RefPtr<Blob> blob;
            if (nsCOMPtr<nsISupportsString> supportsstr =
                    do_QueryInterface(data)) {
              nsAutoString str;
              supportsstr->GetData(str);

              blob = Blob::CreateStringBlob(
                  self->mGlobal, NS_ConvertUTF16toUTF8(str), self->Type());
            } else if (nsCOMPtr<nsIInputStream> istream =
                           do_QueryInterface(data)) {
              uint64_t available;
              void* data = nullptr;
              nsresult rv =
                  NS_ReadInputStreamToBuffer(istream, &data, -1, &available);
              if (NS_WARN_IF(NS_FAILED(rv))) {
                self->RejectPendingPromises(rv);
                return;
              }

              blob = Blob::CreateMemoryBlob(self->mGlobal, data, available,
                                            self->Type());
            } else if (nsCOMPtr<nsISupportsCString> supportscstr =
                           do_QueryInterface(data)) {
              nsAutoCString str;
              supportscstr->GetData(str);

              blob = Blob::CreateStringBlob(self->mGlobal, str, self->Type());
            }

            if (!blob) {
              self->RejectPendingPromises(NS_ERROR_DOM_DATA_ERR);
              return;
            }

            OwningStringOrBlob clipboardData;
            clipboardData.SetAsBlob() = std::move(blob);
            self->MaybeResolvePendingPromises(std::move(clipboardData));
          },
          /* rejected */
          [self = RefPtr{this}](nsresult rv) {
            self->mIsLoadingData = false;
            self->mLoadingPromise.Complete();
            self->RejectPendingPromises(rv);
          })
      ->Track(mLoadingPromise);
}

void ClipboardItem::ItemEntry::LoadDataFromDataPromise(Promise& aDataPromise) {
  MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(),
                        "Data should be uninitialized");
  MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result");
  MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(),
                        "Should not be in the process of loading data");

  mIsLoadingData = true;
  aDataPromise.AppendNativeHandler(this);
}

void ClipboardItem::ItemEntry::ReactGetTypePromise(Promise& aPromise) {
  // Data is still being loaded, either from the system clipboard or the data
  // promise provided to the ClipboardItem constructor.
  if (mIsLoadingData) {
    MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(),
               "Data should be uninitialized");
    MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result.");
    mPendingGetTypeRequests.AppendElement(&aPromise);
    return;
  }

  if (NS_FAILED(mLoadResult.value())) {
    MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(),
               "Data should be uninitialized");
    aPromise.MaybeRejectWithDataError("The data for type '"_ns +
                                      NS_ConvertUTF16toUTF8(mType) +
                                      "' was not found"_ns);
    return;
  }

  MaybeResolveGetTypePromise(mData, aPromise);
}

void ClipboardItem::ItemEntry::MaybeResolveGetTypePromise(
    const OwningStringOrBlob& aData, Promise& aPromise) {
  if (aData.IsBlob()) {
    aPromise.MaybeResolve(aData);
    return;
  }

  // XXX This is for the case that data is from ClipboardItem constructor,
  // maybe we should also load that into a Blob earlier. But Safari returns
  // different `Blob` instances for each `getTypes` call if the string is from
  // ClipboardItem constructor, which is more like our current setup.
  if (RefPtr<Blob> blob = Blob::CreateStringBlob(
          mGlobal, NS_ConvertUTF16toUTF8(aData.GetAsString()), mType)) {
    aPromise.MaybeResolve(blob);
    return;
  }

  aPromise.MaybeRejectWithDataError("The data for type '"_ns +
                                    NS_ConvertUTF16toUTF8(mType) +
                                    "' was not found"_ns);
}

void ClipboardItem::ItemEntry::RejectPendingPromises(nsresult aRv) {
  MOZ_ASSERT(NS_FAILED(aRv), "Should have a failure code here");
  MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(),
                        "Data should be uninitialized");
  MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result");
  MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(),
                        "Should not be in the process of loading data");
  mLoadResult.emplace(aRv);
  auto promiseHolders = std::move(mPendingGetDataRequests);
  for (auto& promiseHolder : promiseHolders) {
    promiseHolder.Reject(aRv, __func__);
  }
  auto getTypePromises = std::move(mPendingGetTypeRequests);
  for (auto& promise : getTypePromises) {
    promise->MaybeReject(aRv);
  }
}

void ClipboardItem::ItemEntry::MaybeResolvePendingPromises(
    OwningStringOrBlob&& aData) {
  MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(),
                        "Data should be uninitialized");
  MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result");
  MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(),
                        "Should not be in the process of loading data");
  mLoadResult.emplace(NS_OK);
  mData = std::move(aData);
  auto getDataPromiseHolders = std::move(mPendingGetDataRequests);
  for (auto& promiseHolder : getDataPromiseHolders) {
    OwningStringOrBlob data(mData);
    promiseHolder.Resolve(std::move(data), __func__);
  }
  auto promises = std::move(mPendingGetTypeRequests);
  for (auto& promise : promises) {
    MaybeResolveGetTypePromise(mData, *promise);
  }
}

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ClipboardItem, mOwner, mItems)

ClipboardItem::ClipboardItem(nsISupports* aOwner,
                             const dom::PresentationStyle aPresentationStyle,
                             nsTArray<RefPtr<ItemEntry>>&& aItems)
    : mOwner(aOwner),
      mPresentationStyle(aPresentationStyle),
      mItems(std::move(aItems)) {}

// static
already_AddRefed<ClipboardItem> ClipboardItem::Constructor(
    const GlobalObject& aGlobal,
    const Record<nsString, OwningNonNull<Promise>>& aItems,
    const ClipboardItemOptions& aOptions, ErrorResult& aRv) {
  if (aItems.Entries().IsEmpty()) {
    aRv.ThrowTypeError("At least one entry required");
    return nullptr;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  MOZ_ASSERT(global);

  nsTArray<RefPtr<ItemEntry>> items;
  for (const auto& entry : aItems.Entries()) {
    RefPtr<ItemEntry> item = MakeRefPtr<ItemEntry>(global, entry.mKey);
    item->LoadDataFromDataPromise(*entry.mValue);
    items.AppendElement(std::move(item));
  }

  RefPtr<ClipboardItem> item = MakeRefPtr<ClipboardItem>(
      global, aOptions.mPresentationStyle, std::move(items));
  return item.forget();
}

void ClipboardItem::GetTypes(nsTArray<nsString>& aTypes) const {
  for (const auto& item : mItems) {
    aTypes.AppendElement(item->Type());
  }
}

already_AddRefed<Promise> ClipboardItem::GetType(const nsAString& aType,
                                                 ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
  RefPtr<Promise> p = Promise::Create(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  for (auto& item : mItems) {
    MOZ_ASSERT(item);

    const nsAString& type = item->Type();
    if (type == aType) {
      nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
      if (NS_WARN_IF(!global)) {
        p->MaybeReject(NS_ERROR_UNEXPECTED);
        return p.forget();
      }

      item->ReactGetTypePromise(*p);
      return p.forget();
    }
  }

  p->MaybeRejectWithNotFoundError(
      "The type '"_ns + NS_ConvertUTF16toUTF8(aType) + "' was not found"_ns);
  return p.forget();
}

JSObject* ClipboardItem::WrapObject(JSContext* aCx,
                                    JS::Handle<JSObject*> aGivenProto) {
  return mozilla::dom::ClipboardItem_Binding::Wrap(aCx, this, aGivenProto);
}

}  // namespace mozilla::dom
