// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Platform from '../../core/platform/platform.js';
import * as Trace from '../../models/trace/trace.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';

import {
  entryIsVisibleInTimeline,
} from './CompatibilityTracksAppender.js';

/**
 * This class can take in a thread that has been generated by the
 * RendererHandler and apply certain actions to it in order to modify what is
 * shown to the user. These actions can be automatically applied by DevTools or
 * applied by the user.
 *
 * Once actions are applied, the invisibleEntries() method will return the
 * entries that are invisible, and this is the list of entries that should be
 * removed before rendering the resulting thread on the timeline.
 */
export class EntriesFilter {
  #parsedTrace: Trace.TraceModel.ParsedTrace;
  // Track the set of invisible entries.
  #invisibleEntries: Trace.Types.Events.Event[] = [];
  // List of entries whose children are hidden. This list is used to
  // keep track of entries that should be identified in the UI as "expandable",
  // since they can be clicked to reveal their hidden children.
  #expandableEntries: Trace.Types.Events.Event[] = [];
  // Cache for descendants of entry that have already been gathered. The descendants
  // will never change so we can avoid running the potentially expensive search again.
  #entryToDescendantsMap = new Map<Trace.Helpers.TreeHelpers.TraceEntryNode, Trace.Types.Events.Event[]>();

  constructor(parsedTrace: Trace.TraceModel.ParsedTrace) {
    this.#parsedTrace = parsedTrace;
  }

  #getEntryNode(entry: Trace.Types.Events.Event): Trace.Helpers.TreeHelpers.TraceEntryNode|undefined {
    // The entry might be either from the Samples handler or Renderer handler. So we need to check both handlers to find
    // the EntryNode.
    return this.#parsedTrace.data.Samples.entryToNode.get(entry) ??
        this.#parsedTrace.data.Renderer.entryToNode.get(entry);
  }

  /**
   * Checks which actions can be applied on an entry. This allows us to only show possible actions in the Context Menu.
   * For example, if an entry has no children, COLLAPSE_FUNCTION will not change the FlameChart, therefore there is no need to show this action as an option.
   */
  findPossibleActions(entry: Trace.Types.Events.Event): PerfUI.FlameChart.PossibleFilterActions {
    const entryNode = this.#getEntryNode(entry);
    if (!entryNode) {
      // Invalid node was given, return no possible actions.
      return {
        [PerfUI.FlameChart.FilterAction.MERGE_FUNCTION]: false,
        [PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION]: false,
        [PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS]: false,
        [PerfUI.FlameChart.FilterAction.RESET_CHILDREN]: false,
        [PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS]: false,
      };
    }
    const entryParent = entryNode.parent;
    const allVisibleDescendants =
        this.#findAllDescendantsOfNode(entryNode).filter(descendant => !this.#invisibleEntries.includes(descendant));
    const allVisibleRepeatingDescendants = this.#findAllRepeatingDescendantsOfNext(entryNode).filter(
        descendant => !this.#invisibleEntries.includes(descendant));
    const allInVisibleDescendants =
        this.#findAllDescendantsOfNode(entryNode).filter(descendant => this.#invisibleEntries.includes(descendant));

    // If there are children to hide, indicate action as possible
    const possibleActions: PerfUI.FlameChart.PossibleFilterActions = {
      [PerfUI.FlameChart.FilterAction.MERGE_FUNCTION]: entryParent !== null,
      [PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION]: allVisibleDescendants.length > 0,
      [PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS]: allVisibleRepeatingDescendants.length > 0,
      [PerfUI.FlameChart.FilterAction.RESET_CHILDREN]: allInVisibleDescendants.length > 0,
      [PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS]: this.#invisibleEntries.length > 0,
    };
    return possibleActions;
  }

  /**
   * Returns the amount of entry descendants that belong to the hidden entries array.
   * */
  findHiddenDescendantsAmount(entry: Trace.Types.Events.Event): number {
    const entryNode = this.#getEntryNode(entry);
    if (!entryNode) {
      return 0;
    }
    const allDescendants = this.#findAllDescendantsOfNode(entryNode);
    return allDescendants.filter(descendant => this.invisibleEntries().includes(descendant)).length;
  }

  /**
   * Returns the set of entries that are invisible given the set of applied actions.
   */
  invisibleEntries(): Trace.Types.Events.Event[] {
    return this.#invisibleEntries;
  }

  /**
   * Sets hidden and expandable. Called when a trace with modifications is loaded and some entries are set as hidden and expandable.
   * Both arrays are set together because if there is one, the other must be present too.
   */
  setHiddenAndExpandableEntries(
      invisibleEntries: Trace.Types.Events.Event[], expandableEntries: Trace.Types.Events.Event[]): void {
    this.#invisibleEntries.push(...invisibleEntries);
    this.#expandableEntries.push(...expandableEntries);
  }

  entryIsInvisible(entry: Trace.Types.Events.Event): boolean {
    return this.#invisibleEntries.includes(entry);
  }

  /**
   * Returns the array of entries that have a sign indicating that entries below are hidden,
   * and so that they can be "expanded" to reveal their hidden children.
   */
  expandableEntries(): Trace.Types.Events.Event[] {
    return this.#expandableEntries;
  }

  /**
   * Applies an action to hide entries or removes entries
   * from hidden entries array depending on the action.
   */
  applyFilterAction(action: PerfUI.FlameChart.UserFilterAction): Trace.Types.Events.Event[] {
    // We apply new user action to the set of all entries, and mark
    // any that should be hidden by adding them to this set.
    // Another approach would be to use splice() to remove items from the
    // array, but doing this would be a mutation of the arry for every hidden
    // event. Instead, we add entries to this set and return it as an array at the end.
    const entriesToHide = new Set<Trace.Types.Events.Event>();

    switch (action.type) {
      case PerfUI.FlameChart.FilterAction.MERGE_FUNCTION: {
        // The entry that was clicked on is merged into its parent. All its
        // children remain visible, so we just have to hide the entry that was
        // selected.
        entriesToHide.add(action.entry);
        // If parent node exists, add it to expandableEntries, so it would be possible to uncollapse its children.
        const actionNode = this.#getEntryNode(action.entry) || null;
        const parentNode = actionNode && this.#firstVisibleParentNodeForEntryNode(actionNode);
        if (parentNode) {
          this.#addExpandableEntry(parentNode.entry);
        }
        break;
      }
      case PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION: {
        // The entry itself remains visible, but all of its descendants are hidden.
        const entryNode = this.#getEntryNode(action.entry);
        if (!entryNode) {
          // Invalid node was given, just ignore and move on.
          break;
        }
        const allDescendants = this.#findAllDescendantsOfNode(entryNode);
        allDescendants.forEach(descendant => entriesToHide.add(descendant));
        this.#addExpandableEntry(action.entry);
        break;
      }
      case PerfUI.FlameChart.FilterAction.COLLAPSE_REPEATING_DESCENDANTS: {
        const entryNode = this.#getEntryNode(action.entry);
        if (!entryNode) {
          // Invalid node was given, just ignore and move on.
          break;
        }
        const allRepeatingDescendants = this.#findAllRepeatingDescendantsOfNext(entryNode);
        allRepeatingDescendants.forEach(descendant => entriesToHide.add(descendant));
        if (entriesToHide.size > 0) {
          this.#addExpandableEntry(action.entry);
        }
        break;
      }
      case PerfUI.FlameChart.FilterAction.UNDO_ALL_ACTIONS: {
        this.#invisibleEntries = [];
        this.#expandableEntries = [];
        break;
      }
      case PerfUI.FlameChart.FilterAction.RESET_CHILDREN: {
        this.#makeEntryChildrenVisible(action.entry);
        break;
      }
      default:
        Platform.assertNever(action.type, `Unknown EntriesFilter action: ${action.type}`);
    }

    this.#invisibleEntries.push(...entriesToHide);

    return this.#invisibleEntries;
  }

  /**
   * Add an entry to the array of entries that have a sign indicating that entries below are hidden.
   * Also, remove all of the child entries of the new expandable entry from the expandable array. Do that because
   * to draw the initiator from the closest visible entry, we need to get the closest entry that is
   * marked as expandable and we do not want to get some that are hidden.
   */
  #addExpandableEntry(entry: Trace.Types.Events.Event): void {
    this.#expandableEntries.push(entry);
    const entryNode = this.#getEntryNode(entry);
    if (!entryNode) {
      // Invalid node was given, just ignore and move on.
      return;
    }
    const allDescendants = this.#findAllDescendantsOfNode(entryNode);
    if (allDescendants.length > 0) {
      this.#expandableEntries = this.#expandableEntries.filter(entry => {
        return !allDescendants.includes(entry);
      });
    }
  }

  firstVisibleParentEntryForEntry(entry: Trace.Types.Events.Event): Trace.Types.Events.Event|null {
    const node = this.#getEntryNode(entry);
    if (!node) {
      return null;
    }
    const parent = this.#firstVisibleParentNodeForEntryNode(node);
    return parent ? parent.entry : null;
  }

  // The direct parent might be hidden by other actions, therefore we look for the next visible parent.
  #firstVisibleParentNodeForEntryNode(node: Trace.Helpers.TreeHelpers.TraceEntryNode):
      Trace.Helpers.TreeHelpers.TraceEntryNode|null {
    let parent = node.parent;
    while ((parent && this.#invisibleEntries.includes(parent.entry)) ||
           (parent && !entryIsVisibleInTimeline(parent.entry))) {
      parent = parent.parent;
    }
    return parent;
  }

  #findAllDescendantsOfNode(root: Trace.Helpers.TreeHelpers.TraceEntryNode): Trace.Types.Events.Event[] {
    const cachedDescendants = this.#entryToDescendantsMap.get(root);
    if (cachedDescendants) {
      return cachedDescendants;
    }

    const descendants: Trace.Types.Events.Event[] = [];

    // Walk through all the descendants, starting at the root node.
    const children: Trace.Helpers.TreeHelpers.TraceEntryNode[] = [...root.children];
    while (children.length > 0) {
      const childNode = children.shift();
      if (childNode) {
        descendants.push(childNode.entry);
        const childNodeCachedDescendants = this.#entryToDescendantsMap.get(childNode);
        // If the descendants of a child are cached, get them from the cache instead of iterating through them again
        if (childNodeCachedDescendants) {
          descendants.push(...childNodeCachedDescendants);
        } else {
          children.push(...childNode.children);
        }
      }
    }

    this.#entryToDescendantsMap.set(root, descendants);
    return descendants;
  }

  #findAllRepeatingDescendantsOfNext(root: Trace.Helpers.TreeHelpers.TraceEntryNode): Trace.Types.Events.Event[] {
    // Walk through all the ancestors, starting at the root node.
    const children: Trace.Helpers.TreeHelpers.TraceEntryNode[] = [...root.children];
    const repeatingNodes: Trace.Types.Events.Event[] = [];
    const rootIsProfileCall = Trace.Types.Events.isProfileCall(root.entry);

    while (children.length > 0) {
      const childNode = children.shift();
      if (childNode) {
        const childIsProfileCall = Trace.Types.Events.isProfileCall(childNode.entry);
        if (/* Handle SyntheticProfileCalls */ rootIsProfileCall && childIsProfileCall) {
          const rootNodeEntry = root.entry as Trace.Types.Events.SyntheticProfileCall;
          const childNodeEntry = childNode.entry as Trace.Types.Events.SyntheticProfileCall;

          if (Trace.Helpers.SamplesIntegrator.SamplesIntegrator.framesAreEqual(
                  rootNodeEntry.callFrame, childNodeEntry.callFrame)) {
            repeatingNodes.push(childNode.entry);
          }
        } /* Handle Renderer events */ else if (!rootIsProfileCall && !childIsProfileCall) {
          if (root.entry.name === childNode.entry.name) {
            repeatingNodes.push(childNode.entry);
          }
        }
        children.push(...childNode.children);
      }
    }

    return repeatingNodes;
  }

  /**
   * If an entry was selected from a link instead of clicking on it,
   * it might be in the invisible entries array.
   * If it is, reveal it by resetting clidren the closest expandable entry,
   */
  revealEntry(entry: Trace.Types.Events.Event): void {
    const entryNode = this.#getEntryNode(entry);
    if (!entryNode) {
      // Invalid node was given, just ignore and move on.
      return;
    }
    let closestExpandableParent = entryNode;
    while (closestExpandableParent.parent && !this.#expandableEntries.includes(closestExpandableParent.entry)) {
      closestExpandableParent = closestExpandableParent.parent;
    }
    this.#makeEntryChildrenVisible(closestExpandableParent.entry);
  }

  /**
   * Removes all of the entry children from the
   * invisible entries array to make them visible.
   */
  #makeEntryChildrenVisible(entry: Trace.Types.Events.Event): void {
    const entryNode = this.#getEntryNode(entry);
    if (!entryNode) {
      // Invalid node was given, just ignore and move on.
      return;
    }
    const descendants = this.#findAllDescendantsOfNode(entryNode);

    /**
     * Filter out all descendant of the node
     * from the invisible entries list.
     */
    this.#invisibleEntries = this.#invisibleEntries.filter(entry => {
      if (descendants.includes(entry)) {
        return false;
      }
      return true;
    });

    /**
     * Filter out all descentants and entry from expandable entries
     * list to not show that some entries below those are hidden.
     */
    this.#expandableEntries = this.#expandableEntries.filter(iterEntry => {
      if (descendants.includes(iterEntry) || iterEntry === entry) {
        return false;
      }
      return true;
    });
  }

  isEntryExpandable(event: Trace.Types.Events.Event): boolean {
    return this.#expandableEntries.includes(event);
  }
}
