/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */

#ifndef mozilla_dom_workers_WorkerLoadContext_h__
#define mozilla_dom_workers_WorkerLoadContext_h__

#include "nsIChannel.h"
#include "nsIInputStream.h"
#include "nsIRequest.h"
#include "mozilla/CORSMode.h"
#include "mozilla/dom/Promise.h"
#include "js/loader/ScriptKind.h"
#include "js/loader/ScriptLoadRequest.h"
#include "js/loader/LoadContextBase.h"

class nsIReferrerInfo;
class nsIURI;

namespace mozilla::dom {

class ClientInfo;
class WorkerPrivate;

namespace workerinternals::loader {
class CacheCreator;
class ScriptLoaderRunnable;
class WorkerScriptLoader;
}  // namespace workerinternals::loader

/*
 * WorkerLoadContext (for all workers)
 *
 * LoadContexts augment the loading of a ScriptLoadRequest. They
 * describe how a ScriptLoadRequests loading and evaluation needs to be
 * augmented, based on the information provided by the loading context. The
 * WorkerLoadContext has the following generic fields applied to all worker
 * ScriptLoadRequests (and primarily used for error handling):
 *
 *    * mMutedErrorFlag
 *        Set when we finish loading a script, and used to determine whether a
 *        given error is thrown or muted.
 *    * mLoadResult
 *        In order to report errors correctly in the worker thread, we need to
 *        move them from the main thread to the worker. This field records the
 *        load error, for throwing when we return to the worker thread.
 *    * mKind
 *        See documentation of WorkerLoadContext::Kind.
 *    * mClientInfo
 *        A snapshot of a global living in the system (see documentation for
 *        ClientInfo). In worker loading, this field is important for CSP
 *        information and knowing what to intercept for Service Worker
 *        interception.
 *    * mChannel
 *        The channel used by this request for it's load. Used for cancellation,
 *        in order to cancel the stream.
 *
 * The rest of the fields on this class focus on enabling the ServiceWorker
 * usecase, in particular -- using the Cache API to store the worker so that
 * in the case of (for example) a page refresh, the service worker itself is
 * persisted so that it can do other work. For more details see the
 * CacheLoadHandler.h file.
 *
 */

class WorkerLoadContext : public JS::loader::LoadContextBase {
 public:
  /* Worker Load Context Kinds
   *
   * A script that is loaded and run as a worker can be one of several species.
   * Each may have slightly different behavior, but they fall into roughly two
   * categories: the Main Worker Script (the script that triggers the first
   * load) and scripts that are attached to this main worker script.
   *
   * In the specification, the Main Worker Script is referred to as the "top
   * level script" and is defined here:
   * https://html.spec.whatwg.org/multipage/webappapis.html#fetching-scripts-is-top-level
   */

  enum Kind {
    // Indicates that the is-top-level bit is true. This may be a Classic script
    // or a Module script.
    MainScript,
    // We are importing a script from the worker via ImportScript. This may only
    // be a Classic script.
    ImportScript,
    // We are importing a script from the worker via a Static Import. This may
    // only
    // be a Module script.
    StaticImport,
    DynamicImport,
    // We have an attached debugger, and these should be treated specially and
    // not like a main script (regardless of their type). This is not part of
    // the specification.
    DebuggerScript
  };

  WorkerLoadContext(Kind aKind, const Maybe<ClientInfo>& aClientInfo,
                    workerinternals::loader::WorkerScriptLoader* aScriptLoader);

  // Used to detect if the `is top-level` bit is set on a given module.
  bool IsTopLevel() {
    return mRequest->IsTopLevel() && (mKind == Kind::MainScript);
  };

  static Kind GetKind(bool isMainScript, bool isDebuggerScript) {
    if (isDebuggerScript) {
      return Kind::DebuggerScript;
    }
    if (isMainScript) {
      return Kind::MainScript;
    }
    return Kind::ImportScript;
  };

  /* These fields are used by all workers */
  Maybe<bool> mMutedErrorFlag;
  nsresult mLoadResult = NS_ERROR_NOT_INITIALIZED;
  bool mLoadingFinished = false;
  bool mIsTopLevel = true;
  Kind mKind;
  Maybe<ClientInfo> mClientInfo;
  nsCOMPtr<nsIChannel> mChannel;
  RefPtr<workerinternals::loader::WorkerScriptLoader> mScriptLoader;

  /* These fields are only used by service workers */
  /* TODO: Split out a ServiceWorkerLoadContext */
  // This full URL string is populated only if this object is used in a
  // ServiceWorker.
  nsString mFullURL;

  // This promise is set only when the script is for a ServiceWorker but
  // it's not in the cache yet. The promise is resolved when the full body is
  // stored into the cache.  mCachePromise will be set to nullptr after
  // resolution.
  RefPtr<Promise> mCachePromise;

  // The reader stream the cache entry should be filled from, for those cases
  // when we're going to have an mCachePromise.
  nsCOMPtr<nsIInputStream> mCacheReadStream;

  enum CacheStatus {
    // By default a normal script is just loaded from the network. But for
    // ServiceWorkers, we have to check if the cache contains the script and
    // load it from the cache.
    Uncached,

    WritingToCache,

    ReadingFromCache,

    // This script has been loaded from the ServiceWorker cache.
    Cached,

    // This script must be stored in the ServiceWorker cache.
    ToBeCached,

    // Something went wrong or the worker went away.
    Cancel
  };

  CacheStatus mCacheStatus = Uncached;

  bool IsAwaitingPromise() const { return bool(mCachePromise); }
};

class ThreadSafeRequestHandle final {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadSafeRequestHandle)

  ThreadSafeRequestHandle(JS::loader::ScriptLoadRequest* aRequest,
                          nsISerialEventTarget* aSyncTarget);

  JS::loader::ScriptLoadRequest* GetRequest() const { return mRequest; }

  WorkerLoadContext* GetContext() { return mRequest->GetWorkerLoadContext(); }

  bool IsEmpty() { return !mRequest; }

  // Runnable controls
  nsresult OnStreamComplete(nsresult aStatus);

  void LoadingFinished(nsresult aRv);

  void MaybeExecuteFinishedScripts();

  bool IsCancelled();

  bool Finished() {
    return GetContext()->mLoadingFinished && !GetContext()->IsAwaitingPromise();
  }

  nsresult GetCancelResult();

  already_AddRefed<JS::loader::ScriptLoadRequest> ReleaseRequest();

  workerinternals::loader::CacheCreator* GetCacheCreator();

  RefPtr<workerinternals::loader::ScriptLoaderRunnable> mRunnable;

  bool mExecutionScheduled = false;

 private:
  ~ThreadSafeRequestHandle();

  RefPtr<JS::loader::ScriptLoadRequest> mRequest;
  nsCOMPtr<nsISerialEventTarget> mOwningEventTarget;
};

}  // namespace mozilla::dom
#endif /* mozilla_dom_workers_WorkerLoadContext_h__ */
