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

#include "AccIterator.h"
#include "LocalAccessible.h"
#include "RemoteAccessible.h"
#include "nsAccUtils.h"
#include "nsIAccessiblePivot.h"

#include "mozilla/a11y/Accessible.h"

using namespace mozilla;
using namespace mozilla::a11y;

////////////////////////////////////////////////////////////////////////////////
// Pivot
////////////////////////////////////////////////////////////////////////////////

Pivot::Pivot(Accessible* aRoot) : mRoot(aRoot) { MOZ_COUNT_CTOR(Pivot); }

Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot); }

Accessible* Pivot::AdjustStartPosition(Accessible* aAnchor, PivotRule& aRule,
                                       uint16_t* aFilterResult) {
  Accessible* matched = aAnchor;
  *aFilterResult = aRule.Match(aAnchor);

  if (aAnchor && aAnchor != mRoot) {
    for (Accessible* temp = aAnchor->Parent(); temp && temp != mRoot;
         temp = temp->Parent()) {
      uint16_t filtered = aRule.Match(temp);
      if (filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) {
        *aFilterResult = filtered;
        matched = temp;
      }
    }
  }

  return matched;
}

Accessible* Pivot::SearchBackward(Accessible* aAnchor, PivotRule& aRule,
                                  bool aSearchCurrent) {
  // Initial position could be unset, in that case return null.
  if (!aAnchor) {
    return nullptr;
  }

  uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;

  Accessible* acc = AdjustStartPosition(aAnchor, aRule, &filtered);

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

  while (acc && acc != mRoot) {
    Accessible* parent = acc->Parent();
#if defined(ANDROID)
    MOZ_ASSERT(
        acc->IsLocal() || (acc->IsRemote() && parent->IsRemote()),
        "Pivot::SearchBackward climbed out of remote subtree in Android!");
#endif
    int32_t idxInParent = acc->IndexInParent();
    while (idxInParent > 0 && parent) {
      acc = parent->ChildAt(--idxInParent);
      if (!acc) {
        continue;
      }

      filtered = aRule.Match(acc);

      Accessible* lastChild = acc->LastChild();
      while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
             lastChild) {
        parent = acc;
        acc = lastChild;
        idxInParent = acc->IndexInParent();
        filtered = aRule.Match(acc);
        lastChild = acc->LastChild();
      }

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

    acc = parent;
    if (!acc) {
      break;
    }

    filtered = aRule.Match(acc);

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

  return nullptr;
}

Accessible* Pivot::SearchForward(Accessible* aAnchor, PivotRule& aRule,
                                 bool aSearchCurrent) {
  // Initial position could be not set, in that case begin search from root.
  Accessible* acc = aAnchor ? aAnchor : mRoot;

  uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
  acc = AdjustStartPosition(acc, aRule, &filtered);
  if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) {
    return acc;
  }

  while (acc) {
    Accessible* firstChild = acc->FirstChild();
    while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
           firstChild) {
      acc = firstChild;
      filtered = aRule.Match(acc);

      if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
        return acc;
      }
      firstChild = acc->FirstChild();
    }

    Accessible* sibling = nullptr;
    Accessible* temp = acc;
    do {
      if (temp == mRoot) {
        break;
      }

      sibling = temp->NextSibling();

      if (sibling) {
        break;
      }
      temp = temp->Parent();
#if defined(ANDROID)
      MOZ_ASSERT(
          acc->IsLocal() || (acc->IsRemote() && temp->IsRemote()),
          "Pivot::SearchForward climbed out of remote subtree in Android!");
#endif

    } while (temp);

    if (!sibling) {
      break;
    }

    acc = sibling;
    filtered = aRule.Match(acc);
    if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
      return acc;
    }
  }

  return nullptr;
}

Accessible* Pivot::Next(Accessible* aAnchor, PivotRule& aRule,
                        bool aIncludeStart) {
  return SearchForward(aAnchor, aRule, aIncludeStart);
}

Accessible* Pivot::Prev(Accessible* aAnchor, PivotRule& aRule,
                        bool aIncludeStart) {
  return SearchBackward(aAnchor, aRule, aIncludeStart);
}

Accessible* Pivot::First(PivotRule& aRule) {
  return SearchForward(mRoot, aRule, true);
}

Accessible* Pivot::Last(PivotRule& aRule) {
  Accessible* lastAcc = mRoot;

  // First go to the last accessible in pre-order
  while (lastAcc && lastAcc->HasChildren()) {
    lastAcc = lastAcc->LastChild();
  }

  // Search backwards from last accessible and find the last occurrence in the
  // doc
  return SearchBackward(lastAcc, aRule, true);
}

Accessible* Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) {
  Accessible* match = nullptr;
  Accessible* child =
      mRoot ? mRoot->ChildAtPoint(aX, aY,
                                  Accessible::EWhichChildAtPoint::DeepestChild)
            : nullptr;
  while (child && (mRoot != child)) {
    uint16_t filtered = aRule.Match(child);

    // 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) {
      LayoutDeviceIntRect childRect = child->IsLocal()
                                          ? child->AsLocal()->Bounds()
                                          : child->AsRemote()->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 (childRect.Contains(aX, aY)) {
        match = child;
      }
    }

    child = child->Parent();
  }

  return match;
}

// Role Rule

PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole)
    : mRole(aRole), mDirectDescendantsFrom(nullptr) {}

PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole,
                             Accessible* aDirectDescendantsFrom)
    : mRole(aRole), mDirectDescendantsFrom(aDirectDescendantsFrom) {}

uint16_t PivotRoleRule::Match(Accessible* aAcc) {
  uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;

  if (nsAccUtils::MustPrune(aAcc)) {
    result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) {
    // If we've specified mDirectDescendantsFrom, we should ignore
    // non-direct descendants of from the specified AoP. Because
    // pivot performs a preorder traversal, the first aAcc
    // object(s) that don't equal mDirectDescendantsFrom will be
    // mDirectDescendantsFrom's children. We'll process them, but ignore
    // their subtrees thereby processing direct descendants of
    // mDirectDescendantsFrom only.
    result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (aAcc && aAcc->Role() == mRole) {
    result |= nsIAccessibleTraversalRule::FILTER_MATCH;
  }

  return result;
}

// State Rule

PivotStateRule::PivotStateRule(uint64_t aState) : mState(aState) {}

uint16_t PivotStateRule::Match(Accessible* aAcc) {
  uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;

  if (nsAccUtils::MustPrune(aAcc)) {
    result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (aAcc && (aAcc->State() & mState)) {
    result = nsIAccessibleTraversalRule::FILTER_MATCH |
             nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  return result;
}

// LocalAccInSameDocRule

uint16_t LocalAccInSameDocRule::Match(Accessible* aAcc) {
  LocalAccessible* acc = aAcc ? aAcc->AsLocal() : nullptr;
  if (!acc) {
    return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }
  if (acc->IsOuterDoc()) {
    return nsIAccessibleTraversalRule::FILTER_MATCH |
           nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }
  return nsIAccessibleTraversalRule::FILTER_MATCH;
}

// Radio Button Name Rule

PivotRadioNameRule::PivotRadioNameRule(const nsString& aName) : mName(aName) {}

uint16_t PivotRadioNameRule::Match(Accessible* aAcc) {
  uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
  RemoteAccessible* remote = aAcc->AsRemote();
  if (!remote) {
    // We need the cache to be able to fetch the name attribute below.
    return result;
  }

  if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) {
    result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (remote->IsHTMLRadioButton()) {
    nsString currName = remote->GetCachedHTMLNameAttribute();
    if (!currName.IsEmpty() && mName.Equals(currName)) {
      result |= nsIAccessibleTraversalRule::FILTER_MATCH;
    }
  }

  return result;
}

// MustPruneSameDocRule

uint16_t MustPruneSameDocRule::Match(Accessible* aAcc) {
  if (!aAcc) {
    return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) {
    return nsIAccessibleTraversalRule::FILTER_MATCH |
           nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  return nsIAccessibleTraversalRule::FILTER_MATCH;
}

// ARIA Selected Rule

uint16_t PivotARIASelectedRule::Match(Accessible* aAcc) {
  uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;

  if (nsAccUtils::MustPrune(aAcc)) {
    result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
  }

  if (aAcc && aAcc->ARIASelected()) {
    result = nsIAccessibleTraversalRule::FILTER_MATCH;
  }

  return result;
}
