1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {Trace, TraceEntry} from 'trace/trace';
19import {TraceType} from 'trace/trace_type';
20import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
21import {Operation} from 'trace/tree_node/operations/operation';
22import {
23  PropertySource,
24  PropertyTreeNode,
25} from 'trace/tree_node/property_tree_node';
26import {TreeNode} from 'trace/tree_node/tree_node';
27import {IsModifiedCallbackType} from 'viewers/common/add_diffs';
28import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
29import {TreeNodeFilter, UiTreeUtils} from 'viewers/common/ui_tree_utils';
30import {UserOptions} from 'viewers/common/user_options';
31import {SimplifyNamesVc} from 'viewers/viewer_view_capture/operations/simplify_names';
32import {AddDiffsHierarchyTree} from './add_diffs_hierarchy_tree';
33import {AddChips} from './operations/add_chips';
34import {Filter} from './operations/filter';
35import {FlattenChildren} from './operations/flatten_children';
36import {SimplifyNames} from './operations/simplify_names';
37import {PropertiesPresenter} from './properties_presenter';
38import {UiTreeFormatter} from './ui_tree_formatter';
39
40export type GetHierarchyTreeNameType = (
41  entry: TraceEntry<HierarchyTreeNode>,
42  tree: HierarchyTreeNode,
43) => string;
44
45export class HierarchyPresenter {
46  private hierarchyFilter: TreeNodeFilter = UiTreeUtils.makeIdFilter('');
47  private pinnedItems: UiHierarchyTreeNode[] = [];
48  private pinnedIds: string[] = [];
49
50  private previousEntries:
51    | Map<Trace<HierarchyTreeNode>, TraceEntry<HierarchyTreeNode>>
52    | undefined;
53  private previousHierarchyTrees? = new Map<
54    Trace<HierarchyTreeNode>,
55    HierarchyTreeNode
56  >();
57
58  private currentEntries:
59    | Map<Trace<HierarchyTreeNode>, TraceEntry<HierarchyTreeNode>>
60    | undefined;
61  private currentHierarchyTrees? = new Map<
62    Trace<HierarchyTreeNode>,
63    HierarchyTreeNode[]
64  >();
65  private currentHierarchyTreeNames:
66    | Map<Trace<HierarchyTreeNode>, string[]>
67    | undefined;
68  private currentFormattedTrees:
69    | Map<Trace<HierarchyTreeNode>, UiHierarchyTreeNode[]>
70    | undefined;
71  private selectedHierarchyTree:
72    | [Trace<HierarchyTreeNode>, HierarchyTreeNode]
73    | undefined;
74
75  constructor(
76    private userOptions: UserOptions,
77    private denylistProperties: string[],
78    private showHeadings: boolean,
79    private forceSelectFirstNode: boolean,
80    private getHierarchyTreeNameStrategy?: GetHierarchyTreeNameType,
81    private customOperations?: Array<Operation<UiHierarchyTreeNode>>,
82  ) {}
83
84  getUserOptions(): UserOptions {
85    return this.userOptions;
86  }
87
88  getCurrentEntryForTrace(
89    trace: Trace<HierarchyTreeNode>,
90  ): TraceEntry<HierarchyTreeNode> | undefined {
91    return this.currentEntries?.get(trace);
92  }
93
94  getCurrentHierarchyTreesForTrace(
95    trace: Trace<HierarchyTreeNode>,
96  ): HierarchyTreeNode[] | undefined {
97    return this.currentHierarchyTrees?.get(trace);
98  }
99
100  getAllCurrentHierarchyTrees():
101    | Array<[Trace<HierarchyTreeNode>, HierarchyTreeNode[]]>
102    | undefined {
103    const currentTrees = [];
104    for (const entry of this.currentHierarchyTrees?.entries() ?? []) {
105      currentTrees.push(entry);
106    }
107    return currentTrees;
108  }
109
110  getCurrentHierarchyTreeNames(
111    trace: Trace<HierarchyTreeNode>,
112  ): string[] | undefined {
113    return this.currentHierarchyTreeNames?.get(trace);
114  }
115
116  async addCurrentHierarchyTrees(
117    value: [Trace<HierarchyTreeNode>, HierarchyTreeNode[]],
118    highlightedItem: string | undefined,
119  ) {
120    const [trace, trees] = value;
121    if (!this.currentHierarchyTrees) {
122      this.currentHierarchyTrees = new Map();
123    }
124    const curr = this.currentHierarchyTrees.get(trace);
125    if (curr) {
126      curr.push(...trees);
127    } else {
128      this.currentHierarchyTrees.set(trace, trees);
129    }
130
131    if (!this.currentFormattedTrees) {
132      this.currentFormattedTrees = new Map();
133    }
134    if (!this.currentFormattedTrees.get(trace)) {
135      this.currentFormattedTrees.set(trace, []);
136    }
137
138    for (let i = 0; i < trees.length; i++) {
139      const tree = trees[i];
140      const formattedTree = await this.formatTreeAndUpdatePinnedItems(
141        trace,
142        tree,
143        i,
144      );
145      assertDefined(this.currentFormattedTrees.get(trace)).push(formattedTree);
146    }
147
148    if (!this.selectedHierarchyTree && highlightedItem) {
149      this.applyHighlightedIdChange(highlightedItem);
150    }
151  }
152
153  getPreviousHierarchyTreeForTrace(
154    trace: Trace<HierarchyTreeNode>,
155  ): HierarchyTreeNode | undefined {
156    return this.previousHierarchyTrees?.get(trace);
157  }
158
159  getPinnedItems(): UiHierarchyTreeNode[] {
160    return this.pinnedItems;
161  }
162
163  getAllFormattedTrees(): UiHierarchyTreeNode[] | undefined {
164    if (!this.currentFormattedTrees || this.currentFormattedTrees.size === 0) {
165      return undefined;
166    }
167    return Array.from(this.currentFormattedTrees.values()).flat();
168  }
169
170  getFormattedTreesByTrace(
171    trace: Trace<HierarchyTreeNode>,
172  ): UiHierarchyTreeNode[] | undefined {
173    return this.currentFormattedTrees?.get(trace);
174  }
175
176  getSelectedTree(): [Trace<HierarchyTreeNode>, HierarchyTreeNode] | undefined {
177    return this.selectedHierarchyTree;
178  }
179
180  setSelectedTree(
181    value: [Trace<HierarchyTreeNode>, HierarchyTreeNode] | undefined,
182  ) {
183    this.selectedHierarchyTree = value;
184  }
185
186  async updatePreviousHierarchyTrees() {
187    if (!this.previousEntries) {
188      this.previousHierarchyTrees = undefined;
189      return;
190    }
191    const previousTrees = new Map<
192      Trace<HierarchyTreeNode>,
193      HierarchyTreeNode
194    >();
195    for (const previousEntry of this.previousEntries.values()) {
196      const trace = previousEntry.getFullTrace();
197      const previousTree = await previousEntry.getValue();
198      previousTrees.set(trace, previousTree);
199    }
200    this.previousHierarchyTrees = previousTrees;
201  }
202
203  async applyTracePositionUpdate(
204    entries: Array<TraceEntry<HierarchyTreeNode>>,
205    highlightedItem: string | undefined,
206  ): Promise<void> {
207    const currEntries = new Map<
208      Trace<HierarchyTreeNode>,
209      TraceEntry<HierarchyTreeNode>
210    >();
211    const currTrees = new Map<Trace<HierarchyTreeNode>, HierarchyTreeNode[]>();
212    const prevEntries = new Map<
213      Trace<HierarchyTreeNode>,
214      TraceEntry<HierarchyTreeNode>
215    >();
216
217    for (const entry of entries) {
218      const trace = entry.getFullTrace();
219      currEntries.set(trace, entry);
220
221      const tree: HierarchyTreeNode | undefined = await entry?.getValue();
222      if (tree) currTrees.set(trace, [tree]);
223
224      const entryIndex = entry.getIndex();
225      if (entryIndex > 0) {
226        prevEntries.set(trace, trace.getEntry(entryIndex - 1));
227      }
228    }
229    this.currentEntries = currEntries.size > 0 ? currEntries : undefined;
230    this.currentHierarchyTrees = currTrees.size > 0 ? currTrees : undefined;
231    this.previousEntries = prevEntries.size > 0 ? prevEntries : undefined;
232    this.previousHierarchyTrees =
233      prevEntries.size > 0
234        ? new Map<Trace<HierarchyTreeNode>, HierarchyTreeNode>()
235        : undefined;
236    this.selectedHierarchyTree = undefined;
237
238    const names = new Map<Trace<HierarchyTreeNode>, string[]>();
239    if (this.getHierarchyTreeNameStrategy && entries.length > 0) {
240      entries.forEach((entry) => {
241        const trace = entry.getFullTrace();
242        const trees = this.currentHierarchyTrees?.get(trace);
243        if (trees) {
244          names.set(
245            entry.getFullTrace(),
246            trees.map((tree) =>
247              assertDefined(this.getHierarchyTreeNameStrategy)(entry, tree),
248            ),
249          );
250        }
251      });
252    }
253    this.currentHierarchyTreeNames = names;
254
255    if (this.userOptions['showDiff']?.isUnavailable !== undefined) {
256      this.userOptions['showDiff'].isUnavailable =
257        this.previousEntries === undefined;
258    }
259
260    if (this.currentHierarchyTrees) {
261      this.pinnedItems = [];
262      this.currentFormattedTrees = assertDefined(
263        await this.formatHierarchyTreesAndUpdatePinnedItems(
264          this.currentHierarchyTrees,
265        ),
266      );
267
268      if (!highlightedItem && this.forceSelectFirstNode) {
269        const firstTrees = Array.from(this.currentHierarchyTrees.entries())[0];
270        this.selectedHierarchyTree = [firstTrees[0], firstTrees[1][0]];
271      } else if (highlightedItem && this.currentFormattedTrees) {
272        this.applyHighlightedIdChange(highlightedItem);
273      }
274    }
275  }
276
277  applyHighlightedIdChange(newId: string) {
278    if (!this.currentHierarchyTrees) {
279      return;
280    }
281    const idMatchFilter = UiTreeUtils.makeIdMatchFilter(newId);
282    for (const [trace, trees] of this.currentHierarchyTrees) {
283      let highlightedNode: HierarchyTreeNode | undefined;
284      trees.find((t) => {
285        const target = t.findDfs(idMatchFilter);
286        if (target) {
287          highlightedNode = target;
288          return true;
289        }
290        return false;
291      });
292      if (highlightedNode) {
293        this.selectedHierarchyTree = [trace, highlightedNode];
294        break;
295      }
296    }
297  }
298
299  applyHighlightedNodeChange(selectedTree: UiHierarchyTreeNode) {
300    if (!this.currentHierarchyTrees) {
301      return;
302    }
303    if (UiTreeUtils.shouldGetProperties(selectedTree)) {
304      const idMatchFilter = UiTreeUtils.makeIdMatchFilter(selectedTree.id);
305      for (const [trace, trees] of this.currentHierarchyTrees) {
306        const hasTree = trees.find((t) => t.findDfs(idMatchFilter));
307        if (hasTree) {
308          this.selectedHierarchyTree = [trace, selectedTree];
309          break;
310        }
311      }
312    }
313  }
314
315  async applyHierarchyUserOptionsChange(userOptions: UserOptions) {
316    this.userOptions = userOptions;
317    this.currentFormattedTrees =
318      await this.formatHierarchyTreesAndUpdatePinnedItems(
319        this.currentHierarchyTrees,
320      );
321  }
322
323  async applyHierarchyFilterChange(filterString: string) {
324    this.hierarchyFilter = UiTreeUtils.makeIdFilter(filterString);
325    this.currentFormattedTrees =
326      await this.formatHierarchyTreesAndUpdatePinnedItems(
327        this.currentHierarchyTrees,
328      );
329  }
330
331  applyPinnedItemChange(pinnedItem: UiHierarchyTreeNode) {
332    const pinnedId = pinnedItem.id;
333    if (this.pinnedItems.map((item) => item.id).includes(pinnedId)) {
334      this.pinnedItems = this.pinnedItems.filter(
335        (pinned) => pinned.id !== pinnedId,
336      );
337    } else {
338      this.pinnedItems.push(pinnedItem);
339    }
340    this.updatePinnedIds(pinnedId);
341  }
342
343  private updatePinnedIds(newId: string) {
344    if (this.pinnedIds.includes(newId)) {
345      this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
346    } else {
347      this.pinnedIds.push(newId);
348    }
349  }
350
351  private async formatHierarchyTreesAndUpdatePinnedItems(
352    hierarchyTrees:
353      | Map<Trace<HierarchyTreeNode>, HierarchyTreeNode[]>
354      | undefined,
355  ): Promise<Map<Trace<HierarchyTreeNode>, UiHierarchyTreeNode[]> | undefined> {
356    if (!hierarchyTrees) return undefined;
357
358    const formattedTrees = new Map<
359      Trace<HierarchyTreeNode>,
360      UiHierarchyTreeNode[]
361    >();
362    for (const [trace, trees] of hierarchyTrees.entries()) {
363      const formatted = [];
364      for (let i = 0; i < trees.length; i++) {
365        const tree = trees[i];
366        const formattedTree = await this.formatTreeAndUpdatePinnedItems(
367          trace,
368          tree,
369          i,
370        );
371        formatted.push(formattedTree);
372      }
373      formattedTrees.set(trace, formatted);
374    }
375    return formattedTrees;
376  }
377
378  private async formatTreeAndUpdatePinnedItems(
379    trace: Trace<HierarchyTreeNode>,
380    hierarchyTree: HierarchyTreeNode,
381    hierarchyTreeIndex: number | undefined,
382  ): Promise<UiHierarchyTreeNode> {
383    const uiTree = UiHierarchyTreeNode.from(hierarchyTree);
384
385    if (!this.showHeadings) {
386      uiTree.forEachNodeDfs((node) => node.setShowHeading(false));
387    }
388    if (hierarchyTreeIndex !== undefined) {
389      const displayName = this.currentHierarchyTreeNames
390        ?.get(trace)
391        ?.at(hierarchyTreeIndex);
392      if (displayName) uiTree.setDisplayName(displayName);
393    }
394
395    const formatter = new UiTreeFormatter<UiHierarchyTreeNode>().setUiTree(
396      uiTree,
397    );
398
399    if (
400      this.userOptions['showDiff']?.enabled &&
401      !this.userOptions['showDiff']?.isUnavailable
402    ) {
403      let prevTree = this.previousHierarchyTrees?.get(trace);
404      if (this.previousHierarchyTrees && !prevTree) {
405        prevTree = await this.previousEntries?.get(trace)?.getValue();
406        if (prevTree) this.previousHierarchyTrees.set(trace, prevTree);
407      }
408      const prevEntryUiTree = prevTree
409        ? UiHierarchyTreeNode.from(prevTree)
410        : undefined;
411      await new AddDiffsHierarchyTree(
412        HierarchyPresenter.isHierarchyTreeModified,
413        this.denylistProperties,
414      ).executeInPlace(uiTree, prevEntryUiTree);
415    }
416
417    if (this.userOptions['flat']?.enabled) {
418      formatter.addOperation(new FlattenChildren());
419    }
420
421    const predicates = [this.hierarchyFilter];
422    if (this.userOptions['showOnlyVisible']?.enabled) {
423      predicates.push(UiTreeUtils.isVisible);
424    }
425
426    formatter
427      .addOperation(new Filter(predicates, true))
428      .addOperation(new AddChips());
429
430    if (this.userOptions['simplifyNames']?.enabled) {
431      formatter.addOperation(
432        trace.type === TraceType.VIEW_CAPTURE
433          ? new SimplifyNamesVc()
434          : new SimplifyNames(),
435      );
436    }
437    this.customOperations?.forEach((op) => formatter.addOperation(op));
438    const formattedTree = formatter.format();
439    this.pinnedItems.push(...this.extractPinnedItems(formattedTree));
440    return formattedTree;
441  }
442
443  private extractPinnedItems(tree: UiHierarchyTreeNode): UiHierarchyTreeNode[] {
444    const pinnedNodes = [];
445
446    if (this.pinnedIds.includes(tree.id)) {
447      pinnedNodes.push(tree);
448    }
449
450    for (const child of tree.getAllChildren()) {
451      pinnedNodes.push(...this.extractPinnedItems(child));
452    }
453
454    return pinnedNodes;
455  }
456
457  static isHierarchyTreeModified: IsModifiedCallbackType = async (
458    newTree: TreeNode | undefined,
459    oldTree: TreeNode | undefined,
460    denylistProperties: string[],
461  ) => {
462    if (!newTree && !oldTree) return false;
463    if (!newTree || !oldTree) return true;
464    if ((newTree as UiHierarchyTreeNode).isRoot()) return false;
465    const newProperties = await (
466      newTree as UiHierarchyTreeNode
467    ).getAllProperties();
468    const oldProperties = await (
469      oldTree as UiHierarchyTreeNode
470    ).getAllProperties();
471
472    return await HierarchyPresenter.isChildPropertyModified(
473      newProperties,
474      oldProperties,
475      denylistProperties,
476    );
477  };
478
479  private static async isChildPropertyModified(
480    newProperties: PropertyTreeNode,
481    oldProperties: PropertyTreeNode,
482    denylistProperties: string[],
483  ): Promise<boolean> {
484    for (const newProperty of newProperties
485      .getAllChildren()
486      .slice()
487      .sort(HierarchyPresenter.sortChildren)) {
488      if (denylistProperties.includes(newProperty.name)) {
489        continue;
490      }
491      if (newProperty.source === PropertySource.CALCULATED) {
492        continue;
493      }
494
495      const oldProperty = oldProperties.getChildByName(newProperty.name);
496      if (!oldProperty) {
497        return true;
498      }
499
500      if (newProperty.getAllChildren().length === 0) {
501        if (
502          await PropertiesPresenter.isPropertyNodeModified(
503            newProperty,
504            oldProperty,
505            denylistProperties,
506          )
507        ) {
508          return true;
509        }
510      } else {
511        const childrenModified =
512          await HierarchyPresenter.isChildPropertyModified(
513            newProperty,
514            oldProperty,
515            denylistProperties,
516          );
517        if (childrenModified) return true;
518      }
519    }
520    return false;
521  }
522
523  private static sortChildren(
524    a: PropertyTreeNode,
525    b: PropertyTreeNode,
526  ): number {
527    return a.name < b.name ? -1 : 1;
528  }
529}
530