// Copyright (c) 2012 The Chromium 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';

/**
 * @fileoverview TimelineModel is a parsed representation of the
 * TraceEvents obtained from base/trace_event in which the begin-end
 * tokens are converted into a hierarchy of processes, threads,
 * subrows, and slices.
 *
 * The building block of the model is a slice. A slice is roughly
 * equivalent to function call executing on a specific thread. As a
 * result, slices may have one or more subslices.
 *
 * A thread contains one or more subrows of slices. Row 0 corresponds to
 * the "root" slices, e.g. the topmost slices. Row 1 contains slices that
 * are nested 1 deep in the stack, and so on. We use these subrows to draw
 * nesting tasks.
 *
 */
base.require('event_target');
base.require('timeline_process');
base.require('timeline_cpu');
base.require('timeline_filter');
base.exportTo('tracing', function() {

  var TimelineProcess = tracing.TimelineProcess;
  var TimelineCpu = tracing.TimelineCpu;

  /**
   * Builds a model from an array of TraceEvent objects.
   * @param {Object=} opt_eventData Data from a single trace to be imported into
   *     the new model. See TimelineModel.importTraces for details on how to
   *     import multiple traces at once.
   * @param {bool=} opt_shiftWorldToZero Whether to shift the world to zero.
   * Defaults to true.
   * @constructor
   */
  function TimelineModel(opt_eventData, opt_shiftWorldToZero) {
    this.cpus = {};
    this.processes = {};
    this.importErrors = [];
    this.metadata = [];
    this.categories = [];

    if (opt_eventData)
      this.importTraces([opt_eventData], opt_shiftWorldToZero);
  }

  var importerConstructors = [];

  /**
   * Registers an importer. All registered importers are considered
   * when processing an import request.
   *
   * @param {Function} importerConstructor The importer's constructor function.
   */
  TimelineModel.registerImporter = function(importerConstructor) {
    importerConstructors.push(importerConstructor);
  };

  function TimelineModelEmptyImporter(events) {
    this.importPriority = 0;
  };

  TimelineModelEmptyImporter.canImport = function(eventData) {
    if (eventData instanceof Array && eventData.length == 0)
      return true;
    if (typeof(eventData) === 'string' || eventData instanceof String) {
      return eventData.length == 0;
    }
    return false;
  };

  TimelineModelEmptyImporter.prototype = {
    __proto__: Object.prototype,

    importEvents: function() {
    },
    finalizeImport: function() {
    }
  };

  TimelineModel.registerImporter(TimelineModelEmptyImporter);

  TimelineModel.prototype = {
    __proto__: base.EventTarget.prototype,

    get numProcesses() {
      var n = 0;
      for (var p in this.processes)
        n++;
      return n;
    },

    /**
     * @return {TimelineProcess} Gets a specific TimelineCpu or creates one if
     * it does not exist.
     */
    getOrCreateCpu: function(cpuNumber) {
      if (!this.cpus[cpuNumber])
        this.cpus[cpuNumber] = new TimelineCpu(cpuNumber);
      return this.cpus[cpuNumber];
    },

    /**
     * @return {TimelineProcess} Gets a TimlineProcess for a specified pid or
     * creates one if it does not exist.
     */
    getOrCreateProcess: function(pid) {
      if (!this.processes[pid])
        this.processes[pid] = new TimelineProcess(pid);
      return this.processes[pid];
    },

    /**
     * Closes any slices that need closing
     */
    autoCloseOpenSlices_: function() {
      this.updateBounds();
      var maxTimestamp = this.maxTimestamp;
      for (var pid in this.processes) {
        var process = this.processes[pid];
        for (var tid in process.threads) {
          var thread = process.threads[tid];
          thread.autoCloseOpenSlices(maxTimestamp);
        }
      }
    },

    /**
     * Generates the set of categories from the slices and counters.
     */
    updateCategories_: function() {
      var threads = this.getAllThreads();
      for (var tI = 0; tI < threads.length; tI++) {
        var slices = threads[tI].slices;
        for (var i = 0; i < slices.length; i++) {
          var category = slices[i].category;
          if (category && this.categories.indexOf(category) == -1) {
            this.categories.push(category);
          }
        }
      }
      var counters = this.getAllCounters();
      for (var tI = 0; tI < counters.length; tI++) {
        var category = counters[tI].category;
        if (category && this.categories.indexOf(category) == -1) {
          this.categories.push(category);
        }
      }
      for (var cpu in this.cpus) {
        var slices = this.cpus[cpu].slices;
        for (var i = 0; i < slices.length; i++) {
          var category = slices[i].category;
          if (category && this.categories.indexOf(category) == -1) {
            this.categories.push(category);
          }
        }
      }
    },

    /**
     * Removes threads from the model that are fully empty.
     */
    pruneEmptyThreads_: function() {
      for (var pid in this.processes) {
        var process = this.processes[pid];
        var threadsToKeep = {};
        for (var tid in process.threads) {
          var thread = process.threads[tid];
          if (!thread.isEmpty)
            threadsToKeep[tid] = thread;
        }
        process.threads = threadsToKeep;
      }
    },

    updateBounds: function() {
      var wmin = Infinity;
      var wmax = -wmin;
      var hasData = false;

      var threads = this.getAllThreads();
      for (var tI = 0; tI < threads.length; tI++) {
        var thread = threads[tI];
        thread.updateBounds();
        if (thread.minTimestamp != undefined &&
            thread.maxTimestamp != undefined) {
          wmin = Math.min(wmin, thread.minTimestamp);
          wmax = Math.max(wmax, thread.maxTimestamp);
          hasData = true;
        }
      }
      var counters = this.getAllCounters();
      for (var tI = 0; tI < counters.length; tI++) {
        var counter = counters[tI];
        counter.updateBounds();
        if (counter.minTimestamp != undefined &&
            counter.maxTimestamp != undefined) {
          hasData = true;
          wmin = Math.min(wmin, counter.minTimestamp);
          wmax = Math.max(wmax, counter.maxTimestamp);
        }
      }

      for (var cpuNumber in this.cpus) {
        var cpu = this.cpus[cpuNumber];
        cpu.updateBounds();
        if (cpu.minTimestamp != undefined &&
            cpu.maxTimestamp != undefined) {
          hasData = true;
          wmin = Math.min(wmin, cpu.minTimestamp);
          wmax = Math.max(wmax, cpu.maxTimestamp);
        }
      }

      if (hasData) {
        this.minTimestamp = wmin;
        this.maxTimestamp = wmax;
      } else {
        this.maxTimestamp = undefined;
        this.minTimestamp = undefined;
      }
    },

    shiftWorldToZero: function() {
      if (this.minTimestamp === undefined)
        return;
      var timeBase = this.minTimestamp;
      for (var pid in this.processes)
        this.processes[pid].shiftTimestampsForward(-timeBase);
      for (var cpuNumber in this.cpus)
        this.cpus[cpuNumber].shiftTimestampsForward(-timeBase);
      this.updateBounds();
    },

    getAllThreads: function() {
      var threads = [];
      for (var pid in this.processes) {
        var process = this.processes[pid];
        for (var tid in process.threads) {
          threads.push(process.threads[tid]);
        }
      }
      return threads;
    },

    /**
     * @return {Array} An array of all cpus in the model.
     */
    getAllCpus: function() {
      var cpus = [];
      for (var cpu in this.cpus)
        cpus.push(this.cpus[cpu]);
      return cpus;
    },

    /**
     * @return {Array} An array of all processes in the model.
     */
    getAllProcesses: function() {
      var processes = [];
      for (var pid in this.processes)
        processes.push(this.processes[pid]);
      return processes;
    },

    /**
     * @return {Array} An array of all the counters in the model.
     */
    getAllCounters: function() {
      var counters = [];
      for (var pid in this.processes) {
        var process = this.processes[pid];
        for (var tid in process.counters) {
          counters.push(process.counters[tid]);
        }
      }
      for (var cpuNumber in this.cpus) {
        var cpu = this.cpus[cpuNumber];
        for (var counterName in cpu.counters)
          counters.push(cpu.counters[counterName]);
      }
      return counters;
    },

    /**
     * @param {String} The name of the thread to find.
     * @return {Array} An array of all the matched threads.
     */
    findAllThreadsNamed: function(name) {
      var namedThreads = [];
      var threads = this.getAllThreads();
      for (var i = 0; i < threads.length; i++) {
        var thread = threads[i];
        if (thread.name == name)
          namedThreads.push(thread);
      }
      return namedThreads;
    },

    createImporter_: function(eventData) {
      var importerConstructor;
      for (var i = 0; i < importerConstructors.length; ++i) {
        if (importerConstructors[i].canImport(eventData)) {
          importerConstructor = importerConstructors[i];
          break;
        }
      }
      if (!importerConstructor)
        throw new Error(
            'Could not find an importer for the provided eventData.');

      var importer = new importerConstructor(
          this, eventData);
      return importer;
    },

    /**
     * Imports the provided traces into the model. The eventData type
     * is undefined and will be passed to all the timeline importers registered
     * via TimelineModel.registerImporter. The first importer that returns true
     * for canImport(events) will be used to import the events.
     *
     * The primary trace is provided via the eventData variable. If multiple
     * traces are to be imported, specify the first one as events, and the
     * remainder in the opt_additionalEventData array.
     *
     * @param {Array} traces An array of eventData to be imported. Each
     * eventData should correspond to a single trace file and will be handled by
     * a separate importer.
     * @param {bool=} opt_shiftWorldToZero Whether to shift the world to zero.
     * Defaults to true.
     */
    importTraces: function(traces,
                           opt_shiftWorldToZero) {
      if (opt_shiftWorldToZero === undefined)
        opt_shiftWorldToZero = true;

      // Figure out which importers to use.
      var importers = [];
      for (var i = 0; i < traces.length; ++i)
        importers.push(this.createImporter_(traces[i]));

      // Sort them on priority. This ensures importing happens in a predictable
      // order, e.g. linux_perf_importer before trace_event_importer.
      importers.sort(function(x, y) {
        return x.importPriority - y.importPriority;
      });

      // Run the import.
      for (var i = 0; i < importers.length; i++)
        importers[i].importEvents(i > 0);

      this.autoCloseOpenSlices_();

      for (var i = 0; i < importers.length; i++)
        importers[i].finalizeImport();

      this.pruneEmptyThreads_();
      this.updateBounds();

      this.updateCategories_();

      if (opt_shiftWorldToZero)
        this.shiftWorldToZero();
    }
  };

  return {
    TimelineModel: TimelineModel
  };

});
