1/*
2 * Copyright 2017, 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 {DiffType} from './utils/diff.js';
18import {regExpTimestampSearch} from './utils/consts';
19
20// kind - a type used for categorization of different levels
21// name - name of the node
22// children - list of child entries. Each child entry is pair list
23//            [raw object, nested transform function].
24// bounds - used to calculate the full bounds of parents
25// stableId - unique id for an entry. Used to maintain selection across frames.
26function transform({
27  obj,
28  kind,
29  name,
30  shortName,
31  children,
32  timestamp,
33  rect,
34  bounds,
35  highlight,
36  rectsTransform,
37  chips,
38  visible,
39  flattened,
40  stableId,
41  freeze = true,
42}) {
43  function call(fn, arg) {
44    return (typeof fn == 'function') ? fn(arg) : fn;
45  }
46  function handleChildren(arg, transform) {
47    return [].concat(...arg.map((item) => {
48      const childrenFunc = item[0];
49      const transformFunc = item[1];
50      const childs = call(childrenFunc, obj);
51      if (childs) {
52        if (typeof childs.map != 'function') {
53          throw new Error(
54              'Childs should be an array, but is: ' + (typeof childs) + '.');
55        }
56        return transform ? childs.map(transformFunc) : childs;
57      } else {
58        return [];
59      }
60    }));
61  }
62  function concat(arg, args, argsmap) {
63    const validArg = arg !== undefined && arg !== null;
64
65    if (Array.isArray(args)) {
66      if (validArg) {
67        return [arg].concat(...args.map(argsmap));
68      } else {
69        return [].concat(...args.map(argsmap));
70      }
71    } else if (validArg) {
72      return [arg];
73    } else {
74      return undefined;
75    }
76  }
77
78  const transformedChildren = handleChildren(children, true /* transform */);
79  rectsTransform = (rectsTransform === undefined) ? (e) => e : rectsTransform;
80
81  const kindResolved = call(kind, obj);
82  const nameResolved = call(name, obj);
83  const shortNameResolved = call(shortName, obj);
84  const rectResolved = call(rect, obj);
85  // eslint-disable-next-line max-len
86  const stableIdResolved = (stableId === undefined) ? kindResolved + '|-|' + nameResolved : call(stableId, obj);
87
88  const result = {
89    kind: kindResolved,
90    name: nameResolved,
91    shortName: shortNameResolved,
92    collapsed: false,
93    children: transformedChildren,
94    obj: obj,
95    timestamp: call(timestamp, obj),
96    skip: handleChildren(children, false /* transform */),
97    bounds: call(bounds, obj) || transformedChildren.map(
98        (e) => e.bounds).find((e) => true) || undefined,
99    rect: rectResolved,
100    rects: rectsTransform(
101        concat(rectResolved, transformedChildren, (e) => e.rects)),
102    highlight: call(highlight, obj),
103    chips: call(chips, obj),
104    stableId: stableIdResolved,
105    visible: call(visible, obj),
106    childrenVisible: transformedChildren.some((c) => {
107      return c.childrenVisible || c.isVisible;
108    }),
109    flattened: call(flattened, obj),
110  };
111
112  if (rectResolved) {
113    rectResolved.ref = result;
114  }
115
116  return freeze ? Object.freeze(result) : result;
117}
118
119function getDiff(val, compareVal) {
120  if (val && isTerminal(compareVal)) {
121    return {type: DiffType.ADDED};
122  } else if (isTerminal(val) && compareVal) {
123    return {type: DiffType.DELETED};
124  } else if (compareVal != val) {
125    return {type: DiffType.MODIFIED};
126  } else {
127    return {type: DiffType.NONE};
128  }
129}
130
131// Represents termination of the object traversal,
132// differentiated with a null value in the object.
133class Terminal { }
134
135function isTerminal(obj) {
136  return obj instanceof Terminal;
137}
138
139class ObjectTransformer {
140  constructor(obj, rootName, stableId) {
141    this.obj = obj;
142    this.rootName = rootName;
143    this.stableId = stableId;
144    this.diff = false;
145  }
146
147  setOptions(options) {
148    this.options = options;
149    return this;
150  }
151
152  withDiff(obj, fieldOptions) {
153    this.diff = true;
154    this.compareWithObj = obj ?? new Terminal();
155    this.compareWithFieldOptions = fieldOptions;
156    return this;
157  }
158
159  /**
160   * Transform the raw JS Object into a TreeView compatible object
161   * @param {Object} transformOptions detailed below
162   * @param {bool} keepOriginal whether or not to store the original object in
163   *                            the obj property of a tree node for future
164   *                            reference
165   * @param {bool} freeze whether or not the returned objected should be frozen
166   *                      to prevent changing any of its properties
167   * @param {string} metadataKey the key that contains a node's metadata to be
168   *                             accessible after the transformation
169   * @return {Object} the transformed JS object compatible with treeviews.
170   */
171  transform(transformOptions = {
172    keepOriginal: false, freeze: true, metadataKey: null,
173  }) {
174    const {formatter} = this.options;
175    if (!formatter) {
176      throw new Error('Missing formatter, please set with setOptions()');
177    }
178
179    return this._transform(this.obj, this.rootName, null,
180        this.compareWithObj, this.rootName, null,
181        this.stableId, transformOptions);
182  }
183
184  /**
185   * @param {Object} obj the object to transform to a treeview compatible object
186   * @param {Object} fieldOptions options on how to transform fields
187   * @param {*} metadataKey if 'obj' contains this key, it is excluded from the
188   *                        transformation
189   * @return {Object} the transformed JS object compatible with treeviews.
190   */
191  _transformObject(obj, fieldOptions, metadataKey) {
192    const {skip, formatter} = this.options;
193    const transformedObj = {
194      obj: {},
195      fieldOptions: {},
196    };
197    let formatted = undefined;
198
199    if (skip && skip.includes(obj)) {
200      // skip
201    } else if ((formatted = formatter(obj))) {
202      // Obj has been formatted into a terminal node — has no children.
203      transformedObj.obj[formatted] = new Terminal();
204      transformedObj.fieldOptions[formatted] = fieldOptions;
205    } else if (Array.isArray(obj)) {
206      obj.forEach((e, i) => {
207        transformedObj.obj['' + i] = e;
208        transformedObj.fieldOptions['' + i] = fieldOptions;
209      });
210    } else if (typeof obj == 'string') {
211      // Object is a primitive type — has no children. Set to terminal
212      // to differentiate between null object and Terminal element.
213      transformedObj.obj[obj] = new Terminal();
214      transformedObj.fieldOptions[obj] = fieldOptions;
215    } else if (typeof obj == 'number' || typeof obj == 'boolean') {
216      // Similar to above — primitive type node has no children.
217      transformedObj.obj['' + obj] = new Terminal();
218      transformedObj.fieldOptions['' + obj] = fieldOptions;
219    } else if (obj && typeof obj == 'object') {
220      Object.keys(obj).forEach((key) => {
221        if (key === metadataKey) {
222          return;
223        }
224        transformedObj.obj[key] = obj[key];
225        transformedObj.fieldOptions[key] = obj.$type?.fields[key]?.options;
226      });
227    } else if (obj === null) {
228      // Null object is a has no children — set to be terminal node.
229      transformedObj.obj.null = new Terminal();
230      transformedObj.fieldOptions.null = undefined;
231    }
232
233    return transformedObj;
234  }
235
236  /**
237   * Extract the value of obj's property with key 'metadataKey'
238   * @param {Object} obj the obj we want to extract the metadata from
239   * @param {string} metadataKey the key that stores the metadata in the object
240   * @return {Object} the metadata value or null in no metadata is present
241   */
242  _getMetadata(obj, metadataKey) {
243    if (metadataKey && obj[metadataKey]) {
244      const metadata = obj[metadataKey];
245      obj[metadataKey] = undefined;
246      return metadata;
247    } else {
248      return null;
249    }
250  }
251
252  _transform(obj, name, fieldOptions,
253      compareWithObj, compareWithName, compareWithFieldOptions,
254      stableId, transformOptions) {
255    const originalObj = obj;
256    const metadata = this._getMetadata(obj, transformOptions.metadataKey);
257
258    const children = [];
259
260    if (!isTerminal(obj)) {
261      const transformedObj =
262          this._transformObject(
263              obj, fieldOptions, transformOptions.metadataKey);
264      obj = transformedObj.obj;
265      fieldOptions = transformedObj.fieldOptions;
266    }
267    if (!isTerminal(compareWithObj)) {
268      const transformedObj =
269          this._transformObject(
270              compareWithObj, compareWithFieldOptions,
271              transformOptions.metadataKey);
272      compareWithObj = transformedObj.obj;
273      compareWithFieldOptions = transformedObj.fieldOptions;
274    }
275
276    for (const key in obj) {
277      if (obj.hasOwnProperty(key)) {
278        let compareWithChild = new Terminal();
279        let compareWithChildName = new Terminal();
280        let compareWithChildFieldOptions = undefined;
281        if (compareWithObj.hasOwnProperty(key)) {
282          compareWithChild = compareWithObj[key];
283          compareWithChildName = key;
284          compareWithChildFieldOptions = compareWithFieldOptions[key];
285        }
286        children.push(this._transform(obj[key], key, fieldOptions[key],
287            compareWithChild, compareWithChildName,
288            compareWithChildFieldOptions,
289            `${stableId}.${key}`, transformOptions));
290      }
291    }
292
293    // Takes care of adding deleted items to final tree
294    for (const key in compareWithObj) {
295      if (!obj.hasOwnProperty(key) && compareWithObj.hasOwnProperty(key)) {
296        children.push(this._transform(new Terminal(), new Terminal(), undefined,
297            compareWithObj[key], key, compareWithFieldOptions[key],
298            `${stableId}.${key}`, transformOptions));
299      }
300    }
301
302    let transformedObj;
303    if (
304      children.length == 1 &&
305      children[0].children.length == 0 &&
306      !children[0].combined
307    ) {
308      // Merge leaf key value pairs.
309      const child = children[0];
310
311      transformedObj = {
312        kind: '',
313        name: (isTerminal(name) ? compareWithName : name) + ': ' + child.name,
314        stableId,
315        children: child.children,
316        combined: true,
317      };
318
319      if (this.diff) {
320        transformedObj.diff = child.diff;
321      }
322    } else {
323      transformedObj = {
324        kind: '',
325        name,
326        stableId,
327        children,
328      };
329
330      let fieldOptionsToUse = fieldOptions;
331
332      if (this.diff) {
333        const diff = getDiff(name, compareWithName);
334        transformedObj.diff = diff;
335
336        if (diff.type == DiffType.DELETED) {
337          transformedObj.name = compareWithName;
338          fieldOptionsToUse = compareWithFieldOptions;
339        }
340      }
341    }
342
343    if (transformOptions.keepOriginal) {
344      transformedObj.obj = originalObj;
345    }
346
347    if (metadata) {
348      transformedObj[transformOptions.metadataKey] = metadata;
349    }
350
351    return transformOptions.freeze ?
352      Object.freeze(transformedObj) : transformedObj;
353  }
354}
355
356// eslint-disable-next-line camelcase
357function nanos_to_string(elapsedRealtimeNanos) {
358  const units = [
359    [1000000, '(ns)'],
360    [1000, 'ms'],
361    [60, 's'],
362    [60, 'm'],
363    [24, 'h'],
364    [Infinity, 'd'],
365  ];
366
367  const parts = [];
368  units.some(([div, str], i) => {
369    const part = (elapsedRealtimeNanos % div).toFixed();
370    if (!str.startsWith('(')) {
371      parts.push(part + str);
372    }
373    elapsedRealtimeNanos = Math.floor(elapsedRealtimeNanos / div);
374    return elapsedRealtimeNanos == 0;
375  });
376
377  return parts.reverse().join('');
378}
379
380function string_to_nanos(stringTime) {
381  //isolate the times for each unit in an array
382  var times = stringTime.split(/\D+/).filter(unit => unit.length > 0);
383
384  //add zeroes to start of array if only partial timestamp is input
385  while (times.length<5) {
386    times.unshift("0");
387  }
388
389  var units = [24*60*60, 60*60, 60, 1, 0.001];
390  var nanos = 0;
391  //multiply the times by the relevant unit and sum
392  for (var x=0; x<5; x++) {
393    nanos += units[x]*parseInt(times[x]);
394  }
395  return nanos*(10**9);
396}
397
398// Returns a UI element used highlight a visible entry.
399// eslint-disable-next-line camelcase
400function get_visible_chip() {
401  return {short: 'V', long: 'visible', class: 'default'};
402}
403
404// Returns closest timestamp in timeline based on search input*/
405function getClosestTimestamp(searchInput, timeline) {
406  if (regExpTimestampSearch.test(searchInput)) {
407    var roundedTimestamp = parseInt(searchInput);
408  } else {
409    var roundedTimestamp = string_to_nanos(searchInput);
410  }
411  const closestTimestamp = timeline.reduce((prev, curr) => {
412    return Math.abs(curr-roundedTimestamp) < Math.abs(prev-roundedTimestamp) ? curr : prev;
413  });
414  return closestTimestamp;
415}
416
417// eslint-disable-next-line camelcase
418export {transform, ObjectTransformer, nanos_to_string, string_to_nanos, get_visible_chip, getClosestTimestamp};
419