/* -*- 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_RemoteWorkerChild_h
#define mozilla_dom_RemoteWorkerChild_h

#include "nsCOMPtr.h"
#include "nsISupportsImpl.h"
#include "nsTArray.h"

#include "mozilla/DataMutex.h"
#include "mozilla/MozPromise.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ThreadBound.h"
#include "mozilla/dom/PRemoteWorkerChild.h"
#include "mozilla/dom/ServiceWorkerOpArgs.h"

class nsISerialEventTarget;
class nsIConsoleReportCollector;

namespace mozilla::dom {

class ErrorValue;
class FetchEventOpProxyChild;
class RemoteWorkerData;
class RemoteWorkerServiceKeepAlive;
class ServiceWorkerOp;
class UniqueMessagePortId;
class WeakWorkerRef;
class WorkerErrorReport;
class WorkerPrivate;

/**
 * Background-managed "Worker Launcher"-thread-resident created via the
 * RemoteWorkerManager to actually spawn the worker. Currently, the worker will
 * be spawned from the main thread due to nsIPrincipal not being able to be
 * created on background threads and other ownership invariants, most of which
 * can be relaxed in the future.
 */
class RemoteWorkerChild final : public PRemoteWorkerChild {
  friend class FetchEventOpProxyChild;
  friend class PRemoteWorkerChild;
  friend class ServiceWorkerOp;

  ~RemoteWorkerChild();

 public:
  // Note that all IPC-using methods must only be invoked on the
  // RemoteWorkerService thread which the inherited
  // IProtocol::GetActorEventTarget() will return for us.
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerChild, final)

  explicit RemoteWorkerChild(const RemoteWorkerData& aData);

  void ExecWorker(const RemoteWorkerData& aData);

  void ErrorPropagationOnMainThread(const WorkerErrorReport* aReport,
                                    bool aIsErrorEvent);

  void NotifyLock(bool aCreated);

  void NotifyWebTransport(bool aCreated);

  void FlushReportsOnMainThread(nsIConsoleReportCollector* aReporter);

  void AddPortIdentifier(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
                         UniqueMessagePortId& aPortIdentifier);

  RefPtr<GenericNonExclusivePromise> GetTerminationPromise();

  RefPtr<GenericPromise> MaybeSendSetServiceWorkerSkipWaitingFlag();

  const nsTArray<uint64_t>& WindowIDs() const { return mWindowIDs; }

 private:
  class InitializeWorkerRunnable;

  class Op;
  class SharedWorkerOp;

  struct WorkerPrivateAccessibleState {
    ~WorkerPrivateAccessibleState();
    RefPtr<WorkerPrivate> mWorkerPrivate;
  };

  // Initial state, mWorkerPrivate is initially null but will be initialized on
  // the main thread by ExecWorkerOnMainThread when the WorkerPrivate is
  // created.  The state will transition to Running or Canceled, also from the
  // main thread.
  struct Pending : WorkerPrivateAccessibleState {
    nsTArray<RefPtr<Op>> mPendingOps;
  };

  // Running, with the state transition happening on the main thread as a result
  // of the worker successfully processing our initialization runnable,
  // indicating that top-level script execution successfully completed.  Because
  // all of our state transitions happen on the main thread and are posed in
  // terms of the main thread's perspective of the worker's state, it's very
  // possible for us to skip directly from Pending to Canceled because we decide
  // to cancel/terminate the worker prior to it finishing script loading or
  // reporting back to us.
  struct Running : WorkerPrivateAccessibleState {};

  // Cancel() has been called on the WorkerPrivate on the main thread by a
  // TerminationOp, top-level script evaluation has failed and canceled the
  // worker, or in the case of a SharedWorker, close() has been called on
  // the global scope by content code and the worker has advanced to the
  // Canceling state.  (Dedicated Workers can also self close, but they will
  // never be RemoteWorkers.  Although a SharedWorker can own DedicatedWorkers.)
  // Browser shutdown will result in a TerminationOp thanks to use of a shutdown
  // blocker in the parent, so the RuntimeService shouldn't get involved, but we
  // would also handle that case acceptably too.
  //
  // Because worker self-closing is still handled by dispatching a runnable to
  // the main thread to effectively call WorkerPrivate::Cancel(), there isn't
  // a race between a worker deciding to self-close and our termination ops.
  //
  // In this state, we have dropped the reference to the WorkerPrivate and will
  // no longer be dispatching runnables to the worker.  We wait in this state
  // until the termination lambda is invoked letting us know that the worker has
  // entirely shutdown and we can advanced to the Killed state.
  struct Canceled {};

  // The worker termination lambda has been invoked and we know the Worker is
  // entirely shutdown.  (Inherently it is possible for us to advance to this
  // state while the nsThread for the worker is still in the process of
  // shutting down, but no more worker code will run on it.)
  //
  // This name is chosen to match the Worker's own state model.
  struct Killed {};

  using State = Variant<Pending, Running, Canceled, Killed>;

  // The state of the WorkerPrivate as perceived by the owner on the main
  // thread.  All state transitions now happen on the main thread, but the
  // Worker Launcher thread will consult the state and will directly append ops
  // to the Pending queue
  DataMutex<State> mState;

  const RefPtr<RemoteWorkerServiceKeepAlive> mServiceKeepAlive;

  class Op {
   public:
    NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING

    virtual ~Op() = default;

    virtual bool MaybeStart(RemoteWorkerChild* aOwner, State& aState) = 0;

    virtual void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) = 0;

    virtual void Cancel() = 0;
  };

  void ActorDestroy(ActorDestroyReason) override;

  mozilla::ipc::IPCResult RecvExecOp(RemoteWorkerOp&& aOp);

  mozilla::ipc::IPCResult RecvExecServiceWorkerOp(
      ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve);

  already_AddRefed<PFetchEventOpProxyChild> AllocPFetchEventOpProxyChild(
      const ParentToChildServiceWorkerFetchEventOpArgs& aArgs);

  mozilla::ipc::IPCResult RecvPFetchEventOpProxyConstructor(
      PFetchEventOpProxyChild* aActor,
      const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) override;

  nsresult ExecWorkerOnMainThread(RemoteWorkerData&& aData);

  void ExceptionalErrorTransitionDuringExecWorker();

  void RequestWorkerCancellation();

  void InitializeOnWorker();

  void CreationSucceededOnAnyThread();

  void CreationFailedOnAnyThread();

  void CreationSucceededOrFailedOnAnyThread(bool aDidCreationSucceed);

  // Cancels the worker if it has been started and ensures that we transition
  // to the Terminated state once the worker has been terminated or we have
  // ensured that it will never start.
  void CloseWorkerOnMainThread();

  void ErrorPropagation(const ErrorValue& aValue);

  void ErrorPropagationDispatch(nsresult aError);

  // When the WorkerPrivate Cancellation lambda is invoked, it's possible that
  // we have not yet advanced to running from pending, so we could be in either
  // state.  This method is expected to be called by the Workers' cancellation
  // lambda and will obtain the lock and call the
  // TransitionStateFromPendingToCanceled if appropriate.  Otherwise it will
  // directly move from the running state to the canceled state which does not
  // require additional cleanup.
  void OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled();
  // A helper used by the above method by the worker cancellation lambda if the
  // the worker hasn't started running, or in exceptional cases where we bail
  // out of the ExecWorker method early.  The caller must be holding the lock
  // (in order to pass in the state).
  void TransitionStateFromPendingToCanceled(State& aState);
  void TransitionStateFromCanceledToKilled();

  void TransitionStateToRunning();

  void TransitionStateToTerminated();

  void TransitionStateToTerminated(State& aState);

  void CancelAllPendingOps(State& aState);

  void MaybeStartOp(RefPtr<Op>&& aOp);

  const bool mIsServiceWorker;

  // Touched on main-thread only.
  nsTArray<uint64_t> mWindowIDs;

  struct LauncherBoundData {
    MozPromiseHolder<GenericNonExclusivePromise> mTerminationPromise;
    // Flag to ensure we report creation at most once.  This could be cleaned up
    // further.
    bool mDidSendCreated = false;
  };

  ThreadBound<LauncherBoundData> mLauncherData;
};

}  // namespace mozilla::dom

#endif  // mozilla_dom_RemoteWorkerChild_h
