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

#include "SerialLogging.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ReadableStream.h"
#include "mozilla/dom/SerialPort.h"
#include "mozilla/dom/SerialPortChild.h"
#include "nsThreadUtils.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION_INHERITED(SerialPortWriteAlgorithms,
                                   WritableStreamToOutputAlgorithms, mPort)
NS_IMPL_ADDREF_INHERITED(SerialPortWriteAlgorithms,
                         WritableStreamToOutputAlgorithms)
NS_IMPL_RELEASE_INHERITED(SerialPortWriteAlgorithms,
                          WritableStreamToOutputAlgorithms)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SerialPortWriteAlgorithms)
NS_INTERFACE_MAP_END_INHERITING(WritableStreamToOutputAlgorithms)

SerialPortWriteAlgorithms::SerialPortWriteAlgorithms(
    nsIGlobalObject* aParent, nsIAsyncOutputStream* aOutput, SerialPort* aPort)
    : WritableStreamToOutputAlgorithms(aParent, aOutput), mPort(aPort) {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortWriteAlgorithms[%p] created for port %p", this, aPort));
}

already_AddRefed<Promise> SerialPortWriteAlgorithms::CloseCallbackImpl(
    JSContext* aCx, ErrorResult& aRv) {
  return CloseOrAbortImpl(true, aRv);
}

already_AddRefed<Promise> SerialPortWriteAlgorithms::AbortCallbackImpl(
    JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
    ErrorResult& aRv) {
  return CloseOrAbortImpl(false, aRv);
}

already_AddRefed<Promise> SerialPortWriteAlgorithms::CloseOrAbortImpl(
    bool aDrain, ErrorResult& aRv) {
  if (!mPort) {
    aRv.ThrowInvalidStateError("Port is not initialized");
    return nullptr;
  }

  RefPtr<SerialPortChild> child = mPort->GetChild();
  if (!child) {
    aRv.ThrowInvalidStateError("Port is not connected");
    return nullptr;
  }

  if (aDrain) {
    MOZ_LOG(
        gWebSerialLog, LogLevel::Debug,
        ("SerialPortWriteAlgorithms[%p]::CloseCallbackImpl draining", this));
    // Close the DataPipe so the parent knows no more data is coming.
    CloseOutput();
  } else {
    MOZ_LOG(
        gWebSerialLog, LogLevel::Debug,
        ("SerialPortWriteAlgorithms[%p]::AbortCallbackImpl flushing", this));
    CloseOutputWithStatus(NS_ERROR_ABORT);
  }

  RefPtr<Promise> promise =
      Promise::CreateInfallible(mPort->GetRelevantGlobal());

  nsCOMPtr<nsISerialEventTarget> owningThread = GetCurrentSerialEventTarget();
  nsISerialEventTarget* actorTarget = child->GetActorEventTarget();

  if (!actorTarget) {
    promise->MaybeRejectWithNetworkError("Actor not available");
    return promise.forget();
  }

  RefPtr<SerialPortWriteAlgorithms> self = this;
  nsCString operationName(aDrain ? "drain" : "flush");
  InvokeAsync(actorTarget,
              aDrain ? "SerialPortWriteAlgorithms::SendDrain"
                     : "SerialPortWriteAlgorithms::SendFlush",
              [child, aDrain]() {
                return aDrain ? child->SendDrain() : child->SendFlush(false);
              })
      ->Then(
          owningThread, __func__,
          [promise, self, aDrain, operationName](nsresult aResult) {
            if (NS_SUCCEEDED(aResult)) {
              MOZ_LOG(gWebSerialLog, LogLevel::Debug,
                      ("SerialPortWriteAlgorithms[%p] %s succeeded", self.get(),
                       operationName.get()));
              promise->MaybeResolveWithUndefined();
            } else {
              MOZ_LOG(gWebSerialLog, LogLevel::Error,
                      ("SerialPortWriteAlgorithms[%p] %s failed: 0x%08x",
                       self.get(), operationName.get(),
                       static_cast<uint32_t>(aResult)));
              if (aDrain) {
                promise->MaybeRejectWithNetworkError(
                    "Failed to drain transmit buffers");
              } else {
                promise->MaybeRejectWithNetworkError(
                    "Failed to discard transmit buffers");
              }
            }
          },
          [promise, self, aDrain,
           operationName](mozilla::ipc::ResponseRejectReason aReason) {
            MOZ_LOG(
                gWebSerialLog, LogLevel::Error,
                ("SerialPortWriteAlgorithms[%p] %s IPC error "
                 "(reason: %d)",
                 self.get(), operationName.get(), static_cast<int>(aReason)));
            if (aDrain) {
              promise->MaybeRejectWithNetworkError(
                  "Failed to drain transmit buffers: IPC "
                  "communication error");
            } else {
              promise->MaybeRejectWithNetworkError(
                  "Failed to discard transmit buffers: IPC "
                  "communication error");
            }
          });

  return promise.forget();
}

SerialPortWriteAlgorithms::~SerialPortWriteAlgorithms() = default;

void SerialPortWriteAlgorithms::ReleaseObjects() {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortWriteAlgorithms[%p]::ReleaseObjects", this));
  WritableStreamToOutputAlgorithms::ReleaseObjects();
  mPort = nullptr;
}

NS_IMPL_CYCLE_COLLECTION_INHERITED(SerialPortReadAlgorithms,
                                   InputToReadableStreamAlgorithms, mPort)
NS_IMPL_ADDREF_INHERITED(SerialPortReadAlgorithms,
                         InputToReadableStreamAlgorithms)
NS_IMPL_RELEASE_INHERITED(SerialPortReadAlgorithms,
                          InputToReadableStreamAlgorithms)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SerialPortReadAlgorithms)
NS_INTERFACE_MAP_END_INHERITING(InputToReadableStreamAlgorithms)

SerialPortReadAlgorithms::SerialPortReadAlgorithms(JSContext* aCx,
                                                   nsIAsyncInputStream* aInput,
                                                   ReadableStream* aStream,
                                                   SerialPort* aPort)
    : InputToReadableStreamAlgorithms(aCx, aInput, aStream),
      mPort(aPort),
      mInputStream(aInput) {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortReadAlgorithms[%p] created for port %p", this, aPort));
}

already_AddRefed<Promise> SerialPortReadAlgorithms::CancelCallbackImpl(
    JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason,
    ErrorResult& aRv) {
  if (!mPort) {
    aRv.ThrowInvalidStateError("Port is not initialized");
    return nullptr;
  }

  RefPtr<SerialPortChild> child = mPort->GetChild();
  if (!child) {
    aRv.ThrowInvalidStateError("Port is not connected");
    return nullptr;
  }

  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortReadAlgorithms[%p]::CancelCallbackImpl flushing receive",
           this));

  // Close the DataPipe to signal cancellation to the parent.
  if (mInputStream) {
    mInputStream->CloseWithStatus(NS_ERROR_ABORT);
  }

  RefPtr<Promise> promise =
      Promise::CreateInfallible(mPort->GetRelevantGlobal());

  nsCOMPtr<nsISerialEventTarget> owningThread = GetCurrentSerialEventTarget();
  nsISerialEventTarget* actorTarget = child->GetActorEventTarget();

  if (!actorTarget) {
    promise->MaybeRejectWithNetworkError("Actor not available");
    return promise.forget();
  }

  RefPtr<SerialPortReadAlgorithms> self = this;
  InvokeAsync(actorTarget, "SerialPortReadAlgorithms::SendFlush",
              [child]() { return child->SendFlush(true); })
      ->Then(
          owningThread, __func__,
          [promise, self](nsresult aResult) {
            if (NS_SUCCEEDED(aResult)) {
              MOZ_LOG(gWebSerialLog, LogLevel::Debug,
                      ("SerialPortReadAlgorithms[%p] flush receive succeeded",
                       self.get()));
              promise->MaybeResolveWithUndefined();
            } else {
              MOZ_LOG(
                  gWebSerialLog, LogLevel::Error,
                  ("SerialPortReadAlgorithms[%p] flush receive failed: 0x%08x",
                   self.get(), static_cast<uint32_t>(aResult)));
              promise->MaybeRejectWithNetworkError(
                  "Failed to discard receive buffers");
            }
          },
          [promise, self](mozilla::ipc::ResponseRejectReason aReason) {
            MOZ_LOG(gWebSerialLog, LogLevel::Error,
                    ("SerialPortReadAlgorithms[%p] flush receive IPC error "
                     "(reason: %d)",
                     self.get(), static_cast<int>(aReason)));
            promise->MaybeRejectWithNetworkError(
                "Failed to discard receive buffers: IPC communication error");
          });

  return promise.forget();
}

SerialPortReadAlgorithms::~SerialPortReadAlgorithms() = default;

void SerialPortReadAlgorithms::ReleaseObjects() {
  MOZ_LOG(gWebSerialLog, LogLevel::Debug,
          ("SerialPortReadAlgorithms[%p]::ReleaseObjects", this));
  InputToReadableStreamAlgorithms::ReleaseObjects();
  mPort = nullptr;
  mInputStream = nullptr;
}

}  // namespace mozilla::dom
