/* 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 "WebSocketLog.h"
#include "WebSocketConnectionParent.h"

#include "nsIHttpChannelInternal.h"
#include "nsITransportSecurityInfo.h"
#include "nsSerializationHelper.h"
#include "nsThreadUtils.h"
#include "WebSocketConnectionListener.h"

namespace mozilla {
namespace net {

NS_IMPL_ISUPPORTS0(WebSocketConnectionParent)

WebSocketConnectionParent::WebSocketConnectionParent(
    nsIHttpUpgradeListener* aListener)
    : mUpgradeListener(aListener),
      mBackgroundThread(GetCurrentSerialEventTarget()) {
  LOG(("WebSocketConnectionParent ctor %p\n", this));
  MOZ_ASSERT(mUpgradeListener);
}

WebSocketConnectionParent::~WebSocketConnectionParent() {
  LOG(("WebSocketConnectionParent dtor %p\n", this));
}

mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnTransportAvailable(
    nsITransportSecurityInfo* aSecurityInfo) {
  LOG(("WebSocketConnectionParent::RecvOnTransportAvailable %p\n", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (aSecurityInfo) {
    MutexAutoLock lock(mMutex);
    mSecurityInfo = aSecurityInfo;
  }

  if (mUpgradeListener) {
    (void)mUpgradeListener->OnWebSocketConnectionAvailable(this);
    mUpgradeListener = nullptr;
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnError(
    const nsresult& aStatus) {
  LOG(("WebSocketConnectionParent::RecvOnError %p\n", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (mListener) {
    mListener->OnError(aStatus);
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnUpgradeFailed(
    const nsresult& aReason) {
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (mUpgradeListener) {
    (void)mUpgradeListener->OnUpgradeFailed(aReason);
    mUpgradeListener = nullptr;
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnTCPClosed() {
  LOG(("WebSocketConnectionParent::RecvOnTCPClosed %p\n", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (mListener) {
    mListener->OnTCPClosed();
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnDataReceived(
    nsTArray<uint8_t>&& aData) {
  LOG(("WebSocketConnectionParent::RecvOnDataReceived %p\n", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (!mListener) {
    return IPC_OK();
  }

  uint8_t* buffer = const_cast<uint8_t*>(aData.Elements());
  nsresult rv = mListener->OnDataReceived(buffer, aData.Length());
  if (NS_FAILED(rv)) {
    RefPtr<WebSocketConnectionListener> listener;
    listener.swap(mListener);
    listener->OnError(rv);
  }

  return IPC_OK();
}

void WebSocketConnectionParent::ActorDestroy(ActorDestroyReason aWhy) {
  LOG(("WebSocketConnectionParent::ActorDestroy %p aWhy=%d\n", this, aWhy));
  if (!mClosed) {
    // Treat this as an error when IPC is closed before
    // WebSocketConnectionParent::Close() is called.
    RefPtr<WebSocketConnectionListener> listener;
    listener.swap(mListener);
    if (listener) {
      listener->OnError(NS_ERROR_FAILURE);
    }
  }
  mBackgroundThread->Dispatch(NS_NewRunnableFunction(
      "WebSocketConnectionParent::DefereredDestroy", [self = RefPtr{this}]() {
        LOG(("WebSocketConnectionParent::DefereredDestroy"));
      }));
};

nsresult WebSocketConnectionParent::Init(
    WebSocketConnectionListener* aListener) {
  NS_ENSURE_ARG_POINTER(aListener);

  mListener = aListener;
  return NS_OK;
}

void WebSocketConnectionParent::GetIoTarget(nsIEventTarget** aTarget) {
  nsCOMPtr<nsIEventTarget> target = mBackgroundThread;
  return target.forget(aTarget);
}

void WebSocketConnectionParent::Close() {
  LOG(("WebSocketConnectionParent::Close %p\n", this));

  mClosed = true;

  auto task = [self = RefPtr{this}]() {
    self->PWebSocketConnectionParent::Close();
  };

  if (mBackgroundThread->IsOnCurrentThread()) {
    task();
  } else {
    mBackgroundThread->Dispatch(NS_NewRunnableFunction(
        "WebSocketConnectionParent::Close", std::move(task)));
  }
}

nsresult WebSocketConnectionParent::WriteOutputData(
    const uint8_t* aHdrBuf, uint32_t aHdrBufLength, const uint8_t* aPayloadBuf,
    uint32_t aPayloadBufLength) {
  LOG(("WebSocketConnectionParent::WriteOutputData %p", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (!CanSend()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsTArray<uint8_t> data;
  data.AppendElements(aHdrBuf, aHdrBufLength);
  data.AppendElements(aPayloadBuf, aPayloadBufLength);
  return SendWriteOutputData(data) ? NS_OK : NS_ERROR_FAILURE;
}

nsresult WebSocketConnectionParent::StartReading() {
  LOG(("WebSocketConnectionParent::StartReading %p\n", this));

  RefPtr<WebSocketConnectionParent> self = this;
  auto task = [self{std::move(self)}]() {
    if (!self->CanSend()) {
      if (self->mListener) {
        self->mListener->OnError(NS_ERROR_NOT_AVAILABLE);
      }
      return;
    }

    (void)self->SendStartReading();
  };

  if (mBackgroundThread->IsOnCurrentThread()) {
    task();
  } else {
    mBackgroundThread->Dispatch(NS_NewRunnableFunction(
        "WebSocketConnectionParent::SendStartReading", std::move(task)));
  }

  return NS_OK;
}

void WebSocketConnectionParent::DrainSocketData() {
  LOG(("WebSocketConnectionParent::DrainSocketData %p\n", this));
  MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread());

  if (!CanSend()) {
    MOZ_ASSERT(mListener);
    mListener->OnError(NS_ERROR_NOT_AVAILABLE);

    return;
  }

  (void)SendDrainSocketData();
}

nsresult WebSocketConnectionParent::GetSecurityInfo(
    nsITransportSecurityInfo** aSecurityInfo) {
  LOG(("WebSocketConnectionParent::GetSecurityInfo() %p\n", this));
  MOZ_ASSERT(NS_IsMainThread());

  NS_ENSURE_ARG_POINTER(aSecurityInfo);

  MutexAutoLock lock(mMutex);
  NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo);
  return NS_OK;
}

}  // namespace net
}  // namespace mozilla
