/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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_CustomElementRegistry_h
#define mozilla_dom_CustomElementRegistry_h

#include "js/GCHashTable.h"
#include "js/TypeDecls.h"
#include "mozilla/Attributes.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/FunctionBinding.h"
#include "mozilla/dom/WebComponentsBinding.h"
#include "nsCycleCollectionParticipant.h"
#include "nsGenericHTMLElement.h"
#include "nsWrapperCache.h"

class nsDocument;

namespace mozilla {
namespace dom {

struct CustomElementData;
struct ElementDefinitionOptions;
class CallbackFunction;
class CustomElementReaction;
class DocGroup;
class Function;
class Promise;

struct LifecycleCallbackArgs
{
  nsString name;
  nsString oldValue;
  nsString newValue;
  nsString namespaceURI;
};

struct LifecycleAdoptedCallbackArgs
{
  nsCOMPtr<nsIDocument> mOldDocument;
  nsCOMPtr<nsIDocument> mNewDocument;
};

class CustomElementCallback
{
public:
  CustomElementCallback(Element* aThisObject,
                        nsIDocument::ElementCallbackType aCallbackType,
                        CallbackFunction* aCallback);
  void Traverse(nsCycleCollectionTraversalCallback& aCb) const;
  void Call();
  void SetArgs(LifecycleCallbackArgs& aArgs)
  {
    MOZ_ASSERT(mType == nsIDocument::eAttributeChanged,
               "Arguments are only used by attribute changed callback.");
    mArgs = aArgs;
  }

  void SetAdoptedCallbackArgs(LifecycleAdoptedCallbackArgs& aAdoptedCallbackArgs)
  {
    MOZ_ASSERT(mType == nsIDocument::eAdopted,
      "Arguments are only used by adopted callback.");
    mAdoptedCallbackArgs = aAdoptedCallbackArgs;
  }

private:
  // The this value to use for invocation of the callback.
  RefPtr<Element> mThisObject;
  RefPtr<CallbackFunction> mCallback;
  // The type of callback (eCreated, eAttached, etc.)
  nsIDocument::ElementCallbackType mType;
  // Arguments to be passed to the callback,
  // used by the attribute changed callback.
  LifecycleCallbackArgs mArgs;
  LifecycleAdoptedCallbackArgs mAdoptedCallbackArgs;
};

class CustomElementConstructor final : public CallbackFunction
{
public:
  explicit CustomElementConstructor(CallbackFunction* aOther)
    : CallbackFunction(aOther)
  {
    MOZ_ASSERT(JS::IsConstructor(mCallback));
  }

  already_AddRefed<Element> Construct(const char* aExecutionReason, ErrorResult& aRv);
};

// Each custom element has an associated callback queue and an element is
// being created flag.
struct CustomElementData
{
  NS_INLINE_DECL_REFCOUNTING(CustomElementData)

  // https://dom.spec.whatwg.org/#concept-element-custom-element-state
  // CustomElementData is only created on the element which is a custom element
  // or an upgrade candidate, so the state of an element without
  // CustomElementData is "uncustomized".
  enum class State {
    eUndefined,
    eFailed,
    eCustom
  };

  explicit CustomElementData(nsIAtom* aType);
  CustomElementData(nsIAtom* aType, State aState);

  // Custom element state as described in the custom element spec.
  State mState;
  // custom element reaction queue as described in the custom element spec.
  // There is 1 reaction in reaction queue, when 1) it becomes disconnected,
  // 2) it’s adopted into a new document, 3) its attributes are changed,
  // appended, removed, or replaced.
  // There are 3 reactions in reaction queue when doing upgrade operation,
  // e.g., create an element, insert a node.
  AutoTArray<UniquePtr<CustomElementReaction>, 3> mReactionQueue;

  void SetCustomElementDefinition(CustomElementDefinition* aDefinition);
  CustomElementDefinition* GetCustomElementDefinition();
  nsIAtom* GetCustomElementType();

  void Traverse(nsCycleCollectionTraversalCallback& aCb) const;
  void Unlink();

private:
  virtual ~CustomElementData() {}

  // Custom element type, for <button is="x-button"> or <x-button>
  // this would be x-button.
  RefPtr<nsIAtom> mType;
  RefPtr<CustomElementDefinition> mCustomElementDefinition;
};

#define ALEADY_CONSTRUCTED_MARKER nullptr

// The required information for a custom element as defined in:
// https://html.spec.whatwg.org/multipage/scripting.html#custom-element-definition
struct CustomElementDefinition
{
  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(CustomElementDefinition)
  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CustomElementDefinition)

  CustomElementDefinition(nsIAtom* aType,
                          nsIAtom* aLocalName,
                          Function* aConstructor,
                          nsCOMArray<nsIAtom>&& aObservedAttributes,
                          JSObject* aPrototype,
                          mozilla::dom::LifecycleCallbacks* aCallbacks,
                          uint32_t aDocOrder);

  // The type (name) for this custom element, for <button is="x-foo"> or <x-foo>
  // this would be x-foo.
  nsCOMPtr<nsIAtom> mType;

  // The localname to (e.g. <button is=type> -- this would be button).
  nsCOMPtr<nsIAtom> mLocalName;

  // The custom element constructor.
  RefPtr<CustomElementConstructor> mConstructor;

  // The list of attributes that this custom element observes.
  nsCOMArray<nsIAtom> mObservedAttributes;

  // The prototype to use for new custom elements of this type.
  JS::Heap<JSObject *> mPrototype;

  // The lifecycle callbacks to call for this custom element.
  UniquePtr<mozilla::dom::LifecycleCallbacks> mCallbacks;

  // A construction stack. Use nullptr to represent an "already constructed marker".
  nsTArray<RefPtr<nsGenericHTMLElement>> mConstructionStack;

  // The document custom element order.
  uint32_t mDocOrder;

  bool IsCustomBuiltIn()
  {
    return mType != mLocalName;
  }

  bool IsInObservedAttributeList(nsIAtom* aName)
  {
    if (mObservedAttributes.IsEmpty()) {
      return false;
    }

    return mObservedAttributes.Contains(aName);
  }

private:
  ~CustomElementDefinition() {}
};

class CustomElementReaction
{
public:
  virtual ~CustomElementReaction() = default;
  virtual void Invoke(Element* aElement, ErrorResult& aRv) = 0;
  virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const
  {
  }

#if DEBUG
  bool IsUpgradeReaction()
  {
    return mIsUpgradeReaction;
  }

protected:
  bool mIsUpgradeReaction = false;
#endif
};

class CustomElementUpgradeReaction final : public CustomElementReaction
{
public:
  explicit CustomElementUpgradeReaction(CustomElementDefinition* aDefinition)
    : mDefinition(aDefinition)
  {
#if DEBUG
    mIsUpgradeReaction = true;
#endif
  }

private:
   virtual void Invoke(Element* aElement, ErrorResult& aRv) override;

   CustomElementDefinition* mDefinition;
};

class CustomElementCallbackReaction final : public CustomElementReaction
{
  public:
    explicit CustomElementCallbackReaction(UniquePtr<CustomElementCallback> aCustomElementCallback)
      : mCustomElementCallback(Move(aCustomElementCallback))
    {
    }

    virtual void Traverse(nsCycleCollectionTraversalCallback& aCb) const override
    {
      mCustomElementCallback->Traverse(aCb);
    }

  private:
    virtual void Invoke(Element* aElement, ErrorResult& aRv) override;
    UniquePtr<CustomElementCallback> mCustomElementCallback;
};

// https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reactions-stack
class CustomElementReactionsStack
{
public:
  NS_INLINE_DECL_REFCOUNTING(CustomElementReactionsStack)

  CustomElementReactionsStack()
    : mIsBackupQueueProcessing(false)
    , mRecursionDepth(0)
    , mIsElementQueuePushedForCurrentRecursionDepth(false)
  {
  }

  // Hold a strong reference of Element so that it does not get cycle collected
  // before the reactions in its reaction queue are invoked.
  // The element reaction queues are stored in CustomElementData.
  // We need to lookup ElementReactionQueueMap again to get relevant reaction queue.
  // The choice of 1 for the auto size here is based on gut feeling.
  typedef AutoTArray<RefPtr<Element>, 1> ElementQueue;

  /**
   * Enqueue a custom element upgrade reaction
   * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-upgrade-reaction
   */
  void EnqueueUpgradeReaction(Element* aElement,
                              CustomElementDefinition* aDefinition);

  /**
   * Enqueue a custom element callback reaction
   * https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-callback-reaction
   */
  void EnqueueCallbackReaction(Element* aElement,
                               UniquePtr<CustomElementCallback> aCustomElementCallback);

  /**
   * [CEReactions] Before executing the algorithm's steps.
   * Increase the current recursion depth, and the element queue is pushed
   * lazily when we really enqueue reactions.
   *
   * @return true if the element queue is pushed for "previous" recursion depth.
   */
  bool EnterCEReactions()
  {
    bool temp = mIsElementQueuePushedForCurrentRecursionDepth;
    mRecursionDepth++;
    // The is-element-queue-pushed flag is initially false when entering a new
    // recursion level. The original value will be cached in AutoCEReaction
    // and restored after leaving this recursion level.
    mIsElementQueuePushedForCurrentRecursionDepth = false;
    return temp;
  }

  /**
   * [CEReactions] After executing the algorithm's steps.
   * Pop and invoke the element queue if it is created and pushed for current
   * recursion depth, then decrease the current recursion depth.
   *
   * @param aCx JSContext used for handling exception thrown by algorithm's
   *            steps, this could be a nullptr.
   *        aWasElementQueuePushed used for restoring status after leaving
   *                               current recursion.
   */
  void LeaveCEReactions(JSContext* aCx, bool aWasElementQueuePushed)
  {
    MOZ_ASSERT(mRecursionDepth);

    if (mIsElementQueuePushedForCurrentRecursionDepth) {
      Maybe<JS::AutoSaveExceptionState> ases;
      if (aCx) {
        ases.emplace(aCx);
      }
      PopAndInvokeElementQueue();
    }
    mRecursionDepth--;
    // Restore the is-element-queue-pushed flag cached in AutoCEReaction when
    // leaving the recursion level.
    mIsElementQueuePushedForCurrentRecursionDepth = aWasElementQueuePushed;

    MOZ_ASSERT_IF(!mRecursionDepth, mReactionsStack.IsEmpty());
  }

private:
  ~CustomElementReactionsStack() {};

  /**
   * Push a new element queue onto the custom element reactions stack.
   */
  void CreateAndPushElementQueue();

  /**
   * Pop the element queue from the custom element reactions stack, and invoke
   * custom element reactions in that queue.
   */
  void PopAndInvokeElementQueue();

  // The choice of 8 for the auto size here is based on gut feeling.
  AutoTArray<UniquePtr<ElementQueue>, 8> mReactionsStack;
  ElementQueue mBackupQueue;
  // https://html.spec.whatwg.org/#enqueue-an-element-on-the-appropriate-element-queue
  bool mIsBackupQueueProcessing;

  void InvokeBackupQueue();

  /**
   * Invoke custom element reactions
   * https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions
   */
  void InvokeReactions(ElementQueue* aElementQueue, nsIGlobalObject* aGlobal);

  void Enqueue(Element* aElement, CustomElementReaction* aReaction);

  // Current [CEReactions] recursion depth.
  uint32_t mRecursionDepth;
  // True if the element queue is pushed into reaction stack for current
  // recursion depth. This will be cached in AutoCEReaction when entering a new
  // CEReaction recursion and restored after leaving the recursion.
  bool mIsElementQueuePushedForCurrentRecursionDepth;

private:
  class BackupQueueMicroTask final : public mozilla::MicroTaskRunnable {
    public:
      explicit BackupQueueMicroTask(CustomElementReactionsStack* aReactionStack)
        : MicroTaskRunnable()
        , mReactionStack(aReactionStack)
      {
        MOZ_ASSERT(!mReactionStack->mIsBackupQueueProcessing,
                   "mIsBackupQueueProcessing should be initially false");
        mReactionStack->mIsBackupQueueProcessing = true;
      }

      virtual void Run(AutoSlowOperation& aAso) override
      {
        mReactionStack->InvokeBackupQueue();
        mReactionStack->mIsBackupQueueProcessing = false;
      }

    private:
      RefPtr<CustomElementReactionsStack> mReactionStack;
  };
};

class CustomElementRegistry final : public nsISupports,
                                    public nsWrapperCache
{
  // Allow nsDocument to access mCustomDefinitions and mCandidatesMap.
  friend class ::nsDocument;

public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CustomElementRegistry)

public:
  static bool IsCustomElementEnabled(JSContext* aCx = nullptr,
                                     JSObject* aObject = nullptr);

  explicit CustomElementRegistry(nsPIDOMWindowInner* aWindow);

  /**
   * Looking up a custom element definition.
   * https://html.spec.whatwg.org/#look-up-a-custom-element-definition
   */
  CustomElementDefinition* LookupCustomElementDefinition(
    nsIAtom* aNameAtom, nsIAtom* aTypeAtom) const;

  CustomElementDefinition* LookupCustomElementDefinition(
    JSContext* aCx, JSObject *aConstructor) const;

  static void EnqueueLifecycleCallback(nsIDocument::ElementCallbackType aType,
                                       Element* aCustomElement,
                                       LifecycleCallbackArgs* aArgs,
                                       LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
                                       CustomElementDefinition* aDefinition);

  /**
   * Upgrade an element.
   * https://html.spec.whatwg.org/multipage/scripting.html#upgrades
   */
  static void Upgrade(Element* aElement, CustomElementDefinition* aDefinition, ErrorResult& aRv);

  /**
   * Registers an unresolved custom element that is a candidate for
   * upgrade. |aTypeName| is the name of the custom element type, if it is not
   * provided, then element name is used. |aTypeName| should be provided
   * when registering a custom element that extends an existing
   * element. e.g. <button is="x-button">.
   */
  void RegisterUnresolvedElement(Element* aElement,
                                 nsIAtom* aTypeName = nullptr);

  /**
   * Unregister an unresolved custom element that is a candidate for
   * upgrade when a custom element is removed from tree.
   */
  void UnregisterUnresolvedElement(Element* aElement,
                                   nsIAtom* aTypeName = nullptr);
private:
  ~CustomElementRegistry();

  static UniquePtr<CustomElementCallback> CreateCustomElementCallback(
    nsIDocument::ElementCallbackType aType, Element* aCustomElement,
    LifecycleCallbackArgs* aArgs,
    LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
    CustomElementDefinition* aDefinition);

  void UpgradeCandidates(nsIAtom* aKey,
                         CustomElementDefinition* aDefinition,
                         ErrorResult& aRv);

  typedef nsRefPtrHashtable<nsISupportsHashKey, CustomElementDefinition>
    DefinitionMap;
  typedef nsClassHashtable<nsISupportsHashKey, nsTArray<nsWeakPtr>>
    CandidateMap;
  typedef JS::GCHashMap<JS::Heap<JSObject*>,
                        nsCOMPtr<nsIAtom>,
                        js::MovableCellHasher<JS::Heap<JSObject*>>,
                        js::SystemAllocPolicy> ConstructorMap;

  // Hashtable for custom element definitions in web components.
  // Custom prototypes are stored in the compartment where definition was
  // defined.
  DefinitionMap mCustomDefinitions;

  // Hashtable for looking up definitions by using constructor as key.
  // Custom elements' name are stored here and we need to lookup
  // mCustomDefinitions again to get definitions.
  ConstructorMap mConstructors;

  typedef nsRefPtrHashtable<nsISupportsHashKey, Promise>
    WhenDefinedPromiseMap;
  WhenDefinedPromiseMap mWhenDefinedPromiseMap;

  // The "upgrade candidates map" from the web components spec. Maps from a
  // namespace id and local name to a list of elements to upgrade if that
  // element is registered as a custom element.
  CandidateMap mCandidatesMap;

  nsCOMPtr<nsPIDOMWindowInner> mWindow;

  // It is used to prevent reentrant invocations of element definition.
  bool mIsCustomDefinitionRunning;

private:
  class MOZ_RAII AutoSetRunningFlag final {
    public:
      explicit AutoSetRunningFlag(CustomElementRegistry* aRegistry)
        : mRegistry(aRegistry)
      {
        MOZ_ASSERT(!mRegistry->mIsCustomDefinitionRunning,
                   "IsCustomDefinitionRunning flag should be initially false");
        mRegistry->mIsCustomDefinitionRunning = true;
      }

      ~AutoSetRunningFlag() {
        mRegistry->mIsCustomDefinitionRunning = false;
      }

    private:
      CustomElementRegistry* mRegistry;
  };

public:
  nsISupports* GetParentObject() const;

  DocGroup* GetDocGroup() const;

  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;

  void Define(const nsAString& aName, Function& aFunctionConstructor,
              const ElementDefinitionOptions& aOptions, ErrorResult& aRv);

  void Get(JSContext* cx, const nsAString& name,
           JS::MutableHandle<JS::Value> aRetVal);

  already_AddRefed<Promise> WhenDefined(const nsAString& aName, ErrorResult& aRv);
};

class MOZ_RAII AutoCEReaction final {
  public:
    // JSContext is allowed to be a nullptr if we are guaranteeing that we're
    // not doing something that might throw but not finish reporting a JS
    // exception during the lifetime of the AutoCEReaction.
    AutoCEReaction(CustomElementReactionsStack* aReactionsStack, JSContext* aCx)
      : mReactionsStack(aReactionsStack)
      , mCx(aCx)
    {
      mIsElementQueuePushedForPreviousRecursionDepth =
        mReactionsStack->EnterCEReactions();
    }

    ~AutoCEReaction()
    {
      mReactionsStack->LeaveCEReactions(
        mCx, mIsElementQueuePushedForPreviousRecursionDepth);
    }

  private:
    RefPtr<CustomElementReactionsStack> mReactionsStack;
    JSContext* mCx;
    bool mIsElementQueuePushedForPreviousRecursionDepth;
};

} // namespace dom
} // namespace mozilla


#endif // mozilla_dom_CustomElementRegistry_h