/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 "nsAccessiblePivot.h"

#include "HyperTextAccessible.h"
#include "nsAccUtils.h"
#include "States.h"
#include "xpcAccessibleDocument.h"

using namespace mozilla::a11y;


/**
 * An object that stores a given traversal rule during the pivot movement.
 */
class RuleCache
{
public:
  explicit RuleCache(nsIAccessibleTraversalRule* aRule) : mRule(aRule),
                                                          mAcceptRoles(nullptr) { }
  ~RuleCache () {
    if (mAcceptRoles)
      nsMemory::Free(mAcceptRoles);
  }

  nsresult ApplyFilter(Accessible* aAccessible, uint16_t* aResult);

private:
  nsCOMPtr<nsIAccessibleTraversalRule> mRule;
  uint32_t* mAcceptRoles;
  uint32_t mAcceptRolesLength;
  uint32_t mPreFilter;
};

////////////////////////////////////////////////////////////////////////////////
// nsAccessiblePivot

nsAccessiblePivot::nsAccessiblePivot(Accessible* aRoot) :
  mRoot(aRoot), mModalRoot(nullptr), mPosition(nullptr),
  mStartOffset(-1), mEndOffset(-1)
{
  NS_ASSERTION(aRoot, "A root accessible is required");
}

nsAccessiblePivot::~nsAccessiblePivot()
{
}

////////////////////////////////////////////////////////////////////////////////
// nsISupports

NS_IMPL_CYCLE_COLLECTION(nsAccessiblePivot, mRoot, mPosition, mObservers)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsAccessiblePivot)
  NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivot)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessiblePivot)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAccessiblePivot)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAccessiblePivot)

////////////////////////////////////////////////////////////////////////////////
// nsIAccessiblePivot

NS_IMETHODIMP
nsAccessiblePivot::GetRoot(nsIAccessible** aRoot)
{
  NS_ENSURE_ARG_POINTER(aRoot);

  NS_IF_ADDREF(*aRoot = ToXPC(mRoot));

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::GetPosition(nsIAccessible** aPosition)
{
  NS_ENSURE_ARG_POINTER(aPosition);

  NS_IF_ADDREF(*aPosition = ToXPC(mPosition));

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::SetPosition(nsIAccessible* aPosition)
{
  nsRefPtr<Accessible> position = nullptr;

  if (aPosition) {
    position = aPosition->ToInternalAccessible();
    if (!position || !IsDescendantOf(position, GetActiveRoot()))
      return NS_ERROR_INVALID_ARG;
  }

  // Swap old position with new position, saves us an AddRef/Release.
  mPosition.swap(position);
  int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
  mStartOffset = mEndOffset = -1;
  NotifyOfPivotChange(position, oldStart, oldEnd,
                      nsIAccessiblePivot::REASON_NONE, false);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::GetModalRoot(nsIAccessible** aModalRoot)
{
  NS_ENSURE_ARG_POINTER(aModalRoot);

  NS_IF_ADDREF(*aModalRoot = ToXPC(mModalRoot));

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::SetModalRoot(nsIAccessible* aModalRoot)
{
  Accessible* modalRoot = nullptr;

  if (aModalRoot) {
    modalRoot = aModalRoot->ToInternalAccessible();
    if (!modalRoot || !IsDescendantOf(modalRoot, mRoot))
      return NS_ERROR_INVALID_ARG;
  }

  mModalRoot = modalRoot;
  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::GetStartOffset(int32_t* aStartOffset)
{
  NS_ENSURE_ARG_POINTER(aStartOffset);

  *aStartOffset = mStartOffset;

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::GetEndOffset(int32_t* aEndOffset)
{
  NS_ENSURE_ARG_POINTER(aEndOffset);

  *aEndOffset = mEndOffset;

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::SetTextRange(nsIAccessibleText* aTextAccessible,
                                int32_t aStartOffset, int32_t aEndOffset,
                                bool aIsFromUserInput, uint8_t aArgc)
{
  NS_ENSURE_ARG(aTextAccessible);

  // Check that start offset is smaller than end offset, and that if a value is
  // smaller than 0, both should be -1.
  NS_ENSURE_TRUE(aStartOffset <= aEndOffset &&
                 (aStartOffset >= 0 || (aStartOffset != -1 && aEndOffset != -1)),
                 NS_ERROR_INVALID_ARG);

  nsCOMPtr<nsIAccessible> xpcAcc = do_QueryInterface(aTextAccessible);
  NS_ENSURE_ARG(xpcAcc);

  nsRefPtr<Accessible> acc = xpcAcc->ToInternalAccessible();
  NS_ENSURE_ARG(acc);

  HyperTextAccessible* position = acc->AsHyperText();
  if (!position || !IsDescendantOf(position, GetActiveRoot()))
    return NS_ERROR_INVALID_ARG;

  // Make sure the given offsets don't exceed the character count.
  if (aEndOffset > static_cast<int32_t>(position->CharacterCount()))
    return NS_ERROR_FAILURE;

  int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
  mStartOffset = aStartOffset;
  mEndOffset = aEndOffset;

  mPosition.swap(acc);
  NotifyOfPivotChange(acc, oldStart, oldEnd,
                      nsIAccessiblePivot::REASON_TEXT,
                      (aArgc > 0) ? aIsFromUserInput : true);

  return NS_OK;
}

// Traversal functions

NS_IMETHODIMP
nsAccessiblePivot::MoveNext(nsIAccessibleTraversalRule* aRule,
                            nsIAccessible* aAnchor, bool aIncludeStart,
                            bool aIsFromUserInput, uint8_t aArgc, bool* aResult)
{
  NS_ENSURE_ARG(aResult);
  NS_ENSURE_ARG(aRule);
  *aResult = false;

  Accessible* anchor = mPosition;
  if (aArgc > 0 && aAnchor)
    anchor = aAnchor->ToInternalAccessible();

  if (anchor && (anchor->IsDefunct() || !IsDescendantOf(anchor, GetActiveRoot())))
    return NS_ERROR_NOT_IN_TREE;

  nsresult rv = NS_OK;
  Accessible* accessible =
    SearchForward(anchor, aRule, (aArgc > 1) ? aIncludeStart : false, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  if (accessible)
    *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_NEXT,
                                 (aArgc > 2) ? aIsFromUserInput : true);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::MovePrevious(nsIAccessibleTraversalRule* aRule,
                                nsIAccessible* aAnchor,
                                bool aIncludeStart, bool aIsFromUserInput,
                                uint8_t aArgc, bool* aResult)
{
  NS_ENSURE_ARG(aResult);
  NS_ENSURE_ARG(aRule);
  *aResult = false;

  Accessible* anchor = mPosition;
  if (aArgc > 0 && aAnchor)
    anchor = aAnchor->ToInternalAccessible();

  if (anchor && (anchor->IsDefunct() || !IsDescendantOf(anchor, GetActiveRoot())))
    return NS_ERROR_NOT_IN_TREE;

  nsresult rv = NS_OK;
  Accessible* accessible =
    SearchBackward(anchor, aRule, (aArgc > 1) ? aIncludeStart : false, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  if (accessible)
    *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_PREV,
                                 (aArgc > 2) ? aIsFromUserInput : true);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::MoveFirst(nsIAccessibleTraversalRule* aRule,
                             bool aIsFromUserInput,
                             uint8_t aArgc, bool* aResult)
{
  NS_ENSURE_ARG(aResult);
  NS_ENSURE_ARG(aRule);

  Accessible* root = GetActiveRoot();
  NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE);

  nsresult rv = NS_OK;
  Accessible* accessible = SearchForward(root, aRule, true, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  if (accessible)
    *aResult = MovePivotInternal(accessible, nsIAccessiblePivot::REASON_FIRST,
                                 (aArgc > 0) ? aIsFromUserInput : true);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::MoveLast(nsIAccessibleTraversalRule* aRule,
                            bool aIsFromUserInput,
                            uint8_t aArgc, bool* aResult)
{
  NS_ENSURE_ARG(aResult);
  NS_ENSURE_ARG(aRule);

  Accessible* root = GetActiveRoot();
  NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE);

  *aResult = false;
  nsresult rv = NS_OK;
  Accessible* lastAccessible = root;
  Accessible* accessible = nullptr;

  // First go to the last accessible in pre-order
  while (lastAccessible->HasChildren())
    lastAccessible = lastAccessible->LastChild();

  // Search backwards from last accessible and find the last occurrence in the doc
  accessible = SearchBackward(lastAccessible, aRule, true, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  if (accessible)
    *aResult = MovePivotInternal(accessible, nsAccessiblePivot::REASON_LAST,
                                 (aArgc > 0) ? aIsFromUserInput : true);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::MoveNextByText(TextBoundaryType aBoundary,
                                  bool aIsFromUserInput, uint8_t aArgc,
                                  bool* aResult)
{
  NS_ENSURE_ARG(aResult);

  *aResult = false;

  int32_t tempStart = mStartOffset, tempEnd = mEndOffset;
  Accessible* tempPosition = mPosition;
  Accessible* root = GetActiveRoot();
  while (true) {
    Accessible* curPosition = tempPosition;
    HyperTextAccessible* text = nullptr;
    // Find the nearest text node using a preorder traversal starting from
    // the current node.
    if (!(text = tempPosition->AsHyperText())) {
      text = SearchForText(tempPosition, false);
      if (!text)
        return NS_OK;
      if (text != curPosition)
        tempStart = tempEnd = -1;
      tempPosition = text;
    }

    // If the search led to the parent of the node we started on (e.g. when
    // starting on a text leaf), start the text movement from the end of that
    // node, otherwise we just default to 0.
    if (tempEnd == -1)
      tempEnd = text == curPosition->Parent() ?
                text->GetChildOffset(curPosition) : 0;

    // If there's no more text on the current node, try to find the next text
    // node; if there isn't one, bail out.
    if (tempEnd == static_cast<int32_t>(text->CharacterCount())) {
      if (tempPosition == root)
        return NS_OK;

      // If we're currently sitting on a link, try move to either the next
      // sibling or the parent, whichever is closer to the current end
      // offset. Otherwise, do a forward search for the next node to land on
      // (we don't do this in the first case because we don't want to go to the
      // subtree).
      Accessible* sibling = tempPosition->NextSibling();
      if (tempPosition->IsLink()) {
        if (sibling && sibling->IsLink()) {
          tempStart = tempEnd = -1;
          tempPosition = sibling;
        } else {
          tempStart = tempPosition->StartOffset();
          tempEnd = tempPosition->EndOffset();
          tempPosition = tempPosition->Parent();
        }
      } else {
        tempPosition = SearchForText(tempPosition, false);
        if (!tempPosition)
          return NS_OK;
        tempStart = tempEnd = -1;
      }
      continue;
    }

    AccessibleTextBoundary startBoundary, endBoundary;
    switch (aBoundary) {
      case CHAR_BOUNDARY:
        startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
        endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
        break;
      case WORD_BOUNDARY:
        startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
        endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
        break;
      default:
        return NS_ERROR_INVALID_ARG;
    }

    nsAutoString unusedText;
    int32_t newStart = 0, newEnd = 0, currentEnd = tempEnd;
    text->TextAtOffset(tempEnd, endBoundary, &newStart, &tempEnd, unusedText);
    text->TextBeforeOffset(tempEnd, startBoundary, &newStart, &newEnd, unusedText);
    int32_t potentialStart = newEnd == tempEnd ? newStart : newEnd;
    tempStart = potentialStart > tempStart ? potentialStart : currentEnd;

    // The offset range we've obtained might have embedded characters in it,
    // limit the range to the start of the first occurrence of an embedded
    // character.
    Accessible* childAtOffset = nullptr;
    for (int32_t i = tempStart; i < tempEnd; i++) {
      childAtOffset = text->GetChildAtOffset(i);
      if (childAtOffset && nsAccUtils::IsEmbeddedObject(childAtOffset)) {
        tempEnd = i;
        break;
      }
    }
    // If there's an embedded character at the very start of the range, we
    // instead want to traverse into it. So restart the movement with
    // the child as the starting point.
    if (childAtOffset && nsAccUtils::IsEmbeddedObject(childAtOffset) &&
        tempStart == static_cast<int32_t>(childAtOffset->StartOffset())) {
      tempPosition = childAtOffset;
      tempStart = tempEnd = -1;
      continue;
    }

    *aResult = true;

    Accessible* startPosition = mPosition;
    int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
    mPosition = tempPosition;
    mStartOffset = tempStart;
    mEndOffset = tempEnd;
    NotifyOfPivotChange(startPosition, oldStart, oldEnd,
                        nsIAccessiblePivot::REASON_TEXT,
                        (aArgc > 0) ? aIsFromUserInput : true);
    return NS_OK;
  }
}

NS_IMETHODIMP
nsAccessiblePivot::MovePreviousByText(TextBoundaryType aBoundary,
                                      bool aIsFromUserInput, uint8_t aArgc,
                                      bool* aResult)
{
  NS_ENSURE_ARG(aResult);

  *aResult = false;

  int32_t tempStart = mStartOffset, tempEnd = mEndOffset;
  Accessible* tempPosition = mPosition;
  Accessible* root = GetActiveRoot();
  while (true) {
    Accessible* curPosition = tempPosition;
    HyperTextAccessible* text;
    // Find the nearest text node using a reverse preorder traversal starting
    // from the current node.
    if (!(text = tempPosition->AsHyperText())) {
      text = SearchForText(tempPosition, true);
      if (!text)
        return NS_OK;
      if (text != curPosition)
        tempStart = tempEnd = -1;
      tempPosition = text;
    }

    // If the search led to the parent of the node we started on (e.g. when
    // starting on a text leaf), start the text movement from the end of that
    // node, otherwise we just default to 0.
    if (tempStart == -1) {
      if (tempPosition != curPosition)
        tempStart = text == curPosition->Parent() ?
                    text->GetChildOffset(curPosition) : text->CharacterCount();
      else
        tempStart = 0;
    }

    // If there's no more text on the current node, try to find the previous
    // text node; if there isn't one, bail out.
    if (tempStart == 0) {
      if (tempPosition == root)
        return NS_OK;

      // If we're currently sitting on a link, try move to either the previous
      // sibling or the parent, whichever is closer to the current end
      // offset. Otherwise, do a forward search for the next node to land on
      // (we don't do this in the first case because we don't want to go to the
      // subtree).
      Accessible* sibling = tempPosition->PrevSibling();
      if (tempPosition->IsLink()) {
        if (sibling && sibling->IsLink()) {
          HyperTextAccessible* siblingText = sibling->AsHyperText();
          tempStart = tempEnd = siblingText ?
                                siblingText->CharacterCount() : -1;
          tempPosition = sibling;
        } else {
          tempStart = tempPosition->StartOffset();
          tempEnd = tempPosition->EndOffset();
          tempPosition = tempPosition->Parent();
        }
      } else {
        HyperTextAccessible* tempText = SearchForText(tempPosition, true);
        if (!tempText)
          return NS_OK;
        tempPosition = tempText;
        tempStart = tempEnd = tempText->CharacterCount();
      }
      continue;
    }

    AccessibleTextBoundary startBoundary, endBoundary;
    switch (aBoundary) {
      case CHAR_BOUNDARY:
        startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
        endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
        break;
      case WORD_BOUNDARY:
        startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
        endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
        break;
      default:
        return NS_ERROR_INVALID_ARG;
    }

    nsAutoString unusedText;
    int32_t newStart = 0, newEnd = 0, currentStart = tempStart, potentialEnd = 0;
    text->TextBeforeOffset(tempStart, startBoundary, &newStart, &newEnd, unusedText);
    if (newStart < tempStart)
      tempStart = newEnd >= currentStart ? newStart : newEnd;
    else // XXX: In certain odd cases newStart is equal to tempStart
      text->TextBeforeOffset(tempStart - 1, startBoundary, &newStart,
                             &tempStart, unusedText);
    text->TextAtOffset(tempStart, endBoundary, &newStart, &potentialEnd,
                       unusedText);
    tempEnd = potentialEnd < tempEnd ? potentialEnd : currentStart;

    // The offset range we've obtained might have embedded characters in it,
    // limit the range to the start of the last occurrence of an embedded
    // character.
    Accessible* childAtOffset = nullptr;
    for (int32_t i = tempEnd - 1; i >= tempStart; i--) {
      childAtOffset = text->GetChildAtOffset(i);
      if (childAtOffset && nsAccUtils::IsEmbeddedObject(childAtOffset)) {
        tempStart = childAtOffset->EndOffset();
        break;
      }
    }
    // If there's an embedded character at the very end of the range, we
    // instead want to traverse into it. So restart the movement with
    // the child as the starting point.
    if (childAtOffset && nsAccUtils::IsEmbeddedObject(childAtOffset) &&
        tempEnd == static_cast<int32_t>(childAtOffset->EndOffset())) {
      tempPosition = childAtOffset;
      tempStart = tempEnd = childAtOffset->AsHyperText()->CharacterCount();
      continue;
    }

    *aResult = true;

    Accessible* startPosition = mPosition;
    int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
    mPosition = tempPosition;
    mStartOffset = tempStart;
    mEndOffset = tempEnd;

    NotifyOfPivotChange(startPosition, oldStart, oldEnd,
                        nsIAccessiblePivot::REASON_TEXT,
                        (aArgc > 0) ? aIsFromUserInput : true);
    return NS_OK;
  }
}

NS_IMETHODIMP
nsAccessiblePivot::MoveToPoint(nsIAccessibleTraversalRule* aRule,
                               int32_t aX, int32_t aY, bool aIgnoreNoMatch,
                               bool aIsFromUserInput, uint8_t aArgc,
                               bool* aResult)
{
  NS_ENSURE_ARG_POINTER(aResult);
  NS_ENSURE_ARG_POINTER(aRule);

  *aResult = false;

  Accessible* root = GetActiveRoot();
  NS_ENSURE_TRUE(root && !root->IsDefunct(), NS_ERROR_NOT_IN_TREE);

  RuleCache cache(aRule);
  Accessible* match = nullptr;
  Accessible* child = root->ChildAtPoint(aX, aY, Accessible::eDeepestChild);
  while (child && root != child) {
    uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
    nsresult rv = cache.ApplyFilter(child, &filtered);
    NS_ENSURE_SUCCESS(rv, rv);

    // Ignore any matching nodes that were below this one
    if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE)
      match = nullptr;

    // Match if no node below this is a match
    if ((filtered & nsIAccessibleTraversalRule::FILTER_MATCH) && !match) {
      nsIntRect childRect = child->Bounds();
      // Double-check child's bounds since the deepest child may have been out
      // of bounds. This assures we don't return a false positive.
      if (aX >= childRect.x && aX < childRect.x + childRect.width &&
          aY >= childRect.y && aY < childRect.y + childRect.height)
        match = child;
    }

    child = child->Parent();
  }

  if (match || !aIgnoreNoMatch)
    *aResult = MovePivotInternal(match, nsIAccessiblePivot::REASON_POINT,
                                 (aArgc > 0) ? aIsFromUserInput : true);

  return NS_OK;
}

// Observer functions

NS_IMETHODIMP
nsAccessiblePivot::AddObserver(nsIAccessiblePivotObserver* aObserver)
{
  NS_ENSURE_ARG(aObserver);

  mObservers.AppendElement(aObserver);

  return NS_OK;
}

NS_IMETHODIMP
nsAccessiblePivot::RemoveObserver(nsIAccessiblePivotObserver* aObserver)
{
  NS_ENSURE_ARG(aObserver);

  return mObservers.RemoveElement(aObserver) ? NS_OK : NS_ERROR_FAILURE;
}

// Private utility methods

bool
nsAccessiblePivot::IsDescendantOf(Accessible* aAccessible, Accessible* aAncestor)
{
  if (!aAncestor || aAncestor->IsDefunct())
    return false;

  // XXX Optimize with IsInDocument() when appropriate. Blocked by bug 759875.
  Accessible* accessible = aAccessible;
  do {
    if (accessible == aAncestor)
      return true;
  } while ((accessible = accessible->Parent()));

  return false;
}

bool
nsAccessiblePivot::MovePivotInternal(Accessible* aPosition,
                                     PivotMoveReason aReason,
                                     bool aIsFromUserInput)
{
  nsRefPtr<Accessible> oldPosition = mPosition.forget();
  mPosition = aPosition;
  int32_t oldStart = mStartOffset, oldEnd = mEndOffset;
  mStartOffset = mEndOffset = -1;

  return NotifyOfPivotChange(oldPosition, oldStart, oldEnd, aReason,
                             aIsFromUserInput);
}

Accessible*
nsAccessiblePivot::AdjustStartPosition(Accessible* aAccessible,
                                       RuleCache& aCache,
                                       uint16_t* aFilterResult,
                                       nsresult* aResult)
{
  Accessible* matched = aAccessible;
  *aResult = aCache.ApplyFilter(aAccessible, aFilterResult);

  if (aAccessible != mRoot && aAccessible != mModalRoot) {
    for (Accessible* temp = aAccessible->Parent();
         temp && temp != mRoot && temp != mModalRoot; temp = temp->Parent()) {
      uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
      *aResult = aCache.ApplyFilter(temp, &filtered);
      NS_ENSURE_SUCCESS(*aResult, nullptr);
      if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) {
        *aFilterResult = filtered;
        matched = temp;
      }
    }
  }

  if (aAccessible == mPosition && mStartOffset != -1 && mEndOffset != -1) {
    HyperTextAccessible* text = aAccessible->AsHyperText();
    if (text) {
      matched = text->GetChildAtOffset(mStartOffset);
    }
  }

  return matched;
}

Accessible*
nsAccessiblePivot::SearchBackward(Accessible* aAccessible,
                                  nsIAccessibleTraversalRule* aRule,
                                  bool aSearchCurrent,
                                  nsresult* aResult)
{
  *aResult = NS_OK;

  // Initial position could be unset, in that case return null.
  if (!aAccessible)
    return nullptr;

  RuleCache cache(aRule);
  uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
  Accessible* accessible = AdjustStartPosition(aAccessible, cache,
                                               &filtered, aResult);
  NS_ENSURE_SUCCESS(*aResult, nullptr);

  if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) {
    return accessible;
  }

  Accessible* root = GetActiveRoot();
  while (accessible != root) {
    Accessible* parent = accessible->Parent();
    int32_t idxInParent = accessible->IndexInParent();
    while (idxInParent > 0) {
      if (!(accessible = parent->GetChildAt(--idxInParent)))
        continue;

      *aResult = cache.ApplyFilter(accessible, &filtered);
      NS_ENSURE_SUCCESS(*aResult, nullptr);

      Accessible* lastChild = nullptr;
      while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
             (lastChild = accessible->LastChild())) {
        parent = accessible;
        accessible = lastChild;
        idxInParent = accessible->IndexInParent();
        *aResult = cache.ApplyFilter(accessible, &filtered);
        NS_ENSURE_SUCCESS(*aResult, nullptr);
      }

      if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)
        return accessible;
    }

    if (!(accessible = parent))
      break;

    *aResult = cache.ApplyFilter(accessible, &filtered);
    NS_ENSURE_SUCCESS(*aResult, nullptr);

    if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)
      return accessible;
  }

  return nullptr;
}

Accessible*
nsAccessiblePivot::SearchForward(Accessible* aAccessible,
                                 nsIAccessibleTraversalRule* aRule,
                                 bool aSearchCurrent,
                                 nsresult* aResult)
{
  *aResult = NS_OK;

  // Initial position could be not set, in that case begin search from root.
  Accessible* root = GetActiveRoot();
  Accessible* accessible = (!aAccessible) ? root : aAccessible;

  RuleCache cache(aRule);

  uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
  accessible = AdjustStartPosition(accessible, cache, &filtered, aResult);
  NS_ENSURE_SUCCESS(*aResult, nullptr);
  if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH))
    return accessible;

  while (true) {
    Accessible* firstChild = nullptr;
    while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
           (firstChild = accessible->FirstChild())) {
      accessible = firstChild;
      *aResult = cache.ApplyFilter(accessible, &filtered);
      NS_ENSURE_SUCCESS(*aResult, nullptr);

      if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)
        return accessible;
    }

    Accessible* sibling = nullptr;
    Accessible* temp = accessible;
    do {
      if (temp == root)
        break;

      sibling = temp->NextSibling();

      if (sibling)
        break;
    } while ((temp = temp->Parent()));

    if (!sibling)
      break;

    accessible = sibling;
    *aResult = cache.ApplyFilter(accessible, &filtered);
    NS_ENSURE_SUCCESS(*aResult, nullptr);

    if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)
      return accessible;
  }

  return nullptr;
}

HyperTextAccessible*
nsAccessiblePivot::SearchForText(Accessible* aAccessible, bool aBackward)
{
  Accessible* root = GetActiveRoot();
  Accessible* accessible = aAccessible;
  while (true) {
    Accessible* child = nullptr;

    while ((child = (aBackward ? accessible->LastChild() :
                                 accessible->FirstChild()))) {
      accessible = child;
      if (child->IsHyperText())
        return child->AsHyperText();
    }

    Accessible* sibling = nullptr;
    Accessible* temp = accessible;
    do {
      if (temp == root)
        break;

      if (temp != aAccessible && temp->IsHyperText())
        return temp->AsHyperText();

      sibling = aBackward ? temp->PrevSibling() : temp->NextSibling();

      if (sibling)
        break;
    } while ((temp = temp->Parent()));

    if (!sibling)
      break;

    accessible = sibling;
    if (accessible->IsHyperText())
      return accessible->AsHyperText();
  }

  return nullptr;
}


bool
nsAccessiblePivot::NotifyOfPivotChange(Accessible* aOldPosition,
                                       int32_t aOldStart, int32_t aOldEnd,
                                       int16_t aReason, bool aIsFromUserInput)
{
  if (aOldPosition == mPosition &&
      aOldStart == mStartOffset && aOldEnd == mEndOffset)
    return false;

  nsCOMPtr<nsIAccessible> xpcOldPos = ToXPC(aOldPosition); // death grip
  nsTObserverArray<nsCOMPtr<nsIAccessiblePivotObserver> >::ForwardIterator iter(mObservers);
  while (iter.HasMore()) {
    nsIAccessiblePivotObserver* obs = iter.GetNext();
    obs->OnPivotChanged(this, xpcOldPos, aOldStart, aOldEnd, aReason,
                        aIsFromUserInput);
  }

  return true;
}

nsresult
RuleCache::ApplyFilter(Accessible* aAccessible, uint16_t* aResult)
{
  *aResult = nsIAccessibleTraversalRule::FILTER_IGNORE;

  if (!mAcceptRoles) {
    nsresult rv = mRule->GetMatchRoles(&mAcceptRoles, &mAcceptRolesLength);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = mRule->GetPreFilter(&mPreFilter);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if (mPreFilter) {
    uint64_t state = aAccessible->State();

    if ((nsIAccessibleTraversalRule::PREFILTER_INVISIBLE & mPreFilter) &&
        (state & states::INVISIBLE))
      return NS_OK;

    if ((nsIAccessibleTraversalRule::PREFILTER_OFFSCREEN & mPreFilter) &&
        (state & states::OFFSCREEN))
      return NS_OK;

    if ((nsIAccessibleTraversalRule::PREFILTER_NOT_FOCUSABLE & mPreFilter) &&
        !(state & states::FOCUSABLE))
      return NS_OK;

    if (nsIAccessibleTraversalRule::PREFILTER_ARIA_HIDDEN & mPreFilter) {
      if (aAccessible->IsARIAHidden()) {
        *aResult |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
        return NS_OK;
      }
    }

    if ((nsIAccessibleTraversalRule::PREFILTER_TRANSPARENT & mPreFilter) &&
        !(state & states::OPAQUE1)) {
      nsIFrame* frame = aAccessible->GetFrame();
      if (frame->StyleDisplay()->mOpacity == 0.0f) {
        *aResult |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
        return NS_OK;
      }
    }
  }

  if (mAcceptRolesLength > 0) {
    uint32_t accessibleRole = aAccessible->Role();
    bool matchesRole = false;
    for (uint32_t idx = 0; idx < mAcceptRolesLength; idx++) {
      matchesRole = mAcceptRoles[idx] == accessibleRole;
      if (matchesRole)
        break;
    }
    if (!matchesRole)
      return NS_OK;
  }

  return mRule->Match(ToXPC(aAccessible), aResult);
}
