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

#include "ServiceWorkerManager.h"
#include "mozilla/dom/WorkerCommon.h"
#include "nsIPrincipal.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"

namespace mozilla::dom {

ServiceWorkerJob::Type ServiceWorkerJob::GetType() const { return mType; }

ServiceWorkerJob::State ServiceWorkerJob::GetState() const { return mState; }

bool ServiceWorkerJob::Canceled() const { return mCanceled; }

bool ServiceWorkerJob::ResultCallbacksInvoked() const {
  return mResultCallbacksInvoked;
}

bool ServiceWorkerJob::IsEquivalentTo(ServiceWorkerJob* aJob) const {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aJob);
  return mType == aJob->mType && mScope.Equals(aJob->mScope) &&
         mScriptSpec.Equals(aJob->mScriptSpec) &&
         mPrincipal->Equals(aJob->mPrincipal);
}

void ServiceWorkerJob::AppendResultCallback(Callback* aCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished);
  MOZ_DIAGNOSTIC_ASSERT(aCallback);
  MOZ_DIAGNOSTIC_ASSERT(mFinalCallback != aCallback);
  MOZ_ASSERT(!mResultCallbackList.Contains(aCallback));
  MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked);
  mResultCallbackList.AppendElement(aCallback);
}

void ServiceWorkerJob::StealResultCallbacksFrom(ServiceWorkerJob* aJob) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aJob);
  MOZ_ASSERT(aJob->mState == State::Initial);

  // Take the callbacks from the other job immediately to avoid the
  // any possibility of them existing on both jobs at once.
  nsTArray<RefPtr<Callback>> callbackList =
      std::move(aJob->mResultCallbackList);

  for (RefPtr<Callback>& callback : callbackList) {
    // Use AppendResultCallback() so that assertion checking is performed on
    // each callback.
    AppendResultCallback(callback);
  }
}

void ServiceWorkerJob::Start(Callback* aFinalCallback) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_DIAGNOSTIC_ASSERT(!mCanceled);

  MOZ_DIAGNOSTIC_ASSERT(aFinalCallback);
  MOZ_DIAGNOSTIC_ASSERT(!mFinalCallback);
  MOZ_ASSERT(!mResultCallbackList.Contains(aFinalCallback));
  mFinalCallback = aFinalCallback;

  MOZ_DIAGNOSTIC_ASSERT(mState == State::Initial);
  mState = State::Started;

  nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
      "ServiceWorkerJob::AsyncExecute", this, &ServiceWorkerJob::AsyncExecute);

  // We may have to wait for the PBackground actor to be initialized
  // before proceeding.  We should always be able to get a ServiceWorkerManager,
  // however, since Start() should not be called during shutdown.
  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (!swm) {
    // browser shutdown
    return;
  }

  // Otherwise start asynchronously.  We should never run a job synchronously.
  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable.forget())));
}

void ServiceWorkerJob::Cancel() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!mCanceled);
  mCanceled = true;

  if (GetState() != State::Started) {
    MOZ_ASSERT(GetState() == State::Initial);

    ErrorResult error(NS_ERROR_DOM_ABORT_ERR);
    InvokeResultCallbacks(error);

    // The callbacks might not consume the error, which is fine.
    error.SuppressException();
  }
}

ServiceWorkerJob::ServiceWorkerJob(Type aType, nsIPrincipal* aPrincipal,
                                   const nsACString& aScope,
                                   nsCString aScriptSpec)
    : mType(aType),
      mPrincipal(aPrincipal),
      mScope(aScope),
      mScriptSpec(std::move(aScriptSpec)),
      mState(State::Initial),
      mCanceled(false),
      mResultCallbacksInvoked(false) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mPrincipal);
  MOZ_ASSERT(!mScope.IsEmpty());

  // Empty script URL if and only if this is an unregister job.
  MOZ_ASSERT((mType == Type::Unregister) == mScriptSpec.IsEmpty());
}

ServiceWorkerJob::~ServiceWorkerJob() {
  MOZ_ASSERT(NS_IsMainThread());
  // Jobs must finish or never be started.  Destroying an actively running
  // job is an error.
  MOZ_ASSERT(mState != State::Started);
  MOZ_ASSERT_IF(mState == State::Finished, mResultCallbacksInvoked);
}

void ServiceWorkerJob::InvokeResultCallbacks(ErrorResult& aRv) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished);
  MOZ_DIAGNOSTIC_ASSERT_IF(mState == State::Initial, Canceled());

  MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked);
  mResultCallbacksInvoked = true;

  nsTArray<RefPtr<Callback>> callbackList = std::move(mResultCallbackList);

  for (RefPtr<Callback>& callback : callbackList) {
    // The callback might consume an exception on the ErrorResult, so we need
    // to clone in order to maintain the error for the next callback.
    ErrorResult rv;
    aRv.CloneTo(rv);

    if (GetState() == State::Started) {
      callback->JobFinished(this, rv);
    } else {
      callback->JobDiscarded(rv);
    }

    // The callback might not consume the error.
    rv.SuppressException();
  }
}

void ServiceWorkerJob::InvokeResultCallbacks(nsresult aRv) {
  ErrorResult converted(aRv);
  InvokeResultCallbacks(converted);
}

void ServiceWorkerJob::Finish(ErrorResult& aRv) {
  MOZ_ASSERT(NS_IsMainThread());

  // Finishing a job is idempotent and potentially expected by the error
  // handling path for ServiceWorkerUpdateJob, so this is not an error.
  if (mState != State::Started) {
    return;
  }

  // Ensure that we only surface SecurityErr, TypeErr or InvalidStateErr to
  // script.
  if (aRv.Failed() && !aRv.ErrorCodeIs(NS_ERROR_DOM_SECURITY_ERR) &&
      !aRv.ErrorCodeIs(NS_ERROR_INTERNAL_ERRORRESULT_TYPEERROR) &&
      !aRv.ErrorCodeIs(NS_ERROR_DOM_INVALID_STATE_ERR)) {
    // Remove the old error code so we can replace it with a TypeError.
    aRv.SuppressException();

    // Throw the type error with a generic error message.  We use a stack
    // reference to bypass the normal static analysis for "return right after
    // throwing", since it's not the right check here: this ErrorResult came in
    // pre-thrown.
    ErrorResult& rv = aRv;
    rv.ThrowTypeError<MSG_SW_INSTALL_ERROR>(mScriptSpec, mScope);
  }

  // The final callback may drop the last ref to this object.
  RefPtr<ServiceWorkerJob> kungFuDeathGrip = this;

  if (!mResultCallbacksInvoked) {
    InvokeResultCallbacks(aRv);
  }

  mState = State::Finished;

  MOZ_DIAGNOSTIC_ASSERT(mFinalCallback);
  if (mFinalCallback) {
    mFinalCallback->JobFinished(this, aRv);
    mFinalCallback = nullptr;
  }

  // The callback might not consume the error.
  aRv.SuppressException();

  // Async release this object to ensure that our caller methods complete
  // as well.
  NS_ReleaseOnMainThread("ServiceWorkerJobProxyRunnable",
                         kungFuDeathGrip.forget(), true /* always proxy */);
}

void ServiceWorkerJob::Finish(nsresult aRv) {
  ErrorResult converted(aRv);
  Finish(converted);
}

}  // namespace mozilla::dom
