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

const { loadItemProperties } = require("resource://devtools/client/shared/components/object-inspector/utils/load-properties.js");
const {
  getPathExpression,
  getParentFront,
  getParentGripValue,
  getValue,
  nodeIsBucket,
  getFront,
} = require("resource://devtools/client/shared/components/object-inspector/utils/node.js");
const { getLoadedProperties } = require("resource://devtools/client/shared/components/object-inspector/reducer.js");

/**
 * This action is responsible for expanding a given node, which also means that
 * it will call the action responsible to fetch properties.
 */
function nodeExpand(node, actor, options) {
  return async ({ dispatch }) => {
    dispatch({ type: "NODE_EXPAND", data: { node } });
    dispatch(nodeLoadProperties(node, actor, options));
  };
}

function nodeCollapse(node) {
  return {
    type: "NODE_COLLAPSE",
    data: { node },
  };
}

/*
 * This action checks if we need to fetch properties, entries, prototype and
 * symbols for a given node. If we do, it will call the appropriate ObjectFront
 * functions.
 */
function nodeLoadProperties(node, actor, options) {
  return async ({ dispatch, client, getState }) => {
    const state = getState();
    const loadedProperties = getLoadedProperties(state);
    if (loadedProperties.has(node.path)) {
      return;
    }

    try {
      const properties = await loadItemProperties(
        node,
        client,
        loadedProperties,
        null,
        options,
      );

      // If the client does not have a releaseActor function, it means the actors are
      // handled directly by the consumer, so we don't need to track them.
      if (!client || !client.releaseActor) {
        actor = null;
      }

      dispatch(nodePropertiesLoaded(node, actor, properties));
    } catch (e) {
      console.error(e);
      dispatch(nodeCollapse(node));
    }
  };
}

function nodePropertiesLoaded(node, actor, properties) {
  return {
    type: "NODE_PROPERTIES_LOADED",
    data: { node, actor, properties },
  };
}

/*
 * This action adds a property watchpoint to an object
 */
function addWatchpoint(item, watchpoint) {
  return async function({ dispatch, client }) {
    const { parent, name } = item;
    let object = getValue(parent);

    if (nodeIsBucket(parent)) {
      object = getValue(parent.parent);
    }

    if (!object) {
      return;
    }

    const path = parent.path;
    const property = name;
    const label = getPathExpression(item);
    const actor = object.actor;

    await client.addWatchpoint(object, property, label, watchpoint);

    dispatch({
      type: "SET_WATCHPOINT",
      data: { path, watchpoint, property, actor },
    });
  };
}

/*
 * This action removes a property watchpoint from an object
 */
function removeWatchpoint(item) {
  return async function({ dispatch, client }) {
    const { parent, name } = item;
    let object = getValue(parent);

    if (nodeIsBucket(parent)) {
      object = getValue(parent.parent);
    }

    const property = name;
    const path = parent.path;
    const actor = object.actor;

    await client.removeWatchpoint(object, property);

    dispatch({
      type: "REMOVE_WATCHPOINT",
      data: { path, property, actor },
    });
  };
}

function getActorIDs(roots) {
  if (!roots) {
    return []
  }

  const actorIds = [];
  for (const root of roots) {
    const front = getFront(root);
    if (front?.actorID) {
      actorIds.push(front.actorID);
    }
  }

  return actorIds;
}

function closeObjectInspector(roots) {
  return ({ client }) => {
    releaseActors(client, roots);
  };
}

/*
 * This action is dispatched when the `roots` prop, provided by a consumer of
 * the ObjectInspector (inspector, console, …), is modified. It will clean the
 * internal state properties (expandedPaths, loadedProperties, …) and release
 * the actors consumed with the previous roots.
 * It takes a props argument which reflects what is passed by the upper-level
 * consumer.
 */
function rootsChanged(roots, oldRoots, autoReleaseObjectActors) {
  return ({ dispatch, client }) => {
    if (autoReleaseObjectActors) {
      releaseActors(client, oldRoots, roots);
    }
    dispatch({
      type: "ROOTS_CHANGED",
      data: roots,
    });
  };
}

/**
 * Release any actors we don't need anymore
 *
 * @param {object} client: Object with a `releaseActor` method
 * @param {Array} oldRoots: The roots in which we want to cleanup now-unused actors
 * @param {Array} newRoots: The current roots (might have item that are also in oldRoots)
 */
async function releaseActors(client, oldRoots, newRoots = []) {
  if (!client?.releaseActor ) {
    return;
  }

  let actorIdsToRelease = getActorIDs(oldRoots);
  if (newRoots.length) {
    const newActorIds = getActorIDs(newRoots);
    actorIdsToRelease = actorIdsToRelease.filter(id => !newActorIds.includes(id));
  }

  if (!actorIdsToRelease.length) {
    return;
  }
  await Promise.all(actorIdsToRelease.map(client.releaseActor));
}

function invokeGetter(node, receiverId) {
  return async ({ dispatch, client, _getState }) => {
    try {
      const objectFront =
        getParentFront(node) ||
        client.createObjectFront(getParentGripValue(node));
      const getterName = node.propertyName || node.name;

      const result = await objectFront.getPropertyValue(getterName, receiverId);
      dispatch({
        type: "GETTER_INVOKED",
        data: {
          node,
          result,
        },
      });
    } catch (e) {
      console.error(e);
    }
  };
}

module.exports = {
  closeObjectInspector,
  invokeGetter,
  nodeExpand,
  nodeCollapse,
  nodeLoadProperties,
  nodePropertiesLoaded,
  rootsChanged,
  addWatchpoint,
  removeWatchpoint,
};
