// 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 Implements a WebSocket client that receives
 * a stream of slices from a server.
 *
 */

base.require('timeline_model');
base.require('timeline_slice');

base.exportTo('tracing', function() {

  var STATE_PAUSED = 0x1;
  var STATE_CAPTURING = 0x2;

  /**
   * Converts a stream of trace data from a websocket into a
   * timeline model.
   *
   * Events consumed by this importer have the following JSON structure:
   *
   * {
   *   'cmd': 'commandName',
   *   ... command specific data
   * }
   *
   * The importer understands 2 commands:
   *      'ptd' (Process Thread Data)
   *      'pcd' (Process Counter Data)
   *
   * The command specific data is as follows:
   *
   * {
   *   'pid': 'Remote Process Id',
   *   'td': {
   *                  'n': 'Thread Name Here',
   *                  's: [ {
   *                              'l': 'Slice Label',
   *                              's': startTime,
   *                              'e': endTime
   *                              }, ... ]
   *         }
   * }
   *
   * {
   *  'pid' 'Remote Process Id',
   *  'cd': {
   *      'n': 'Counter Name',
   *      'sn': ['Series Name',...]
   *      'sc': [seriesColor, ...]
   *      'c': [
   *            {
   *              't': timestamp,
   *              'v': [value0, value1, ...]
   *            },
   *            ....
   *           ]
   *       }
   * }
   * @param {TimelineModel} model that will be updated
   * when events are received.
   * @constructor
   */
  function TimelineStreamImporter(model) {
    var self = this;
    this.model_ = model;
    this.connection_ = undefined;
    this.state_ = STATE_CAPTURING;
    this.connectionOpenHandler_ =
      this.connectionOpenHandler_.bind(this);
    this.connectionCloseHandler_ =
      this.connectionCloseHandler_.bind(this);
    this.connectionErrorHandler_ =
      this.connectionErrorHandler_.bind(this);
    this.connectionMessageHandler_ =
      this.connectionMessageHandler_.bind(this);
  }

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

    cleanup_: function() {
      if (!this.connection_)
        return;
      this.connection_.removeEventListener('open',
        this.connectionOpenHandler_);
      this.connection_.removeEventListener('close',
        this.connectionCloseHandler_);
      this.connection_.removeEventListener('error',
        this.connectionErrorHandler_);
      this.connection_.removeEventListener('message',
        this.connectionMessageHandler_);
    },

    connectionOpenHandler_: function() {
      this.dispatchEvent({'type': 'connect'});
    },

    connectionCloseHandler_: function() {
      this.dispatchEvent({'type': 'disconnect'});
      this.cleanup_();
    },

    connectionErrorHandler_: function() {
      this.dispatchEvent({'type': 'connectionerror'});
      this.cleanup_();
    },

    connectionMessageHandler_: function(event) {
      var packet = JSON.parse(event.data);
      var command = packet['cmd'];
      var pid = packet['pid'];
      var modelDirty = false;
      if (command == 'ptd') {
        var process = this.model_.getOrCreateProcess(pid);
        var threadData = packet['td'];
        var threadName = threadData['n'];
        var threadSlices = threadData['s'];
        var thread = process.getOrCreateThread(threadName);
        for (var s = 0; s < threadSlices.length; s++) {
          var slice = threadSlices[s];
          thread.slices.push(new tracing.TimelineSlice('streamed',
            slice['l'],
            0,
            slice['s'],
            {},
            slice['e'] - slice['s']));
        }
        modelDirty = true;
      } else if (command == 'pcd') {
        var process = this.model_.getOrCreateProcess(pid);
        var counterData = packet['cd'];
        var counterName = counterData['n'];
        var counterSeriesNames = counterData['sn'];
        var counterSeriesColors = counterData['sc'];
        var counterValues = counterData['c'];
        var counter = process.getOrCreateCounter('streamed', counterName);
        if (counterSeriesNames.length != counterSeriesColors.length) {
          var importError = 'Streamed counter name length does not match' +
                            'counter color length' + counterSeriesNames.length +
                            ' vs ' + counterSeriesColors.length;
          this.model_.importErrors.push(importError);
          return;
        }
        if (counter.seriesNames.length == 0) {
          // First time
          counter.seriesNames = counterSeriesNames;
          counter.seriesColors = counterSeriesColors;
        } else {
          if (counter.seriesNames.length != counterSeriesNames.length) {
            var importError = 'Streamed counter ' + counterName +
              'changed number of seriesNames';
            this.model_.importErrors.push(importError);
            return;
          } else {
            for (var i = 0; i < counter.seriesNames.length; i++) {
              var oldSeriesName = counter.seriesNames[i];
              var newSeriesName = counterSeriesNames[i];
              if (oldSeriesName != newSeriesName) {
                var importError = 'Streamed counter ' + counterName +
                  'series name changed from ' +
                  oldSeriesName + ' to ' +
                  newSeriesName;
                this.model_.importErrors.push(importError);
                return;
              }
            }
          }
        }
        for (var c = 0; c < counterValues.length; c++) {
          var count = counterValues[c];
          var x = count['t'];
          var y = count['v'];
          counter.timestamps.push(x);
          counter.samples = counter.samples.concat(y);
        }
        modelDirty = true;
      }
      if (modelDirty == true) {
        this.model_.updateBounds();
        this.dispatchEvent({'type': 'modelchange',
          'model': this.model_});
      }
    },

    get connected() {
      if (this.connection_ !== undefined &&
        this.connection_.readyState == WebSocket.OPEN) {
        return true;
      }
      return false;
    },

    get paused() {
      return this.state_ == STATE_PAUSED;
    },

    /**
     * Connects the stream to a websocket.
     * @param {WebSocket} wsConnection The websocket to use for the stream
     */
    connect: function(wsConnection) {
      this.connection_ = wsConnection;
      this.connection_.addEventListener('open',
        this.connectionOpenHandler_);
      this.connection_.addEventListener('close',
        this.connectionCloseHandler_);
      this.connection_.addEventListener('error',
        this.connectionErrorHandler_);
      this.connection_.addEventListener('message',
        this.connectionMessageHandler_);
    },

    pause: function() {
      if (this.state_ == STATE_PAUSED)
        throw new Error('Already paused.');
      if (!this.connection_)
        throw new Error('Not connected.');
      this.connection_.send(JSON.stringify({'cmd': 'pause'}));
      this.state_ = STATE_PAUSED;
    },

    resume: function() {
      if (this.state_ == STATE_CAPTURING)
        throw new Error('Already capturing.');
      if (!this.connection_)
        throw new Error('Not connected.');
      this.connection_.send(JSON.stringify({'cmd': 'resume'}));
      this.state_ = STATE_CAPTURING;
    }
  };

  return {
    TimelineStreamImporter: TimelineStreamImporter
  };
});
