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

#include "DocAccessible.h"
#include "HTMLFormControlAccessible.h"
#include "LocalAccessible-inl.h"
#include "Relation.h"
#include "States.h"
#include "TreeWalker.h"
#include "XULMenuAccessible.h"
#include "mozilla/a11y/Role.h"
#include "mozilla/dom/Element.h"
#include "nsAccUtils.h"
#include "nsIContentInlines.h"
#include "nsIDOMXULButtonElement.h"
#include "nsIDOMXULMenuListElement.h"
#include "nsIDOMXULRadioGroupElement.h"
#include "nsIDOMXULSelectCntrlItemEl.h"
#include "nsIFrame.h"
#include "nsMenuPopupFrame.h"
#include "nsNameSpaceManager.h"

using namespace mozilla::a11y;

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible
////////////////////////////////////////////////////////////////////////////////

XULButtonAccessible::XULButtonAccessible(nsIContent* aContent,
                                         DocAccessible* aDoc)
    : AccessibleWrap(aContent, aDoc) {
  if (ContainsMenu()) {
    mGenericTypes |= eMenuButton;
  } else {
    mGenericTypes |= eButton;
  }
}

XULButtonAccessible::~XULButtonAccessible() = default;

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible: nsISupports

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible: nsIAccessible

bool XULButtonAccessible::HasPrimaryAction() const { return true; }

void XULButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
  if (aIndex == eAction_Click) aName.AssignLiteral("press");
}

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible: LocalAccessible

role XULButtonAccessible::NativeRole() const {
  // Buttons can be checked; they simply appear pressed in rather than checked.
  // In this case, we must expose them as toggle buttons.
  nsCOMPtr<nsIDOMXULButtonElement> xulButtonElement = Elm()->AsXULButton();
  if (xulButtonElement) {
    nsAutoString type;
    xulButtonElement->GetType(type);
    if (type.EqualsLiteral("checkbox") || type.EqualsLiteral("radio")) {
      return roles::TOGGLE_BUTTON;
    }
  }
  return roles::PUSHBUTTON;
}

uint64_t XULButtonAccessible::NativeState() const {
  // Possible states: focused, focusable, unavailable(disabled).

  // get focus and disable status from base class
  uint64_t state = LocalAccessible::NativeState();

  nsCOMPtr<nsIDOMXULButtonElement> xulButtonElement = Elm()->AsXULButton();
  if (xulButtonElement) {
    // Some buttons can have their checked state set without being of type
    // checkbox or radio. Expose the pressed state unconditionally.
    bool checked = false;
    xulButtonElement->GetChecked(&checked);
    if (checked) {
      state |= states::PRESSED;
    }
  }

  if (ContainsMenu()) state |= states::HASPOPUP;

  if (mContent->AsElement()->HasAttr(nsGkAtoms::_default)) {
    state |= states::DEFAULT;
  }

  return state;
}

bool XULButtonAccessible::AttributeChangesState(nsAtom* aAttribute) {
  if (aAttribute == nsGkAtoms::checked) {
    return true;
  }
  return AccessibleWrap::AttributeChangesState(aAttribute);
}

void XULButtonAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
                                              nsAtom* aAttribute,
                                              AttrModType aModType,
                                              const nsAttrValue* aOldValue,
                                              uint64_t aOldState) {
  AccessibleWrap::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
                                      aOldValue, aOldState);
  if (aAttribute == nsGkAtoms::label) {
    mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
  }
}

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible: Widgets

bool XULButtonAccessible::IsWidget() const { return true; }

bool XULButtonAccessible::IsActiveWidget() const {
  return FocusMgr()->HasDOMFocus(mContent);
}

bool XULButtonAccessible::AreItemsOperable() const {
  if (IsMenuButton()) {
    LocalAccessible* menuPopup = mChildren.SafeElementAt(0, nullptr);
    if (menuPopup) {
      nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(menuPopup->GetFrame());
      return menuPopupFrame->IsOpen();
    }
  }
  return false;  // no items
}

bool XULButtonAccessible::IsAcceptableChild(nsIContent* aEl) const {
  // In general XUL buttons should not have accessible children. However:
  return
      //   menu buttons can have popup accessibles (@type="menu" or
      //   columnpicker).
      aEl->IsXULElement(nsGkAtoms::menupopup) ||
      // A XUL button can be labelled by a direct child text node, so we need to
      // allow that as a child so it will be picked up when computing name from
      // subtree.
      (aEl->IsText() && aEl->GetParent() == mContent);
}

////////////////////////////////////////////////////////////////////////////////
// XULButtonAccessible protected

bool XULButtonAccessible::ContainsMenu() const {
  return mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
                                            nsGkAtoms::menu, eCaseMatters);
}

////////////////////////////////////////////////////////////////////////////////
// XULDropmarkerAccessible
////////////////////////////////////////////////////////////////////////////////

XULDropmarkerAccessible::XULDropmarkerAccessible(nsIContent* aContent,
                                                 DocAccessible* aDoc)
    : LeafAccessible(aContent, aDoc) {}

bool XULDropmarkerAccessible::HasPrimaryAction() const { return true; }

bool XULDropmarkerAccessible::DropmarkerOpen(bool aToggleOpen) const {
  bool isOpen = false;

  nsIContent* parent = mContent->GetFlattenedTreeParent();

  while (parent) {
    nsCOMPtr<nsIDOMXULButtonElement> parentButtonElement =
        parent->AsElement()->AsXULButton();
    if (parentButtonElement) {
      parentButtonElement->GetOpen(&isOpen);
      if (aToggleOpen) parentButtonElement->SetOpen(!isOpen);
      return isOpen;
    }

    nsCOMPtr<nsIDOMXULMenuListElement> parentMenuListElement =
        parent->AsElement()->AsXULMenuList();
    if (parentMenuListElement) {
      parentMenuListElement->GetOpen(&isOpen);
      if (aToggleOpen) parentMenuListElement->SetOpen(!isOpen);
      return isOpen;
    }
    parent = parent->GetFlattenedTreeParent();
  }

  return isOpen;
}

void XULDropmarkerAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
  aName.Truncate();
  if (aIndex == eAction_Click) {
    if (DropmarkerOpen(false)) {
      aName.AssignLiteral("close");
    } else {
      aName.AssignLiteral("open");
    }
  }
}

bool XULDropmarkerAccessible::DoAction(uint8_t index) const {
  if (index == eAction_Click) {
    DropmarkerOpen(true);  // Reverse the open attribute
    return true;
  }
  return false;
}

role XULDropmarkerAccessible::NativeRole() const { return roles::PUSHBUTTON; }

uint64_t XULDropmarkerAccessible::NativeState() const {
  return DropmarkerOpen(false) ? states::PRESSED : 0;
}

////////////////////////////////////////////////////////////////////////////////
// XULGroupboxAccessible
////////////////////////////////////////////////////////////////////////////////

XULGroupboxAccessible::XULGroupboxAccessible(nsIContent* aContent,
                                             DocAccessible* aDoc)
    : AccessibleWrap(aContent, aDoc) {}

role XULGroupboxAccessible::NativeRole() const { return roles::GROUPING; }

ENameValueFlag XULGroupboxAccessible::NativeName(nsString& aName) const {
  // XXX: we use the first related accessible only.
  LocalAccessible* label =
      RelationByType(RelationType::LABELLED_BY).LocalNext();
  if (label) {
    label->Name(aName);
    return eNameFromRelations;
  }

  return eNameOK;
}

Relation XULGroupboxAccessible::RelationByType(RelationType aType) const {
  Relation rel = AccessibleWrap::RelationByType(aType);

  // The label for xul:groupbox is generated from the first xul:label
  if (aType == RelationType::LABELLED_BY && ChildCount() > 0) {
    LocalAccessible* childAcc = LocalChildAt(0);
    if (childAcc->Role() == roles::LABEL &&
        childAcc->GetContent()->IsXULElement(nsGkAtoms::label)) {
      rel.AppendTarget(childAcc);
    }
  }

  return rel;
}

////////////////////////////////////////////////////////////////////////////////
// XULRadioButtonAccessible
////////////////////////////////////////////////////////////////////////////////

XULRadioButtonAccessible::XULRadioButtonAccessible(nsIContent* aContent,
                                                   DocAccessible* aDoc)
    : RadioButtonAccessible(aContent, aDoc) {}

uint64_t XULRadioButtonAccessible::NativeState() const {
  uint64_t state = LeafAccessible::NativeState();
  state |= states::CHECKABLE;
  if (Elm()->State().HasState(dom::ElementState::CHECKED)) {
    state |= states::CHECKED;
  }
  return state;
}

uint64_t XULRadioButtonAccessible::NativeInteractiveState() const {
  return NativelyUnavailable() ? states::UNAVAILABLE : states::FOCUSABLE;
}

////////////////////////////////////////////////////////////////////////////////
// XULRadioButtonAccessible: Widgets

LocalAccessible* XULRadioButtonAccessible::ContainerWidget() const {
  return mParent;
}

////////////////////////////////////////////////////////////////////////////////
// XULRadioGroupAccessible
////////////////////////////////////////////////////////////////////////////////

/**
 * XUL Radio Group
 *   The Radio Group proxies for the Radio Buttons themselves. The Group gets
 *   focus whereas the Buttons do not. So we only have an accessible object for
 *   this for the purpose of getting the proper RadioButton. Need this here to
 *   avoid circular reference problems when navigating the accessible tree and
 *   for getting to the radiobuttons.
 */

XULRadioGroupAccessible::XULRadioGroupAccessible(nsIContent* aContent,
                                                 DocAccessible* aDoc)
    : XULSelectControlAccessible(aContent, aDoc) {}

role XULRadioGroupAccessible::NativeRole() const { return roles::RADIO_GROUP; }

uint64_t XULRadioGroupAccessible::NativeInteractiveState() const {
  // The radio group is not focusable. Sometimes the focus controller will
  // report that it is focused. That means that the actual selected radio button
  // should be considered focused.
  return NativelyUnavailable() ? states::UNAVAILABLE : 0;
}

////////////////////////////////////////////////////////////////////////////////
// XULRadioGroupAccessible: Widgets

bool XULRadioGroupAccessible::IsWidget() const { return true; }

bool XULRadioGroupAccessible::IsActiveWidget() const {
  return FocusMgr()->HasDOMFocus(mContent);
}

bool XULRadioGroupAccessible::AreItemsOperable() const { return true; }

LocalAccessible* XULRadioGroupAccessible::CurrentItem() const {
  if (!mSelectControl) {
    return nullptr;
  }

  RefPtr<dom::Element> currentItemElm;
  nsCOMPtr<nsIDOMXULRadioGroupElement> group =
      mSelectControl->AsXULRadioGroup();
  if (group) {
    group->GetFocusedItem(getter_AddRefs(currentItemElm));
  }

  if (currentItemElm) {
    DocAccessible* document = Document();
    if (document) {
      return document->GetAccessible(currentItemElm);
    }
  }

  return nullptr;
}

void XULRadioGroupAccessible::SetCurrentItem(const LocalAccessible* aItem) {
  if (!mSelectControl) {
    return;
  }

  nsCOMPtr<dom::Element> itemElm = aItem->Elm();
  nsCOMPtr<nsIDOMXULRadioGroupElement> group =
      mSelectControl->AsXULRadioGroup();
  if (group) {
    group->SetFocusedItem(itemElm);
  }
}

////////////////////////////////////////////////////////////////////////////////
// XULStatusBarAccessible
////////////////////////////////////////////////////////////////////////////////

XULStatusBarAccessible::XULStatusBarAccessible(nsIContent* aContent,
                                               DocAccessible* aDoc)
    : AccessibleWrap(aContent, aDoc) {}

role XULStatusBarAccessible::NativeRole() const { return roles::STATUSBAR; }

////////////////////////////////////////////////////////////////////////////////
// XULToolbarButtonAccessible
////////////////////////////////////////////////////////////////////////////////

XULToolbarButtonAccessible::XULToolbarButtonAccessible(nsIContent* aContent,
                                                       DocAccessible* aDoc)
    : XULButtonAccessible(aContent, aDoc) {}

void XULToolbarButtonAccessible::GetPositionAndSetSize(int32_t* aPosInSet,
                                                       int32_t* aSetSize) {
  int32_t setSize = 0;
  int32_t posInSet = 0;

  LocalAccessible* parent = LocalParent();
  if (!parent) return;

  uint32_t childCount = parent->ChildCount();
  for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
    LocalAccessible* child = parent->LocalChildAt(childIdx);
    if (IsSeparator(child)) {  // end of a group of buttons
      if (posInSet) break;     // we've found our group, so we're done

      setSize = 0;  // not our group, so start a new group

    } else {
      setSize++;  // another button in the group

      if (child == this) posInSet = setSize;  // we've found our button
    }
  }

  *aPosInSet = posInSet;
  *aSetSize = setSize;
}

bool XULToolbarButtonAccessible::IsSeparator(LocalAccessible* aAccessible) {
  nsIContent* content = aAccessible->GetContent();
  return content && content->IsAnyOfXULElements(nsGkAtoms::toolbarseparator,
                                                nsGkAtoms::toolbarspacer,
                                                nsGkAtoms::toolbarspring);
}

////////////////////////////////////////////////////////////////////////////////
// XULToolbarButtonAccessible: Widgets

bool XULToolbarButtonAccessible::IsAcceptableChild(nsIContent* aEl) const {
  return XULButtonAccessible::IsAcceptableChild(aEl) ||
         // In addition to the children allowed by buttons, toolbarbuttons can
         // have labels as children, but only if the label attribute is not
         // present.
         (aEl->IsXULElement(nsGkAtoms::label) &&
          !mContent->AsElement()->HasAttr(nsGkAtoms::label));
}

////////////////////////////////////////////////////////////////////////////////
// XULToolbarAccessible
////////////////////////////////////////////////////////////////////////////////

XULToolbarAccessible::XULToolbarAccessible(nsIContent* aContent,
                                           DocAccessible* aDoc)
    : AccessibleWrap(aContent, aDoc) {}

role XULToolbarAccessible::NativeRole() const { return roles::TOOLBAR; }

ENameValueFlag XULToolbarAccessible::NativeName(nsString& aName) const {
  if (mContent->AsElement()->GetAttr(nsGkAtoms::toolbarname, aName)) {
    aName.CompressWhitespace();
  }

  return eNameOK;
}

////////////////////////////////////////////////////////////////////////////////
// XULToolbarAccessible
////////////////////////////////////////////////////////////////////////////////

XULToolbarSeparatorAccessible::XULToolbarSeparatorAccessible(
    nsIContent* aContent, DocAccessible* aDoc)
    : LeafAccessible(aContent, aDoc) {}

role XULToolbarSeparatorAccessible::NativeRole() const {
  return roles::SEPARATOR;
}

uint64_t XULToolbarSeparatorAccessible::NativeState() const { return 0; }
