/* 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 "EditorBase.h"
#include "HTMLEditor.h"
#include "HTMLEditorInlines.h"
#include "HTMLEditorNestedClasses.h"

#include "CSSEditUtils.h"
#include "EditAction.h"
#include "EditorDOMPoint.h"
#include "EditorLineBreak.h"
#include "EditorUtils.h"
#include "HTMLEditHelpers.h"
#include "HTMLEditUtils.h"
#include "PendingStyles.h"  // for SpecifiedStyle
#include "WhiteSpaceVisibilityKeeper.h"
#include "WSRunScanner.h"

#include "ErrorList.h"
#include "mozilla/Assertions.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/EditorForwards.h"
#include "mozilla/Maybe.h"
#include "mozilla/PresShell.h"
#include "mozilla/TextComposition.h"
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/dom/Selection.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsFrameSelection.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsINode.h"
#include "nsRange.h"
#include "nsTArray.h"
#include "nsTextNode.h"

class nsISupports;

namespace mozilla {

using namespace dom;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using EmptyCheckOptions = HTMLEditUtils::EmptyCheckOptions;

nsresult HTMLEditor::InsertLineBreakAsSubAction() {
  MOZ_ASSERT(IsEditActionDataAvailable());
  MOZ_ASSERT(!IsSelectionRangeContainerNotContent());

  if (NS_WARN_IF(!mInitSucceeded)) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  {
    Result<EditActionResult, nsresult> result =
        CanHandleHTMLEditSubAction(CheckSelectionInReplacedElement::No);
    if (MOZ_UNLIKELY(result.isErr())) {
      NS_WARNING("HTMLEditor::CanHandleHTMLEditSubAction() failed");
      return result.unwrapErr();
    }
    if (result.inspect().Canceled()) {
      return NS_OK;
    }
  }

  if (GetEditActionEditContext()) {
    // Don't insert line break if there is an EditContext
    return NS_OK;
  }

  // XXX This may be called by execCommand() with "insertLineBreak".
  //     In such case, naming the transaction "TypingTxnName" is odd.
  AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
                                             ScrollSelectionIntoView::Yes,
                                             __FUNCTION__);

  // calling it text insertion to trigger moz br treatment by rules
  // XXX Why do we use EditSubAction::eInsertText here?  Looks like
  //     EditSubAction::eInsertLineBreak or EditSubAction::eInsertNode
  //     is better.
  IgnoredErrorResult ignoredError;
  AutoEditSubActionNotifier startToHandleEditSubAction(
      *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
  if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
    return ignoredError.StealNSResult();
  }
  NS_WARNING_ASSERTION(
      !ignoredError.Failed(),
      "HTMLEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");

  UndefineCaretBidiLevel();

  // If the selection isn't collapsed, delete it.
  if (!SelectionRef().IsCollapsed()) {
    nsresult rv =
        DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eStrip);
    if (NS_FAILED(rv)) {
      NS_WARNING(
          "EditorBase::DeleteSelectionAsSubAction(eNone, eStrip) failed");
      return rv;
    }
  }

  const RefPtr<Element> editingHost =
      ComputeEditingHost(LimitInBodyElement::No);
  if (NS_WARN_IF(!editingHost)) {
    return NS_ERROR_FAILURE;
  }

  AutoInsertLineBreakHandler handler(*this, *editingHost);
  nsresult rv = handler.Run();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "AutoInsertLineBreakHandler::Run() failed");
  return rv;
}

nsresult HTMLEditor::AutoInsertLineBreakHandler::Run() {
  MOZ_ASSERT(mHTMLEditor.IsEditActionDataAvailable());

  const auto atStartOfSelection =
      mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
  if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) {
    return NS_ERROR_FAILURE;
  }
  MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc());

  const Maybe<LineBreakType> lineBreakType =
      mHTMLEditor.GetPreferredLineBreakType(
          *atStartOfSelection.ContainerAs<nsIContent>(), mEditingHost);
  if (MOZ_UNLIKELY(!lineBreakType)) {
    return NS_SUCCESS_DOM_NO_OPERATION;  // Cannot insert a line break there.
  }
  if (lineBreakType.value() == LineBreakType::BRElement) {
    nsresult rv = HandleInsertBRElement();
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "AutoInsertLineBreakHandler::HandleInsertBRElement()");
    return rv;
  }

  nsresult rv = HandleInsertLinefeed();
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "AutoInsertLineBreakHandler::HandleInsertLinefeed() failed");
  return rv;
}

nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertBRElement() {
  const EditorDOMPoint pointToInsert = [&]() {
    const auto atStartOfSelection =
        mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
    MOZ_ASSERT(atStartOfSelection.IsInContentNode());
    return HTMLEditUtils::GetPossiblePointToInsert(
        atStartOfSelection, *nsGkAtoms::br, mEditingHost);
  }();
  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }
  MOZ_ASSERT(pointToInsert.IsInContentNode());

  // XXX Should we check the preferred line break again?

  Result<CreateLineBreakResult, nsresult> insertLineBreakResultOrError =
      mHTMLEditor.InsertLineBreak(WithTransaction::Yes,
                                  LineBreakType::BRElement, pointToInsert,
                                  nsIEditor::eNext);
  if (MOZ_UNLIKELY(insertLineBreakResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
        "LineBreakType::BRElement, eNext) failed");
    return insertLineBreakResultOrError.unwrapErr();
  }
  CreateLineBreakResult insertLineBreakResult =
      insertLineBreakResultOrError.unwrap();
  MOZ_ASSERT(insertLineBreakResult.Handled());
  insertLineBreakResult.IgnoreCaretPointSuggestion();

  auto pointToPutCaret = insertLineBreakResult.UnwrapCaretPoint();
  if (MOZ_UNLIKELY(!pointToPutCaret.IsSet())) {
    NS_WARNING("Inserted <br> was unexpectedly removed");
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  {
    AutoTrackDOMPoint trackPointToPutCaret(mHTMLEditor.RangeUpdaterRef(),
                                           &pointToPutCaret);
    Result<CreateLineBreakResult, nsresult> insertPaddingBRResultOrError =
        mHTMLEditor.InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(
            insertLineBreakResult.AfterLineBreak<EditorDOMPoint>(),
            mEditingHost);
    if (insertPaddingBRResultOrError.isErr()) [[unlikely]] {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() "
          "failed");
      return insertPaddingBRResultOrError.propagateErr();
    }
    CreateLineBreakResult insertPaddingBRResult =
        insertPaddingBRResultOrError.unwrap();
    insertPaddingBRResult.IgnoreCaretPointSuggestion();
    trackPointToPutCaret.Flush(StopTracking::Yes);
    if (!insertPaddingBRResult.Handled()) {
      // If the line break is followed by visible content, we want to put caret
      // to start of the following content to preserve the style at start of the
      // line.
      const WSScanResult nextThingOfBR =
          WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
              {WSRunScanner::Option::OnlyEditableNodes,
               WSRunScanner::Option::StopAtAnyEmptyInlineContainers},
              insertLineBreakResult.AfterLineBreak<EditorRawDOMPoint>());
      if (nextThingOfBR.InVisibleOrCollapsibleCharacters() ||
          nextThingOfBR.ReachedSpecialContent()) {
        pointToPutCaret = nextThingOfBR.PointAtReachedContent<EditorDOMPoint>();
      } else if (nextThingOfBR.ReachedEmptyInlineContainerElement()) {
        pointToPutCaret = EditorDOMPoint(nextThingOfBR.ElementPtr(), 0u);
      }
    }
  }

  nsresult rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::CollapseSelectionTo() failed");
  return rv;
}

nsresult HTMLEditor::AutoInsertLineBreakHandler::HandleInsertLinefeed() {
  nsresult rv = mHTMLEditor.EnsureNoPaddingBRElementForEmptyEditor();
  if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
    return NS_ERROR_EDITOR_DESTROYED;
  }
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::EnsureNoPaddingBRElementForEmptyEditor() "
                       "failed, but ignored");

  if (NS_SUCCEEDED(rv) && mHTMLEditor.SelectionRef().IsCollapsed()) {
    nsresult rv =
        mHTMLEditor.EnsureCaretNotAfterInvisibleBRElement(mEditingHost);
    if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
      return NS_ERROR_EDITOR_DESTROYED;
    }
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "HTMLEditor::EnsureCaretNotAfterInvisibleBRElement() "
                         "failed, but ignored");
    if (NS_SUCCEEDED(rv)) {
      nsresult rv = mHTMLEditor.PrepareInlineStylesForCaret();
      if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
        return NS_ERROR_EDITOR_DESTROYED;
      }
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "HTMLEditor::PrepareInlineStylesForCaret() failed, but ignored");
    }
  }

  const EditorDOMPoint atStartOfSelection =
      mHTMLEditor.GetFirstSelectionStartPoint<EditorDOMPoint>();
  if (NS_WARN_IF(!atStartOfSelection.IsInContentNode())) {
    return NS_ERROR_FAILURE;
  }
  MOZ_ASSERT(atStartOfSelection.IsSetAndValidInComposedDoc());

  // Do nothing if the node is read-only
  if (!HTMLEditUtils::IsSimplyEditableNode(
          *atStartOfSelection.GetContainer())) {
    return NS_SUCCESS_DOM_NO_OPERATION;
  }

  Result<EditorDOMPoint, nsresult> insertLineFeedResult =
      AutoInsertLineBreakHandler::InsertLinefeed(
          mHTMLEditor, atStartOfSelection, mEditingHost);
  if (MOZ_UNLIKELY(insertLineFeedResult.isErr())) {
    NS_WARNING("AutoInsertLineBreakHandler::InsertLinefeed() failed");
    return insertLineFeedResult.unwrapErr();
  }
  EditorDOMPoint pointToPutCaret = insertLineFeedResult.unwrap();
  // If the line break is followed by visible content, we want to put caret
  // to start of the following content to preserve the style at start of the
  // line.
  const WSScanResult nextThingOfLinefeed =
      WSRunScanner::ScanInclusiveNextVisibleNodeOrBlockBoundary(
          {WSRunScanner::Option::OnlyEditableNodes,
           WSRunScanner::Option::StopAtAnyEmptyInlineContainers},
          pointToPutCaret);
  if (nextThingOfLinefeed.InVisibleOrCollapsibleCharacters() ||
      nextThingOfLinefeed.ReachedSpecialContent()) {
    pointToPutCaret =
        nextThingOfLinefeed.PointAtReachedContent<EditorDOMPoint>();
  } else if (nextThingOfLinefeed.ReachedEmptyInlineContainerElement()) {
    pointToPutCaret = EditorDOMPoint(nextThingOfLinefeed.ElementPtr(), 0u);
  }
  rv = mHTMLEditor.CollapseSelectionTo(pointToPutCaret);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::CollapseSelectionTo() failed");
  return rv;
}

// static
Result<EditorDOMPoint, nsresult>
HTMLEditor::AutoInsertLineBreakHandler::InsertLinefeed(
    HTMLEditor& aHTMLEditor, const EditorDOMPoint& aPointToBreak,
    const Element& aEditingHost) {
  if (NS_WARN_IF(!aPointToBreak.IsSet())) {
    return Err(NS_ERROR_INVALID_ARG);
  }

  const RefPtr<Document> document = aHTMLEditor.GetDocument();
  MOZ_DIAGNOSTIC_ASSERT(document);
  if (NS_WARN_IF(!document)) {
    return Err(NS_ERROR_FAILURE);
  }

  // TODO: The following code is duplicated from `HandleInsertText`.  They
  //       should be merged when we fix bug 92921.

  Result<EditorDOMPoint, nsresult> setStyleResult =
      aHTMLEditor.CreateStyleForInsertText(aPointToBreak, aEditingHost);
  if (MOZ_UNLIKELY(setStyleResult.isErr())) {
    NS_WARNING("HTMLEditor::CreateStyleForInsertText() failed");
    return setStyleResult.propagateErr();
  }

  EditorDOMPoint pointToInsert = setStyleResult.inspect().IsSet()
                                     ? setStyleResult.inspect()
                                     : aPointToBreak;
  if (NS_WARN_IF(!pointToInsert.IsSetAndValid()) ||
      NS_WARN_IF(!pointToInsert.IsInContentNode())) {
    return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
  }
  MOZ_ASSERT(pointToInsert.IsSetAndValid());

  // The node may not be able to have a text node so that we need to check it
  // here.
  pointToInsert = HTMLEditUtils::GetPossiblePointToInsert(
      pointToInsert, *nsGkAtoms::textTagName, aEditingHost);
  if (NS_WARN_IF(!pointToInsert.IsSet())) {
    return Err(NS_ERROR_FAILURE);
  }
  MOZ_ASSERT(pointToInsert.IsInContentNode());

  // FIXME: If the computed point does not preformat linefeed, we should switch
  // back to inserting a <br>.  However, I think it should be handled before
  // calling this.

  AutoRestore<bool> disableListener(
      aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener);
  aHTMLEditor.EditSubActionDataRef().mAdjustChangedRangeFromListener = false;

  // TODO: We don't need AutoTransactionsConserveSelection here in the normal
  //       cases, but removing this may cause the behavior with the legacy
  //       mutation event listeners.  We should try to delete this in a bug.
  AutoTransactionsConserveSelection dontChangeMySelection(aHTMLEditor);

  AutoTrackDOMPoint trackingInsertingPosition(aHTMLEditor.RangeUpdaterRef(),
                                              &pointToInsert);
  Result<CreateLineBreakResult, nsresult> insertLinefeedResultOrError =
      aHTMLEditor.InsertLineBreak(WithTransaction::Yes, LineBreakType::Linefeed,
                                  pointToInsert, eNext);
  if (MOZ_UNLIKELY(insertLinefeedResultOrError.isErr())) {
    NS_WARNING(
        "HTMLEditor::InsertLineBreak(WithTransaction::Yes, "
        "LineBreakType::Linefeed, eNext) failed");
    return insertLinefeedResultOrError.propagateErr();
  }
  trackingInsertingPosition.Flush(StopTracking::Yes);
  CreateLineBreakResult insertLinefeedResult =
      insertLinefeedResultOrError.unwrap();
  EditorDOMPoint pointToPutCaret = insertLinefeedResult.UnwrapCaretPoint();

  // Insert a padding <br> if the inserted linefeed is followed by a block
  // boundary.  Note that it should always be <br> for avoiding padding line
  // breaks appear in `.textContent` value.
  if (pointToPutCaret.IsInContentNode() && pointToPutCaret.IsEndOfContainer()) {
    AutoTrackDOMPoint trackingInsertedPosition(aHTMLEditor.RangeUpdaterRef(),
                                               &pointToInsert);
    AutoTrackDOMPoint trackingNewCaretPosition(aHTMLEditor.RangeUpdaterRef(),
                                               &pointToPutCaret);
    Result<CreateLineBreakResult, nsresult> insertPaddingBRResultOrError =
        aHTMLEditor.InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded(
            insertLinefeedResult.AfterLineBreak<EditorDOMPoint>(),
            aEditingHost);
    if (insertPaddingBRResultOrError.isErr()) [[unlikely]] {
      NS_WARNING(
          "HTMLEditor::InsertPaddingBRElementToMakeEmptyLineVisibleIfNeeded() "
          "failed");
      return insertPaddingBRResultOrError.propagateErr();
    }
    insertPaddingBRResultOrError.unwrap().IgnoreCaretPointSuggestion();
  }

  // manually update the doc changed range so that
  // OnEndHandlingTopLevelEditSubActionInternal will clean up the correct
  // portion of the document.
  MOZ_ASSERT(pointToPutCaret.IsSet());
  if (NS_WARN_IF(!pointToPutCaret.IsSet())) {
    // XXX Here is odd.  We did mChangedRange->SetStartAndEnd(pointToInsert,
    //     pointToPutCaret), but it always fails because of the latter is unset.
    //     Therefore, always returning NS_ERROR_FAILURE from here is the
    //     traditional behavior...
    // TODO: Stop updating the interline position of Selection with fixing here
    //       and returning expected point.
    DebugOnly<nsresult> rvIgnored =
        aHTMLEditor.SelectionRef().SetInterlinePosition(
            InterlinePosition::EndOfLine);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                         "Selection::SetInterlinePosition(InterlinePosition::"
                         "EndOfLine) failed, but ignored");
    if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef()
                      .mChangedRange->CollapseTo(pointToInsert))) {
      NS_WARNING("nsRange::CollapseTo() failed");
      return Err(NS_ERROR_FAILURE);
    }
    NS_WARNING(
        "We always return NS_ERROR_FAILURE here because of a failure of "
        "updating mChangedRange");
    return Err(NS_ERROR_FAILURE);
  }

  if (NS_FAILED(aHTMLEditor.TopLevelEditSubActionDataRef()
                    .mChangedRange->SetStartAndEnd(
                        pointToInsert.ToRawRangeBoundary(),
                        pointToPutCaret.ToRawRangeBoundary()))) {
    NS_WARNING("nsRange::SetStartAndEnd() failed");
    return Err(NS_ERROR_FAILURE);
  }

  pointToPutCaret.SetInterlinePosition(InterlinePosition::EndOfLine);
  return pointToPutCaret;
}

}  // namespace mozilla
