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

const {
  AUDIT,
  FETCH_CHILDREN,
  HIGHLIGHT,
  RESET,
  SELECT,
} = require("resource://devtools/client/accessibility/constants.js");

/**
 * Initial state definition
 */
function getInitialState() {
  return new Map();
}

/**
 * Maintain a cache of received accessibles responses from the backend.
 */
function accessibles(state = getInitialState(), action) {
  switch (action.type) {
    case FETCH_CHILDREN:
      return onReceiveChildren(state, action);
    case HIGHLIGHT:
    case SELECT:
      return onReceiveAncestry(state, action);
    case AUDIT:
      return onAudit(state, action);
    case RESET:
      return getInitialState();
    default:
      return state;
  }
}

function getActorID(accessible) {
  return accessible.actorID || accessible._form?.actor;
}

/**
 * If accessible is cached recursively remove all its children and remove itself
 * from cache.
 *
 * @param {Map}    cache      Previous state maintaining a cache of previously
 *                            fetched accessibles.
 * @param {object} accessible Accessible object to remove from cache.
 */
function cleanupChild(cache, accessible) {
  const actorID = getActorID(accessible);
  const cached = cache.get(actorID);
  if (!cached) {
    return;
  }

  for (const child of cached.children) {
    cleanupChild(cache, child);
  }

  cache.delete(actorID);
}

/**
 * Determine if accessible in cache is stale. Accessible object is stale if its
 * cached children array has the size other than the value of its childCount
 * property that updates on accessible actor event.
 *
 * @param {Map}    cache      Previous state maintaining a cache of previously
 *                             fetched accessibles.
 * @param {object} accessible Accessible object to test for staleness.
 */
function staleChildren(cache, accessible) {
  const cached = cache.get(getActorID(accessible));
  if (!cached) {
    return false;
  }

  return cached.children.length !== accessible.childCount;
}

function updateChildrenCache(cache, accessible, children) {
  const actorID = getActorID(accessible);

  if (cache.has(actorID)) {
    const cached = cache.get(actorID);
    for (const child of cached.children) {
      // If exhisting children cache includes an accessible that is not present
      // any more or if child accessible is stale remove it and all its children
      // from cache.
      if (!children.includes(child) || staleChildren(cache, child)) {
        cleanupChild(cache, child);
      }
    }
    cached.children = children;
    cache.set(actorID, cached);
  } else {
    cache.set(actorID, { children });
  }

  return cache;
}

function updateAncestry(cache, ancestry) {
  ancestry.forEach(({ accessible, children }) =>
    updateChildrenCache(cache, accessible, children)
  );

  return cache;
}

/**
 * Handles fetching of accessible children.
 *
 * @param {Map}     cache  Previous state maintaining a cache of previously
 *                         fetched accessibles.
 * @param {object}  action Redux action object.
 * @return {object} updated state
 */
function onReceiveChildren(cache, action) {
  const { error, accessible, response: children } = action;
  if (!error) {
    return updateChildrenCache(new Map(cache), accessible, children);
  }

  if (!accessible.isDestroyed()) {
    console.warn(`Error fetching children: `, error);
    return cache;
  }

  const newCache = new Map(cache);
  cleanupChild(newCache, accessible);
  return newCache;
}

function onReceiveAncestry(cache, action) {
  const { error, response: ancestry } = action;
  if (error) {
    console.warn(`Error fetching ancestry: `, error);
    return cache;
  }

  return updateAncestry(new Map(cache), ancestry);
}

function onAudit(cache, action) {
  const { error, response: ancestries } = action;
  if (error) {
    console.warn(`Error performing an audit: `, error);
    return cache;
  }

  const newCache = new Map(cache);
  ancestries.forEach(ancestry => updateAncestry(newCache, ancestry));

  return newCache;
}

exports.accessibles = accessibles;
