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

#include "XULBroadcastManager.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/DocumentInlines.h"
#include "nsXULElement.h"

struct BroadcastListener {
  nsWeakPtr mListener;
  RefPtr<nsAtom> mAttribute;
};

struct BroadcasterMapEntry : public PLDHashEntryHdr {
  mozilla::dom::Element* mBroadcaster;  // [WEAK]
  nsTArray<BroadcastListener*>
      mListeners;  // [OWNING] of BroadcastListener objects
};

struct nsAttrNameInfo {
  nsAttrNameInfo(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix)
      : mNamespaceID(aNamespaceID), mName(aName), mPrefix(aPrefix) {}
  nsAttrNameInfo(const nsAttrNameInfo& aOther) = delete;
  nsAttrNameInfo(nsAttrNameInfo&& aOther) = default;

  int32_t mNamespaceID;
  RefPtr<nsAtom> mName;
  RefPtr<nsAtom> mPrefix;
};

static void ClearBroadcasterMapEntry(PLDHashTable* aTable,
                                     PLDHashEntryHdr* aEntry) {
  BroadcasterMapEntry* entry = static_cast<BroadcasterMapEntry*>(aEntry);
  for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
    delete entry->mListeners[i];
  }
  entry->mListeners.Clear();

  // N.B. that we need to manually run the dtor because we
  // constructed the nsTArray object in-place.
  entry->mListeners.~nsTArray<BroadcastListener*>();
}

static bool CanBroadcast(int32_t aNameSpaceID, nsAtom* aAttribute) {
  // Don't push changes to the |id|, |persist|, |command| or
  // |observes| attribute.
  if (aNameSpaceID == kNameSpaceID_None) {
    if ((aAttribute == nsGkAtoms::id) || (aAttribute == nsGkAtoms::persist) ||
        (aAttribute == nsGkAtoms::command) ||
        (aAttribute == nsGkAtoms::observes)) {
      return false;
    }
  }
  return true;
}

namespace mozilla::dom {
static LazyLogModule sXULBroadCastManager("XULBroadcastManager");

class XULBroadcastManager::nsDelayedBroadcastUpdate {
 public:
  nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
                           const nsAString& aAttr)
      : mBroadcaster(aBroadcaster),
        mListener(aListener),
        mAttr(aAttr),
        mSetAttr(false),
        mNeedsAttrChange(false) {}

  nsDelayedBroadcastUpdate(Element* aBroadcaster, Element* aListener,
                           nsAtom* aAttrName, const nsAString& aAttr,
                           bool aSetAttr, bool aNeedsAttrChange)
      : mBroadcaster(aBroadcaster),
        mListener(aListener),
        mAttr(aAttr),
        mAttrName(aAttrName),
        mSetAttr(aSetAttr),
        mNeedsAttrChange(aNeedsAttrChange) {}

  nsDelayedBroadcastUpdate(const nsDelayedBroadcastUpdate& aOther) = delete;
  nsDelayedBroadcastUpdate(nsDelayedBroadcastUpdate&& aOther) = default;

  RefPtr<Element> mBroadcaster;
  RefPtr<Element> mListener;
  // Note if mAttrName isn't used, this is the name of the attr, otherwise
  // this is the value of the attribute.
  nsString mAttr;
  RefPtr<nsAtom> mAttrName;
  bool mSetAttr;
  bool mNeedsAttrChange;

  class Comparator {
   public:
    static bool Equals(const nsDelayedBroadcastUpdate& a,
                       const nsDelayedBroadcastUpdate& b) {
      return a.mBroadcaster == b.mBroadcaster && a.mListener == b.mListener &&
             a.mAttrName == b.mAttrName;
    }
  };
};

/* static */
bool XULBroadcastManager::MayNeedListener(const Element& aElement) {
  if (aElement.NodeInfo()->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
    return true;
  }
  if (aElement.HasAttr(nsGkAtoms::observes)) {
    return true;
  }
  if (aElement.HasAttr(nsGkAtoms::command) &&
      !(aElement.NodeInfo()->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
        aElement.NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL))) {
    return true;
  }
  return false;
}

XULBroadcastManager::XULBroadcastManager(Document* aDocument)
    : mDocument(aDocument),
      mBroadcasterMap(nullptr),
      mHandlingDelayedAttrChange(false),
      mHandlingDelayedBroadcasters(false) {}

XULBroadcastManager::~XULBroadcastManager() { delete mBroadcasterMap; }

void XULBroadcastManager::DropDocumentReference(void) { mDocument = nullptr; }

void XULBroadcastManager::SynchronizeBroadcastListener(Element* aBroadcaster,
                                                       Element* aListener,
                                                       const nsAString& aAttr) {
  if (!nsContentUtils::IsSafeToRunScript()) {
    mDelayedBroadcasters.EmplaceBack(aBroadcaster, aListener, aAttr);
    MaybeBroadcast();
    return;
  }
  bool notify = mHandlingDelayedBroadcasters;

  if (aAttr.EqualsLiteral("*")) {
    uint32_t count = aBroadcaster->GetAttrCount();
    nsTArray<nsAttrNameInfo> attributes(count);
    for (uint32_t i = 0; i < count; ++i) {
      const nsAttrName* attrName = aBroadcaster->GetAttrNameAt(i);
      int32_t nameSpaceID = attrName->NamespaceID();
      nsAtom* name = attrName->LocalName();

      // _Don't_ push the |id|, |ref|, or |persist| attribute's value!
      if (!CanBroadcast(nameSpaceID, name)) continue;

      attributes.AppendElement(
          nsAttrNameInfo(nameSpaceID, name, attrName->GetPrefix()));
    }

    count = attributes.Length();
    while (count-- > 0) {
      int32_t nameSpaceID = attributes[count].mNamespaceID;
      nsAtom* name = attributes[count].mName;
      nsAutoString value;
      if (aBroadcaster->GetAttr(nameSpaceID, name, value)) {
        aListener->SetAttr(nameSpaceID, name, attributes[count].mPrefix, value,
                           notify);
      }

#if 0
            // XXX we don't fire the |onbroadcast| handler during
            // initial hookup: doing so would potentially run the
            // |onbroadcast| handler before the |onload| handler,
            // which could define JS properties that mask XBL
            // properties, etc.
            ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
#endif
    }
  } else {
    // Find out if the attribute is even present at all.
    RefPtr<nsAtom> name = NS_Atomize(aAttr);

    nsAutoString value;
    if (aBroadcaster->GetAttr(name, value)) {
      aListener->SetAttr(kNameSpaceID_None, name, value, notify);
    } else {
      aListener->UnsetAttr(kNameSpaceID_None, name, notify);
    }

#if 0
        // XXX we don't fire the |onbroadcast| handler during initial
        // hookup: doing so would potentially run the |onbroadcast|
        // handler before the |onload| handler, which could define JS
        // properties that mask XBL properties, etc.
        ExecuteOnBroadcastHandlerFor(aBroadcaster, aListener, name);
#endif
  }
}

void XULBroadcastManager::AddListenerFor(Element& aBroadcaster,
                                         Element& aListener,
                                         const nsAString& aAttr,
                                         ErrorResult& aRv) {
  if (!mDocument) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  nsresult rv = nsContentUtils::CheckSameOrigin(mDocument, &aBroadcaster);

  if (NS_FAILED(rv)) {
    aRv.Throw(rv);
    return;
  }

  rv = nsContentUtils::CheckSameOrigin(mDocument, &aListener);

  if (NS_FAILED(rv)) {
    aRv.Throw(rv);
    return;
  }

  static const PLDHashTableOps gOps = {
      PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub,
      PLDHashTable::MoveEntryStub, ClearBroadcasterMapEntry, nullptr};

  if (!mBroadcasterMap) {
    mBroadcasterMap = new PLDHashTable(&gOps, sizeof(BroadcasterMapEntry));
  }

  auto entry =
      static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
  if (!entry) {
    entry = static_cast<BroadcasterMapEntry*>(
        mBroadcasterMap->Add(&aBroadcaster, fallible));

    if (!entry) {
      aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
      return;
    }

    entry->mBroadcaster = &aBroadcaster;

    // N.B. placement new to construct the nsTArray object in-place
    new (&entry->mListeners) nsTArray<BroadcastListener*>();
  }

  // Only add the listener if it's not there already!
  RefPtr<nsAtom> attr = NS_Atomize(aAttr);

  for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
    BroadcastListener* bl = entry->mListeners[i];
    nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);

    if (blListener == &aListener && bl->mAttribute == attr) return;
  }

  BroadcastListener* bl = new BroadcastListener;
  bl->mListener = do_GetWeakReference(&aListener);
  bl->mAttribute = attr;

  entry->mListeners.AppendElement(bl);

  SynchronizeBroadcastListener(&aBroadcaster, &aListener, aAttr);
}

void XULBroadcastManager::RemoveListenerFor(Element& aBroadcaster,
                                            Element& aListener,
                                            const nsAString& aAttr) {
  // If we haven't added any broadcast listeners, then there sure
  // aren't any to remove.
  if (!mBroadcasterMap) return;

  auto entry =
      static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(&aBroadcaster));
  if (entry) {
    RefPtr<nsAtom> attr = NS_Atomize(aAttr);
    for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
      BroadcastListener* bl = entry->mListeners[i];
      nsCOMPtr<Element> blListener = do_QueryReferent(bl->mListener);

      if (blListener == &aListener && bl->mAttribute == attr) {
        entry->mListeners.RemoveElementAt(i);
        delete bl;

        if (entry->mListeners.IsEmpty()) mBroadcasterMap->RemoveEntry(entry);

        break;
      }
    }
  }
}

nsresult XULBroadcastManager::ExecuteOnBroadcastHandlerFor(
    Element* aBroadcaster, Element* aListener, nsAtom* aAttr) {
  if (!mDocument) {
    return NS_OK;
  }
  // Now we execute the onchange handler in the context of the
  // observer. We need to find the observer in order to
  // execute the handler.

  for (nsCOMPtr<nsIContent> child = aListener->GetFirstChild(); child;
       child = child->GetNextSibling()) {
    // Look for an <observes> element beneath the listener. This
    // ought to have an |element| attribute that refers to
    // aBroadcaster, and an |attribute| element that tells us what
    // attriubtes we're listening for.
    if (!child->IsXULElement(nsGkAtoms::observes)) continue;

    // Is this the element that was listening to us?
    nsAutoString listeningToID;
    child->AsElement()->GetAttr(nsGkAtoms::element, listeningToID);

    nsAutoString broadcasterID;
    aBroadcaster->GetAttr(nsGkAtoms::id, broadcasterID);

    if (listeningToID != broadcasterID) continue;

    // We are observing the broadcaster, but is this the right
    // attribute?
    nsAutoString listeningToAttribute;
    child->AsElement()->GetAttr(nsGkAtoms::attribute, listeningToAttribute);

    if (!aAttr->Equals(listeningToAttribute) &&
        !listeningToAttribute.EqualsLiteral("*")) {
      continue;
    }

    // This is the right <observes> element. Execute the
    // |onbroadcast| event handler
    WidgetEvent event(true, eXULBroadcast);

    if (RefPtr<nsPresContext> presContext = mDocument->GetPresContext()) {
      // Handle the DOM event
      nsEventStatus status = nsEventStatus_eIgnore;
      EventDispatcher::Dispatch(child, presContext, &event, nullptr, &status);
    }
  }

  return NS_OK;
}

void XULBroadcastManager::AttributeChanged(Element* aElement,
                                           int32_t aNameSpaceID,
                                           nsAtom* aAttribute) {
  if (!mDocument) {
    return;
  }
  NS_ASSERTION(aElement->OwnerDoc() == mDocument, "unexpected doc");

  // Synchronize broadcast listeners
  if (mBroadcasterMap && CanBroadcast(aNameSpaceID, aAttribute)) {
    auto entry =
        static_cast<BroadcasterMapEntry*>(mBroadcasterMap->Search(aElement));

    if (entry) {
      // We've got listeners: push the value.
      nsAutoString value;
      bool attrSet = aElement->GetAttr(aAttribute, value);

      for (size_t i = entry->mListeners.Length() - 1; i != (size_t)-1; --i) {
        BroadcastListener* bl = entry->mListeners[i];
        if ((bl->mAttribute == aAttribute) ||
            (bl->mAttribute == nsGkAtoms::_asterisk)) {
          nsCOMPtr<Element> listenerEl = do_QueryReferent(bl->mListener);
          if (listenerEl) {
            nsAutoString currentValue;
            bool hasAttr = listenerEl->GetAttr(aAttribute, currentValue);
            // We need to update listener only if we're
            // (1) removing an existing attribute,
            // (2) adding a new attribute or
            // (3) changing the value of an attribute.
            bool needsAttrChange =
                attrSet != hasAttr || !value.Equals(currentValue);
            nsDelayedBroadcastUpdate delayedUpdate(aElement, listenerEl,
                                                   aAttribute, value, attrSet,
                                                   needsAttrChange);

            size_t index = mDelayedAttrChangeBroadcasts.IndexOf(
                delayedUpdate, 0, nsDelayedBroadcastUpdate::Comparator());
            if (index != mDelayedAttrChangeBroadcasts.NoIndex) {
              if (mHandlingDelayedAttrChange) {
                NS_WARNING("Broadcasting loop!");
                continue;
              }
              mDelayedAttrChangeBroadcasts.RemoveElementAt(index);
            }

            mDelayedAttrChangeBroadcasts.AppendElement(
                std::move(delayedUpdate));
          }
        }
      }
    }
  }
}

void XULBroadcastManager::MaybeBroadcast() {
  // Only broadcast when not in an update and when safe to run scripts.
  if (mDocument && mDocument->UpdateNestingLevel() == 0 &&
      (mDelayedAttrChangeBroadcasts.Length() ||
       mDelayedBroadcasters.Length())) {
    if (!nsContentUtils::IsSafeToRunScript()) {
      if (mDocument) {
        nsContentUtils::AddScriptRunner(
            NewRunnableMethod("dom::XULBroadcastManager::MaybeBroadcast", this,
                              &XULBroadcastManager::MaybeBroadcast));
      }
      return;
    }
    if (!mHandlingDelayedAttrChange) {
      mHandlingDelayedAttrChange = true;
      for (uint32_t i = 0; i < mDelayedAttrChangeBroadcasts.Length(); ++i) {
        RefPtr<nsAtom> attrName = mDelayedAttrChangeBroadcasts[i].mAttrName;
        RefPtr<Element> listener = mDelayedAttrChangeBroadcasts[i].mListener;
        if (mDelayedAttrChangeBroadcasts[i].mNeedsAttrChange) {
          const nsString& value = mDelayedAttrChangeBroadcasts[i].mAttr;
          if (mDelayedAttrChangeBroadcasts[i].mSetAttr) {
            listener->SetAttr(kNameSpaceID_None, attrName, value, true);
          } else {
            listener->UnsetAttr(kNameSpaceID_None, attrName, true);
          }
        }
        RefPtr<Element> broadcaster =
            mDelayedAttrChangeBroadcasts[i].mBroadcaster;
        ExecuteOnBroadcastHandlerFor(broadcaster, listener, attrName);
      }
      mDelayedAttrChangeBroadcasts.Clear();
      mHandlingDelayedAttrChange = false;
    }

    uint32_t length = mDelayedBroadcasters.Length();
    if (length) {
      bool oldValue = mHandlingDelayedBroadcasters;
      mHandlingDelayedBroadcasters = true;
      nsTArray<nsDelayedBroadcastUpdate> delayedBroadcasters =
          std::move(mDelayedBroadcasters);
      for (uint32_t i = 0; i < length; ++i) {
        SynchronizeBroadcastListener(delayedBroadcasters[i].mBroadcaster,
                                     delayedBroadcasters[i].mListener,
                                     delayedBroadcasters[i].mAttr);
      }
      mHandlingDelayedBroadcasters = oldValue;
    }
  }
}

nsresult XULBroadcastManager::FindBroadcaster(Element* aElement,
                                              Element** aListener,
                                              nsString& aBroadcasterID,
                                              nsString& aAttribute,
                                              Element** aBroadcaster) {
  NodeInfo* ni = aElement->NodeInfo();
  *aListener = nullptr;
  *aBroadcaster = nullptr;

  if (ni->Equals(nsGkAtoms::observes, kNameSpaceID_XUL)) {
    // It's an <observes> element, which means that the actual
    // listener is the _parent_ node. This element should have an
    // 'element' attribute that specifies the ID of the
    // broadcaster element, and an 'attribute' element, which
    // specifies the name of the attribute to observe.
    nsIContent* parent = aElement->GetParent();
    if (!parent) {
      // <observes> is the root element
      return NS_FINDBROADCASTER_NOT_FOUND;
    }

    *aListener = Element::FromNode(parent);
    NS_IF_ADDREF(*aListener);

    aElement->GetAttr(nsGkAtoms::element, aBroadcasterID);
    if (aBroadcasterID.IsEmpty()) {
      return NS_FINDBROADCASTER_NOT_FOUND;
    }
    aElement->GetAttr(nsGkAtoms::attribute, aAttribute);
  } else {
    // It's a generic element, which means that we'll use the
    // value of the 'observes' attribute to determine the ID of
    // the broadcaster element, and we'll watch _all_ of its
    // values.
    aElement->GetAttr(nsGkAtoms::observes, aBroadcasterID);

    // Bail if there's no aBroadcasterID
    if (aBroadcasterID.IsEmpty()) {
      // Try the command attribute next.
      aElement->GetAttr(nsGkAtoms::command, aBroadcasterID);
      if (!aBroadcasterID.IsEmpty()) {
        // We've got something in the command attribute.  We
        // only treat this as a normal broadcaster if we are
        // not a menuitem or a key.

        if (ni->Equals(nsGkAtoms::menuitem, kNameSpaceID_XUL) ||
            ni->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) {
          return NS_FINDBROADCASTER_NOT_FOUND;
        }
      } else {
        return NS_FINDBROADCASTER_NOT_FOUND;
      }
    }

    *aListener = aElement;
    NS_ADDREF(*aListener);

    aAttribute.Assign('*');
  }

  // Make sure we got a valid listener.
  NS_ENSURE_TRUE(*aListener, NS_ERROR_UNEXPECTED);

  // Try to find the broadcaster element in the document.
  Document* doc = aElement->GetComposedDoc();
  if (doc) {
    *aBroadcaster = doc->GetElementById(aBroadcasterID);
  }

  // The broadcaster element is missing.
  if (!*aBroadcaster) {
    return NS_FINDBROADCASTER_NOT_FOUND;
  }

  NS_ADDREF(*aBroadcaster);

  return NS_FINDBROADCASTER_FOUND;
}

nsresult XULBroadcastManager::UpdateListenerHookup(Element* aElement,
                                                   HookupAction aAction) {
  // Resolve a broadcaster hookup. Look at the element that we're
  // trying to resolve: it could be an '<observes>' element, or just
  // a vanilla element with an 'observes' attribute on it.
  nsresult rv;

  nsCOMPtr<Element> listener;
  nsAutoString broadcasterID;
  nsAutoString attribute;
  nsCOMPtr<Element> broadcaster;

  rv = FindBroadcaster(aElement, getter_AddRefs(listener), broadcasterID,
                       attribute, getter_AddRefs(broadcaster));
  switch (rv) {
    case NS_FINDBROADCASTER_NOT_FOUND:
      return NS_OK;
    case NS_FINDBROADCASTER_FOUND:
      break;
    default:
      return rv;
  }

  NS_ENSURE_ARG(broadcaster && listener);
  if (aAction == eHookupAdd) {
    ErrorResult domRv;
    AddListenerFor(*broadcaster, *listener, attribute, domRv);
    if (domRv.Failed()) {
      return domRv.StealNSResult();
    }
  } else {
    RemoveListenerFor(*broadcaster, *listener, attribute);
  }

  // Tell the world we succeeded
  if (MOZ_LOG_TEST(sXULBroadCastManager, LogLevel::Debug)) {
    nsCOMPtr<nsIContent> content = listener;
    NS_ASSERTION(content != nullptr, "not an nsIContent");
    if (!content) {
      return rv;
    }

    nsAutoCString attributeC, broadcasteridC;
    LossyCopyUTF16toASCII(attribute, attributeC);
    LossyCopyUTF16toASCII(broadcasterID, broadcasteridC);
    MOZ_LOG(sXULBroadCastManager, LogLevel::Debug,
            ("xul: broadcaster hookup <%s attribute='%s'> to %s",
             nsAtomCString(content->NodeInfo()->NameAtom()).get(),
             attributeC.get(), broadcasteridC.get()));
  }

  return NS_OK;
}

nsresult XULBroadcastManager::AddListener(Element* aElement) {
  return UpdateListenerHookup(aElement, eHookupAdd);
}

nsresult XULBroadcastManager::RemoveListener(Element* aElement) {
  return UpdateListenerHookup(aElement, eHookupRemove);
}

}  // namespace mozilla::dom
