/* 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/. */

/* global PrefCache, Roles, Prefilters, States, Filters, Utils,
   TraversalRules, Components, XPCOMUtils */
/* exported TraversalRules, TraversalHelper */

'use strict';

const Ci = Components.interfaces;
const Cu = Components.utils;

this.EXPORTED_SYMBOLS = ['TraversalRules', 'TraversalHelper']; // jshint ignore:line

Cu.import('resource://gre/modules/accessibility/Utils.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Roles',  // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Filters',  // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'States',  // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Prefilters',  // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');

var gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');

function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter, aContainerRule) {
  this._explicitMatchRoles = new Set(aRoles);
  this._matchRoles = aRoles;
  if (aRoles.length) {
    if (aRoles.indexOf(Roles.LABEL) < 0) {
      this._matchRoles.push(Roles.LABEL);
    }
    if (aRoles.indexOf(Roles.INTERNAL_FRAME) < 0) {
      // Used for traversing in to child OOP frames.
      this._matchRoles.push(Roles.INTERNAL_FRAME);
    }
  }
  this._matchFunc = aMatchFunc || function() { return Filters.MATCH; };
  this.preFilter = aPreFilter || gSimplePreFilter;
  this.containerRule = aContainerRule;
}

BaseTraversalRule.prototype = {
    getMatchRoles: function BaseTraversalRule_getmatchRoles(aRoles) {
      aRoles.value = this._matchRoles;
      return aRoles.value.length;
    },

    match: function BaseTraversalRule_match(aAccessible)
    {
      let role = aAccessible.role;
      if (role == Roles.INTERNAL_FRAME) {
        return (Utils.getMessageManager(aAccessible.DOMNode)) ?
          Filters.MATCH  | Filters.IGNORE_SUBTREE : Filters.IGNORE;
      }

      let matchResult =
        (this._explicitMatchRoles.has(role) || !this._explicitMatchRoles.size) ?
        this._matchFunc(aAccessible) : Filters.IGNORE;

      // If we are on a label that nests a checkbox/radio we should land on it.
      // It is a bigger touch target, and it reduces clutter.
      if (role == Roles.LABEL && !(matchResult & Filters.IGNORE_SUBTREE)) {
        let control = Utils.getEmbeddedControl(aAccessible);
        if (control && this._explicitMatchRoles.has(control.role)) {
          matchResult = this._matchFunc(control) | Filters.IGNORE_SUBTREE;
        }
      }

      return matchResult;
    },

    QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule])
};

var gSimpleTraversalRoles =
  [Roles.MENUITEM,
   Roles.LINK,
   Roles.PAGETAB,
   Roles.GRAPHIC,
   Roles.STATICTEXT,
   Roles.TEXT_LEAF,
   Roles.PUSHBUTTON,
   Roles.CHECKBUTTON,
   Roles.RADIOBUTTON,
   Roles.COMBOBOX,
   Roles.PROGRESSBAR,
   Roles.BUTTONDROPDOWN,
   Roles.BUTTONMENU,
   Roles.CHECK_MENU_ITEM,
   Roles.PASSWORD_TEXT,
   Roles.RADIO_MENU_ITEM,
   Roles.TOGGLE_BUTTON,
   Roles.ENTRY,
   Roles.KEY,
   Roles.HEADER,
   Roles.HEADING,
   Roles.SLIDER,
   Roles.SPINBUTTON,
   Roles.OPTION,
   Roles.LISTITEM,
   Roles.GRID_CELL,
   Roles.COLUMNHEADER,
   Roles.ROWHEADER,
   Roles.STATUSBAR,
   Roles.SWITCH,
   Roles.MATHML_MATH];

var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) {
  // An object is simple, if it either has a single child lineage,
  // or has a flat subtree.
  function isSingleLineage(acc) {
    for (let child = acc; child; child = child.firstChild) {
      if (Utils.visibleChildCount(child) > 1) {
        return false;
      }
    }
    return true;
  }

  function isFlatSubtree(acc) {
    for (let child = acc.firstChild; child; child = child.nextSibling) {
      // text leafs inherit the actionCount of any ancestor that has a click
      // listener.
      if ([Roles.TEXT_LEAF, Roles.STATICTEXT].indexOf(child.role) >= 0) {
        continue;
      }
      if (Utils.visibleChildCount(child) > 0 || child.actionCount > 0) {
        return false;
      }
    }
    return true;
  }

  switch (aAccessible.role) {
  case Roles.COMBOBOX:
    // We don't want to ignore the subtree because this is often
    // where the list box hangs out.
    return Filters.MATCH;
  case Roles.TEXT_LEAF:
    {
      // Nameless text leaves are boring, skip them.
      let name = aAccessible.name;
      return (name && name.trim()) ? Filters.MATCH : Filters.IGNORE;
    }
  case Roles.STATICTEXT:
    // Ignore prefix static text in list items. They are typically bullets or numbers.
    return Utils.isListItemDecorator(aAccessible) ?
      Filters.IGNORE : Filters.MATCH;
  case Roles.GRAPHIC:
    return TraversalRules._shouldSkipImage(aAccessible);
  case Roles.HEADER:
  case Roles.HEADING:
  case Roles.COLUMNHEADER:
  case Roles.ROWHEADER:
  case Roles.STATUSBAR:
    if ((aAccessible.childCount > 0 || aAccessible.name) &&
        (isSingleLineage(aAccessible) || isFlatSubtree(aAccessible))) {
      return Filters.MATCH | Filters.IGNORE_SUBTREE;
    }
    return Filters.IGNORE;
  case Roles.GRID_CELL:
    return isSingleLineage(aAccessible) || isFlatSubtree(aAccessible) ?
      Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
  case Roles.LISTITEM:
    {
      let item = aAccessible.childCount === 2 &&
        aAccessible.firstChild.role === Roles.STATICTEXT ?
        aAccessible.lastChild : aAccessible;
        return isSingleLineage(item) || isFlatSubtree(item) ?
          Filters.MATCH | Filters.IGNORE_SUBTREE : Filters.IGNORE;
    }
  default:
    // Ignore the subtree, if there is one. So that we don't land on
    // the same content that was already presented by its parent.
    return Filters.MATCH |
      Filters.IGNORE_SUBTREE;
  }
};

var gSimplePreFilter = Prefilters.DEFUNCT |
  Prefilters.INVISIBLE |
  Prefilters.ARIA_HIDDEN |
  Prefilters.TRANSPARENT;

this.TraversalRules = { // jshint ignore:line
  Simple: new BaseTraversalRule(gSimpleTraversalRoles, gSimpleMatchFunc),

  SimpleOnScreen: new BaseTraversalRule(
    gSimpleTraversalRoles, gSimpleMatchFunc,
    Prefilters.DEFUNCT | Prefilters.INVISIBLE | Prefilters.ARIA_HIDDEN |
    Prefilters.TRANSPARENT | Prefilters.OFFSCREEN),

  Anchor: new BaseTraversalRule(
    [Roles.LINK],
    function Anchor_match(aAccessible)
    {
      // We want to ignore links, only focus named anchors.
      if (Utils.getState(aAccessible).contains(States.LINKED)) {
        return Filters.IGNORE;
      } else {
        return Filters.MATCH;
      }
    }),

  Button: new BaseTraversalRule(
    [Roles.PUSHBUTTON,
     Roles.SPINBUTTON,
     Roles.TOGGLE_BUTTON,
     Roles.BUTTONDROPDOWN,
     Roles.BUTTONDROPDOWNGRID]),

  Combobox: new BaseTraversalRule(
    [Roles.COMBOBOX,
     Roles.LISTBOX]),

  Landmark: new BaseTraversalRule(
    [],
    function Landmark_match(aAccessible) {
      return Utils.getLandmarkName(aAccessible) ? Filters.MATCH :
        Filters.IGNORE;
    }, null, true),

  /* A rule for Android's section navigation, lands on landmarks, regions, and
     on headings to aid navigation of traditionally structured documents */
  Section: new BaseTraversalRule(
    [],
    function Section_match(aAccessible) {
      if (aAccessible.role === Roles.HEADING) {
        return Filters.MATCH;
      }

      let matchedRole = Utils.matchRoles(aAccessible, [
        'banner',
        'complementary',
        'contentinfo',
        'main',
        'navigation',
        'search',
        'region'
        ]);

      return matchedRole ? Filters.MATCH : Filters.IGNORE;
    }, null, true),

  Entry: new BaseTraversalRule(
    [Roles.ENTRY,
     Roles.PASSWORD_TEXT]),

  FormElement: new BaseTraversalRule(
    [Roles.PUSHBUTTON,
     Roles.SPINBUTTON,
     Roles.TOGGLE_BUTTON,
     Roles.BUTTONDROPDOWN,
     Roles.BUTTONDROPDOWNGRID,
     Roles.COMBOBOX,
     Roles.LISTBOX,
     Roles.ENTRY,
     Roles.PASSWORD_TEXT,
     Roles.PAGETAB,
     Roles.RADIOBUTTON,
     Roles.RADIO_MENU_ITEM,
     Roles.SLIDER,
     Roles.CHECKBUTTON,
     Roles.CHECK_MENU_ITEM,
     Roles.SWITCH]),

  Graphic: new BaseTraversalRule(
    [Roles.GRAPHIC],
    function Graphic_match(aAccessible) {
      return TraversalRules._shouldSkipImage(aAccessible);
    }),

  Heading: new BaseTraversalRule(
    [Roles.HEADING],
    function Heading_match(aAccessible) {
      return aAccessible.childCount > 0 ? Filters.MATCH : Filters.IGNORE;
    }),

  ListItem: new BaseTraversalRule(
    [Roles.LISTITEM,
     Roles.TERM]),

  Link: new BaseTraversalRule(
    [Roles.LINK],
    function Link_match(aAccessible)
    {
      // We want to ignore anchors, only focus real links.
      if (Utils.getState(aAccessible).contains(States.LINKED)) {
        return Filters.MATCH;
      } else {
        return Filters.IGNORE;
      }
    }),

  /* For TalkBack's "Control" granularity. Form conrols and links */
  Control: new BaseTraversalRule(
    [Roles.PUSHBUTTON,
     Roles.SPINBUTTON,
     Roles.TOGGLE_BUTTON,
     Roles.BUTTONDROPDOWN,
     Roles.BUTTONDROPDOWNGRID,
     Roles.COMBOBOX,
     Roles.LISTBOX,
     Roles.ENTRY,
     Roles.PASSWORD_TEXT,
     Roles.PAGETAB,
     Roles.RADIOBUTTON,
     Roles.RADIO_MENU_ITEM,
     Roles.SLIDER,
     Roles.CHECKBUTTON,
     Roles.CHECK_MENU_ITEM,
     Roles.SWITCH,
     Roles.LINK,
     Roles.MENUITEM],
    function Control_match(aAccessible)
    {
      // We want to ignore anchors, only focus real links.
      if (aAccessible.role == Roles.LINK &&
          !Utils.getState(aAccessible).contains(States.LINKED)) {
        return Filters.IGNORE;
      }
      return Filters.MATCH;
    }),

  List: new BaseTraversalRule(
    [Roles.LIST,
     Roles.DEFINITION_LIST],
    null, null, true),

  PageTab: new BaseTraversalRule(
    [Roles.PAGETAB]),

  Paragraph: new BaseTraversalRule(
    [Roles.PARAGRAPH,
     Roles.SECTION],
    function Paragraph_match(aAccessible) {
      for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
        if (child.role === Roles.TEXT_LEAF) {
          return Filters.MATCH | Filters.IGNORE_SUBTREE;
        }
      }

      return Filters.IGNORE;
    }),

  RadioButton: new BaseTraversalRule(
    [Roles.RADIOBUTTON,
     Roles.RADIO_MENU_ITEM]),

  Separator: new BaseTraversalRule(
    [Roles.SEPARATOR]),

  Table: new BaseTraversalRule(
    [Roles.TABLE]),

  Checkbox: new BaseTraversalRule(
    [Roles.CHECKBUTTON,
     Roles.CHECK_MENU_ITEM,
     Roles.SWITCH /* A type of checkbox that represents on/off values */]),

  _shouldSkipImage: function _shouldSkipImage(aAccessible) {
    if (gSkipEmptyImages.value && aAccessible.name === '') {
      return Filters.IGNORE;
    }
    return Filters.MATCH;
  }
};

this.TraversalHelper = {
  _helperPivotCache: null,

  get helperPivotCache() {
    delete this.helperPivotCache;
    this.helperPivotCache = new WeakMap();
    return this.helperPivotCache;
  },

  getHelperPivot: function TraversalHelper_getHelperPivot(aRoot) {
    let pivot = this.helperPivotCache.get(aRoot.DOMNode);
    if (!pivot) {
      pivot = Utils.AccService.createAccessiblePivot(aRoot);
      this.helperPivotCache.set(aRoot.DOMNode, pivot);
    }

    return pivot;
  },

  move: function TraversalHelper_move(aVirtualCursor, aMethod, aRule) {
    let rule = TraversalRules[aRule];

    if (rule.containerRule) {
      let moved = false;
      let helperPivot = this.getHelperPivot(aVirtualCursor.root);
      helperPivot.position = aVirtualCursor.position;

      // We continue to step through containers until there is one with an
      // atomic child (via 'Simple') on which we could land.
      while (!moved) {
        if (helperPivot[aMethod](rule)) {
          aVirtualCursor.modalRoot = helperPivot.position;
          moved = aVirtualCursor.moveFirst(TraversalRules.Simple);
          aVirtualCursor.modalRoot = null;
        } else {
          // If we failed to step to another container, break and return false.
          break;
        }
      }

      return moved;
    } else {
      return aVirtualCursor[aMethod](rule);
    }
  }

};
