// 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 {LogReader, parseString, parseVarArgs} from '../logreader.mjs';
import {Profile} from '../profile.mjs';

import {DeoptLogEntry} from './log/deopt.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {Edge, MapLogEntry} from './log/map.mjs';
import {Timeline} from './timeline.mjs';

// ===========================================================================

export class Processor extends LogReader {
  _profile = new Profile();
  _mapTimeline = new Timeline();
  _icTimeline = new Timeline();
  _deoptTimeline = new Timeline();
  _formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
  MAJOR_VERSION = 7;
  MINOR_VERSION = 6;
  constructor(logString) {
    super();
    this.propertyICParser = [
      parseInt, parseInt, parseInt, parseInt, parseString, parseString,
      parseString, parseString, parseString, parseString
    ];
    this.dispatchTable_ = {
      __proto__: null,
      'code-creation': {
        parsers: [
          parseString, parseInt, parseInt, parseInt, parseInt, parseString,
          parseVarArgs
        ],
        processor: this.processCodeCreation
      },
      'code-deopt': {
        parsers: [
          parseInt, parseInt, parseInt, parseInt, parseInt, parseString,
          parseString, parseString
        ],
        processor: this.processCodeDeopt
      },
      'v8-version': {
        parsers: [
          parseInt,
          parseInt,
        ],
        processor: this.processV8Version
      },
      'script-source': {
        parsers: [parseInt, parseString, parseString],
        processor: this.processScriptSource
      },
      'code-move':
          {parsers: [parseInt, parseInt], processor: this.processCodeMove},
      'code-delete': {parsers: [parseInt], processor: this.processCodeDelete},
      'sfi-move':
          {parsers: [parseInt, parseInt], processor: this.processFunctionMove},
      'map-create':
          {parsers: [parseInt, parseString], processor: this.processMapCreate},
      'map': {
        parsers: [
          parseString, parseInt, parseString, parseString, parseInt, parseInt,
          parseInt, parseString, parseString
        ],
        processor: this.processMap
      },
      'map-details': {
        parsers: [parseInt, parseString, parseString],
        processor: this.processMapDetails
      },
      'LoadGlobalIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'LoadGlobalIC')
      },
      'StoreGlobalIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'StoreGlobalIC')
      },
      'LoadIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'LoadIC')
      },
      'StoreIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'StoreIC')
      },
      'KeyedLoadIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'KeyedLoadIC')
      },
      'KeyedStoreIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'KeyedStoreIC')
      },
      'StoreInArrayLiteralIC': {
        parsers: this.propertyICParser,
        processor: this.processPropertyIC.bind(this, 'StoreInArrayLiteralIC')
      },
    };
    if (logString) this.processString(logString);
  }

  printError(str) {
    console.error(str);
    throw str
  }

  processString(string) {
    let end = string.length;
    let current = 0;
    let next = 0;
    let line;
    let i = 0;
    let entry;
    try {
      while (current < end) {
        next = string.indexOf('\n', current);
        if (next === -1) break;
        i++;
        line = string.substring(current, next);
        current = next + 1;
        this.processLogLine(line);
      }
    } catch (e) {
      console.error(`Error occurred during parsing, trying to continue: ${e}`);
    }
    this.finalize();
  }

  processLogFile(fileName) {
    this.collectEntries = true;
    this.lastLogFileName_ = fileName;
    let i = 1;
    let line;
    try {
      while (line = readline()) {
        this.processLogLine(line);
        i++;
      }
    } catch (e) {
      console.error(
          `Error occurred during parsing line ${i}` +
          ', trying to continue: ' + e);
    }
    this.finalize();
  }

  finalize() {
    // TODO(cbruni): print stats;
    this._mapTimeline.transitions = new Map();
    let id = 0;
    this._mapTimeline.forEach(map => {
      if (map.isRoot()) id = map.finalizeRootMap(id + 1);
      if (map.edge && map.edge.name) {
        const edge = map.edge;
        const list = this._mapTimeline.transitions.get(edge.name);
        if (list === undefined) {
          this._mapTimeline.transitions.set(edge.name, [edge]);
        } else {
          list.push(edge);
        }
      }
    });
  }

  /**
   * Parser for dynamic code optimization state.
   */
  parseState(s) {
    switch (s) {
      case '':
        return Profile.CodeState.COMPILED;
      case '~':
        return Profile.CodeState.OPTIMIZABLE;
      case '*':
        return Profile.CodeState.OPTIMIZED;
    }
    throw new Error(`unknown code state: ${s}`);
  }

  processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
    if (maybe_func.length) {
      const funcAddr = parseInt(maybe_func[0]);
      const state = this.parseState(maybe_func[1]);
      this._profile.addFuncCode(
          type, name, timestamp, start, size, funcAddr, state);
    } else {
      this._profile.addCode(type, name, timestamp, start, size);
    }
  }

  processCodeDeopt(
      timestamp, codeSize, instructionStart, inliningId, scriptOffset,
      deoptKind, deoptLocation, deoptReason) {
    this._deoptTimeline.push(new DeoptLogEntry(deoptKind, timestamp));
  }

  processV8Version(majorVersion, minorVersion) {
    if ((majorVersion == this.MAJOR_VERSION &&
         minorVersion <= this.MINOR_VERSION) ||
        (majorVersion < this.MAJOR_VERSION)) {
      window.alert(
          `Unsupported version ${majorVersion}.${minorVersion}. \n` +
          `Please use the matching tool for given the V8 version.`);
    }
  }

  processScriptSource(scriptId, url, source) {
    this._profile.addScriptSource(scriptId, url, source);
  }

  processCodeMove(from, to) {
    this._profile.moveCode(from, to);
  }

  processCodeDelete(start) {
    this._profile.deleteCode(start);
  }

  processFunctionMove(from, to) {
    this._profile.moveFunc(from, to);
  }

  formatName(entry) {
    if (!entry) return '<unknown>';
    let name = entry.func.getName();
    let re = /(.*):[0-9]+:[0-9]+$/;
    let array = re.exec(name);
    if (!array) return name;
    return entry.getState() + array[1];
  }

  processPropertyIC(
      type, pc, time, line, column, old_state, new_state, map, key, modifier,
      slow_reason) {
    let fnName = this.functionName(pc);
    let parts = fnName.split(' ');
    let fileName = parts[parts.length - 1];
    let script = this.getScript(fileName);
    // TODO: Use SourcePosition here directly
    let entry = new IcLogEntry(
        type, fnName, time, line, column, key, old_state, new_state, map,
        slow_reason, script, modifier);
    if (script) {
      entry.sourcePosition = script.addSourcePosition(line, column, entry);
    }
    this._icTimeline.push(entry);
  }

  functionName(pc) {
    let entry = this._profile.findEntry(pc);
    return this.formatName(entry);
  }
  formatPC(pc, line, column) {
    let entry = this._profile.findEntry(pc);
    if (!entry) return '<unknown>'
      if (entry.type === 'Builtin') {
        return entry.name;
      }
    let name = entry.func.getName();
    let array = this._formatPCRegexp.exec(name);
    if (array === null) {
      entry = name;
    } else {
      entry = entry.getState() + array[1];
    }
    return entry + ':' + line + ':' + column;
  }

  processFileName(filePositionLine) {
    if (!filePositionLine.includes(' ')) return;
    // Try to handle urls with file positions: https://foo.bar.com/:17:330"
    filePositionLine = filePositionLine.split(' ');
    let parts = filePositionLine[1].split(':');
    if (parts[0].length <= 5) return parts[0] + ':' + parts[1];
    return parts[1];
  }

  processMap(type, time, from, to, pc, line, column, reason, name) {
    let time_ = parseInt(time);
    if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
    let from_ = this.getExistingMapEntry(from, time_);
    let to_ = this.getExistingMapEntry(to, time_);
    // TODO: use SourcePosition directly.
    let edge = new Edge(type, name, reason, time, from_, to_);
    to_.filePosition = this.formatPC(pc, line, column);
    let fileName = this.processFileName(to_.filePosition);
    // TODO: avoid undefined source positions.
    if (fileName !== undefined) {
      to_.script = this.getScript(fileName);
    }
    if (to_.script) {
      to_.sourcePosition = to_.script.addSourcePosition(line, column, to_)
    }
    edge.finishSetup();
  }

  deprecateMap(type, time, id) {
    this.getExistingMapEntry(id, time).deprecate();
  }

  processMapCreate(time, id) {
    // map-create events might override existing maps if the addresses get
    // recycled. Hence we do not check for existing maps.
    let map = this.createMapEntry(id, time);
  }

  processMapDetails(time, id, string) {
    // TODO(cbruni): fix initial map logging.
    let map = this.getExistingMapEntry(id, time);
    map.description = string;
  }

  createMapEntry(id, time) {
    let map = new MapLogEntry(id, time);
    this._mapTimeline.push(map);
    return map;
  }

  getExistingMapEntry(id, time) {
    if (id === '0x000000000000') return undefined;
    let map = MapLogEntry.get(id, time);
    if (map === undefined) {
      console.error(`No map details provided: id=${id}`);
      // Manually patch in a map to continue running.
      return this.createMapEntry(id, time);
    };
    return map;
  }

  getScript(url) {
    const script = this._profile.getScript(url);
    // TODO create placeholder script for empty urls.
    if (script === undefined) {
      console.error(`Could not find script for url: '${url}'`)
    }
    return script;
  }

  get icTimeline() {
    return this._icTimeline;
  }

  get mapTimeline() {
    return this._mapTimeline;
  }

  get deoptTimeline() {
    return this._deoptTimeline;
  }

  get scripts() {
    return this._profile.scripts_.filter(script => script !== undefined);
  }
}