/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */

"use strict";

// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
  this
);

// DOM panel actions.
const constants = require("resource://devtools/client/dom/content/constants.js");

// Uncomment this pref to dump all devtools emitted events to the console.
// Services.prefs.setBoolPref("devtools.dump.emit", true);

/**
 * Add a new test tab in the browser and load the given url.
 * @param {String} url
 *        The url to be loaded in the new tab
 * @return a promise that resolves to the tab object when
 *        the url is loaded
 */
async function addTestTab(url) {
  info("Adding a new test tab with URL: '" + url + "'");

  const tab = await addTab(url);

  // Select the DOM panel and wait till it's initialized.
  const panel = await initDOMPanel(tab);

  // FETCH_PROPERTIES should be fired during the call to initDOMPanel
  // But note that this behavior changed during a change in webconsole
  // initialization. So this might be racy.
  const doc = panel.panelWin.document;
  const nodes = [...doc.querySelectorAll(".treeLabel")];
  ok(!!nodes.length, "The DOM panel is already populated");

  return {
    tab,
    browser: tab.linkedBrowser,
    panel,
  };
}

/**
 * Open the DOM panel for the given tab.
 *
 * @param {Element} tab
 *        Optional tab element for which you want open the DOM panel.
 *        The default tab is taken from the global variable |tab|.
 * @return a promise that is resolved once the web console is open.
 */
async function initDOMPanel(tab) {
  tab = tab || gBrowser.selectedTab;
  const toolbox = await gDevTools.showToolboxForTab(tab, { toolId: "dom" });
  const panel = toolbox.getCurrentPanel();
  return panel;
}

/**
 * Synthesize asynchronous click event (with clean stack trace).
 */
function synthesizeMouseClickSoon(panel, element) {
  return new Promise(resolve => {
    executeSoon(() => {
      EventUtils.synthesizeMouse(element, 2, 2, {}, panel.panelWin);
      resolve();
    });
  });
}

/**
 * Returns tree row with specified label.
 */
function getRowByLabel(panel, text) {
  const doc = panel.panelWin.document;
  const labels = [...doc.querySelectorAll(".treeLabel")];
  const label = labels.find(node => node.textContent == text);
  return label ? label.closest(".treeRow") : null;
}

/**
 * Returns tree row with specified index.
 */
function getRowByIndex(panel, id) {
  const doc = panel.panelWin.document;
  const labels = [...doc.querySelectorAll(".treeLabel")];
  const label = labels.find((node, i) => i == id);
  return label ? label.closest(".treeRow") : null;
}

/**
 * Returns the children (tree row text) of the specified object name as an
 * array.
 */
function getAllRowsForLabel(panel, text) {
  let rootObjectLevel;
  let node;
  const result = [];
  const doc = panel.panelWin.document;
  const nodes = [...doc.querySelectorAll(".treeLabel")];

  // Find the label (object name) for which we want the children. We remove
  // nodes from the start of the array until we reach the property. The children
  // are then at the start of the array.
  while (true) {
    node = nodes.shift();

    if (!node || node.textContent === text) {
      rootObjectLevel = node.getAttribute("data-level");
      break;
    }
  }

  // Return an empty array if the node is not found.
  if (!node) {
    return result;
  }

  // Now get the children.
  for (node of nodes) {
    const level = node.getAttribute("data-level");

    if (level > rootObjectLevel) {
      result.push({
        name: normalizeTreeValue(node.textContent),
        value: normalizeTreeValue(
          node.parentNode.nextElementSibling.textContent
        ),
      });
    } else {
      break;
    }
  }

  return result;
}

/**
 * Strings in the tree are in the form ""a"" and numbers in the form "1". We
 * normalize these values by converting ""a"" to "a" and "1" to 1.
 *
 * @param  {String} value
 *         The value to normalize.
 * @return {String|Number}
 *         The normalized value.
 */
function normalizeTreeValue(value) {
  if (value === `""`) {
    return "";
  }
  if (value.startsWith(`"`) && value.endsWith(`"`)) {
    return value.substr(1, value.length - 2);
  }
  if (isFinite(value) && parseInt(value, 10) == value) {
    return parseInt(value, 10);
  }

  return value;
}

/**
 * Expands elements with given label and waits till
 * children are received from the backend.
 */
function expandRow(panel, labelText) {
  const row = getRowByLabel(panel, labelText);
  return synthesizeMouseClickSoon(panel, row).then(() => {
    // Wait till children (properties) are fetched
    // from the backend.
    const store = getReduxStoreFromPanel(panel);
    return waitForDispatch(store, "FETCH_PROPERTIES");
  });
}

function refreshPanel(panel) {
  const doc = panel.panelWin.document;
  const button = doc.querySelector("#dom-refresh-button");
  return synthesizeMouseClickSoon(panel, button).then(() => {
    // Wait till children (properties) are fetched
    // from the backend.
    const store = getReduxStoreFromPanel(panel);
    return waitForDispatch(store, "FETCH_PROPERTIES");
  });
}

function getReduxStoreFromPanel(panel) {
  return panel.panelWin.view.mainFrame.store;
}
