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

#include "ServiceWorkerManager.h"
#include "ServiceWorkerPrivate.h"
#include "ServiceWorkerScriptCache.h"
#include "ServiceWorkerUtils.h"
#include "mozilla/dom/ClientIPCTypes.h"
#include "mozilla/dom/ClientState.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/RemoteWorkerTypes.h"
#include "mozilla/dom/WorkerPrivate.h"

namespace mozilla::dom {

using mozilla::ipc::PrincipalInfo;

static_assert(nsIServiceWorkerInfo::STATE_PARSED ==
                  static_cast<uint16_t>(ServiceWorkerState::Parsed),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_INSTALLING ==
                  static_cast<uint16_t>(ServiceWorkerState::Installing),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_INSTALLED ==
                  static_cast<uint16_t>(ServiceWorkerState::Installed),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_ACTIVATING ==
                  static_cast<uint16_t>(ServiceWorkerState::Activating),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_ACTIVATED ==
                  static_cast<uint16_t>(ServiceWorkerState::Activated),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_REDUNDANT ==
                  static_cast<uint16_t>(ServiceWorkerState::Redundant),
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");
static_assert(nsIServiceWorkerInfo::STATE_UNKNOWN ==
                  ContiguousEnumSize<ServiceWorkerState>::value,
              "ServiceWorkerState enumeration value should match state values "
              "from nsIServiceWorkerInfo.");

NS_IMPL_ISUPPORTS(ServiceWorkerInfo, nsIServiceWorkerInfo)

NS_IMETHODIMP
ServiceWorkerInfo::GetId(nsAString& aId) {
  MOZ_ASSERT(NS_IsMainThread());
  aId = mWorkerPrivateId;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetScriptSpec(nsAString& aScriptSpec) {
  MOZ_ASSERT(NS_IsMainThread());
  CopyUTF8toUTF16(mDescriptor.ScriptURL(), aScriptSpec);
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetCacheName(nsAString& aCacheName) {
  MOZ_ASSERT(NS_IsMainThread());
  aCacheName = mCacheName;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetLaunchCount(uint32_t* aLaunchCount) {
  MOZ_ASSERT(aLaunchCount);
  MOZ_ASSERT(NS_IsMainThread());
  *aLaunchCount = mServiceWorkerPrivate->GetLaunchCount();
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetState(uint16_t* aState) {
  MOZ_ASSERT(aState);
  MOZ_ASSERT(NS_IsMainThread());
  *aState = static_cast<uint16_t>(State());
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetDebugger(nsIWorkerDebugger** aResult) {
  if (NS_WARN_IF(!aResult)) {
    return NS_ERROR_FAILURE;
  }

  return mServiceWorkerPrivate->GetDebugger(aResult);
}

NS_IMETHODIMP
ServiceWorkerInfo::GetHandlesFetchEvents(bool* aValue) {
  MOZ_ASSERT(aValue);
  MOZ_ASSERT(NS_IsMainThread());

  if (mHandlesFetch == Unknown) {
    return NS_ERROR_FAILURE;
  }

  *aValue = HandlesFetch();
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetInstalledTime(PRTime* _retval) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(_retval);
  *_retval = mInstalledTime;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetActivatedTime(PRTime* _retval) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(_retval);
  *_retval = mActivatedTime;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetRedundantTime(PRTime* _retval) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(_retval);
  *_retval = mRedundantTime;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetLifetimeDeadline(double* aLifetimeDeadline) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aLifetimeDeadline);
  TimeStamp deadline = mServiceWorkerPrivate->GetLifetimeDeadline();
  if (deadline.IsNull()) {
    *aLifetimeDeadline = 0;
    return NS_OK;
  }
  *aLifetimeDeadline =
      (deadline - TimeStamp::ProcessCreation()).ToMilliseconds();
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetNavigationFaultCount(uint32_t* aNavigationFaultCount) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aNavigationFaultCount);
  *aNavigationFaultCount = mNavigationFaultCount;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::GetTestingInjectCancellation(
    nsresult* aTestingInjectCancellation) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aTestingInjectCancellation);
  *aTestingInjectCancellation = mTestingInjectCancellation;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::SetTestingInjectCancellation(
    nsresult aTestingInjectCancellation) {
  MOZ_ASSERT(NS_IsMainThread());
  mTestingInjectCancellation = aTestingInjectCancellation;
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerInfo::AttachDebugger() {
  return mServiceWorkerPrivate->AttachDebugger();
}

NS_IMETHODIMP
ServiceWorkerInfo::DetachDebugger() {
  return mServiceWorkerPrivate->DetachDebugger();
}

NS_IMETHODIMP
ServiceWorkerInfo::TerminateWorker(JSContext* aCx,
                                   mozilla::dom::Promise** aPromise) {
  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!global)) {
    return NS_ERROR_FAILURE;
  }

  ErrorResult rv;
  RefPtr<Promise> outer = Promise::Create(global, rv);
  if (NS_WARN_IF(rv.Failed())) {
    return rv.StealNSResult();
  }

  mServiceWorkerPrivate->TerminateWorker(Some(outer));
  return NS_OK;
}

void ServiceWorkerInfo::UpdateState(ServiceWorkerState aState) {
  MOZ_ASSERT(NS_IsMainThread());
#ifdef DEBUG
  // Any state can directly transition to redundant, but everything else is
  // ordered.
  if (aState != ServiceWorkerState::Redundant) {
    MOZ_ASSERT_IF(State() == ServiceWorkerState::Installing,
                  aState == ServiceWorkerState::Installed);
    MOZ_ASSERT_IF(State() == ServiceWorkerState::Installed,
                  aState == ServiceWorkerState::Activating);
    MOZ_ASSERT_IF(State() == ServiceWorkerState::Activating,
                  aState == ServiceWorkerState::Activated);
  }
  // Activated can only go to redundant.
  MOZ_ASSERT_IF(State() == ServiceWorkerState::Activated,
                aState == ServiceWorkerState::Redundant);
#endif
  // Flush any pending functional events to the worker when it transitions to
  // the activated state.
  // TODO: Do we care that these events will race with the propagation of the
  //       state change?
  if (State() != aState) {
    mServiceWorkerPrivate->UpdateState(aState);
  }
  mDescriptor.SetState(aState);
  if (State() == ServiceWorkerState::Redundant) {
    serviceWorkerScriptCache::PurgeCache(mPrincipal, mCacheName);
    mServiceWorkerPrivate->NoteDeadServiceWorkerInfo();
  }
}

ServiceWorkerInfo::ServiceWorkerInfo(
    nsIPrincipal* aPrincipal, const nsACString& aScope, const WorkerType& aType,
    uint64_t aRegistrationId, uint64_t aRegistrationVersion,
    const nsACString& aScriptSpec, const nsAString& aCacheName,
    nsLoadFlags aImportsLoadFlags)
    : mPrincipal(aPrincipal),
      mDescriptor(GetNextID(), aRegistrationId, aRegistrationVersion,
                  aPrincipal, aScope, aType, aScriptSpec,
                  ServiceWorkerState::Parsed),
      mCacheName(aCacheName),
      mWorkerPrivateId(ComputeWorkerPrivateId()),
      mImportsLoadFlags(aImportsLoadFlags),
      mCreationTime(PR_Now()),
      mCreationTimeStamp(TimeStamp::Now()),
      mInstalledTime(0),
      mActivatedTime(0),
      mRedundantTime(0),
      mServiceWorkerPrivate(new ServiceWorkerPrivate(this)),
      mSkipWaitingFlag(false),
      mHandlesFetch(Unknown),
      mNavigationFaultCount(0),
      mTestingInjectCancellation(NS_OK) {
  MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
  MOZ_ASSERT(mPrincipal);
  // cache origin attributes so we can use them off main thread
  mOriginAttributes = mPrincipal->OriginAttributesRef();
  MOZ_ASSERT(!mDescriptor.ScriptURL().IsEmpty());
  MOZ_ASSERT(!mCacheName.IsEmpty());
  MOZ_ASSERT(!mWorkerPrivateId.IsEmpty());

  // Scripts of a service worker should always be loaded bypass service workers.
  // Otherwise, we might not be able to update a service worker correctly, if
  // there is a service worker generating the script.
  MOZ_DIAGNOSTIC_ASSERT(mImportsLoadFlags &
                        nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
}

ServiceWorkerInfo::~ServiceWorkerInfo() {
  MOZ_ASSERT(mServiceWorkerPrivate);
  mServiceWorkerPrivate->NoteDeadServiceWorkerInfo();
}

static uint64_t gServiceWorkerInfoCurrentID = 0;

uint64_t ServiceWorkerInfo::GetNextID() const {
  return ++gServiceWorkerInfoCurrentID;
}

void ServiceWorkerInfo::PostMessage(ipc::StructuredCloneData* aData,
                                    const PostMessageSource& aSource) {
  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (NS_WARN_IF(!swm)) {
    return;
  }

  ServiceWorkerLifetimeExtension lifetime =
      ServiceWorkerLifetimeExtension(NoLifetimeExtension{});

  switch (aSource.type()) {
    case PostMessageSource::TClientInfoAndState:
      lifetime = swm->DetermineLifetimeForClient(
          ClientInfo(aSource.get_ClientInfoAndState().info()));
      break;
    case PostMessageSource::TIPCServiceWorkerDescriptor:
      lifetime = swm->DetermineLifetimeForServiceWorker(
          ServiceWorkerDescriptor(aSource.get_IPCServiceWorkerDescriptor()));
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("Unexpected source type");
      return;
  }

  mServiceWorkerPrivate->SendMessageEvent(aData, lifetime, aSource);
}

Maybe<ClientInfo> ServiceWorkerInfo::GetClientInfo() {
  return mServiceWorkerPrivate->GetClientInfo();
}

TimeStamp ServiceWorkerInfo::LifetimeDeadline() {
  return mServiceWorkerPrivate->GetLifetimeDeadline();
}

void ServiceWorkerInfo::UpdateInstalledTime() {
  MOZ_ASSERT(State() == ServiceWorkerState::Installed);
  MOZ_ASSERT(mInstalledTime == 0);

  mInstalledTime =
      mCreationTime +
      static_cast<PRTime>(
          (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
}

void ServiceWorkerInfo::UpdateActivatedTime() {
  MOZ_ASSERT(State() == ServiceWorkerState::Activated);
  MOZ_ASSERT(mActivatedTime == 0);

  mActivatedTime =
      mCreationTime +
      static_cast<PRTime>(
          (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
}

void ServiceWorkerInfo::UpdateRedundantTime() {
  MOZ_ASSERT(State() == ServiceWorkerState::Redundant);
  MOZ_ASSERT(mRedundantTime == 0);

  mRedundantTime =
      mCreationTime +
      static_cast<PRTime>(
          (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
}

void ServiceWorkerInfo::SetRegistrationVersion(uint64_t aVersion) {
  mDescriptor.SetRegistrationVersion(aVersion);
}

}  // namespace mozilla::dom
