/* 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 "HTMLEditor.h"

#include "EditAction.h"
#include "EditorDOMAPIWrapper.h"
#include "EditorUtils.h"
#include "HTMLEditorNestedClasses.h"

#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/Logging.h"
#include "mozilla/Maybe.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Element.h"
#include "mozInlineSpellChecker.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIMutationObserver.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsThreadUtils.h"

namespace mozilla {

using namespace dom;

/******************************************************************************
 * DOM mutation logger
 ******************************************************************************/

// - HTMLEditorMutation:3: Logging only mutations in editable containers which
// is not expected.
// - HTMLEditorMutation:4: Logging only mutations in editable containers which
// is either expected or not expected.
// - HTMLEditorMutation:5: Logging any mutations including in
// non-editable containers.
LazyLogModule gHTMLEditorMutationLog("HTMLEditorMutation");

LogLevel HTMLEditor::MutationLogLevelOf(
    nsIContent* aContent,
    const CharacterDataChangeInfo* aCharacterDataChangeInfo,
    DOMMutationType aDOMMutationType) const {
  // Should be called only when the "info" level is enabled at least since we
  // shouldn't add any new unnecessary calls in the hot paths when the logging
  // is disabled.
  MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info));

  if (MOZ_UNLIKELY(!aContent->IsInComposedDoc())) {
    return LogLevel::Disabled;
  }
  Element* const containerElement = aContent->GetAsElementOrParentElement();
  if (!containerElement || !containerElement->IsEditable()) {
    return MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Verbose)
               ? LogLevel::Verbose
               : LogLevel::Disabled;
  }
  if (!mRunningDOMAPIWrapper) {
    return LogLevel::Info;
  }
  switch (aDOMMutationType) {
    case DOMMutationType::ContentAppended:
      return mRunningDOMAPIWrapper->IsExpectedContentAppended(aContent)
                 ? LogLevel::Debug
                 : LogLevel::Info;
    case DOMMutationType::ContentInserted:
      return mRunningDOMAPIWrapper->IsExpectedContentInserted(aContent)
                 ? LogLevel::Debug
                 : LogLevel::Info;
    case DOMMutationType::ContentWillBeRemoved:
      return mRunningDOMAPIWrapper->IsExpectedContentWillBeRemoved(aContent)
                 ? LogLevel::Debug
                 : LogLevel::Info;
    case DOMMutationType::CharacterDataChanged:
      MOZ_ASSERT(aCharacterDataChangeInfo);
      return mRunningDOMAPIWrapper->IsExpectedCharacterDataChanged(
                 aContent, *aCharacterDataChangeInfo)
                 ? LogLevel::Debug
                 : LogLevel::Info;
    default:
      MOZ_ASSERT_UNREACHABLE("Invalid DOMMutationType value");
      return LogLevel::Disabled;
  }
}

// - HTMLEditorAttrMutation:3: Logging only mutations of editable element which
// is not expected.
// - HTMLEditorAttrMutation:4: Logging only mutations of editable element which
// is either expected or not expected.
// - HTMLEditorAttrMutation:5: Logging any mutations including non-editable
// elements' attributes.
LazyLogModule gHTMLEditorAttrMutationLog("HTMLEditorAttrMutation");

LogLevel HTMLEditor::AttrMutationLogLevelOf(
    Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute,
    AttrModType aModType, const nsAttrValue* aOldValue) const {
  // Should be called only when the "info" level is enabled at least since we
  // shouldn't add any new unnecessary calls in the hot paths when the logging
  // is disabled.
  MOZ_ASSERT(MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info));
  if (MOZ_UNLIKELY(!aElement->IsInComposedDoc())) {
    return LogLevel::Disabled;
  }
  if (!aElement->IsEditable()) {
    return MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Verbose)
               ? LogLevel::Verbose
               : LogLevel::Disabled;
  }
  if (!mRunningDOMAPIWrapper) {
    return LogLevel::Info;
  }
  return mRunningDOMAPIWrapper->IsExpectedAttributeChanged(
             aElement, aNameSpaceID, aAttribute, aModType, aOldValue)
             ? LogLevel::Debug
             : LogLevel::Info;
}

void HTMLEditor::MaybeLogContentAppended(nsIContent* aFirstNewContent) const {
  const LogLevel logLevel = MutationLogLevelOf(
      aFirstNewContent, nullptr, DOMMutationType::ContentAppended);
  if (logLevel == LogLevel::Disabled) {
    return;
  }
  MOZ_LOG(
      gHTMLEditorMutationLog, logLevel,
      ("%p %s ContentAppended: %s (previousSibling=%s, nextSibling=%s)", this,
       logLevel == LogLevel::Debug ? "HTMLEditor  " : "SomebodyElse",
       NodeToString(aFirstNewContent).get(),
       NodeToString(aFirstNewContent ? aFirstNewContent->GetPreviousSibling()
                                     : nullptr)
           .get(),
       NodeToString(aFirstNewContent ? aFirstNewContent->GetNextSibling()
                                     : nullptr)
           .get()));
}

void HTMLEditor::MaybeLogContentInserted(nsIContent* aChild) const {
  const LogLevel logLevel =
      MutationLogLevelOf(aChild, nullptr, DOMMutationType::ContentInserted);
  if (logLevel == LogLevel::Disabled) {
    return;
  }
  MOZ_LOG(gHTMLEditorMutationLog, logLevel,
          ("%p %s ContentInserted: %s (previousSibling=%s, nextSibling=%s)",
           this, logLevel == LogLevel::Debug ? "HTMLEditor  " : "SomebodyElse",
           NodeToString(aChild).get(),
           NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(),
           NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get()));
}

void HTMLEditor::MaybeLogContentWillBeRemoved(nsIContent* aChild) const {
  const LogLevel logLevel = MutationLogLevelOf(
      aChild, nullptr, DOMMutationType::ContentWillBeRemoved);
  if (logLevel == LogLevel::Disabled) {
    return;
  }
  MOZ_LOG(
      gHTMLEditorMutationLog, logLevel,
      ("%p %s ContentWillBeRemoved: %s (previousSibling=%s, nextSibling=%s)",
       this, logLevel == LogLevel::Debug ? "HTMLEditor  " : "SomebodyElse",
       NodeToString(aChild).get(),
       NodeToString(aChild ? aChild->GetPreviousSibling() : nullptr).get(),
       NodeToString(aChild ? aChild->GetNextSibling() : nullptr).get()));
}

void HTMLEditor::MaybeLogCharacterDataChanged(
    nsIContent* aContent, const CharacterDataChangeInfo& aInfo) const {
  const LogLevel logLevel = MutationLogLevelOf(
      aContent, &aInfo, DOMMutationType::CharacterDataChanged);
  if (logLevel == LogLevel::Disabled) {
    return;
  }
  nsAutoString data;
  aContent->GetCharacterDataBuffer()->AppendTo(data);
  MarkSelectionAndShrinkLongString shrunkenData(
      data, aInfo.mChangeStart, aInfo.mChangeStart + aInfo.mReplaceLength);
  MakeHumanFriendly(shrunkenData);
  MOZ_LOG(
      gHTMLEditorMutationLog, logLevel,
      ("%p %s CharacterDataChanged: %s, data=\"%s\", (length=%u), aInfo=%s",
       this, logLevel == LogLevel::Debug ? "HTMLEditor  " : "SomebodyElse",
       ToString(*aContent).c_str(), NS_ConvertUTF16toUTF8(shrunkenData).get(),
       aContent->Length(), ToString(aInfo).c_str()));
}

void HTMLEditor::MaybeLogAttributeChanged(Element* aElement,
                                          int32_t aNameSpaceID,
                                          nsAtom* aAttribute,
                                          AttrModType aModType,
                                          const nsAttrValue* aOldValue) const {
  const LogLevel logLevel = AttrMutationLogLevelOf(
      aElement, aNameSpaceID, aAttribute, aModType, aOldValue);
  if (logLevel == LogLevel::Disabled) {
    return;
  }
  nsAutoString value;
  aElement->GetAttr(aAttribute, value);
  MOZ_LOG(
      gHTMLEditorAttrMutationLog, logLevel,
      ("%p %s AttributeChanged: %s of %s %s", this,
       logLevel == LogLevel::Debug ? "HTMLEditor  " : "SomebodyElse",
       nsAutoAtomCString(aAttribute).get(), NodeToString(aElement).get(),
       aModType == AttrModType::Removal
           ? "Removed"
           : nsPrintfCString("to \"%s\"", NS_ConvertUTF16toUTF8(value).get())
                 .get()));
}

/******************************************************************************
 * mozilla::HTMLEditor - Start/end of a DOM API call to modify the DOM
 ******************************************************************************/

const AutoDOMAPIWrapperBase* HTMLEditor::OnDOMAPICallStart(
    const AutoDOMAPIWrapperBase& aWrapperBase) {
  const AutoDOMAPIWrapperBase* const prevRunner = mRunningDOMAPIWrapper;
  mRunningDOMAPIWrapper = &aWrapperBase;
  MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning,
          (">>>> %p Calling DOM API: %s", this,
           ToString(*mRunningDOMAPIWrapper).c_str()));
  return prevRunner;
}

void HTMLEditor::OnDOMAPICallEnd(const AutoDOMAPIWrapperBase* aPrevWrapper) {
  MOZ_LOG(gHTMLEditorMutationLog, LogLevel::Warning,
          ("<<<< %p Called DOM API: %s", this,
           ToString(mRunningDOMAPIWrapper->Type()).c_str()));
  mRunningDOMAPIWrapper = aPrevWrapper;
}

/******************************************************************************
 * mozilla::HTMLEditor - mutation observers/handlers
 ******************************************************************************/

void HTMLEditor::NotifyRootChanged() {
  MOZ_ASSERT(mPendingRootElementUpdatedRunner,
             "HTMLEditor::NotifyRootChanged() should be called via a runner");
  mPendingRootElementUpdatedRunner = nullptr;

  nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);

  AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
  if (NS_WARN_IF(!editActionData.CanHandle())) {
    return;
  }

  RemoveEventListeners();
  nsresult rv = InstallEventListeners();
  if (NS_FAILED(rv)) {
    NS_WARNING("HTMLEditor::InstallEventListeners() failed, but ignored");
    return;
  }

  UpdateRootElement();

  if (MOZ_LIKELY(mRootElement)) {
    rv = MaybeCollapseSelectionAtFirstEditableNode(false);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(false) "
          "failed, "
          "but ignored");
      return;
    }

    // When this editor has focus, we need to reset the selection limiter to
    // new root.  Otherwise, that is going to be done when this gets focus.
    nsCOMPtr<nsINode> node = GetFocusedNode();
    if (node) {
      DebugOnly<nsresult> rvIgnored = InitializeSelection(*node);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rvIgnored),
          "EditorBase::InitializeSelection() failed, but ignored");
    }

    SyncRealTimeSpell();
  }

  RefPtr<Element> newRootElement(mRootElement);
  IMEStateManager::OnUpdateHTMLEditorRootElement(*this, newRootElement);
}

// nsStubMutationObserver::ContentAppended override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentAppended(
    nsIContent* aFirstNewContent, const ContentAppendInfo&) {
  DoContentInserted(aFirstNewContent, ContentNodeIs::Appended);
}

// nsStubMutationObserver::ContentInserted override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentInserted(
    nsIContent* aChild, const ContentInsertInfo&) {
  DoContentInserted(aChild, ContentNodeIs::Inserted);
}

bool HTMLEditor::IsInObservedSubtree(nsIContent* aChild) {
  if (!aChild) {
    return false;
  }

  // FIXME(emilio, bug 1596856): This should probably work if the root is in the
  // same shadow tree as the child, probably? I don't know what the
  // contenteditable-in-shadow-dom situation is.
  if (Element* root = GetRoot()) {
    // To be super safe here, check both ChromeOnlyAccess and NAC / Shadow DOM.
    // That catches (also unbound) native anonymous content and ShadowDOM.
    if (root->ChromeOnlyAccess() != aChild->ChromeOnlyAccess() ||
        root->IsInNativeAnonymousSubtree() !=
            aChild->IsInNativeAnonymousSubtree() ||
        root->IsInShadowTree() != aChild->IsInShadowTree()) {
      return false;
    }
  }

  return !aChild->ChromeOnlyAccess() && !aChild->IsInShadowTree() &&
         !aChild->IsInNativeAnonymousSubtree();
}

bool HTMLEditor::ShouldReplaceRootElement() const {
  if (!mRootElement) {
    // If we don't know what is our root element, we should find our root.
    return true;
  }

  // If we temporary set document root element to mRootElement, but there is
  // body element now, we should replace the root element by the body element.
  return mRootElement != GetBodyElement();
}

void HTMLEditor::DoContentInserted(nsIContent* aChild,
                                   ContentNodeIs aContentNodeIs) {
  MOZ_ASSERT(aChild);
  nsINode* container = aChild->GetParentNode();
  MOZ_ASSERT(container);

  if (!IsInObservedSubtree(aChild)) {
    return;
  }

  if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
    if (aContentNodeIs == ContentNodeIs::Appended) {
      MaybeLogContentAppended(aChild);
    } else {
      MOZ_ASSERT(aContentNodeIs == ContentNodeIs::Inserted);
      MaybeLogContentInserted(aChild);
    }
  }

  // XXX Why do we need this? This method is a helper of mutation observer.
  //     So, the callers of mutation observer should guarantee that this won't
  //     be deleted at least during the call.
  RefPtr<HTMLEditor> kungFuDeathGrip(this);

  // Do not create AutoEditActionDataSetter here because it grabs `Selection`,
  // but that appear in the profile. If you need to create to it in some cases,
  // you should do it in the minimum scope.

  if (ShouldReplaceRootElement()) {
    // Forget maybe disconnected root element right now because nobody should
    // work with it.
    mRootElement = nullptr;
    if (mPendingRootElementUpdatedRunner) {
      return;
    }
    mPendingRootElementUpdatedRunner = NewRunnableMethod(
        "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
    nsContentUtils::AddScriptRunner(
        do_AddRef(mPendingRootElementUpdatedRunner));
    return;
  }

  // We don't need to handle our own modifications
  // (If firing textupdate, then we do want to run spell check,
  //  since the textupdate handler may update the DOM arbitrarily.)
  if ((!GetTopLevelEditSubAction() || IsFiringTextUpdate()) &&
      container->IsEditable()) {
    if (EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
      // Ignore insertion of the padding <br> element.
      return;
    }
    nsresult rv = RunOrScheduleOnModifyDocument();
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored");

    // Update spellcheck for only the newly-inserted node (bug 743819)
    if (mInlineSpellChecker) {
      nsIContent* endContent = aChild;
      if (aContentNodeIs == ContentNodeIs::Appended) {
        nsIContent* child = nullptr;
        for (child = aChild; child; child = child->GetNextSibling()) {
          if (child->InclusiveDescendantMayNeedSpellchecking(this)) {
            break;
          }
        }
        if (!child) {
          // No child needed spellchecking, return.
          return;
        }

        // Maybe more than 1 child was appended.
        endContent = container->GetLastChild();
      } else if (!aChild->InclusiveDescendantMayNeedSpellchecking(this)) {
        return;
      }

      RefPtr<nsRange> range = nsRange::Create(aChild);
      range->SelectNodesInContainer(container, aChild, endContent);
      DebugOnly<nsresult> rvIgnored =
          mInlineSpellChecker->SpellCheckRangeIgnoringWordAtCaret(range);
      NS_WARNING_ASSERTION(
          rvIgnored == NS_ERROR_NOT_INITIALIZED || NS_SUCCEEDED(rvIgnored),
          "mozInlineSpellChecker::SpellCheckRange() failed, but ignored");
    }
  }
}

// nsStubMutationObserver::ContentWillBeRemoved override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::ContentWillBeRemoved(
    nsIContent* aChild, const ContentRemoveInfo&) {
  if (mLastCollapsibleWhiteSpaceAppendedTextNode == aChild) {
    mLastCollapsibleWhiteSpaceAppendedTextNode = nullptr;
  }

  if (!IsInObservedSubtree(aChild)) {
    return;
  }

  if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
    MaybeLogContentWillBeRemoved(aChild);
  }

  // XXX Why do we need to do this?  This method is a mutation observer's
  //     method.  Therefore, the caller should guarantee that this won't be
  //     deleted during the call.
  RefPtr<HTMLEditor> kungFuDeathGrip(this);

  // Do not create AutoEditActionDataSetter here because it grabs `Selection`,
  // but that appear in the profile. If you need to create to it in some cases,
  // you should do it in the minimum scope.

  // FYI: mRootElement may be the <body> of the document or the root element.
  // Therefore, we don't need to check it across shadow DOM boundaries.
  if (mRootElement && mRootElement->IsInclusiveDescendantOf(aChild)) {
    // Forget the disconnected root element right now because nobody should work
    // with it.
    mRootElement = nullptr;
    if (mPendingRootElementUpdatedRunner) {
      return;
    }
    mPendingRootElementUpdatedRunner = NewRunnableMethod(
        "HTMLEditor::NotifyRootChanged", this, &HTMLEditor::NotifyRootChanged);
    nsContentUtils::AddScriptRunner(
        do_AddRef(mPendingRootElementUpdatedRunner));
    return;
  }

  // We don't need to handle our own modifications
  if (!GetTopLevelEditSubAction() && aChild->GetParentNode()->IsEditable()) {
    if (aChild && EditorUtils::IsPaddingBRElementForEmptyEditor(*aChild)) {
      // Ignore removal of the padding <br> element for empty editor.
      return;
    }

    nsresult rv = RunOrScheduleOnModifyDocument(aChild);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return;
    }
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::RunOrScheduleOnModifyDocument() failed, but ignored");
  }
}

// nsStubMutationObserver::CharacterDataChanged override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::CharacterDataChanged(
    nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
  if (!IsInObservedSubtree(aContent)) {
    return;
  }
  if (MOZ_LOG_TEST(gHTMLEditorMutationLog, LogLevel::Info)) {
    MaybeLogCharacterDataChanged(aContent, aInfo);
  }
  // We don't need to handle our own modifications
  // (If firing textupdate, then we do want to run spell check,
  //  since the textupdate handler may update the DOM arbitrarily.)
  if (!mInlineSpellChecker || !aContent->IsEditable() ||
      (GetTopLevelEditSubAction() != EditSubAction::eNone &&
       !IsFiringTextUpdate())) {
    return;
  }

  nsIContent* parent = aContent->GetParent();
  if (!parent || !parent->InclusiveDescendantMayNeedSpellchecking(this)) {
    return;
  }

  RefPtr<nsRange> range = nsRange::Create(aContent);
  range->SelectNodesInContainer(parent, aContent, aContent);
  DebugOnly<nsresult> rvIgnored =
      mInlineSpellChecker->SpellCheckRangeIgnoringWordAtCaret(range);
}

nsresult HTMLEditor::RunOrScheduleOnModifyDocument(
    const nsIContent* aContentWillBeRemoved /* = nullptr */) {
  if (mPendingDocumentModifiedRunner) {
    return NS_OK;  // We've already posted same runnable into the queue.
  }
  mPendingDocumentModifiedRunner = new DocumentModifiedEvent(*this);
  nsContentUtils::AddScriptRunner(do_AddRef(mPendingDocumentModifiedRunner));
  // Be aware, if OnModifyDocument() may be called synchronously, the
  // editor might have been destroyed here.
  return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}

// nsStubMutationObserver::AttributeChanged override
MOZ_CAN_RUN_SCRIPT_BOUNDARY void HTMLEditor::AttributeChanged(
    Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute,
    AttrModType aModType, const nsAttrValue* aOldValue) {
  if (MOZ_LOG_TEST(gHTMLEditorAttrMutationLog, LogLevel::Info) &&
      IsInObservedSubtree(aElement)) {
    MaybeLogAttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
                             aOldValue);
  }
}

nsresult HTMLEditor::OnModifyDocument(const DocumentModifiedEvent& aRunner) {
  MOZ_ASSERT(mPendingDocumentModifiedRunner,
             "HTMLEditor::OnModifyDocument() should be called via a runner");
  MOZ_ASSERT(&aRunner == mPendingDocumentModifiedRunner);
  mPendingDocumentModifiedRunner = nullptr;

  Maybe<AutoEditActionDataSetter> editActionData;
  if (!IsEditActionDataAvailable()) {
    editActionData.emplace(*this,
                           EditAction::eCreatePaddingBRElementForEmptyEditor);
    if (NS_WARN_IF(!editActionData->CanHandle())) {
      return NS_ERROR_NOT_AVAILABLE;
    }
  }

  // EnsureNoPaddingBRElementForEmptyEditor() below may cause a flush, which
  // could destroy the editor
  nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;

  // Delete our padding <br> element for empty editor, if we have one, since
  // the document might not be empty any more.
  nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return rv;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  // Try to recreate the padding <br> element for empty editor if needed.
  rv = MaybeCreatePaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");

  return rv;
}

}  // namespace mozilla
