/* 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 "FileSystemWritableFileStream.h"

#include "fs/FileSystemAsyncCopy.h"
#include "fs/FileSystemShutdownBlocker.h"
#include "fs/FileSystemThreadSafeStreamOwner.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/InputStreamLengthHelper.h"
#include "mozilla/MozPromise.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/TaskQueue.h"
#include "mozilla/dom/Blob.h"
#include "mozilla/dom/FileSystemHandle.h"
#include "mozilla/dom/FileSystemLog.h"
#include "mozilla/dom/FileSystemManager.h"
#include "mozilla/dom/FileSystemWritableFileStreamBinding.h"
#include "mozilla/dom/FileSystemWritableFileStreamChild.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/dom/WritableStreamDefaultController.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/TargetPtrHolder.h"
#include "mozilla/ipc/RandomAccessStreamUtils.h"
#include "nsAsyncStreamCopier.h"
#include "nsIInputStream.h"
#include "nsIRequestObserver.h"
#include "nsISupportsImpl.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsStreamUtils.h"
#include "nsStringStream.h"

namespace mozilla::dom {

namespace {

CopyableErrorResult RejectWithConvertedErrors(nsresult aRv) {
  CopyableErrorResult err;
  switch (aRv) {
    case NS_ERROR_DOM_FILE_NOT_FOUND_ERR:
      [[fallthrough]];
    case NS_ERROR_FILE_NOT_FOUND:
      err.ThrowNotFoundError("File not found");
      break;
    case NS_ERROR_FILE_NO_DEVICE_SPACE:
      err.ThrowQuotaExceededError("Quota exceeded");
      break;
    default:
      err.Throw(aRv);
  }

  return err;
}

RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise(
    const Int64Promise::ResolveOrRejectValue& aValue) {
  MOZ_ASSERT(aValue.IsResolve());
  return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve(
      Some(aValue.ResolveValue()), __func__);
}

RefPtr<FileSystemWritableFileStream::WriteDataPromise> ResolvePromise(
    const BoolPromise::ResolveOrRejectValue& aValue) {
  MOZ_ASSERT(aValue.IsResolve());
  return FileSystemWritableFileStream::WriteDataPromise::CreateAndResolve(
      Nothing(), __func__);
}

class WritableFileStreamUnderlyingSinkAlgorithms final
    : public UnderlyingSinkAlgorithmsWrapper {
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
      WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)

  explicit WritableFileStreamUnderlyingSinkAlgorithms(
      FileSystemWritableFileStream& aStream)
      : mStream(&aStream) {}

  already_AddRefed<Promise> WriteCallbackImpl(
      JSContext* aCx, JS::Handle<JS::Value> aChunk,
      WritableStreamDefaultController& aController, ErrorResult& aRv) override;

  already_AddRefed<Promise> CloseCallbackImpl(JSContext* aCx,
                                              ErrorResult& aRv) override;

  already_AddRefed<Promise> AbortCallbackImpl(
      JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
      ErrorResult& aRv) override;

  void ReleaseObjects() override;

 private:
  ~WritableFileStreamUnderlyingSinkAlgorithms() = default;

  RefPtr<FileSystemWritableFileStream> mStream;
};

}  // namespace

class FileSystemWritableFileStream::Command {
 public:
  explicit Command(RefPtr<FileSystemWritableFileStream> aWritableFileStream)
      : mWritableFileStream(std::move(aWritableFileStream)) {
    MOZ_ASSERT(mWritableFileStream);
  }

  NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::Command)

 private:
  ~Command() { mWritableFileStream->NoteFinishedCommand(); }

  RefPtr<FileSystemWritableFileStream> mWritableFileStream;
};

class FileSystemWritableFileStream::CloseHandler {
  enum struct State : uint8_t { Initial = 0, Open, Closing, Closed };

 public:
  CloseHandler()
      : mShutdownBlocker(fs::FileSystemShutdownBlocker::CreateForWritable()),
        mClosePromiseHolder(),
        mState(State::Initial) {}

  NS_INLINE_DECL_REFCOUNTING(FileSystemWritableFileStream::CloseHandler)

  /**
   * @brief Are we not yet closing?
   */
  bool IsOpen() const { return State::Open == mState; }

  /**
   * @brief Are we not open and not closed?
   */
  bool IsClosing() const { return State::Closing == mState; }

  /**
   * @brief Are we already fully closed?
   */
  bool IsClosed() const { return State::Closed == mState; }

  /**
   * @brief Transition from open to closing state
   *
   * @return true if the state was open and became closing after the call
   * @return false in all the other cases the previous state is preserved
   */
  bool SetClosing() {
    const bool isOpen = State::Open == mState;

    if (isOpen) {
      mState = State::Closing;
    }

    return isOpen;
  }

  RefPtr<BoolPromise> GetClosePromise() const {
    MOZ_ASSERT(State::Open != mState,
               "Please call SetClosing before GetClosePromise");

    if (State::Closing == mState) {
      return mClosePromiseHolder.Ensure(__func__);
    }

    // Instant resolve for initial state due to early shutdown or closed state
    return BoolPromise::CreateAndResolve(true, __func__);
  }

  /**
   * @brief Transition from initial to open state. In initial state
   *
   */
  void Open(std::function<void()>&& aCallback) {
    MOZ_ASSERT(State::Initial == mState);

    mShutdownBlocker->SetCallback(std::move(aCallback));
    mShutdownBlocker->Block();

    mState = State::Open;
  }

  /**
   * @brief Transition to closed state and resolve all pending promises.
   *
   */
  void Close() {
    mShutdownBlocker->Unblock();
    mShutdownBlocker = nullptr;
    mState = State::Closed;
    mClosePromiseHolder.ResolveIfExists(true, __func__);
  }

 protected:
  virtual ~CloseHandler() = default;

 private:
  RefPtr<fs::FileSystemShutdownBlocker> mShutdownBlocker;

  mutable MozPromiseHolder<BoolPromise> mClosePromiseHolder;

  State mState;
};

FileSystemWritableFileStream::FileSystemWritableFileStream(
    const nsCOMPtr<nsIGlobalObject>& aGlobal,
    RefPtr<FileSystemManager>& aManager,
    mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
    RefPtr<FileSystemWritableFileStreamChild> aActor,
    already_AddRefed<TaskQueue> aTaskQueue,
    const fs::FileSystemEntryMetadata& aMetadata)
    : WritableStream(aGlobal, HoldDropJSObjectsCaller::Explicit),
      mManager(aManager),
      mActor(std::move(aActor)),
      mTaskQueue(aTaskQueue),
      mStreamParams(std::move(aStreamParams)),
      mMetadata(std::move(aMetadata)),
      mCloseHandler(MakeAndAddRef<CloseHandler>()),
      mCommandActive(false) {
  LOG(("Created WritableFileStream %p", this));

  // Connect with the actor directly in the constructor. This way the actor
  // can call `FileSystemWritableFileStream::ClearActor` when we call
  // `PFileSystemWritableFileStreamChild::Send__delete__` even when
  // FileSystemWritableFileStream::Create fails, in which case the not yet
  // fully constructed FileSystemWritableFileStream is being destroyed.
  mActor->SetStream(this);

  mozilla::HoldJSObjects(this);
}

FileSystemWritableFileStream::~FileSystemWritableFileStream() {
  MOZ_ASSERT(!mCommandActive);
  MOZ_ASSERT(IsDone());

  mozilla::DropJSObjects(this);
}

// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
// * This is fallible because of OOM handling of JSAPI. See bug 1762233.
// XXX(krosylight): _BOUNDARY because SetUpNative here can't run script because
// StartCallback here is no-op. Can we let the static check automatically detect
// this situation?
/* static */
MOZ_CAN_RUN_SCRIPT_BOUNDARY
Result<RefPtr<FileSystemWritableFileStream>, nsresult>
FileSystemWritableFileStream::Create(
    const nsCOMPtr<nsIGlobalObject>& aGlobal,
    RefPtr<FileSystemManager>& aManager,
    mozilla::ipc::RandomAccessStreamParams&& aStreamParams,
    RefPtr<FileSystemWritableFileStreamChild> aActor,
    const fs::FileSystemEntryMetadata& aMetadata) {
  MOZ_ASSERT(aGlobal);

  QM_TRY_UNWRAP(auto streamTransportService,
                MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
                                        MOZ_SELECT_OVERLOAD(do_GetService),
                                        NS_STREAMTRANSPORTSERVICE_CONTRACTID));

  RefPtr<TaskQueue> taskQueue =
      TaskQueue::Create(streamTransportService.forget(), "WritableStreamQueue");
  MOZ_ASSERT(taskQueue);

  AutoJSAPI jsapi;
  if (!jsapi.Init(aGlobal)) {
    return Err(NS_ERROR_FAILURE);
  }
  JSContext* cx = jsapi.cx();

  // Step 1. Let stream be a new FileSystemWritableFileStream in realm.
  // Step 2. Set stream.[[file]] to file. (Covered by the constructor)
  RefPtr<FileSystemWritableFileStream> stream =
      new FileSystemWritableFileStream(
          aGlobal, aManager, std::move(aStreamParams), std::move(aActor),
          taskQueue.forget(), aMetadata);

  auto autoClose = MakeScopeExit([stream] {
    stream->mCloseHandler->Close();
    stream->mActor->SendClose(/* aAbort */ true);
  });

  QM_TRY_UNWRAP(
      RefPtr<StrongWorkerRef> workerRef,
      ([stream]() -> Result<RefPtr<StrongWorkerRef>, nsresult> {
        WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate();
        if (!workerPrivate) {
          return RefPtr<StrongWorkerRef>();
        }

        RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
            workerPrivate, "FileSystemWritableFileStream::Create", [stream]() {
              if (stream->IsOpen()) {
                // We don't need the promise, we just
                // begin the closing process.
                (void)stream->BeginAbort();
              }
            });
        QM_TRY(MOZ_TO_RESULT(workerRef));

        return workerRef;
      }()));

  // Step 3 - 5
  auto algorithms =
      MakeRefPtr<WritableFileStreamUnderlyingSinkAlgorithms>(*stream);

  // Step 8: Set up stream with writeAlgorithm set to writeAlgorithm,
  // closeAlgorithm set to closeAlgorithm, abortAlgorithm set to
  // abortAlgorithm, highWaterMark set to highWaterMark, and
  // sizeAlgorithm set to sizeAlgorithm.
  IgnoredErrorResult rv;
  stream->SetUpNative(cx, *algorithms,
                      // Step 6. Let highWaterMark be 1.
                      Some(1),
                      // Step 7. Let sizeAlgorithm be an algorithm
                      // that returns 1. (nullptr returns 1, See
                      // WritableStream::Constructor for details)
                      nullptr, rv);
  if (rv.Failed()) {
    return Err(rv.StealNSResult());
  }

  autoClose.release();

  stream->mWorkerRef = std::move(workerRef);

  // The close handler passes this callback to `FileSystemShutdownBlocker`
  // which has separate handling for debug and release builds. Basically,
  // there's no content process shutdown blocking in release builds, so the
  // callback might be executed in debug builds only.
  stream->mCloseHandler->Open([stream]() {
    if (stream->IsOpen()) {
      // We don't need the promise, we just begin the closing process.
      (void)stream->BeginAbort();
    }
  });

  // Step 9: Return stream.
  return stream;
}

NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemWritableFileStream,
                                               WritableStream)

NS_IMPL_CYCLE_COLLECTION_CLASS(FileSystemWritableFileStream)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileSystemWritableFileStream,
                                                WritableStream)
  // Per the comment for the FileSystemManager class, don't unlink mManager!
  if (tmp->IsOpen()) {
    (void)tmp->BeginAbort();
  }
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileSystemWritableFileStream,
                                                  WritableStream)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

void FileSystemWritableFileStream::LastRelease() {
  // We can't call `FileSystemWritableFileStream::Close` here because it may
  // need to keep FileSystemWritableFileStream object alive which isn't possible
  // when the object is about to be deleted. There are other mechanisms which
  // ensure that the object is correctly closed before destruction. For example
  // the object unlinking and the worker shutdown (we get notified about it via
  // the callback passed to `StrongWorkerRef`) are used to close the object if
  // it hasn't been closed yet.

  if (mActor) {
    PFileSystemWritableFileStreamChild::Send__delete__(mActor);
    MOZ_ASSERT(!mActor);
  }
}

RefPtr<FileSystemWritableFileStream::Command>
FileSystemWritableFileStream::CreateCommand() {
  MOZ_ASSERT(!mCommandActive);

  mCommandActive = true;

  return MakeRefPtr<Command>(this);
}

bool FileSystemWritableFileStream::IsCommandActive() const {
  return mCommandActive;
}

void FileSystemWritableFileStream::ClearActor() {
  MOZ_ASSERT(mActor);

  mActor = nullptr;
}

bool FileSystemWritableFileStream::IsOpen() const {
  return mCloseHandler->IsOpen();
}

bool FileSystemWritableFileStream::IsFinishing() const {
  return mCloseHandler->IsClosing();
}

bool FileSystemWritableFileStream::IsDone() const {
  return mCloseHandler->IsClosed();
}

RefPtr<BoolPromise> FileSystemWritableFileStream::BeginFinishing(
    bool aShouldAbort) {
  using ClosePromise = PFileSystemWritableFileStreamChild::ClosePromise;
  MOZ_ASSERT(IsOpen());

  if (mCloseHandler->SetClosing()) {
    Finish()
        ->Then(mTaskQueue, __func__,
               [selfHolder = quota::TargetPtrHolder(this)]() mutable {
                 if (selfHolder->mStreamOwner) {
                   selfHolder->mStreamOwner->Close();
                 } else {
                   // If the stream was not deserialized, `mStreamParams` still
                   // contains a pre-opened file descriptor which needs to be
                   // closed here by moving `mStreamParams` to a local variable
                   // (the file descriptor will be closed for real when
                   // `streamParams` goes out of scope).

                   mozilla::ipc::RandomAccessStreamParams streamParams(
                       std::move(selfHolder->mStreamParams));
                 }

                 return BoolPromise::CreateAndResolve(true, __func__);
               })
        ->Then(GetCurrentSerialEventTarget(), __func__,
               [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) {
                 return self->mTaskQueue->BeginShutdown();
               })
        ->Then(GetCurrentSerialEventTarget(), __func__,
               [aShouldAbort, self = RefPtr(this)](
                   const ShutdownPromise::ResolveOrRejectValue& /* aValue */) {
                 if (!self->mActor) {
                   return ClosePromise::CreateAndResolve(void_t(), __func__);
                 }

                 return self->mActor->SendClose(aShouldAbort);
               })
        ->Then(GetCurrentSerialEventTarget(), __func__,
               [self = RefPtr(this)](
                   const ClosePromise::ResolveOrRejectValue& aValue) {
                 self->mWorkerRef = nullptr;
                 self->mCloseHandler->Close();

                 QM_TRY(OkIf(aValue.IsResolve()), QM_VOID);
               });
  }

  return mCloseHandler->GetClosePromise();
}

RefPtr<BoolPromise> FileSystemWritableFileStream::BeginClose() {
  MOZ_ASSERT(IsOpen());
  return BeginFinishing(/* aShouldAbort */ false);
}

RefPtr<BoolPromise> FileSystemWritableFileStream::BeginAbort() {
  MOZ_ASSERT(IsOpen());
  return BeginFinishing(/* aShouldAbort */ true);
}

RefPtr<BoolPromise> FileSystemWritableFileStream::OnDone() {
  MOZ_ASSERT(!IsOpen());

  return mCloseHandler->GetClosePromise();
}

already_AddRefed<Promise> FileSystemWritableFileStream::Write(
    JSContext* aCx, JS::Handle<JS::Value> aChunk, ErrorResult& aError) {
  // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
  // Step 3. Let writeAlgorithm be an algorithm which takes a chunk argument
  // and returns the result of running the write a chunk algorithm with stream
  // and chunk.

  aError.MightThrowJSException();

  // https://fs.spec.whatwg.org/#write-a-chunk
  // Step 1. Let input be the result of converting chunk to a
  // FileSystemWriteChunkType.

  ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams data;
  if (!data.Init(aCx, aChunk)) {
    aError.StealExceptionFromJSContext(aCx);
    // XXX(krosylight): The Streams spec does not provide a way to catch errors
    // thrown from the write algorithm, and the File System spec does not try
    // catching them either. This is unfortunate, as per the spec the type error
    // from write() must immediately return a rejected promise without any file
    // handle closure. For now we handle it here manually. See also:
    // - https://github.com/whatwg/streams/issues/636
    // - https://github.com/whatwg/fs/issues/153
    if (IsOpen()) {
      (void)BeginAbort();
    }
    return nullptr;
  }

  // Step 2. Let p be a new promise.
  RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject());

  RefPtr<Command> command = CreateCommand();

  // Step 3.3.
  // XXX: This should ideally be also handled by the streams but we don't
  // currently have the hook. See https://github.com/whatwg/streams/issues/620.
  Write(data)->Then(
      GetCurrentSerialEventTarget(), __func__,
      [self = RefPtr{this}, command,
       promise](const WriteDataPromise::ResolveOrRejectValue& aValue) {
        MOZ_ASSERT(!aValue.IsNothing());
        if (aValue.IsResolve()) {
          const Maybe<int64_t>& maybeWritten = aValue.ResolveValue();
          if (maybeWritten.isSome()) {
            promise->MaybeResolve(maybeWritten.value());
            return;
          }

          promise->MaybeResolveWithUndefined();
          return;
        }

        CopyableErrorResult err = aValue.RejectValue();

        if (self->IsOpen()) {
          self->BeginAbort()->Then(
              GetCurrentSerialEventTarget(), __func__,
              [promise, err = std::move(err)](
                  const BoolPromise::ResolveOrRejectValue&) mutable {
                // Do not capture command to this context:
                // close cannot proceed
                promise->MaybeReject(std::move(err));
              });
        } else if (self->IsFinishing()) {
          self->OnDone()->Then(
              GetCurrentSerialEventTarget(), __func__,
              [promise, err = std::move(err)](
                  const BoolPromise::ResolveOrRejectValue&) mutable {
                // Do not capture command to this context:
                // close cannot proceed
                promise->MaybeReject(std::move(err));
              });

        } else {
          promise->MaybeReject(std::move(err));
        }
      });

  return promise.forget();
}

RefPtr<FileSystemWritableFileStream::WriteDataPromise>
FileSystemWritableFileStream::Write(
    ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData) {
  auto rejectWithTypeError = [](const auto& aMessage) {
    CopyableErrorResult err;
    err.ThrowTypeError(aMessage);
    return WriteDataPromise::CreateAndReject(err, __func__);
  };

  auto rejectWithSyntaxError = [](const auto& aMessage) {
    CopyableErrorResult err;
    err.ThrowSyntaxError(aMessage);
    return WriteDataPromise::CreateAndReject(err, __func__);
  };

  if (!IsOpen()) {
    return rejectWithTypeError("WritableFileStream closed");
  }

  auto tryResolve = [self = RefPtr{this}](const auto& aValue)
      -> RefPtr<FileSystemWritableFileStream::WriteDataPromise> {
    MOZ_ASSERT(self->IsCommandActive());

    if (aValue.IsResolve()) {
      return ResolvePromise(aValue);
    }

    MOZ_ASSERT(aValue.IsReject());
    return WriteDataPromise::CreateAndReject(
        RejectWithConvertedErrors(aValue.RejectValue()), __func__);
  };

  auto tryResolveInt64 =
      [tryResolve](const Int64Promise::ResolveOrRejectValue& aValue) {
        return tryResolve(aValue);
      };

  auto tryResolveBool =
      [tryResolve](const BoolPromise::ResolveOrRejectValue& aValue) {
        return tryResolve(aValue);
      };

  // Step 3.3. Let command be input.type if input is a WriteParams, ...
  if (aData.IsWriteParams()) {
    const WriteParams& params = aData.GetAsWriteParams();
    switch (params.mType) {
      // Step 3.4. If command is "write":
      case WriteCommandType::Write: {
        if (!params.mData.WasPassed()) {
          return rejectWithSyntaxError("write() requires data");
        }

        // Step 3.4.2. If data is undefined, reject p with a TypeError and
        // abort.
        if (params.mData.Value().IsNull()) {
          return rejectWithTypeError("write() of null data");
        }

        Maybe<uint64_t> position;

        if (params.mPosition.WasPassed()) {
          if (params.mPosition.Value().IsNull()) {
            return rejectWithTypeError("write() with null position");
          }

          position = Some(params.mPosition.Value().Value());
        }

        return Write(params.mData.Value().Value(), position)
            ->Then(GetCurrentSerialEventTarget(), __func__,
                   std::move(tryResolveInt64));
      }

      // Step 3.5. Otherwise, if command is "seek":
      case WriteCommandType::Seek:
        if (!params.mPosition.WasPassed()) {
          return rejectWithSyntaxError("seek() requires a position");
        }

        // Step 3.5.1. If chunk.position is undefined, reject p with a
        // TypeError and abort.
        if (params.mPosition.Value().IsNull()) {
          return rejectWithTypeError("seek() with null position");
        }

        return Seek(params.mPosition.Value().Value())
            ->Then(GetCurrentSerialEventTarget(), __func__,
                   std::move(tryResolveBool));

      // Step 3.6. Otherwise, if command is "truncate":
      case WriteCommandType::Truncate:
        if (!params.mSize.WasPassed()) {
          return rejectWithSyntaxError("truncate() requires a size");
        }

        // Step 3.6.1. If chunk.size is undefined, reject p with a TypeError
        // and abort.
        if (params.mSize.Value().IsNull()) {
          return rejectWithTypeError("truncate() with null size");
        }

        return Truncate(params.mSize.Value().Value())
            ->Then(GetCurrentSerialEventTarget(), __func__,
                   std::move(tryResolveBool));

      default:
        MOZ_CRASH("Bad WriteParams value!");
    }
  }

  // Step 3.3. ... and "write" otherwise.
  // Step 3.4. If command is "write":
  return Write(aData, Nothing())
      ->Then(GetCurrentSerialEventTarget(), __func__,
             std::move(tryResolveInt64));
}

// WebIDL Boilerplate

JSObject* FileSystemWritableFileStream::WrapObject(
    JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
  return FileSystemWritableFileStream_Binding::Wrap(aCx, this, aGivenProto);
}

// WebIDL Interface

already_AddRefed<Promise> FileSystemWritableFileStream::Write(
    const ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData,
    ErrorResult& aError) {
  // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-write
  // Step 1. Let writer be the result of getting a writer for this.
  RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
  if (aError.Failed()) {
    return nullptr;
  }

  // Step 2. Let result be the result of writing a chunk to writer given data.
  AutoJSAPI jsapi;
  if (!jsapi.Init(GetParentObject())) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  JSContext* cx = jsapi.cx();

  JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));

  JS::Rooted<JS::Value> val(cx);
  if (!aData.ToJSVal(cx, global, &val)) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  RefPtr<Promise> promise = writer->Write(cx, val, aError);

  // Step 3. Release writer.
  writer->ReleaseLock(cx);

  // Step 4. Return result.
  return promise.forget();
}

already_AddRefed<Promise> FileSystemWritableFileStream::Seek(
    uint64_t aPosition, ErrorResult& aError) {
  // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-seek
  // Step 1. Let writer be the result of getting a writer for this.
  RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
  if (aError.Failed()) {
    return nullptr;
  }

  // Step 2. Let result be the result of writing a chunk to writer given
  // «[ "type" → "seek", "position" → position ]».
  AutoJSAPI jsapi;
  if (!jsapi.Init(GetParentObject())) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  JSContext* cx = jsapi.cx();

  RootedDictionary<WriteParams> writeParams(cx);
  writeParams.mType = WriteCommandType::Seek;
  writeParams.mPosition.Construct(aPosition);

  JS::Rooted<JS::Value> val(cx);
  if (!ToJSValue(cx, writeParams, &val)) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  RefPtr<Promise> promise = writer->Write(cx, val, aError);

  // Step 3. Release writer.
  writer->ReleaseLock(cx);

  // Step 4. Return result.
  return promise.forget();
}

already_AddRefed<Promise> FileSystemWritableFileStream::Truncate(
    uint64_t aSize, ErrorResult& aError) {
  // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-truncate
  // Step 1. Let writer be the result of getting a writer for this.
  RefPtr<WritableStreamDefaultWriter> writer = GetWriter(aError);
  if (aError.Failed()) {
    return nullptr;
  }

  // Step 2. Let result be the result of writing a chunk to writer given
  // «[ "type" → "truncate", "size" → size ]».
  AutoJSAPI jsapi;
  if (!jsapi.Init(GetParentObject())) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  JSContext* cx = jsapi.cx();

  RootedDictionary<WriteParams> writeParams(cx);
  writeParams.mType = WriteCommandType::Truncate;
  writeParams.mSize.Construct(aSize);

  JS::Rooted<JS::Value> val(cx);
  if (!ToJSValue(cx, writeParams, &val)) {
    aError.ThrowUnknownError("Internal error");
    return nullptr;
  }

  RefPtr<Promise> promise = writer->Write(cx, val, aError);

  // Step 3. Release writer.
  writer->ReleaseLock(cx);

  // Step 4. Return result.
  return promise.forget();
}

template <typename T>
RefPtr<Int64Promise> FileSystemWritableFileStream::Write(
    const T& aData, const Maybe<uint64_t> aPosition) {
  MOZ_ASSERT(IsOpen());

  nsCOMPtr<nsIInputStream> inputStream;

  // https://fs.spec.whatwg.org/#write-a-chunk
  // Step 3.4.6 If data is a BufferSource, let dataBytes be a copy of data.
  auto vectorFromTypedArray = CreateFromTypedArrayData<Vector<uint8_t>>(aData);
  if (vectorFromTypedArray.isSome()) {
    Maybe<Vector<uint8_t>>& maybeVector = vectorFromTypedArray.ref();
    QM_TRY(MOZ_TO_RESULT(maybeVector.isSome()), CreateAndRejectInt64Promise);

    // Here we copy

    size_t length = maybeVector->length();
    QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream(
               getter_AddRefs(inputStream),
               AsChars(Span(maybeVector->extractOrCopyRawBuffer(), length)),
               NS_ASSIGNMENT_ADOPT)),
           CreateAndRejectInt64Promise);

    return WriteImpl(std::move(inputStream), aPosition);
  }

  // Step 3.4.7 Otherwise, if data is a Blob ...
  if (aData.IsBlob()) {
    Blob& blob = aData.GetAsBlob();

    ErrorResult error;
    blob.CreateInputStream(getter_AddRefs(inputStream), error);
    QM_TRY((MOZ_TO_RESULT(!error.Failed()).mapErr([&error](const nsresult rv) {
             return error.StealNSResult();
           })),
           CreateAndRejectInt64Promise);

    return WriteImpl(std::move(inputStream), aPosition);
  }

  // Step 3.4.8 Otherwise ...
  MOZ_ASSERT(aData.IsUTF8String());

  // Here we copy
  nsCString dataString;
  if (!dataString.Assign(aData.GetAsUTF8String(), mozilla::fallible)) {
    return Int64Promise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
  }

  // Input stream takes ownership
  QM_TRY(MOZ_TO_RESULT(NS_NewCStringInputStream(getter_AddRefs(inputStream),
                                                std::move(dataString))),
         CreateAndRejectInt64Promise);

  return WriteImpl(std::move(inputStream), aPosition);
}

RefPtr<Int64Promise> FileSystemWritableFileStream::WriteImpl(
    nsCOMPtr<nsIInputStream> aInputStream, const Maybe<uint64_t> aPosition) {
  return InvokeAsync(
      mTaskQueue, __func__,
      [selfHolder = quota::TargetPtrHolder(this),
       inputStream = std::move(aInputStream), aPosition]() {
        QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
               CreateAndRejectInt64Promise);

        if (aPosition.isSome()) {
          LOG(("%p: Seeking to %" PRIu64, selfHolder->mStreamOwner.get(),
               aPosition.value()));

          QM_TRY(
              MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition.value())),
              CreateAndRejectInt64Promise);
        }

        nsCOMPtr<nsIOutputStream> streamSink =
            selfHolder->mStreamOwner->OutputStream();

        auto written = std::make_shared<int64_t>(0);
        auto writingProgress = [written](uint32_t aDelta) {
          *written += static_cast<int64_t>(aDelta);
        };

        auto promiseHolder = MakeUnique<MozPromiseHolder<Int64Promise>>();
        RefPtr<Int64Promise> promise = promiseHolder->Ensure(__func__);

        auto writingCompletion =
            [written,
             promiseHolder = std::move(promiseHolder)](nsresult aStatus) {
              if (NS_SUCCEEDED(aStatus)) {
                promiseHolder->ResolveIfExists(*written, __func__);
                return;
              }

              promiseHolder->RejectIfExists(aStatus, __func__);
            };

        QM_TRY(MOZ_TO_RESULT(fs::AsyncCopy(
                   inputStream, streamSink, selfHolder->mTaskQueue,
                   nsAsyncCopyMode::NS_ASYNCCOPY_VIA_READSEGMENTS,
                   /* aCloseSource */ true, /* aCloseSink */ false,
                   std::move(writingProgress), std::move(writingCompletion))),
               CreateAndRejectInt64Promise);

        return promise;
      });
}

RefPtr<BoolPromise> FileSystemWritableFileStream::Seek(uint64_t aPosition) {
  MOZ_ASSERT(IsOpen());

  LOG_VERBOSE(("%p: Seeking to %" PRIu64, mStreamOwner.get(), aPosition));

  return InvokeAsync(
      mTaskQueue, __func__,
      [selfHolder = quota::TargetPtrHolder(this), aPosition]() mutable {
        QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
               CreateAndRejectBoolPromise);

        QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Seek(aPosition)),
               CreateAndRejectBoolPromise);

        return BoolPromise::CreateAndResolve(true, __func__);
      });
}

RefPtr<BoolPromise> FileSystemWritableFileStream::Truncate(uint64_t aSize) {
  MOZ_ASSERT(IsOpen());

  return InvokeAsync(
      mTaskQueue, __func__,
      [selfHolder = quota::TargetPtrHolder(this), aSize]() mutable {
        QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()),
               CreateAndRejectBoolPromise);

        QM_TRY(MOZ_TO_RESULT(selfHolder->mStreamOwner->Truncate(aSize)),
               CreateAndRejectBoolPromise);

        return BoolPromise::CreateAndResolve(true, __func__);
      });
}

nsresult FileSystemWritableFileStream::EnsureStream() {
  if (!mStreamOwner) {
    QM_TRY_UNWRAP(MovingNotNull<nsCOMPtr<nsIRandomAccessStream>> stream,
                  DeserializeRandomAccessStream(mStreamParams),
                  NS_ERROR_FAILURE);

    mozilla::ipc::RandomAccessStreamParams streamParams(
        std::move(mStreamParams));

    mStreamOwner = MakeRefPtr<fs::FileSystemThreadSafeStreamOwner>(
        this, std::move(stream));
  }

  return NS_OK;
}

void FileSystemWritableFileStream::NoteFinishedCommand() {
  MOZ_ASSERT(mCommandActive);

  mCommandActive = false;

  mFinishPromiseHolder.ResolveIfExists(true, __func__);
}

RefPtr<BoolPromise> FileSystemWritableFileStream::Finish() {
  if (!mCommandActive) {
    return BoolPromise::CreateAndResolve(true, __func__);
  }

  return mFinishPromiseHolder.Ensure(__func__);
}

NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
    WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase)
NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms,
                                   UnderlyingSinkAlgorithmsBase, mStream)

// Step 3 of
// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
already_AddRefed<Promise>
WritableFileStreamUnderlyingSinkAlgorithms::WriteCallbackImpl(
    JSContext* aCx, JS::Handle<JS::Value> aChunk,
    WritableStreamDefaultController& aController, ErrorResult& aRv) {
  return mStream->Write(aCx, aChunk, aRv);
}

// Step 4 of
// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
already_AddRefed<Promise>
WritableFileStreamUnderlyingSinkAlgorithms::CloseCallbackImpl(
    JSContext* aCx, ErrorResult& aRv) {
  RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  if (!mStream->IsOpen()) {
    promise->MaybeRejectWithTypeError("WritableFileStream closed");
    return promise.forget();
  }

  mStream->BeginClose()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [promise](const BoolPromise::ResolveOrRejectValue& aValue) {
        // Step 2.3. Return a promise resolved with undefined.
        if (aValue.IsResolve()) {
          promise->MaybeResolveWithUndefined();
          return;
        }
        promise->MaybeRejectWithAbortError(
            "Internal error closing file stream");
      });

  return promise.forget();
}

// Step 5 of
// https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream
already_AddRefed<Promise>
WritableFileStreamUnderlyingSinkAlgorithms::AbortCallbackImpl(
    JSContext* aCx, const Optional<JS::Handle<JS::Value>>& /* aReason */,
    ErrorResult& aRv) {
  // https://streams.spec.whatwg.org/#writablestream-set-up
  // Step 3. Let abortAlgorithmWrapper be an algorithm that runs these steps:
  // Step 3.3. Return a promise resolved with undefined.

  RefPtr<Promise> promise = Promise::Create(mStream->GetParentObject(), aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  if (!mStream->IsOpen()) {
    promise->MaybeRejectWithTypeError("WritableFileStream closed");
    return promise.forget();
  }

  mStream->BeginAbort()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [promise](const BoolPromise::ResolveOrRejectValue& aValue) {
        // Step 2.3. Return a promise resolved with undefined.
        if (aValue.IsResolve()) {
          promise->MaybeResolveWithUndefined();
          return;
        }
        promise->MaybeRejectWithAbortError(
            "Internal error closing file stream");
      });

  return promise.forget();
}

void WritableFileStreamUnderlyingSinkAlgorithms::ReleaseObjects() {
  // WritableStream transitions to errored state whenever a rejected promise is
  // returned. At the end of the transition, ReleaseObjects is called.
  // Because there is no way to release the locks synchronously,
  // we assume this has been initiated before the rejected promise is returned.
  MOZ_ASSERT(!mStream->IsOpen());
}

}  // namespace mozilla::dom
