// 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.

'use strict';

import {Isolate} from './model.js';

defineCustomElement('trace-file-reader', (templateText) =>
 class TraceFileReader extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = templateText;
    this.addEventListener('click', e => this.handleClick(e));
    this.addEventListener('dragover', e => this.handleDragOver(e));
    this.addEventListener('drop', e => this.handleChange(e));
    this.$('#file').addEventListener('change', e => this.handleChange(e));
    this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e));
  }

  $(id) {
    return this.shadowRoot.querySelector(id);
  }

  get section() {
    return this.$('#fileReaderSection');
  }

  updateLabel(text) {
    this.$('#label').innerText = text;
  }

  handleKeyEvent(event) {
    if (event.key == "Enter") this.handleClick(event);
  }

  handleClick(event) {
    this.$('#file').click();
  }

  handleChange(event) {
    // Used for drop and file change.
    event.preventDefault();
    var host = event.dataTransfer ? event.dataTransfer : event.target;
    this.readFile(host.files[0]);
  }

  handleDragOver(event) {
    event.preventDefault();
  }

  connectedCallback() {
    this.$('#fileReader').focus();
  }

  readFile(file) {
    if (!file) {
      this.updateLabel('Failed to load file.');
      return;
    }
    this.$('#fileReader').blur();

    this.section.className = 'loading';
    const reader = new FileReader();

    if (['application/gzip', 'application/x-gzip'].includes(file.type)) {
      reader.onload = (e) => {
        try {
          // Decode data as strings of 64Kb chunks. Bigger chunks may cause
          // parsing failures in Oboe.js.
          const chunkedInflate = new pako.Inflate(
            {to: 'string', chunkSize: 65536}
          );
          let processingState = undefined;
          chunkedInflate.onData = (chunk) => {
            if (processingState === undefined) {
              processingState = this.startProcessing(file, chunk);
            } else {
              processingState.processChunk(chunk);
            }
          };
          chunkedInflate.onEnd = () => {
            if (processingState !== undefined) {
              const result_data = processingState.endProcessing();
              this.processLoadedData(file, result_data);
            }
          };
          console.log("======");
          const textResult = chunkedInflate.push(e.target.result);

          this.section.className = 'success';
          this.$('#fileReader').classList.add('done');
        } catch (err) {
          console.error(err);
          this.section.className = 'failure';
        }
      };
      // Delay the loading a bit to allow for CSS animations to happen.
      setTimeout(() => reader.readAsArrayBuffer(file), 0);
    } else {
      reader.onload = (e) => {
        try {
          // Process the whole file in at once.
          const processingState = this.startProcessing(file, e.target.result);
          const dataModel = processingState.endProcessing();
          this.processLoadedData(file, dataModel);

          this.section.className = 'success';
          this.$('#fileReader').classList.add('done');
        } catch (err) {
          console.error(err);
          this.section.className = 'failure';
        }
      };
      // Delay the loading a bit to allow for CSS animations to happen.
      setTimeout(() => reader.readAsText(file), 0);
    }
  }

  processLoadedData(file, dataModel) {
    console.log("Trace file parsed successfully.");
    this.extendAndSanitizeModel(dataModel);
    this.updateLabel('Finished loading \'' + file.name + '\'.');
    this.dispatchEvent(new CustomEvent(
        'change', {bubbles: true, composed: true, detail: dataModel}));
  }

  createOrUpdateEntryIfNeeded(data, entry) {
    console.assert(entry.isolate, 'entry should have an isolate');
    if (!(entry.isolate in data)) {
      data[entry.isolate] = new Isolate(entry.isolate);
    }
  }

  extendAndSanitizeModel(data) {
    const checkNonNegativeProperty = (obj, property) => {
      console.assert(obj[property] >= 0, 'negative property', obj, property);
    };

    Object.values(data).forEach(isolate => isolate.finalize());
  }

  processOneZoneStatsEntry(data, entry_stats) {
    this.createOrUpdateEntryIfNeeded(data, entry_stats);
    const isolate_data = data[entry_stats.isolate];
    let zones = undefined;
    const entry_zones = entry_stats.zones;
    if (entry_zones !== undefined) {
      zones = new Map();
      entry_zones.forEach(zone => {
        // There might be multiple occurrences of the same zone in the set,
        // combine numbers in this case.
        const existing_zone_stats = zones.get(zone.name);
        if (existing_zone_stats !== undefined) {
          existing_zone_stats.allocated += zone.allocated;
          existing_zone_stats.used += zone.used;
          existing_zone_stats.freed += zone.freed;
        } else {
          zones.set(zone.name, { allocated: zone.allocated,
                                 used: zone.used,
                                 freed: zone.freed });
        }
      });
    }
    const time = entry_stats.time;
    const sample = {
      time: time,
      allocated: entry_stats.allocated,
      used: entry_stats.used,
      freed: entry_stats.freed,
      zones: zones
    };
    isolate_data.samples.set(time, sample);
  }

  startProcessing(file, chunk) {
    const isV8TraceFile = chunk.includes('v8-zone-trace');
    const processingState =
        isV8TraceFile ? this.startProcessingAsV8TraceFile(file)
                      : this.startProcessingAsChromeTraceFile(file);

    processingState.processChunk(chunk);
    return processingState;
  }

  startProcessingAsChromeTraceFile(file) {
    console.log(`Processing log as chrome trace file.`);
    const data = Object.create(null);  // Final data container.
    const parseOneZoneEvent = (actual_data) => {
      if ('stats' in actual_data) {
        try {
          const entry_stats = JSON.parse(actual_data.stats);
          this.processOneZoneStatsEntry(data, entry_stats);
        } catch (e) {
          console.error('Unable to parse data set entry', e);
        }
      }
    };
    const zone_events_filter = (event) => {
      if (event.name == 'V8.Zone_Stats') {
        parseOneZoneEvent(event.args);
      }
      return oboe.drop;
    };

    const oboe_stream = oboe();
    // Trace files support two formats.
    oboe_stream
        // 1) {traceEvents: [ data ]}
        .node('traceEvents.*', zone_events_filter)
        // 2) [ data ]
        .node('!.*', zone_events_filter)
        .fail((errorReport) => {
          throw new Error("Trace data parse failed: " + errorReport.thrown);
        });

    let failed = false;

    const processingState = {
      file: file,

      processChunk(chunk) {
        if (failed) return false;
        try {
          oboe_stream.emit('data', chunk);
          return true;
        } catch (e) {
          console.error('Unable to parse chrome trace file.', e);
          failed = true;
          return false;
        }
      },

      endProcessing() {
        if (failed) return null;
        oboe_stream.emit('end');
        return data;
      },
    };
    return processingState;
  }

  startProcessingAsV8TraceFile(file) {
    console.log('Processing log as V8 trace file.');
    const data = Object.create(null);  // Final data container.

    const processOneLine = (line) => {
      try {
        // Strip away a potentially present adb logcat prefix.
        line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, '');

        const entry = JSON.parse(line);
        if (entry === null || entry.type === undefined) return;
        if ((entry.type === 'v8-zone-trace') && ('stats' in entry)) {
          const entry_stats = entry.stats;
          this.processOneZoneStatsEntry(data, entry_stats);
        } else {
          console.log('Unknown entry type: ' + entry.type);
        }
      } catch (e) {
        console.log('Unable to parse line: \'' + line + '\' (' + e + ')');
      }
    };

    let prev_chunk_leftover = "";

    const processingState = {
      file: file,

      processChunk(chunk) {
        const contents = chunk.split('\n');
        const last_line = contents.pop();
        const linesCount = contents.length;
        if (linesCount == 0) {
          // There was only one line in the chunk, it may still be unfinished.
          prev_chunk_leftover += last_line;
        } else {
          contents[0] = prev_chunk_leftover + contents[0];
          prev_chunk_leftover = last_line;
          for (let line of contents) {
            processOneLine(line);
          }
        }
        return true;
      },

      endProcessing() {
        if (prev_chunk_leftover.length > 0) {
          processOneLine(prev_chunk_leftover);
          prev_chunk_leftover = "";
        }
        return data;
      },
    };
    return processingState;
  }
});