/* 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 "mozilla/Assertions.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/dom/Element.h"
#include "nsContentUtils.h"
#include "nsIAtom.h"
#include "nsIContent.h"
#include "nsIDocument.h"

#include "nsMenuContainer.h"

#include "nsNativeMenuDocListener.h"

using namespace mozilla;

uint32_t nsNativeMenuDocListener::sUpdateBlockersCount = 0;

nsNativeMenuDocListenerTArray* gPendingListeners;

/*
 * Small helper which caches a single listener, so that consecutive
 * events which go to the same node avoid multiple hash table lookups
 */
class MOZ_STACK_CLASS DispatchHelper {
public:
    DispatchHelper(nsNativeMenuDocListener* aListener,
                   nsIContent* aContent
                   MOZ_GUARD_OBJECT_NOTIFIER_PARAM) :
                   mObserver(nullptr) {
        MOZ_GUARD_OBJECT_NOTIFIER_INIT;
        if (aContent == aListener->mLastSource) {
            mObserver = aListener->mLastTarget;
        } else {
            mObserver = aListener->mContentToObserverTable.Get(aContent);
            if (mObserver) {
                aListener->mLastSource = aContent;
                aListener->mLastTarget = mObserver;
            }
        }
    }

    ~DispatchHelper() { };

    nsNativeMenuChangeObserver* Observer() const {
      return mObserver; 
    }

    bool HasObserver() const {
      return !!mObserver;
    }

private:
    nsNativeMenuChangeObserver* mObserver;
    MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};

NS_IMPL_ISUPPORTS(nsNativeMenuDocListener, nsIMutationObserver)

nsNativeMenuDocListener::~nsNativeMenuDocListener() {
    MOZ_ASSERT(mContentToObserverTable.Count() == 0,
               "Some nodes forgot to unregister listeners. This is bad! (and we're lucky we made it this far)");
    MOZ_COUNT_DTOR(nsNativeMenuDocListener);
}

void
nsNativeMenuDocListener::AttributeChanged(nsIDocument* aDocument,
                                          mozilla::dom::Element* aElement,
                                          int32_t aNameSpaceID,
                                          nsIAtom* aAttribute,
                                          int32_t aModType,
                                          const nsAttrValue* aOldValue) {
    if (sUpdateBlockersCount == 0) {
        DoAttributeChanged(aElement, aAttribute);
        return;
    }

    MutationRecord* m =* mPendingMutations.AppendElement(new MutationRecord);
    m->mType = MutationRecord::eAttributeChanged;
    m->mTarget = aElement;
    m->mAttribute = aAttribute;

    ScheduleFlush(this);
}

void
nsNativeMenuDocListener::ContentAppended(nsIDocument* aDocument,
                                         nsIContent* aContainer,
                                         nsIContent* aFirstNewContent,
                                         int32_t aNewIndexInContainer) {
    for (nsIContent* c = aFirstNewContent; c; c = c->GetNextSibling()) {
        ContentInserted(aDocument, aContainer, c, 0);
    }
}

void
nsNativeMenuDocListener::ContentInserted(nsIDocument* aDocument,
                                         nsIContent* aContainer,
                                         nsIContent* aChild,
                                         int32_t aIndexInContainer) {
    nsIContent* prevSibling = nsMenuContainer::GetPreviousSupportedSibling(aChild);

    if (sUpdateBlockersCount == 0) {
        DoContentInserted(aContainer, aChild, prevSibling);
        return;
    }

    MutationRecord* m =* mPendingMutations.AppendElement(new MutationRecord);
    m->mType = MutationRecord::eContentInserted;
    m->mTarget = aContainer;
    m->mChild = aChild;
    m->mPrevSibling = prevSibling;

    ScheduleFlush(this);
}

void
nsNativeMenuDocListener::ContentRemoved(nsIDocument* aDocument,
                                        nsIContent* aContainer,
                                        nsIContent* aChild,
                                        int32_t aIndexInContainer,
                                        nsIContent* aPreviousSibling) {
    if (sUpdateBlockersCount == 0) {
        DoContentRemoved(aContainer, aChild);
        return;
    }

    MutationRecord* m =* mPendingMutations.AppendElement(new MutationRecord);
    m->mType = MutationRecord::eContentRemoved;
    m->mTarget = aContainer;
    m->mChild = aChild;

    ScheduleFlush(this);
}                                                           

void
nsNativeMenuDocListener::NodeWillBeDestroyed(const nsINode* aNode) {
    mDocument = nullptr;
}

void
nsNativeMenuDocListener::DoAttributeChanged(nsIContent* aContent,
                                            nsIAtom* aAttribute) {
    DispatchHelper h(this, aContent);
    if (h.HasObserver()) {
        h.Observer()->OnAttributeChanged(aContent, aAttribute);
    }
}

void
nsNativeMenuDocListener::DoContentInserted(nsIContent* aContainer,
                                           nsIContent* aChild,
                                           nsIContent* aPrevSibling) {
    DispatchHelper h(this, aContainer);
    if (h.HasObserver()) {
        h.Observer()->OnContentInserted(aContainer, aChild, aPrevSibling);
    }
}

void
nsNativeMenuDocListener::DoContentRemoved(nsIContent* aContainer,
                                          nsIContent* aChild) {
    DispatchHelper h(this, aContainer);
    if (h.HasObserver()) {
        h.Observer()->OnContentRemoved(aContainer, aChild);
    }
}

void
nsNativeMenuDocListener::DoBeginUpdates(nsIContent* aTarget) {
    DispatchHelper h(this, aTarget);
    if (h.HasObserver()) {
        h.Observer()->OnBeginUpdates(aTarget);
    }
}

void
nsNativeMenuDocListener::DoEndUpdates(nsIContent* aTarget) {
    DispatchHelper h(this, aTarget);
    if (h.HasObserver()) {
        h.Observer()->OnEndUpdates();
    }
}

void
nsNativeMenuDocListener::FlushPendingMutations() {
    nsIContent* currentTarget = nullptr;
    bool inUpdateSequence = false;

    while (mPendingMutations.Length() > 0) {
        MutationRecord* m = mPendingMutations[0];

        if (m->mTarget != currentTarget) {
            if (inUpdateSequence) {
                DoEndUpdates(currentTarget);
                inUpdateSequence = false;
            }

            currentTarget = m->mTarget;

            if (mPendingMutations.Length() > 1 &&
                mPendingMutations[1]->mTarget == currentTarget) {
                DoBeginUpdates(currentTarget);
                inUpdateSequence = true;
            }
        }

        switch (m->mType) {
            case MutationRecord::eAttributeChanged:
                DoAttributeChanged(m->mTarget, m->mAttribute);
                break;
            case MutationRecord::eContentInserted:
                DoContentInserted(m->mTarget, m->mChild, m->mPrevSibling);
                break;
            case MutationRecord::eContentRemoved:
                DoContentRemoved(m->mTarget, m->mChild);
                break;
            default:
                NS_NOTREACHED("Invalid type");
        }

        mPendingMutations.RemoveElementAt(0);
    }

    if (inUpdateSequence) {
        DoEndUpdates(currentTarget);
    }
}

/* static */ void
nsNativeMenuDocListener::ScheduleFlush(nsNativeMenuDocListener* aListener) {
    MOZ_ASSERT(sUpdateBlockersCount > 0, "Shouldn't be doing this now");

    if (!gPendingListeners) {
        gPendingListeners = new nsNativeMenuDocListenerTArray;
    }

    if (gPendingListeners->IndexOf(aListener) ==
        nsNativeMenuDocListenerTArray::NoIndex) {
        gPendingListeners->AppendElement(aListener);
    }
}

/* static */ void
nsNativeMenuDocListener::CancelFlush(nsNativeMenuDocListener* aListener) {
    if (!gPendingListeners) {
        return;
    }

    gPendingListeners->RemoveElement(aListener);
}

/* static */ void
nsNativeMenuDocListener::RemoveUpdateBlocker() {
    if (sUpdateBlockersCount == 1 && gPendingListeners) {
        while (gPendingListeners->Length() > 0) {
            (*gPendingListeners)[0]->FlushPendingMutations();
            gPendingListeners->RemoveElementAt(0);
        }
    }
 
    MOZ_ASSERT(sUpdateBlockersCount > 0, "Negative update blockers count!");
    sUpdateBlockersCount--;
}

nsNativeMenuDocListener::nsNativeMenuDocListener(nsIContent* aRootNode) :
    mRootNode(aRootNode),
    mDocument(nullptr),
    mLastSource(nullptr),
    mLastTarget(nullptr) {
    MOZ_COUNT_CTOR(nsNativeMenuDocListener);
}

void
nsNativeMenuDocListener::RegisterForContentChanges(nsIContent* aContent,
                                                   nsNativeMenuChangeObserver* aObserver) {
    MOZ_ASSERT(aContent, "Need content parameter");
    MOZ_ASSERT(aObserver, "Need observer parameter");
    if (!aContent || !aObserver) {
        return;
    }

    DebugOnly<nsNativeMenuChangeObserver* > old;
    MOZ_ASSERT(!mContentToObserverTable.Get(aContent, &old) || old == aObserver,
               "Multiple observers for the same content node are not supported");

    mContentToObserverTable.Put(aContent, aObserver);
}

void
nsNativeMenuDocListener::UnregisterForContentChanges(nsIContent* aContent) {
    MOZ_ASSERT(aContent, "Need content parameter");
    if (!aContent) {
        return;
    }

    mContentToObserverTable.Remove(aContent);
    if (aContent == mLastSource) {
        mLastSource = nullptr;
        mLastTarget = nullptr;
    }
}

void
nsNativeMenuDocListener::Start() {
    if (mDocument) {
        return;
    }

    mDocument = mRootNode->OwnerDoc();
    if (!mDocument) {
        return;
    }

    mDocument->AddMutationObserver(this);
}

void
nsNativeMenuDocListener::Stop() {
    if (mDocument) {
        mDocument->RemoveMutationObserver(this);
        mDocument = nullptr;
    }

    CancelFlush(this);
    mPendingMutations.Clear();
}