/* 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/. */
"use strict";

/* global EVENTS */

const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");

// React & Redux
const {
  createFactory,
  createElement,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs");
const {
  Provider,
} = require("resource://devtools/client/shared/vendor/react-redux.js");

// Accessibility Panel
const MainFrame = createFactory(
  require("resource://devtools/client/accessibility/components/MainFrame.js")
);

// Store
const createStore = require("resource://devtools/client/shared/redux/create-store.js");

// Reducers
const {
  reducers,
} = require("resource://devtools/client/accessibility/reducers/index.js");
const thunkOptions = { options: {} };
const store = createStore(reducers, {
  // Thunk options will be updated, when we [re]initialize the accessibility
  // view.
  thunkOptions,
});

// Actions
const {
  reset,
} = require("resource://devtools/client/accessibility/actions/ui.js");
const {
  select,
  highlight,
} = require("resource://devtools/client/accessibility/actions/accessibles.js");

/**
 * This object represents view of the Accessibility panel and is responsible
 * for rendering the content. It renders the top level ReactJS
 * component: the MainFrame.
 */
class AccessibilityView {
  constructor(localStore) {
    addEventListener(
      "devtools/chrome/message",
      this.onMessage.bind(this),
      true
    );
    this.store = localStore;
  }
  /**
   * Initialize accessibility view, create its top level component and set the
   * data store.
   *
   * @param {object}
   *        Object that contains the following properties:
   * - supports                               {JSON}
   *                                          a collection of flags indicating
   *                                          which accessibility panel features
   *                                          are supported by the current
   *                                          serverside version.
   * - fluentBundles                          {Array}
   *                                          array of FluentBundles elements
   *                                          for localization
   * - toolbox                                {Object}
   *                                          devtools toolbox.
   * - getAccessibilityTreeRoot               {Function}
   *                                          Returns the topmost accessibiliity
   *                                          walker that is used as the root of
   *                                          the accessibility tree.
   * - startListeningForAccessibilityEvents   {Function}
   *                                          Add listeners for specific
   *                                          accessibility events.
   * - stopListeningForAccessibilityEvents    {Function}
   *                                          Remove listeners for specific
   *                                          accessibility events.
   * - audit                                  {Function}
   *                                          Audit function that will start
   *                                          accessibility audit for given types
   *                                          of accessibility issues.
   * - simulate                               {null|Function}
   *                                          Apply simulation of a given type
   *                                          (by setting color matrices in
   *                                          docShell).
   * - toggleDisplayTabbingOrder              {Function}
   *                                          Toggle the highlight of focusable
   *                                          elements along with their tabbing
   *                                          index.
   * - enableAccessibility                    {Function}
   *                                          Enable accessibility services.
   * - resetAccessiblity                      {Function}
   *                                          Reset the state of the
   *                                          accessibility services.
   * - startListeningForLifecycleEvents       {Function}
   *                                          Add listeners for accessibility
   *                                          service lifecycle events.
   * - stopListeningForLifecycleEvents        {Function}
   *                                          Remove listeners for accessibility
   *                                          service lifecycle events.
   * - startListeningForParentLifecycleEvents {Function}
   *                                          Add listeners for parent process
   *                                          accessibility service lifecycle
   *                                          events.
   * - stopListeningForParentLifecycleEvents  {Function}
   *                                          Remove listeners for parent
   *                                          process accessibility service
   *                                          lifecycle events.
   * - highlightAccessible                    {Function}
   *                                          Highlight accessible object.
   * - unhighlightAccessible                  {Function}
   *                                          Unhighlight accessible object.
   */
  async initialize({
    supports,
    fluentBundles,
    toolbox,
    getAccessibilityTreeRoot,
    startListeningForAccessibilityEvents,
    stopListeningForAccessibilityEvents,
    audit,
    simulate,
    toggleDisplayTabbingOrder,
    enableAccessibility,
    resetAccessiblity,
    startListeningForLifecycleEvents,
    stopListeningForLifecycleEvents,
    startListeningForParentLifecycleEvents,
    stopListeningForParentLifecycleEvents,
    highlightAccessible,
    unhighlightAccessible,
  }) {
    // Make sure state is reset every time accessibility panel is initialized.
    await this.store.dispatch(reset(resetAccessiblity, supports));
    const container = document.getElementById("content");
    const mainFrame = MainFrame({
      fluentBundles,
      toolbox,
      getAccessibilityTreeRoot,
      startListeningForAccessibilityEvents,
      stopListeningForAccessibilityEvents,
      audit,
      simulate,
      enableAccessibility,
      resetAccessiblity,
      startListeningForLifecycleEvents,
      stopListeningForLifecycleEvents,
      startListeningForParentLifecycleEvents,
      stopListeningForParentLifecycleEvents,
      highlightAccessible,
      unhighlightAccessible,
    });
    thunkOptions.options.toggleDisplayTabbingOrder = toggleDisplayTabbingOrder;
    // Render top level component
    const provider = createElement(Provider, { store: this.store }, mainFrame);

    // If the AccessibilityService can not be turned on, directly emit the INITIALIZED event
    // so the panel can be rendered (with the Description element).
    if (!this.store.getState().ui.canBeEnabled) {
      window.emit(EVENTS.INITIALIZED);
    } else {
      window.once(EVENTS.PROPERTIES_UPDATED).then(() => {
        window.emit(EVENTS.INITIALIZED);
      });
    }
    this.mainFrame = ReactDOM.render(provider, container);
  }

  destroy() {
    const container = document.getElementById("content");
    ReactDOM.unmountComponentAtNode(container);
  }

  async selectAccessible(accessible) {
    await this.store.dispatch(select(accessible));
    window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
  }

  async highlightAccessible(accessible) {
    await this.store.dispatch(highlight(accessible));
    window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_HIGHLIGHTED);
  }

  async selectNodeAccessible(node) {
    if (!node) {
      return;
    }

    const accessibilityFront = await node.targetFront.getFront("accessibility");
    const accessibleWalkerFront = await accessibilityFront.getWalker();
    let accessible = await accessibleWalkerFront.getAccessibleFor(node);
    if (accessible) {
      await accessible.hydrate();
    }

    // If node does not have an accessible object, try to find node's child text node and
    // try to retrieve an accessible object for that child instead. This is the best
    // effort approach until there's accessibility API to retrieve accessible object at
    // point.
    if (!accessible || accessible.indexInParent < 0) {
      const { nodes: children } = await node.walkerFront.children(node);
      for (const child of children) {
        if (child.nodeType === nodeConstants.TEXT_NODE) {
          accessible = await accessibleWalkerFront.getAccessibleFor(child);
          // indexInParent property is only available with additional request
          // for data (hydration) about the accessible object.
          if (accessible) {
            await accessible.hydrate();
            if (accessible.indexInParent >= 0) {
              break;
            }
          }
        }
      }
    }

    // Attempt to find closest accessible ancestor for a given node.
    if (!accessible || accessible.indexInParent < 0) {
      let parentNode = node.parentNode();
      while (parentNode) {
        accessible = await accessibleWalkerFront.getAccessibleFor(parentNode);
        if (accessible) {
          await accessible.hydrate();
          if (accessible.indexInParent >= 0) {
            break;
          }
        }

        parentNode = parentNode.parentNode();
      }
    }

    // Do not set the selected state if there is no corresponding accessible.
    if (!accessible) {
      console.warn(
        `No accessible object found for a node or a node in its ancestry: ${node.actorID}`
      );
      return;
    }

    await this.store.dispatch(select(accessible));
    window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
  }

  /**
   * Process message from accessibility panel.
   *
   * @param {object} event  message type and data.
   */
  onMessage(event) {
    const data = event.data;
    const method = data.type;

    if (typeof this[method] === "function") {
      this[method](...data.args);
    }
  }
}

window.view = new AccessibilityView(store);
