// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {LogEntry} from './log.mjs';

// ===========================================================================
// Map Log Events

export const kChunkHeight = 200;
export const kChunkWidth = 10;
export const kChunkVisualWidth = 6;

function define(prototype, name, fn) {
  Object.defineProperty(prototype, name, {value: fn, enumerable: false});
}

define(Array.prototype, 'max', function(fn) {
  if (this.length === 0) return undefined;
  if (fn === undefined) fn = (each) => each;
  let max = fn(this[0]);
  for (let i = 1; i < this.length; i++) {
    max = Math.max(max, fn(this[i]));
  }
  return max;
})
define(Array.prototype, 'first', function() {
  return this[0]
});
define(Array.prototype, 'last', function() {
  return this[this.length - 1]
});

// ===========================================================================
// Map Log Events

export class MapLogEntry extends LogEntry {
  constructor(id, time) {
    if (!time) throw new Error('Invalid time');
    // Use MapLogEntry.type getter instead of property, since we only know the
    // type lazily from the incoming transition.
    super(undefined, time);
    this.id = id;
    MapLogEntry.set(id, this);
    this.edge = undefined;
    this.children = [];
    this.depth = 0;
    this._isDeprecated = false;
    this.deprecatedTargets = null;
    this.leftId = 0;
    this.rightId = 0;
    this.entry = undefined;
    this.description = '';
  }

  get functionName() {
    return this.entry?.functionName;
  }

  get code() {
    return this.entry?.logEntry;
  }

  toString() {
    return `Map(${this.id})`;
  }

  finalizeRootMap(id) {
    let stack = [this];
    while (stack.length > 0) {
      let current = stack.pop();
      if (current.leftId !== 0) {
        console.warn('Skipping potential parent loop between maps:', current)
        continue;
      }
      current.finalize(id);
      id += 1;
      current.children.forEach(edge => stack.push(edge.to));
      // TODO implement rightId
    }
    return id;
  }

  finalize(id) {
    // Initialize preorder tree traversal Ids for fast subtree inclusion checks
    if (id <= 0) throw 'invalid id';
    let currentId = id;
    this.leftId = currentId
  }

  get parent() {
    return this.edge?.from;
  }

  isDeprecated() {
    return this._isDeprecated;
  }

  deprecate() {
    this._isDeprecated = true;
  }

  isRoot() {
    return this.edge === undefined || this.edge.from === undefined;
  }

  contains(map) {
    return this.leftId < map.leftId && map.rightId < this.rightId;
  }

  addEdge(edge) {
    this.children.push(edge);
  }

  chunkIndex(chunks) {
    // Did anybody say O(n)?
    for (let i = 0; i < chunks.length; i++) {
      let chunk = chunks[i];
      if (chunk.isEmpty()) continue;
      if (chunk.last().time < this.time) continue;
      return i;
    }
    return -1;
  }

  position(chunks) {
    const index = this.chunkIndex(chunks);
    if (index === -1) return [0, 0];
    const xFrom = (index * kChunkWidth + kChunkVisualWidth / 2) | 0;
    const yFrom = kChunkHeight - chunks[index].yOffset(this) | 0;
    return [xFrom, yFrom];
  }

  transitions() {
    let transitions = Object.create(null);
    let current = this;
    while (current) {
      let edge = current.edge;
      if (edge && edge.isTransition()) {
        transitions[edge.name] = edge;
      }
      current = current.parent;
    }
    return transitions;
  }

  get type() {
    return this.edge?.type ?? 'new';
  }

  get reason() {
    return this.edge?.reason;
  }

  get property() {
    return this.edge?.name;
  }

  isBootstrapped() {
    return this.edge === undefined;
  }

  getParents() {
    let parents = [];
    let current = this.parent;
    while (current) {
      parents.push(current);
      current = current.parent;
    }
    return parents;
  }

  static get(id, time = undefined) {
    let maps = this.cache.get(id);
    if (maps === undefined) return undefined;
    if (time !== undefined) {
      for (let i = 1; i < maps.length; i++) {
        if (maps[i].time > time) {
          return maps[i - 1];
        }
      }
    }
    // default return the latest
    return maps[maps.length - 1];
  }

  static set(id, map) {
    if (this.cache.has(id)) {
      this.cache.get(id).push(map);
    } else {
      this.cache.set(id, [map]);
    }
  }

  static get propertyNames() {
    return [
      'type', 'reason', 'property', 'parent', 'functionName', 'sourcePosition',
      'script', 'code', 'id', 'description'
    ];
  }
}

MapLogEntry.cache = new Map();

// ===========================================================================
export class Edge {
  constructor(type, name, reason, time, from, to) {
    this.type = type;
    this.name = name;
    this.reason = reason;
    this.time = time;
    this.from = from;
    this.to = to;
  }

  updateFrom(edge) {
    if (this.to !== edge.to || this.from !== edge.from) {
      throw new Error('Invalid Edge updated', this, to);
    }
    this.type = edge.type;
    this.name = edge.name;
    this.reason = edge.reason;
    this.time = edge.time;
  }

  finishSetup() {
    const from = this.from;
    const to = this.to;
    if (to?.time < from?.time) {
      // This happens for map deprecation where the transition tree is converted
      // in reverse order.
      console.warn('Invalid time order');
    }
    if (from) from.addEdge(this);
    if (to === undefined) return;
    to.edge = this;
    if (from === undefined) return;
    if (to === from) throw 'From and to must be distinct.';
    let newDepth = from.depth + 1;
    if (to.depth > 0 && to.depth != newDepth) {
      console.warn('Depth has already been initialized');
    }
    to.depth = newDepth;
  }

  chunkIndex(chunks) {
    // Did anybody say O(n)?
    for (let i = 0; i < chunks.length; i++) {
      let chunk = chunks[i];
      if (chunk.isEmpty()) continue;
      if (chunk.last().time < this.time) continue;
      return i;
    }
    return -1;
  }

  parentEdge() {
    if (!this.from) return undefined;
    return this.from.edge;
  }

  chainLength() {
    let length = 0;
    let prev = this;
    while (prev) {
      prev = this.parent;
      length++;
    }
    return length;
  }

  isTransition() {
    return this.type === 'Transition'
  }

  isFastToSlow() {
    return this.type === 'Normalize'
  }

  isSlowToFast() {
    return this.type === 'SlowToFast'
  }

  isInitial() {
    return this.type === 'InitialMap'
  }

  isBootstrapped() {
    return this.type === 'new'
  }

  isReplaceDescriptors() {
    return this.type === 'ReplaceDescriptors'
  }

  isCopyAsPrototype() {
    return this.reason === 'CopyAsPrototype'
  }

  isOptimizeAsPrototype() {
    return this.reason === 'OptimizeAsPrototype'
  }

  symbol() {
    if (this.isTransition()) return '+';
    if (this.isFastToSlow()) return '⊡';
    if (this.isSlowToFast()) return '⊛';
    if (this.isReplaceDescriptors()) {
      if (this.name) return '+';
      return '∥';
    }
    return '';
  }

  toString() {
    let s = this.symbol();
    if (this.isTransition()) return s + this.name;
    if (this.isFastToSlow()) return s + this.reason;
    if (this.isCopyAsPrototype()) return s + 'Copy as Prototype';
    if (this.isOptimizeAsPrototype()) {
      return s + 'Optimize as Prototype';
    }
    if (this.isReplaceDescriptors() && this.name) {
      return this.type + ' ' + this.symbol() + this.name;
    }
    return this.type + ' ' + (this.reason ? this.reason : '') + ' ' +
        (this.name ? this.name : '')
  }
}