/* 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_extensions_ExtensionAPIRequestForwarder_h
#define mozilla_extensions_ExtensionAPIRequestForwarder_h

#include "ExtensionAPIRequest.h"

#include "mozilla/dom/PromiseWorkerProxy.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/SerializedStackHolder.h"
#include "mozilla/dom/StructuredCloneHolder.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/ToJSValue.h"

namespace mozilla {
namespace dom {
class ClientInfoAndState;
class Function;
}  // namespace dom
namespace extensions {

class ExtensionAPIRequestForwarder;

// A class used to forward an API request (a method call, add/remote listener or
// a property getter) originated from a WebExtensions global (a window, a
// content script sandbox or a service worker) to the JS privileged API request
// handler available on the main thread (mozIExtensionAPIRequestHandler).
//
// Instances of this class are meant to be short-living, and destroyed when the
// caller function is exiting.
class ExtensionAPIRequestForwarder {
  friend class ExtensionAPIRequest;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExtensionAPIRequestForwarder)

 public:
  using APIRequestType = mozIExtensionAPIRequest::RequestType;
  using APIResultType = mozIExtensionAPIRequestResult::ResultType;

  static nsresult JSArrayToSequence(JSContext* aCx,
                                    JS::Handle<JS::Value> aJSValue,
                                    dom::Sequence<JS::Value>& aResult);

  static void ThrowUnexpectedError(JSContext* aCx, ErrorResult& aRv);

  static mozIExtensionAPIRequestHandler& APIRequestHandler();

  ExtensionAPIRequestForwarder(const APIRequestType aRequestType,
                               const nsAString& aApiNamespace,
                               const nsAString& aApiMethod,
                               const nsAString& aApiObjectType = u""_ns,
                               const nsAString& aApiObjectId = u""_ns);

  mozIExtensionAPIRequest::RequestType GetRequestType() const {
    return mRequestType;
  }

  const ExtensionAPIRequestTarget* GetRequestTarget() {
    return &mRequestTarget;
  }

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv);

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           const dom::Sequence<JS::Value>& aArgs,
           ExtensionEventListener* aListener, ErrorResult& aRv);

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           const dom::Sequence<JS::Value>& aArgs,
           JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           const dom::Sequence<JS::Value>& aArgs,
           ExtensionEventListener* aListener,
           JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           const dom::Sequence<JS::Value>& aArgs,
           const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv);

  void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
           JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);

  void SetSerializedCallerStack(
      UniquePtr<dom::SerializedStackHolder> aCallerStack);

 protected:
  virtual ~ExtensionAPIRequestForwarder() = default;

 private:
  already_AddRefed<ExtensionAPIRequest> CreateAPIRequest(
      nsIGlobalObject* aGlobal, JSContext* aCx,
      const dom::Sequence<JS::Value>& aArgs, ExtensionEventListener* aListener,
      ErrorResult& aRv);

  APIRequestType mRequestType;
  ExtensionAPIRequestTarget mRequestTarget;
  Maybe<UniquePtr<dom::SerializedStackHolder>> mStackHolder;
};

/*
 * This runnable is used internally by ExtensionAPIRequestForwader class
 * to call the JS privileged code that handle the API requests originated
 * from the WebIDL bindings instantiated in a worker thread.
 *
 * The runnable is meant to block the worker thread until we get a result
 * from the JS privileged code that handles the API request.
 *
 * For async API calls we still need to block the worker thread until
 * we get a promise (which we link to the worker thread promise and
 * at that point we unblock the worker thread), because the JS privileged
 * code handling the API request may need to throw some errors synchonously
 * (e.g. in case of additional validations based on the API schema definition
 * for the parameter, like strings that has to pass additional validation
 * or normalizations).
 */
class RequestWorkerRunnable : public dom::WorkerMainThreadRunnable {
 public:
  using APIRequestType = mozIExtensionAPIRequest::RequestType;
  using APIResultType = mozIExtensionAPIRequestResult::ResultType;

  RequestWorkerRunnable(dom::WorkerPrivate* aWorkerPrivate,
                        ExtensionAPIRequestForwarder* aOuterAPIRequest);

  void SetSerializedCallerStack(
      UniquePtr<dom::SerializedStackHolder> aCallerStack);

  /**
   * Init a request runnable for AddListener and RemoveListener API requests
   * (which do have an event callback callback and do not expect any return
   * value).
   */
  void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
            const dom::Sequence<JS::Value>& aArgs,
            ExtensionEventListener* aListener, ErrorResult& aRv);

  /**
   * Init a request runnable for CallFunctionNoReturn API requests (which do
   * do not expect any return value).
   */
  void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
            const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
    Init(aGlobal, aCx, aArgs, nullptr, aRv);
  }

  /**
   * Init a request runnable for CallAsyncFunction API requests (which do
   * expect a promise as return value).
   */
  void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
            const dom::Sequence<JS::Value>& aArgs,
            const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv);

  bool MainThreadRun() override;

  void ReadResult(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
                  ErrorResult& aRv);

  Maybe<mozIExtensionAPIRequestResult::ResultType> GetResultType() {
    return mResultType;
  }

 protected:
  virtual bool ProcessHandlerResult(JSContext* aCx,
                                    JS::MutableHandle<JS::Value> aRetval);

  already_AddRefed<WebExtensionPolicy> GetWebExtensionPolicy();
  already_AddRefed<ExtensionAPIRequest> CreateAPIRequest(JSContext* aCx);

  void SerializeCallerStack(JSContext* aCx);
  void DeserializeCallerStack(JSContext* aCx,
                              JS::MutableHandle<JS::Value> aRetval);
  void SerializeArgs(JSContext* aCx, const dom::Sequence<JS::Value>& aArgs,
                     ErrorResult& aRv);
  nsresult DeserializeArgs(JSContext* aCx, JS::MutableHandle<JS::Value> aArgs);

  bool HandleAPIRequest(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);

  Maybe<mozIExtensionAPIRequestResult::ResultType> mResultType;
  Maybe<UniquePtr<dom::StructuredCloneHolder>> mResultHolder;
  RefPtr<dom::PromiseWorkerProxy> mPromiseProxy;
  Maybe<UniquePtr<dom::StructuredCloneHolder>> mArgsHolder;
  Maybe<UniquePtr<dom::SerializedStackHolder>> mStackHolder;
  Maybe<dom::ClientInfo> mClientInfo;
  uint64_t mSWDescriptorId = 0;

  // Only set for addListener/removeListener API requests.
  RefPtr<ExtensionEventListener> mEventListener;

  // The outer request object is kept alive by the caller for the
  // entire life of the inner worker runnable.
  ExtensionAPIRequestForwarder* mOuterRequest;
};

class RequestInitWorkerRunnable : public dom::WorkerMainThreadRunnable {
  Maybe<dom::ClientInfo> mClientInfo;

 public:
  RequestInitWorkerRunnable(dom::WorkerPrivate* aWorkerPrivate,
                            Maybe<dom::ClientInfo>& aSWClientInfo);
  bool MainThreadRun() override;
};

class NotifyWorkerLoadedRunnable : public Runnable {
  uint64_t mSWDescriptorId = 0;
  nsCOMPtr<nsIURI> mSWBaseURI;

 public:
  explicit NotifyWorkerLoadedRunnable(const uint64_t aServiceWorkerDescriptorId,
                                      const nsCOMPtr<nsIURI>& aWorkerBaseURI)
      : Runnable("extensions::NotifyWorkerLoadedRunnable"),
        mSWDescriptorId(aServiceWorkerDescriptorId),
        mSWBaseURI(aWorkerBaseURI) {
    MOZ_ASSERT(mSWDescriptorId > 0);
    MOZ_ASSERT(mSWBaseURI);
  }

  NS_IMETHOD Run() override;

  NS_INLINE_DECL_REFCOUNTING_INHERITED(NotifyWorkerLoadedRunnable, Runnable)

 private:
  ~NotifyWorkerLoadedRunnable() = default;
};

class NotifyWorkerDestroyedRunnable : public Runnable {
  uint64_t mSWDescriptorId = 0;
  nsCOMPtr<nsIURI> mSWBaseURI;

 public:
  explicit NotifyWorkerDestroyedRunnable(
      const uint64_t aServiceWorkerDescriptorId,
      const nsCOMPtr<nsIURI>& aWorkerBaseURI)
      : Runnable("extensions::NotifyWorkerDestroyedRunnable"),
        mSWDescriptorId(aServiceWorkerDescriptorId),
        mSWBaseURI(aWorkerBaseURI) {
    MOZ_ASSERT(mSWDescriptorId > 0);
    MOZ_ASSERT(mSWBaseURI);
  }

  NS_IMETHOD Run() override;

  NS_INLINE_DECL_REFCOUNTING_INHERITED(NotifyWorkerDestroyedRunnable, Runnable)

 private:
  ~NotifyWorkerDestroyedRunnable() = default;
};

}  // namespace extensions
}  // namespace mozilla

#endif  // mozilla_extensions_ExtensionAPIRequestForwarder_h
