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

#include <stddef.h>

#include "EditAction.h"
#include "EditorBase.h"
#include "HTMLEditHelpers.h"  // for EditorInlineStyle, EditorInlineStyleAndValue
#include "HTMLEditor.h"
#include "HTMLEditUtils.h"

#include "mozilla/mozalloc.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/MouseEvent.h"
#include "mozilla/dom/Selection.h"

#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsINode.h"
#include "nsISupports.h"
#include "nsISupportsImpl.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsTArray.h"

// from vs/Windows Kits/10/.../rpcndr.h
#ifdef small
#  undef small
#endif

namespace mozilla {

using namespace dom;

/********************************************************************
 * mozilla::PendingStyle
 *******************************************************************/

EditorInlineStyle PendingStyle::ToInlineStyle() const {
  return mTag ? EditorInlineStyle(*mTag, mAttribute)
              : EditorInlineStyle::RemoveAllStyles();
}

EditorInlineStyleAndValue PendingStyle::ToInlineStyleAndValue() const {
  MOZ_ASSERT(mTag);
  return mAttribute ? EditorInlineStyleAndValue(*mTag, *mAttribute,
                                                mAttributeValueOrCSSValue)
                    : EditorInlineStyleAndValue(*mTag);
}

/********************************************************************
 * mozilla::PendingStyleCache
 *******************************************************************/

EditorInlineStyle PendingStyleCache::ToInlineStyle() const {
  return EditorInlineStyle(mTag, mAttribute);
}

/********************************************************************
 * mozilla::PendingStyles
 *******************************************************************/

NS_IMPL_CYCLE_COLLECTION_CLASS(PendingStyles)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PendingStyles)
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastSelectionPoint)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PendingStyles)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastSelectionPoint)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

nsresult PendingStyles::UpdateSelState(const HTMLEditor& aHTMLEditor) {
  if (!aHTMLEditor.SelectionRef().IsCollapsed()) {
    return NS_OK;
  }

  mLastSelectionPoint =
      aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
  if (!mLastSelectionPoint.IsSet()) {
    return NS_ERROR_FAILURE;
  }
  // We need to store only offset because referring child may be removed by
  // we'll check the point later.
  AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
  return NS_OK;
}

void PendingStyles::PreHandleMouseEvent(const MouseEvent& aMouseDownOrUpEvent) {
  MOZ_ASSERT(aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown ||
             aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseUp);
  bool& eventFiredInLinkElement =
      aMouseDownOrUpEvent.WidgetEventPtr()->mMessage == eMouseDown
          ? mMouseDownFiredInLinkElement
          : mMouseUpFiredInLinkElement;
  eventFiredInLinkElement = false;
  if (aMouseDownOrUpEvent.DefaultPrevented()) {
    return;
  }
  // If mouse button is down or up in a link element, we shouldn't unlink
  // it when we get a notification of selection change.
  EventTarget* target = aMouseDownOrUpEvent.GetExplicitOriginalTarget();
  if (NS_WARN_IF(!target)) {
    return;
  }
  nsIContent* targetContent = nsIContent::FromEventTarget(target);
  if (NS_WARN_IF(!targetContent)) {
    return;
  }
  eventFiredInLinkElement =
      HTMLEditUtils::IsContentInclusiveDescendantOfLink(*targetContent);
}

void PendingStyles::PreHandleSelectionChangeCommand(Command aCommand) {
  mLastSelectionCommand = aCommand;
}

void PendingStyles::PostHandleSelectionChangeCommand(
    const HTMLEditor& aHTMLEditor, Command aCommand) {
  if (mLastSelectionCommand != aCommand) {
    return;
  }

  // If `OnSelectionChange()` hasn't been called for `mLastSelectionCommand`,
  // it means that it didn't cause selection change.
  if (!aHTMLEditor.SelectionRef().IsCollapsed() ||
      !aHTMLEditor.SelectionRef().RangeCount()) {
    return;
  }

  const auto caretPoint =
      aHTMLEditor.GetFirstSelectionStartPoint<EditorRawDOMPoint>();
  if (NS_WARN_IF(!caretPoint.IsSet())) {
    return;
  }

  if (!HTMLEditUtils::IsPointAtEdgeOfLink(caretPoint)) {
    return;
  }

  // If all styles are cleared or link style is explicitly set, we
  // shouldn't reset them without caret move.
  if (AreAllStylesCleared() || IsLinkStyleSet()) {
    return;
  }
  // And if non-link styles are cleared or some styles are set, we
  // shouldn't reset them too, but we may need to change the link
  // style.
  if (AreSomeStylesSet() ||
      (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
    ClearLinkAndItsSpecifiedStyle();
    return;
  }

  Reset();
  ClearLinkAndItsSpecifiedStyle();
}

void PendingStyles::OnSelectionChange(const HTMLEditor& aHTMLEditor,
                                      int16_t aReason) {
  // XXX: Selection currently generates bogus selection changed notifications
  // XXX: (bug 140303). It can notify us when the selection hasn't actually
  // XXX: changed, and it notifies us more than once for the same change.
  // XXX:
  // XXX: The following code attempts to work around the bogus notifications,
  // XXX: and should probably be removed once bug 140303 is fixed.
  // XXX:
  // XXX: This code temporarily fixes the problem where clicking the mouse in
  // XXX: the same location clears the type-in-state.

  const bool causedByFrameSelectionMoveCaret =
      (aReason & (nsISelectionListener::KEYPRESS_REASON |
                  nsISelectionListener::COLLAPSETOSTART_REASON |
                  nsISelectionListener::COLLAPSETOEND_REASON)) &&
      !(aReason & nsISelectionListener::JS_REASON);

  Command lastSelectionCommand = mLastSelectionCommand;
  if (causedByFrameSelectionMoveCaret) {
    mLastSelectionCommand = Command::DoNothing;
  }

  bool mouseEventFiredInLinkElement = false;
  if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
                 nsISelectionListener::MOUSEUP_REASON)) {
    MOZ_ASSERT((aReason & (nsISelectionListener::MOUSEDOWN_REASON |
                           nsISelectionListener::MOUSEUP_REASON)) !=
               (nsISelectionListener::MOUSEDOWN_REASON |
                nsISelectionListener::MOUSEUP_REASON));
    bool& eventFiredInLinkElement =
        aReason & nsISelectionListener::MOUSEDOWN_REASON
            ? mMouseDownFiredInLinkElement
            : mMouseUpFiredInLinkElement;
    mouseEventFiredInLinkElement = eventFiredInLinkElement;
    eventFiredInLinkElement = false;
  }

  bool unlink = false;
  bool resetAllStyles = true;
  if (aHTMLEditor.SelectionRef().IsCollapsed() &&
      aHTMLEditor.SelectionRef().RangeCount()) {
    const auto selectionStartPoint =
        aHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    if (MOZ_UNLIKELY(NS_WARN_IF(!selectionStartPoint.IsSet()))) {
      return;
    }

    if (mLastSelectionPoint == selectionStartPoint) {
      // If all styles are cleared or link style is explicitly set, we
      // shouldn't reset them without caret move.
      if (AreAllStylesCleared() || IsLinkStyleSet()) {
        return;
      }
      // And if non-link styles are cleared or some styles are set, we
      // shouldn't reset them too, but we may need to change the link
      // style.
      if (AreSomeStylesSet() ||
          (AreSomeStylesCleared() && !IsOnlyLinkStyleCleared())) {
        resetAllStyles = false;
      }
    }

    RefPtr<Element> linkElement;
    if (HTMLEditUtils::IsPointAtEdgeOfLink(selectionStartPoint,
                                           getter_AddRefs(linkElement))) {
      // If caret comes from outside of <a href> element, we should clear "link"
      // style after reset.
      if (causedByFrameSelectionMoveCaret) {
        MOZ_ASSERT(!(aReason & (nsISelectionListener::MOUSEDOWN_REASON |
                                nsISelectionListener::MOUSEUP_REASON)));
        // If caret is moves in a link per character, we should keep inserting
        // new text to the link because user may want to keep extending the link
        // text.  Otherwise, e.g., using `End` or `Home` key. we should insert
        // new text outside the link because it should be possible to user
        // choose it, and this is similar to the other browsers.
        switch (lastSelectionCommand) {
          case Command::CharNext:
          case Command::CharPrevious:
          case Command::MoveLeft:
          case Command::MoveLeft2:
          case Command::MoveRight:
          case Command::MoveRight2:
            // If selection becomes collapsed, we should unlink new text.
            if (!mLastSelectionPoint.IsSet()) {
              unlink = true;
              break;
            }
            // Special case, if selection isn't moved, it means that caret is
            // positioned at start or end of an editing host.  In this case,
            // we can unlink it even with arrow key press.
            // TODO: This does not work as expected for `ArrowLeft` key press
            //       at start of an editing host.
            if (mLastSelectionPoint == selectionStartPoint) {
              unlink = true;
              break;
            }
            // Otherwise, if selection is moved in a link element, we should
            // keep inserting new text into the link.  Note that this is our
            // traditional behavior, but different from the other browsers.
            // If this breaks some web apps, we should change our behavior,
            // but let's wait a report because our traditional behavior allows
            // user to type text into start/end of a link only when user
            // moves caret inside the link with arrow keys.
            unlink =
                !mLastSelectionPoint.GetContainer()->IsInclusiveDescendantOf(
                    linkElement);
            break;
          default:
            // If selection is moved without arrow keys, e.g., `Home` and
            // `End`, we should not insert new text into the link element.
            // This is important for web-compat especially when the link is
            // the last content in the block.
            unlink = true;
            break;
        }
      } else if (aReason & (nsISelectionListener::MOUSEDOWN_REASON |
                            nsISelectionListener::MOUSEUP_REASON)) {
        // If the corresponding mouse event is fired in a link element,
        // we should keep treating inputting content as content in the link,
        // but otherwise, i.e., clicked outside the link, we should stop
        // treating inputting content as content in the link.
        unlink = !mouseEventFiredInLinkElement;
      } else if (aReason & nsISelectionListener::JS_REASON) {
        // If this is caused by a call of Selection API or something similar
        // API, we should not contain new inserting content to the link.
        unlink = true;
      } else {
        switch (aHTMLEditor.GetEditAction()) {
          case EditAction::eDeleteBackward:
          case EditAction::eDeleteForward:
          case EditAction::eDeleteSelection:
          case EditAction::eDeleteToBeginningOfSoftLine:
          case EditAction::eDeleteToEndOfSoftLine:
          case EditAction::eDeleteWordBackward:
          case EditAction::eDeleteWordForward:
            // This selection change is caused by the editor and the edit
            // action is deleting content at edge of a link, we shouldn't
            // keep the link style for new inserted content.
            unlink = true;
            break;
          default:
            break;
        }
      }
    } else if (mLastSelectionPoint == selectionStartPoint) {
      return;
    }

    mLastSelectionPoint = selectionStartPoint;
    // We need to store only offset because referring child may be removed by
    // we'll check the point later.
    AutoEditorDOMPointChildInvalidator saveOnlyOffset(mLastSelectionPoint);
  } else {
    if (aHTMLEditor.SelectionRef().RangeCount()) {
      // If selection starts from a link, we shouldn't preserve the link style
      // unless the range is entirely in the link.
      EditorRawDOMRange firstRange(*aHTMLEditor.SelectionRef().GetRangeAt(0));
      if (firstRange.StartRef().IsInContentNode() &&
          HTMLEditUtils::IsContentInclusiveDescendantOfLink(
              *firstRange.StartRef().ContainerAs<nsIContent>())) {
        unlink = !HTMLEditUtils::IsRangeEntirelyInLink(firstRange);
      }
    }
    mLastSelectionPoint.Clear();
  }

  if (resetAllStyles) {
    Reset();
    if (unlink) {
      ClearLinkAndItsSpecifiedStyle();
    }
    return;
  }

  if (unlink == IsExplicitlyLinkStyleCleared()) {
    return;
  }

  // Even if we shouldn't touch existing style, we need to set/clear only link
  // style in some cases.
  if (unlink) {
    ClearLinkAndItsSpecifiedStyle();
    return;
  }
  CancelClearingStyle(*nsGkAtoms::a, nullptr);
}

void PendingStyles::PreserveStyles(
    const nsTArray<EditorInlineStyleAndValue>& aStylesToPreserve) {
  for (const EditorInlineStyleAndValue& styleToPreserve : aStylesToPreserve) {
    PreserveStyle(styleToPreserve.HTMLPropertyRef(), styleToPreserve.mAttribute,
                  styleToPreserve.mAttributeValue);
  }
}

void PendingStyles::PreserveStyle(nsStaticAtom& aHTMLProperty,
                                  nsAtom* aAttribute,
                                  const nsAString& aAttributeValueOrCSSValue) {
  // special case for big/small, these nest
  if (nsGkAtoms::big == &aHTMLProperty) {
    mRelativeFontSize++;
    return;
  }
  if (nsGkAtoms::small == &aHTMLProperty) {
    mRelativeFontSize--;
    return;
  }

  Maybe<size_t> index = IndexOfPreservingStyle(aHTMLProperty, aAttribute);
  if (index.isSome()) {
    // If it's already set, update the value
    mPreservingStyles[index.value()]->UpdateAttributeValueOrCSSValue(
        aAttributeValueOrCSSValue);
    return;
  }

  // font-size and font-family need to be applied outer-most because height of
  // outer inline elements of them are computed without these styles.  E.g.,
  // background-color may be applied bottom-half of the text.  Therefore, we
  // need to apply the font styles first.
  UniquePtr<PendingStyle> style = MakeUnique<PendingStyle>(
      &aHTMLProperty, aAttribute, aAttributeValueOrCSSValue);
  if (&aHTMLProperty == nsGkAtoms::font && aAttribute != nsGkAtoms::bgcolor) {
    MOZ_ASSERT(aAttribute == nsGkAtoms::color ||
               aAttribute == nsGkAtoms::face || aAttribute == nsGkAtoms::size);
    mPreservingStyles.InsertElementAt(0, std::move(style));
  } else {
    mPreservingStyles.AppendElement(std::move(style));
  }

  CancelClearingStyle(aHTMLProperty, aAttribute);
}

void PendingStyles::ClearStyles(
    const nsTArray<EditorInlineStyle>& aStylesToClear) {
  for (const EditorInlineStyle& styleToClear : aStylesToClear) {
    if (styleToClear.IsStyleToClearAllInlineStyles()) {
      ClearAllStyles();
      return;
    }
    if (styleToClear.mHTMLProperty == nsGkAtoms::href ||
        styleToClear.mHTMLProperty == nsGkAtoms::name) {
      ClearStyleInternal(nsGkAtoms::a, nullptr);
    } else {
      ClearStyleInternal(styleToClear.mHTMLProperty, styleToClear.mAttribute);
    }
  }
}

void PendingStyles::ClearStyleInternal(
    nsStaticAtom* aHTMLProperty, nsAtom* aAttribute,
    SpecifiedStyle aSpecifiedStyle /* = SpecifiedStyle::Preserve */) {
  if (IsStyleCleared(aHTMLProperty, aAttribute)) {
    return;
  }

  CancelPreservingStyle(aHTMLProperty, aAttribute);

  mClearingStyles.AppendElement(MakeUnique<PendingStyle>(
      aHTMLProperty, aAttribute, u""_ns, aSpecifiedStyle));
}

void PendingStyles::TakeAllPreservedStyles(
    nsTArray<EditorInlineStyleAndValue>& aOutStylesAndValues) {
  aOutStylesAndValues.SetCapacity(aOutStylesAndValues.Length() +
                                  mPreservingStyles.Length());
  for (const UniquePtr<PendingStyle>& preservedStyle : mPreservingStyles) {
    aOutStylesAndValues.AppendElement(
        preservedStyle->GetAttribute()
            ? EditorInlineStyleAndValue(
                  *preservedStyle->GetTag(), *preservedStyle->GetAttribute(),
                  preservedStyle->AttributeValueOrCSSValueRef())
            : EditorInlineStyleAndValue(*preservedStyle->GetTag()));
  }
  mPreservingStyles.Clear();
}

/**
 * TakeRelativeFontSize() hands back relative font value, which is then
 * cleared out.
 */
int32_t PendingStyles::TakeRelativeFontSize() {
  int32_t relSize = mRelativeFontSize;
  mRelativeFontSize = 0;
  return relSize;
}

PendingStyleState PendingStyles::GetStyleState(
    nsStaticAtom& aHTMLProperty, nsAtom* aAttribute /* = nullptr */,
    nsString* aOutNewAttributeValueOrCSSValue /* = nullptr */) const {
  if (IndexOfPreservingStyle(aHTMLProperty, aAttribute,
                             aOutNewAttributeValueOrCSSValue)
          .isSome()) {
    return PendingStyleState::BeingPreserved;
  }

  if (IsStyleCleared(&aHTMLProperty, aAttribute)) {
    return PendingStyleState::BeingCleared;
  }

  return PendingStyleState::NotUpdated;
}

void PendingStyles::CancelPreservingStyle(nsStaticAtom* aHTMLProperty,
                                          nsAtom* aAttribute) {
  if (!aHTMLProperty) {
    mPreservingStyles.Clear();
    mRelativeFontSize = 0;
    return;
  }
  Maybe<size_t> index = IndexOfPreservingStyle(*aHTMLProperty, aAttribute);
  if (index.isSome()) {
    mPreservingStyles.RemoveElementAt(index.value());
  }
}

void PendingStyles::CancelClearingStyle(nsStaticAtom& aHTMLProperty,
                                        nsAtom* aAttribute) {
  Maybe<size_t> index =
      IndexOfStyleInArray(&aHTMLProperty, aAttribute, nullptr, mClearingStyles);
  if (index.isSome()) {
    mClearingStyles.RemoveElementAt(index.value());
  }
}

Maybe<size_t> PendingStyles::IndexOfStyleInArray(
    nsStaticAtom* aHTMLProperty, nsAtom* aAttribute, nsAString* aOutValue,
    const nsTArray<UniquePtr<PendingStyle>>& aArray) {
  if (aAttribute == nsGkAtoms::_empty) {
    aAttribute = nullptr;
  }
  for (size_t i : IntegerRange(aArray.Length())) {
    const UniquePtr<PendingStyle>& item = aArray[i];
    if (item->GetTag() == aHTMLProperty && item->GetAttribute() == aAttribute) {
      if (aOutValue) {
        *aOutValue = item->AttributeValueOrCSSValueRef();
      }
      return Some(i);
    }
  }
  return Nothing();
}

}  // namespace mozilla
