/* 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 Components, XPCOMUtils, Services, PluralForm, Logger, Rect, Utils,
          States, Relations, Roles, dump, Events, PivotContext, PrefCache */
/* exported Utils, Logger, PivotContext, PrefCache, SettingCache */

'use strict';

const {classes: Cc, utils: Cu, interfaces: Ci} = Components;

Cu.import('resource://gre/modules/XPCOMUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
  'resource://gre/modules/Services.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Rect', // jshint ignore:line
  'resource://gre/modules/Geometry.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Roles', // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Events', // jshint ignore:line
  'resource://gre/modules/accessibility/Constants.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'Relations', // 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, 'PluralForm', // jshint ignore:line
  'resource://gre/modules/PluralForm.jsm');

this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache',  // jshint ignore:line
                         'SettingCache'];

this.Utils = { // jshint ignore:line
  _buildAppMap: {
    '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g',
    '{d1bfe7d9-c01e-4237-998b-7b5f960a4314}': 'graphene',
    '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser',
    '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android',
    '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul'
  },

  init: function Utils_init(aWindow) {
    if (this._win) {
      // XXX: only supports attaching to one window now.
      throw new Error('Only one top-level window could used with AccessFu');
    }
    this._win = Cu.getWeakReference(aWindow);
  },

  uninit: function Utils_uninit() {
    if (!this._win) {
      return;
    }
    delete this._win;
  },

  get win() {
    if (!this._win) {
      return null;
    }
    return this._win.get();
  },

  get winUtils() {
    let win = this.win;
    if (!win) {
      return null;
    }
    return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
      Ci.nsIDOMWindowUtils);
  },

  get AccRetrieval() {
    if (!this._AccRetrieval) {
      this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1'].
        getService(Ci.nsIAccessibleRetrieval);
    }

    return this._AccRetrieval;
  },

  set MozBuildApp(value) {
    this._buildApp = value;
  },

  get MozBuildApp() {
    if (!this._buildApp) {
      this._buildApp = this._buildAppMap[Services.appinfo.ID];
    }
    return this._buildApp;
  },

  get OS() {
    if (!this._OS) {
      this._OS = Services.appinfo.OS;
    }
    return this._OS;
  },

  get widgetToolkit() {
    if (!this._widgetToolkit) {
      this._widgetToolkit = Services.appinfo.widgetToolkit;
    }
    return this._widgetToolkit;
  },

  get ScriptName() {
    if (!this._ScriptName) {
      this._ScriptName =
        (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu';
    }
    return this._ScriptName;
  },

  get AndroidSdkVersion() {
    if (!this._AndroidSdkVersion) {
      if (Services.appinfo.OS == 'Android') {
        this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32(
          'version');
      } else {
        // Most useful in desktop debugging.
        this._AndroidSdkVersion = 16;
      }
    }
    return this._AndroidSdkVersion;
  },

  set AndroidSdkVersion(value) {
    // When we want to mimic another version.
    this._AndroidSdkVersion = value;
  },

  get BrowserApp() {
    if (!this.win) {
      return null;
    }
    switch (this.MozBuildApp) {
      case 'mobile/android':
        return this.win.BrowserApp;
      case 'browser':
        return this.win.gBrowser;
      case 'b2g':
        return this.win.shell;
      default:
        return null;
    }
  },

  get CurrentBrowser() {
    if (!this.BrowserApp) {
      return null;
    }
    if (this.MozBuildApp == 'b2g') {
      return this.BrowserApp.contentBrowser;
    }
    return this.BrowserApp.selectedBrowser;
  },

  get CurrentContentDoc() {
    let browser = this.CurrentBrowser;
    return browser ? browser.contentDocument : null;
  },

  get AllMessageManagers() {
    let messageManagers = new Set();

    function collectLeafMessageManagers(mm) {
      for (let i = 0; i < mm.childCount; i++) {
        let childMM = mm.getChildAt(i);

        if ('sendAsyncMessage' in childMM) {
          messageManagers.add(childMM);
        } else {
          collectLeafMessageManagers(childMM);
        }
      }
    }

    collectLeafMessageManagers(this.win.messageManager);

    let document = this.CurrentContentDoc;

    if (document) {
      if (document.location.host === 'b2g') {
        // The document is a b2g app chrome (ie. Mulet).
        let contentBrowser = this.win.content.shell.contentBrowser;
        messageManagers.add(this.getMessageManager(contentBrowser));
        document = contentBrowser.contentDocument;
      }

      let remoteframes = document.querySelectorAll('iframe');

      for (let i = 0; i < remoteframes.length; ++i) {
        let mm = this.getMessageManager(remoteframes[i]);
        if (mm) {
          messageManagers.add(mm);
        }
      }

    }

    return messageManagers;
  },

  get isContentProcess() {
    delete this.isContentProcess;
    this.isContentProcess =
      Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
    return this.isContentProcess;
  },

  localize: function localize(aOutput) {
    let outputArray = Array.isArray(aOutput) ? aOutput : [aOutput];
    let localized =
      outputArray.map(details => this.stringBundle.get(details));
    // Clean up the white space.
    return localized.filter(word => word).map(word => word.trim()).
      filter(trimmed => trimmed);
  },

  get stringBundle() {
    delete this.stringBundle;
    let bundle = Services.strings.createBundle(
      'chrome://global/locale/AccessFu.properties');
    this.stringBundle = {
      get: function stringBundle_get(aDetails = {}) {
        if (!aDetails || typeof aDetails === 'string') {
          return aDetails;
        }
        let str = '';
        let string = aDetails.string;
        if (!string) {
          return str;
        }
        try {
          let args = aDetails.args;
          let count = aDetails.count;
          if (args) {
            str = bundle.formatStringFromName(string, args, args.length);
          } else {
            str = bundle.GetStringFromName(string);
          }
          if (count) {
            str = PluralForm.get(count, str);
            str = str.replace('#1', count);
          }
        } catch (e) {
          Logger.debug('Failed to get a string from a bundle for', string);
        } finally {
          return str;
        }
      }
    };
    return this.stringBundle;
  },

  getMessageManager: function getMessageManager(aBrowser) {
    try {
      return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner).
         frameLoader.messageManager;
    } catch (x) {
      return null;
    }
  },

  getState: function getState(aAccessibleOrEvent) {
    if (aAccessibleOrEvent instanceof Ci.nsIAccessibleStateChangeEvent) {
      return new State(
        aAccessibleOrEvent.isExtraState ? 0 : aAccessibleOrEvent.state,
        aAccessibleOrEvent.isExtraState ? aAccessibleOrEvent.state : 0);
    } else {
      let state = {};
      let extState = {};
      aAccessibleOrEvent.getState(state, extState);
      return new State(state.value, extState.value);
    }
  },

  getAttributes: function getAttributes(aAccessible) {
    let attributes = {};

    if (aAccessible && aAccessible.attributes) {
      let attributesEnum = aAccessible.attributes.enumerate();

      // Populate |attributes| object with |aAccessible|'s attribute key-value
      // pairs.
      while (attributesEnum.hasMoreElements()) {
        let attribute = attributesEnum.getNext().QueryInterface(
          Ci.nsIPropertyElement);
        attributes[attribute.key] = attribute.value;
      }
    }

    return attributes;
  },

  getVirtualCursor: function getVirtualCursor(aDocument) {
    let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument :
      this.AccRetrieval.getAccessibleFor(aDocument);

    return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
  },

  getContentResolution: function _getContentResolution(aAccessible) {
    let res = { value: 1 };
    aAccessible.document.window.QueryInterface(
      Ci.nsIInterfaceRequestor).getInterface(
      Ci.nsIDOMWindowUtils).getResolution(res);
    return res.value;
  },

  getBounds: function getBounds(aAccessible, aPreserveContentScale) {
    let objX = {}, objY = {}, objW = {}, objH = {};
    aAccessible.getBounds(objX, objY, objW, objH);

    let scale = aPreserveContentScale ? 1 :
      this.getContentResolution(aAccessible);

    return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
      scale, scale);
  },

  getTextBounds: function getTextBounds(aAccessible, aStart, aEnd,
                                        aPreserveContentScale) {
    let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText);
    let objX = {}, objY = {}, objW = {}, objH = {};
    accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH,
      Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE);

    let scale = aPreserveContentScale ? 1 :
      this.getContentResolution(aAccessible);

    return new Rect(objX.value, objY.value, objW.value, objH.value).scale(
      scale, scale);
  },

  /**
   * Get current display DPI.
   */
  get dpi() {
    delete this.dpi;
    this.dpi = this.winUtils.displayDPI;
    return this.dpi;
  },

  isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) {
    let acc = aAccessible;

    // If aSubTreeRoot is an accessible document, we will only walk up the
    // ancestry of documents and skip everything else.
    if (aSubTreeRoot instanceof Ci.nsIAccessibleDocument) {
      while (acc) {
        let parentDoc = acc instanceof Ci.nsIAccessibleDocument ?
          acc.parentDocument : acc.document;
        if (parentDoc === aSubTreeRoot) {
          return true;
        }
        acc = parentDoc;
      }
      return false;
    }

    while (acc) {
      if (acc == aSubTreeRoot) {
        return true;
      }

      try {
        acc = acc.parent;
      } catch (x) {
        Logger.debug('Failed to get parent:', x);
        acc = null;
      }
    }

    return false;
  },

  isHidden: function isHidden(aAccessible) {
    // Need to account for aria-hidden, so can't just check for INVISIBLE
    // state.
    let hidden = Utils.getAttributes(aAccessible).hidden;
    return hidden && hidden === 'true';
  },

  visibleChildCount: function visibleChildCount(aAccessible) {
    let count = 0;
    for (let child = aAccessible.firstChild; child; child = child.nextSibling) {
      if (!this.isHidden(child)) {
        ++count;
      }
    }
    return count;
  },

  inHiddenSubtree: function inHiddenSubtree(aAccessible) {
    for (let acc=aAccessible; acc; acc=acc.parent) {
      if (this.isHidden(acc)) {
        return true;
      }
    }
    return false;
  },

  isAliveAndVisible: function isAliveAndVisible(aAccessible, aIsOnScreen) {
    if (!aAccessible) {
      return false;
    }

    try {
      let state = this.getState(aAccessible);
      if (state.contains(States.DEFUNCT) || state.contains(States.INVISIBLE) ||
          (aIsOnScreen && state.contains(States.OFFSCREEN)) ||
          Utils.inHiddenSubtree(aAccessible)) {
        return false;
      }
    } catch (x) {
      return false;
    }

    return true;
  },

  matchAttributeValue: function matchAttributeValue(aAttributeValue, values) {
    let attrSet = new Set(aAttributeValue.split(' '));
    for (let value of values) {
      if (attrSet.has(value)) {
        return value;
      }
    }
  },

  getLandmarkName: function getLandmarkName(aAccessible) {
    return this.matchRoles(aAccessible, [
      'banner',
      'complementary',
      'contentinfo',
      'main',
      'navigation',
      'search'
    ]);
  },

  getMathRole: function getMathRole(aAccessible) {
    return this.matchRoles(aAccessible, [
      'base',
      'close-fence',
      'denominator',
      'numerator',
      'open-fence',
      'overscript',
      'presubscript',
      'presuperscript',
      'root-index',
      'subscript',
      'superscript',
      'underscript'
    ]);
  },

  matchRoles: function matchRoles(aAccessible, aRoles) {
    let roles = this.getAttributes(aAccessible)['xml-roles'];
    if (!roles) {
      return;
    }

    // Looking up a role that would match any in the provided roles.
    return this.matchAttributeValue(roles, aRoles);
  },

  getEmbeddedControl: function getEmbeddedControl(aLabel) {
    if (aLabel) {
      let relation = aLabel.getRelationByType(Relations.LABEL_FOR);
      for (let i = 0; i < relation.targetsCount; i++) {
        let target = relation.getTarget(i);
        if (target.parent === aLabel) {
          return target;
        }
      }
    }

    return null;
  },

  isListItemDecorator: function isListItemDecorator(aStaticText,
                                                    aExcludeOrdered) {
    let parent = aStaticText.parent;
    if (aExcludeOrdered && parent.parent.DOMNode.nodeName === 'OL') {
      return false;
    }

    return parent.role === Roles.LISTITEM && parent.childCount > 1 &&
      aStaticText.indexInParent === 0;
  },

  dispatchChromeEvent: function dispatchChromeEvent(aType, aDetails) {
    let details = {
      type: aType,
      details: JSON.stringify(
        typeof aDetails === 'string' ? { eventType : aDetails } : aDetails)
    };
    let window = this.win;
    let shell = window.shell || window.content.shell;
    if (shell) {
      // On B2G device.
      shell.sendChromeEvent(details);
    } else {
      // Dispatch custom event to have support for desktop and screen reader
      // emulator add-on.
      window.dispatchEvent(new window.CustomEvent(aType, {
        bubbles: true,
        cancelable: true,
        detail: details
      }));
    }

  },

  isActivatableOnFingerUp: function isActivatableOnFingerUp(aAccessible) {
    if (aAccessible.role === Roles.KEY) {
      return true;
    }
    let quick_activate = this.getAttributes(aAccessible)['moz-quick-activate'];
    return quick_activate && JSON.parse(quick_activate);
  }
};

/**
 * State object used internally to process accessible's states.
 * @param {Number} aBase     Base state.
 * @param {Number} aExtended Extended state.
 */
function State(aBase, aExtended) {
  this.base = aBase;
  this.extended = aExtended;
}

State.prototype = {
  contains: function State_contains(other) {
    return !!(this.base & other.base || this.extended & other.extended);
  },
  toString: function State_toString() {
    let stateStrings = Utils.AccRetrieval.
      getStringStates(this.base, this.extended);
    let statesArray = new Array(stateStrings.length);
    for (let i = 0; i < statesArray.length; i++) {
      statesArray[i] = stateStrings.item(i);
    }
    return '[' + statesArray.join(', ') + ']';
  }
};

this.Logger = { // jshint ignore:line
  GESTURE: -1,
  DEBUG: 0,
  INFO: 1,
  WARNING: 2,
  ERROR: 3,
  _LEVEL_NAMES: ['GESTURE', 'DEBUG', 'INFO', 'WARNING', 'ERROR'],

  logLevel: 1, // INFO;

  test: false,

  log: function log(aLogLevel) {
    if (aLogLevel < this.logLevel) {
      return;
    }

    let args = Array.prototype.slice.call(arguments, 1);
    let message = (typeof(args[0]) === 'function' ? args[0]() : args).join(' ');
    message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel + 1] +
      ' ' + message + '\n';
    dump(message);
    // Note: used for testing purposes. If |this.test| is true, also log to
    // the console service.
    if (this.test) {
      try {
        Services.console.logStringMessage(message);
      } catch (ex) {
        // There was an exception logging to the console service.
      }
    }
  },

  info: function info() {
    this.log.apply(
      this, [this.INFO].concat(Array.prototype.slice.call(arguments)));
  },

  gesture: function gesture() {
    this.log.apply(
      this, [this.GESTURE].concat(Array.prototype.slice.call(arguments)));
  },

  debug: function debug() {
    this.log.apply(
      this, [this.DEBUG].concat(Array.prototype.slice.call(arguments)));
  },

  warning: function warning() {
    this.log.apply(
      this, [this.WARNING].concat(Array.prototype.slice.call(arguments)));
  },

  error: function error() {
    this.log.apply(
      this, [this.ERROR].concat(Array.prototype.slice.call(arguments)));
  },

  logException: function logException(
    aException, aErrorMessage = 'An exception has occured') {
    try {
      let stackMessage = '';
      if (aException.stack) {
        stackMessage = '  ' + aException.stack.replace(/\n/g, '\n  ');
      } else if (aException.location) {
        let frame = aException.location;
        let stackLines = [];
        while (frame && frame.lineNumber) {
          stackLines.push(
            '  ' + frame.name + '@' + frame.filename + ':' + frame.lineNumber);
          frame = frame.caller;
        }
        stackMessage = stackLines.join('\n');
      } else {
        stackMessage =
          '(' + aException.fileName + ':' + aException.lineNumber + ')';
      }
      this.error(aErrorMessage + ':\n ' +
                 aException.message + '\n' +
                 stackMessage);
    } catch (x) {
      this.error(x);
    }
  },

  accessibleToString: function accessibleToString(aAccessible) {
    if (!aAccessible) {
      return '[ null ]';
    }

    try {
      return'[ ' + Utils.AccRetrieval.getStringRole(aAccessible.role) +
        ' | ' + aAccessible.name + ' ]';
    } catch (x) {
      return '[ defunct ]';
    }
  },

  eventToString: function eventToString(aEvent) {
    let str = Utils.AccRetrieval.getStringEventType(aEvent.eventType);
    if (aEvent.eventType == Events.STATE_CHANGE) {
      let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
      let stateStrings = event.isExtraState ?
        Utils.AccRetrieval.getStringStates(0, event.state) :
        Utils.AccRetrieval.getStringStates(event.state, 0);
      str += ' (' + stateStrings.item(0) + ')';
    }

    if (aEvent.eventType == Events.VIRTUALCURSOR_CHANGED) {
      let event = aEvent.QueryInterface(
        Ci.nsIAccessibleVirtualCursorChangeEvent);
      let pivot = aEvent.accessible.QueryInterface(
        Ci.nsIAccessibleDocument).virtualCursor;
      str += ' (' + this.accessibleToString(event.oldAccessible) + ' -> ' +
	this.accessibleToString(pivot.position) + ')';
    }

    return str;
  },

  statesToString: function statesToString(aAccessible) {
    return Utils.getState(aAccessible).toString();
  },

  dumpTree: function dumpTree(aLogLevel, aRootAccessible) {
    if (aLogLevel < this.logLevel) {
      return;
    }

    this._dumpTreeInternal(aLogLevel, aRootAccessible, 0);
  },

  _dumpTreeInternal:
    function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) {
      let indentStr = '';
      for (let i = 0; i < aIndent; i++) {
        indentStr += ' ';
      }
      this.log(aLogLevel, indentStr,
               this.accessibleToString(aAccessible),
               '(' + this.statesToString(aAccessible) + ')');
      for (let i = 0; i < aAccessible.childCount; i++) {
        this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i),
          aIndent + 1);
      }
    }
};

/**
 * PivotContext: An object that generates and caches context information
 * for a given accessible and its relationship with another accessible.
 *
 * If the given accessible is a label for a nested control, then this
 * context will represent the nested control instead of the label.
 * With the exception of bounds calculation, which will use the containing
 * label. In this case the |accessible| field would be the embedded control,
 * and the |accessibleForBounds| field would be the label.
 */
this.PivotContext = function PivotContext(aAccessible, aOldAccessible, // jshint ignore:line
  aStartOffset, aEndOffset, aIgnoreAncestry = false,
  aIncludeInvisible = false) {
  this._accessible = aAccessible;
  this._nestedControl = Utils.getEmbeddedControl(aAccessible);
  this._oldAccessible =
    this._isDefunct(aOldAccessible) ? null : aOldAccessible;
  this.startOffset = aStartOffset;
  this.endOffset = aEndOffset;
  this._ignoreAncestry = aIgnoreAncestry;
  this._includeInvisible = aIncludeInvisible;
};

PivotContext.prototype = {
  get accessible() {
    // If the current pivot accessible has a nested control,
    // make this context use it publicly.
    return this._nestedControl || this._accessible;
  },

  get oldAccessible() {
    return this._oldAccessible;
  },

  get isNestedControl() {
    return !!this._nestedControl;
  },

  get accessibleForBounds() {
    return this._accessible;
  },

  get textAndAdjustedOffsets() {
    if (this.startOffset === -1 && this.endOffset === -1) {
      return null;
    }

    if (!this._textAndAdjustedOffsets) {
      let result = {startOffset: this.startOffset,
                    endOffset: this.endOffset,
                    text: this._accessible.QueryInterface(Ci.nsIAccessibleText).
                          getText(0,
                            Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)};
      let hypertextAcc = this._accessible.QueryInterface(
        Ci.nsIAccessibleHyperText);

      // Iterate through the links in backwards order so text replacements don't
      // affect the offsets of links yet to be processed.
      for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) {
        let link = hypertextAcc.getLinkAt(i);
        let linkText = '';
        if (link instanceof Ci.nsIAccessibleText) {
          linkText = link.QueryInterface(Ci.nsIAccessibleText).
                          getText(0,
                            Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
        }

        let start = link.startIndex;
        let end = link.endIndex;
        for (let offset of ['startOffset', 'endOffset']) {
          if (this[offset] >= end) {
            result[offset] += linkText.length - (end - start);
          }
        }
        result.text = result.text.substring(0, start) + linkText +
                      result.text.substring(end);
      }

      this._textAndAdjustedOffsets = result;
    }

    return this._textAndAdjustedOffsets;
  },

  /**
   * Get a list of |aAccessible|'s ancestry up to the root.
   * @param  {nsIAccessible} aAccessible.
   * @return {Array} Ancestry list.
   */
  _getAncestry: function _getAncestry(aAccessible) {
    let ancestry = [];
    let parent = aAccessible;
    try {
      while (parent && (parent = parent.parent)) {
       ancestry.push(parent);
      }
    } catch (x) {
      // A defunct accessible will raise an exception geting parent.
      Logger.debug('Failed to get parent:', x);
    }
    return ancestry.reverse();
  },

  /**
   * A list of the old accessible's ancestry.
   */
  get oldAncestry() {
    if (!this._oldAncestry) {
      if (!this._oldAccessible || this._ignoreAncestry) {
        this._oldAncestry = [];
      } else {
        this._oldAncestry = this._getAncestry(this._oldAccessible);
        this._oldAncestry.push(this._oldAccessible);
      }
    }
    return this._oldAncestry;
  },

  /**
   * A list of the current accessible's ancestry.
   */
  get currentAncestry() {
    if (!this._currentAncestry) {
      this._currentAncestry = this._ignoreAncestry ? [] :
        this._getAncestry(this.accessible);
    }
    return this._currentAncestry;
  },

  /*
   * This is a list of the accessible's ancestry up to the common ancestor
   * of the accessible and the old accessible. It is useful for giving the
   * user context as to where they are in the heirarchy.
   */
  get newAncestry() {
    if (!this._newAncestry) {
      this._newAncestry = this._ignoreAncestry ? [] :
        this.currentAncestry.filter(
          (currentAncestor, i) => currentAncestor !== this.oldAncestry[i]);
    }
    return this._newAncestry;
  },

  /*
   * Traverse the accessible's subtree in pre or post order.
   * It only includes the accessible's visible chidren.
   * Note: needSubtree is a function argument that can be used to determine
   * whether aAccessible's subtree is required.
   */
  _traverse: function* _traverse(aAccessible, aPreorder, aStop) {
    if (aStop && aStop(aAccessible)) {
      return;
    }
    let child = aAccessible.firstChild;
    while (child) {
      let include;
      if (this._includeInvisible) {
        include = true;
      } else {
        include = !Utils.isHidden(child);
      }
      if (include) {
        if (aPreorder) {
          yield child;
          for (let node of this._traverse(child, aPreorder, aStop)) {
            yield node;
          }
        } else {
          for (let node of this._traverse(child, aPreorder, aStop)) {
            yield node;
          }
          yield child;
        }
      }
      child = child.nextSibling;
    }
  },

  /**
   * Get interaction hints for the context ancestry.
   * @return {Array} Array of interaction hints.
   */
  get interactionHints() {
    let hints = [];
    this.newAncestry.concat(this.accessible).reverse().forEach(aAccessible => {
      let hint = Utils.getAttributes(aAccessible)['moz-hint'];
      if (hint) {
        hints.push(hint);
      } else if (aAccessible.actionCount > 0) {
        hints.push({
          string: Utils.AccRetrieval.getStringRole(
            aAccessible.role).replace(/\s/g, '') + '-hint'
        });
      }
    });
    return hints;
  },

  /*
   * A subtree generator function, used to generate a flattened
   * list of the accessible's subtree in pre or post order.
   * It only includes the accessible's visible chidren.
   * @param {boolean} aPreorder A flag for traversal order. If true, traverse
   * in preorder; if false, traverse in postorder.
   * @param {function} aStop An optional function, indicating whether subtree
   * traversal should stop.
   */
  subtreeGenerator: function subtreeGenerator(aPreorder, aStop) {
    return this._traverse(this.accessible, aPreorder, aStop);
  },

  getCellInfo: function getCellInfo(aAccessible) {
    if (!this._cells) {
      this._cells = new WeakMap();
    }

    let domNode = aAccessible.DOMNode;
    if (this._cells.has(domNode)) {
      return this._cells.get(domNode);
    }

    let cellInfo = {};
    let getAccessibleCell = function getAccessibleCell(aAccessible) {
      if (!aAccessible) {
        return null;
      }
      if ([
            Roles.CELL,
            Roles.COLUMNHEADER,
            Roles.ROWHEADER,
            Roles.MATHML_CELL
          ].indexOf(aAccessible.role) < 0) {
          return null;
      }
      try {
        return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell);
      } catch (x) {
        Logger.logException(x);
        return null;
      }
    };
    let getHeaders = function* getHeaders(aHeaderCells) {
      let enumerator = aHeaderCells.enumerate();
      while (enumerator.hasMoreElements()) {
        yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name;
      }
    };

    cellInfo.current = getAccessibleCell(aAccessible);

    if (!cellInfo.current) {
      Logger.warning(aAccessible,
        'does not support nsIAccessibleTableCell interface.');
      this._cells.set(domNode, null);
      return null;
    }

    let table = cellInfo.current.table;
    if (table.isProbablyForLayout()) {
      this._cells.set(domNode, null);
      return null;
    }

    cellInfo.previous = null;
    let oldAncestry = this.oldAncestry.reverse();
    let ancestor = oldAncestry.shift();
    while (!cellInfo.previous && ancestor) {
      let cell = getAccessibleCell(ancestor);
      if (cell && cell.table === table) {
        cellInfo.previous = cell;
      }
      ancestor = oldAncestry.shift();
    }

    if (cellInfo.previous) {
      cellInfo.rowChanged = cellInfo.current.rowIndex !==
        cellInfo.previous.rowIndex;
      cellInfo.columnChanged = cellInfo.current.columnIndex !==
        cellInfo.previous.columnIndex;
    } else {
      cellInfo.rowChanged = true;
      cellInfo.columnChanged = true;
    }

    cellInfo.rowExtent = cellInfo.current.rowExtent;
    cellInfo.columnExtent = cellInfo.current.columnExtent;
    cellInfo.columnIndex = cellInfo.current.columnIndex;
    cellInfo.rowIndex = cellInfo.current.rowIndex;

    cellInfo.columnHeaders = [];
    if (cellInfo.columnChanged && cellInfo.current.role !==
      Roles.COLUMNHEADER) {
      cellInfo.columnHeaders = [...getHeaders(cellInfo.current.columnHeaderCells)];
    }
    cellInfo.rowHeaders = [];
    if (cellInfo.rowChanged &&
        (cellInfo.current.role === Roles.CELL ||
         cellInfo.current.role === Roles.MATHML_CELL)) {
      cellInfo.rowHeaders = [...getHeaders(cellInfo.current.rowHeaderCells)];
    }

    this._cells.set(domNode, cellInfo);
    return cellInfo;
  },

  get bounds() {
    if (!this._bounds) {
      this._bounds = Utils.getBounds(this.accessibleForBounds);
    }

    return this._bounds.clone();
  },

  _isDefunct: function _isDefunct(aAccessible) {
    try {
      return Utils.getState(aAccessible).contains(States.DEFUNCT);
    } catch (x) {
      return true;
    }
  }
};

this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { // jshint ignore:line
  this.name = aName;
  this.callback = aCallback;

  let branch = Services.prefs;
  this.value = this._getValue(branch);

  if (this.callback && aRunCallbackNow) {
    try {
      this.callback(this.name, this.value, true);
    } catch (x) {
      Logger.logException(x);
    }
  }

  branch.addObserver(aName, this, true);
};

PrefCache.prototype = {
  _getValue: function _getValue(aBranch) {
    try {
      if (!this.type) {
        this.type = aBranch.getPrefType(this.name);
      }
      switch (this.type) {
        case Ci.nsIPrefBranch.PREF_STRING:
          return aBranch.getCharPref(this.name);
        case Ci.nsIPrefBranch.PREF_INT:
          return aBranch.getIntPref(this.name);
        case Ci.nsIPrefBranch.PREF_BOOL:
          return aBranch.getBoolPref(this.name);
        default:
          return null;
      }
    } catch (x) {
      // Pref does not exist.
      return null;
    }
  },

  observe: function observe(aSubject) {
    this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch));
    Logger.info('pref changed', this.name, this.value);
    if (this.callback) {
      try {
        this.callback(this.name, this.value, false);
      } catch (x) {
        Logger.logException(x);
      }
    }
  },

  QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
};

this.SettingCache = function SettingCache(aName, aCallback, aOptions = {}) { // jshint ignore:line
  this.value = aOptions.defaultValue;
  let runCallback = () => {
    if (aCallback) {
      aCallback(aName, this.value);
      if (aOptions.callbackOnce) {
        runCallback = () => {};
      }
    }
  };

  let settings = Utils.win.navigator.mozSettings;
  if (!settings) {
    if (aOptions.callbackNow) {
      runCallback();
    }
    return;
  }


  let lock = settings.createLock();
  let req = lock.get(aName);

  req.addEventListener('success', () => {
    this.value = req.result[aName] === undefined ?
      aOptions.defaultValue : req.result[aName];
    if (aOptions.callbackNow) {
      runCallback();
    }
  });

  settings.addObserver(aName,
                       (evt) => {
                         this.value = evt.settingValue;
                         runCallback();
                       });
};
