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

#include "LocalAccessible-inl.h"
#include "chrome/common/ipc_channel.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/PerfStats.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/a11y/CacheConstants.h"
#include "mozilla/a11y/FocusManager.h"
#include "nsAccessibilityService.h"
#ifdef A11Y_LOG
#  include "Logging.h"
#endif
#include "TextLeafRange.h"

namespace mozilla {
namespace a11y {

// Exceeding the IPDL maximum message size will cause a crash. Try to avoid
// this by only including kMaxAccsPerMessage Accessibles in a single IPDL
// call. If there are Accessibles beyond this, they will be split across
// multiple calls.
static constexpr uint32_t kMaxAccsPerMessage = 1000;

/* static */
void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot,
                                     nsTArray<LocalAccessible*>& aTree) {
  MOZ_ASSERT(!aRoot->IsDoc(), "documents shouldn't be serialized");

  aTree.AppendElement(aRoot);
  // OuterDocAccessibles are special because we don't want to serialize the
  // child doc here, we'll call PDocAccessibleConstructor in
  // NotificationController.
  uint32_t childCount = aRoot->IsOuterDoc() ? 0 : aRoot->ChildCount();

  for (uint32_t i = 0; i < childCount; i++) {
    FlattenTree(aRoot->LocalChildAt(i), aTree);
  }
}

/* static */
AccessibleData DocAccessibleChild::SerializeAcc(LocalAccessible* aAcc) {
  AccGenericType genericTypes = aAcc->GenericTypes();
  if (aAcc->ARIAHasNumericValue()) {
    // XXX: We need to do this because this requires a state check.
    genericTypes |= eNumericValue;
  }

  RefPtr<AccAttributes> fields;
  // Even though we send moves as a hide and a show, we don't want to
  // push the cache again for moves.
  if (!aAcc->Document()->IsAccessibleBeingMoved(aAcc)) {
    fields = aAcc->BundleFieldsForCache(
        aAcc->Document()->EffectiveCacheDomains(), CacheUpdateType::Initial);
    if (fields->Count() == 0) {
      fields = nullptr;
    }
  }

  return AccessibleData(aAcc->ID(), aAcc->NativeRole(),
                        aAcc->LocalParent()->ID(), aAcc->IndexInParent(),
                        aAcc->mType, genericTypes, aAcc->mRoleMapEntryIndex,
                        fields);
}

void DocAccessibleChild::InsertIntoIpcTree(LocalAccessible* aChild,
                                           bool aSuppressShowEvent) {
  nsTArray<LocalAccessible*> shownTree;
  FlattenTree(aChild, shownTree);
  uint32_t totalAccs = shownTree.Length();
  nsTArray<AccessibleData> data(std::min(
      kMaxAccsPerMessage - mMutationEventBatcher.AccCount(), totalAccs));

  for (uint32_t accIndex = 0; accIndex < totalAccs; ++accIndex) {
    // This batch of mutation events has no more room left without exceeding our
    // limit. Write the show event data to the queue.
    if (data.Length() + mMutationEventBatcher.AccCount() ==
        kMaxAccsPerMessage) {
      if (AppShutdown::IsShutdownImpending()) {
        return;
      }
      // Note: bool(...) used on aSuppressShowEvent to force selection of the
      // ShowEventData constructor that takes all rvalue reference arguments.
      const uint32_t accCount = data.Length();
      PushMutationEventData(
          ShowEventData{std::move(data), bool(aSuppressShowEvent), false,
                        false},
          accCount);

      // Reset data to avoid relying on state of moved-from object.
      // Preallocate an appropriate capacity to avoid resizing.
      data = nsTArray<AccessibleData>(
          std::min(kMaxAccsPerMessage, totalAccs - accIndex));
    }
    LocalAccessible* child = shownTree[accIndex];
    data.AppendElement(SerializeAcc(child));
  }
  if (AppShutdown::IsShutdownImpending()) {
    return;
  }
  if (!data.IsEmpty()) {
    const uint32_t accCount = data.Length();
    PushMutationEventData(
        ShowEventData{std::move(data), std::move(aSuppressShowEvent), true,
                      false},
        accCount);
  }
}

void DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent) {
  AUTO_PROFILER_MARKER_TEXT("DocAccessibleChild::ShowEvent", A11Y, {}, ""_ns);
  PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_ShowEvent>
      autoRecording;
  // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.

  LocalAccessible* child = aShowEvent->GetAccessible();
  InsertIntoIpcTree(child, /* aSuppressShowEvent */ false);
}

void DocAccessibleChild::PushMutationEventData(MutationEventData aData,
                                               uint32_t aAccCount) {
  mMutationEventBatcher.PushMutationEventData(std::move(aData), aAccCount,
                                              *this);
  // Once all mutation events for this tick are sent, we defer all updates
  // until the parent process sends us a single ACK. We set that flag here, but
  // we request that ACK in NotificationController::WillRefresh once all
  // mutation events have been sent.
  mHasUnackedMutationEvents = true;
}

void DocAccessibleChild::SendQueuedMutationEvents() {
  mMutationEventBatcher.SendQueuedMutationEvents(*this);
}

size_t DocAccessibleChild::MutationEventQueueLength() const {
  return mMutationEventBatcher.EventCount();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeFocus(const uint64_t& aID) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    acc->TakeFocus();
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTo(
    const uint64_t& aID, const uint32_t& aScrollType) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    RefPtr<PresShell> presShell = acc->Document()->PresShellPtr();
    nsCOMPtr<nsIContent> content = acc->GetContent();
    nsCoreUtils::ScrollTo(presShell, content, aScrollType);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeSelection(
    const uint64_t& aID) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    acc->TakeSelection();
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvSetSelected(
    const uint64_t& aID, const bool& aSelect) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    acc->SetSelected(aSelect);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvVerifyCache(
    const uint64_t& aID, const uint64_t& aCacheDomain, AccAttributes* aFields) {
#ifdef A11Y_LOG
  LocalAccessible* acc = IdToAccessible(aID);
  if (!acc) {
    return IPC_OK();
  }

  RefPtr<AccAttributes> localFields =
      acc->BundleFieldsForCache(aCacheDomain, CacheUpdateType::Update);
  bool mismatches = false;

  for (auto prop : *localFields) {
    if (prop.Value<DeleteEntry>()) {
      if (aFields->HasAttribute(prop.Name())) {
        if (!mismatches) {
          logging::MsgBegin("Mismatch!", "Local and remote values differ");
          logging::AccessibleInfo("", acc);
          mismatches = true;
        }
        nsAutoCString propName;
        prop.Name()->ToUTF8String(propName);
        nsAutoString val;
        aFields->GetAttribute(prop.Name(), val);
        logging::MsgEntry(
            "Remote value for %s should be empty, but instead it is '%s'",
            propName.get(), NS_ConvertUTF16toUTF8(val).get());
      }
      continue;
    }

    nsAutoString localVal;
    prop.ValueAsString(localVal);
    nsAutoString remoteVal;
    aFields->GetAttribute(prop.Name(), remoteVal);
    if (!localVal.Equals(remoteVal)) {
      if (!mismatches) {
        logging::MsgBegin("Mismatch!", "Local and remote values differ");
        logging::AccessibleInfo("", acc);
        mismatches = true;
      }
      nsAutoCString propName;
      prop.Name()->ToUTF8String(propName);
      logging::MsgEntry("Fields differ: %s '%s' != '%s'", propName.get(),
                        NS_ConvertUTF16toUTF8(remoteVal).get(),
                        NS_ConvertUTF16toUTF8(localVal).get());
    }
  }
  if (mismatches) {
    logging::MsgEnd();
  }
#endif  // A11Y_LOG

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvDoActionAsync(
    const uint64_t& aID, const uint8_t& aIndex) {
  if (LocalAccessible* acc = IdToAccessible(aID)) {
    (void)acc->DoAction(aIndex);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvSetTextSelection(
    const uint64_t& aStartID, const int32_t& aStartOffset,
    const uint64_t& aEndID, const int32_t& aEndOffset,
    const int32_t& aSelectionNum, const bool& aSetFocus) {
  TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset),
                      TextLeafPoint(IdToAccessible(aEndID), aEndOffset));
  if (range) {
    range.SetSelection(aSelectionNum, aSetFocus);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTextLeafRangeIntoView(
    const uint64_t& aStartID, const int32_t& aStartOffset,
    const uint64_t& aEndID, const int32_t& aEndOffset,
    const uint32_t& aScrollType) {
  TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset),
                      TextLeafPoint(IdToAccessible(aEndID), aEndOffset));
  if (range) {
    range.ScrollIntoView(aScrollType);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvRemoveTextSelection(
    const uint64_t& aID, const int32_t& aSelectionNum) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->RemoveFromSelection(aSelectionNum);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvSetCurValue(
    const uint64_t& aID, const double& aValue) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    acc->SetCurValue(aValue);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvReplaceText(
    const uint64_t& aID, const nsAString& aText) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->ReplaceText(aText);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvInsertText(
    const uint64_t& aID, const nsAString& aText, const int32_t& aPosition) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->InsertText(aText, aPosition);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvCopyText(
    const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->CopyText(aStartPos, aEndPos);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvCutText(
    const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->CutText(aStartPos, aEndPos);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvDeleteText(
    const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->DeleteText(aStartPos, aEndPos);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvPasteText(
    const uint64_t& aID, const int32_t& aPosition) {
  RefPtr<HyperTextAccessible> acc = IdToHyperTextAccessible(aID);
  if (acc && acc->IsTextRole()) {
    acc->PasteText(aPosition);
  }

  return IPC_OK();
}

ipc::IPCResult DocAccessibleChild::RecvRestoreFocus() {
  if (FocusManager* focusMgr = FocusMgr()) {
    focusMgr->ForceFocusEvent();
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollToPoint(
    const uint64_t& aID, const uint32_t& aScrollType, const int32_t& aX,
    const int32_t& aY) {
  LocalAccessible* acc = IdToAccessible(aID);
  if (acc) {
    acc->ScrollToPoint(aScrollType, aX, aY);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollSubstringToPoint(
    const uint64_t& aID, const int32_t& aStartOffset, const int32_t& aEndOffset,
    const uint32_t& aCoordinateType, const int32_t& aX, const int32_t& aY) {
  HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
  if (acc) {
    acc->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoordinateType, aX,
                                aY);
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult DocAccessibleChild::RecvAckMutationEvents() {
  mHasUnackedMutationEvents = false;
  return IPC_OK();
}

/* static */
mozilla::LayoutDeviceIntRect DocAccessibleChild::GetCaretRectForIPCEvent(
    LocalAccessible* aAcc) {
  HyperTextAccessible* ht = aAcc->AsHyperText();
  if (ht) {
    auto [rect, widget] = ht->GetCaretRect();
    // Remove doc offset and reapply in parent.
    LayoutDeviceIntRect docBounds = ht->Document()->Bounds();
    rect.MoveBy(-docBounds.X(), -docBounds.Y());
    return rect;
  }
  return LayoutDeviceIntRect();
}

LocalAccessible* DocAccessibleChild::IdToAccessible(const uint64_t& aID) const {
  if (!aID) return mDoc;

  if (!mDoc) return nullptr;

  return mDoc->GetAccessibleByUniqueID(reinterpret_cast<void*>(aID));
}

HyperTextAccessible* DocAccessibleChild::IdToHyperTextAccessible(
    const uint64_t& aID) const {
  LocalAccessible* acc = IdToAccessible(aID);
  return acc && acc->IsHyperText() ? acc->AsHyperText() : nullptr;
}

void DocAccessibleChild::MutationEventBatcher::PushMutationEventData(
    MutationEventData aData, uint32_t aAccCount, DocAccessibleChild& aDocAcc) {
  // We want to send the mutation events in batches. The number of events in a
  // batch is unscientific. The goal is to avoid sending more data than would
  // overwhelm the IPC mechanism (see IPC::Channel::kMaximumMessageSize), but we
  // stop short of measuring actual message size here. We also don't want to
  // send too many events in one message, since that could choke up the parent
  // process as it tries to fire all the events synchronously. To address these
  // constraints, we construct batches of mutation event data, limiting our
  // events by number of Accessibles touched.
  MOZ_ASSERT(aAccCount <= kMaxAccsPerMessage,
             "More Accessibles given than can fit in a single batch");
  MOZ_ASSERT(aAccCount > 0, "Attempting to send an empty mutation event.");

  // If we hit the exact limit of max Accessibles per message, send the queued
  // mutation events. This happens somewhat often due to the logic in
  // InsertIntoIpcTree that attempts to generate perfectly-sized ShowEventData.
  if (mAccCount + aAccCount == kMaxAccsPerMessage) {
    mMutationEventData.AppendElement(std::move(aData));
    SendQueuedMutationEvents(aDocAcc);
    return;
  }

  // If the batch cannot accommodate the number of new Accessibles, send the
  // queued events, then append the event data.
  if (mAccCount + aAccCount > kMaxAccsPerMessage) {
    SendQueuedMutationEvents(aDocAcc);
  }
  mMutationEventData.AppendElement(std::move(aData));
  mAccCount += aAccCount;
}

void DocAccessibleChild::MutationEventBatcher::SendQueuedMutationEvents(
    DocAccessibleChild& aDocAcc) {
  if (AppShutdown::IsShutdownImpending()) {
    return;
  }
  aDocAcc.SendMutationEvents(mMutationEventData);

  // Reset the batcher state.
  mMutationEventData.Clear();
  mAccCount = 0;
}

}  // namespace a11y
}  // namespace mozilla
