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

/**
 * Different data types that each require their own labelled axis.
 */
var TimelineDataType = {
  SOURCE_COUNT: 0,
  BYTES_PER_SECOND: 1
};

/**
 * A TimelineDataSeries collects an ordered series of (time, value) pairs,
 * and converts them to graph points.  It also keeps track of its color and
 * current visibility state.  DataSeries are solely responsible for tracking
 * data, and do not send notifications on state changes.
 *
 * Abstract class, doesn't implement onReceivedLogEntry.
 */
var TimelineDataSeries = (function() {
  'use strict';

  /**
   * @constructor
   */
  function TimelineDataSeries(dataType) {
    // List of DataPoints in chronological order.
    this.dataPoints_ = [];

    // Data type of the DataSeries.  This is used to scale all values with
    // the same units in the same way.
    this.dataType_ = dataType;
    // Default color.  Should always be overridden prior to display.
    this.color_ = 'red';
    // Whether or not the data series should be drawn.
    this.isVisible_ = false;

    this.cacheStartTime_ = null;
    this.cacheStepSize_ = 0;
    this.cacheValues_ = [];
  }

  TimelineDataSeries.prototype = {
    /**
     * Adds a DataPoint to |this| with the specified time and value.
     * DataPoints are assumed to be received in chronological order.
     */
    addPoint: function(timeTicks, value) {
      var time = timeutil.convertTimeTicksToDate(timeTicks).getTime();
      this.dataPoints_.push(new DataPoint(time, value));
    },

    isVisible: function() {
      return this.isVisible_;
    },

    show: function(isVisible) {
      this.isVisible_ = isVisible;
    },

    getColor: function() {
      return this.color_;
    },

    setColor: function(color) {
      this.color_ = color;
    },

    getDataType: function() {
      return this.dataType_;
    },

    /**
     * Returns a list containing the values of the data series at |count|
     * points, starting at |startTime|, and |stepSize| milliseconds apart.
     * Caches values, so showing/hiding individual data series is fast, and
     * derived data series can be efficiently computed, if we add any.
     */
    getValues: function(startTime, stepSize, count) {
      // Use cached values, if we can.
      if (this.cacheStartTime_ == startTime &&
          this.cacheStepSize_ == stepSize &&
          this.cacheValues_.length == count) {
        return this.cacheValues_;
      }

      // Do all the work.
      this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count);
      this.cacheStartTime_ = startTime;
      this.cacheStepSize_ = stepSize;

      return this.cacheValues_;
    },

    /**
     * Does all the work of getValues when we can't use cached data.
     *
     * The default implementation just uses the |value| of the most recently
     * seen DataPoint before each time, but other DataSeries may use some
     * form of interpolation.
     * TODO(mmenke):  Consider returning the maximum value over each interval
     *                to create graphs more stable with respect to zooming.
     */
    getValuesInternal_: function(startTime, stepSize, count) {
      var values = [];
      var nextPoint = 0;
      var currentValue = 0;
      var time = startTime;
      for (var i = 0; i < count; ++i) {
        while (nextPoint < this.dataPoints_.length &&
               this.dataPoints_[nextPoint].time < time) {
          currentValue = this.dataPoints_[nextPoint].value;
          ++nextPoint;
        }
        values[i] = currentValue;
        time += stepSize;
      }
      return values;
    }
  };

  /**
   * A single point in a data series.  Each point has a time, in the form of
   * milliseconds since the Unix epoch, and a numeric value.
   * @constructor
   */
  function DataPoint(time, value) {
    this.time = time;
    this.value = value;
  }

  return TimelineDataSeries;
})();

/**
 * Tracks how many sources of the given type have seen a begin
 * event of type |eventType| more recently than an end event.
 */
var SourceCountDataSeries = (function() {
  'use strict';

  var superClass = TimelineDataSeries;

  /**
   * @constructor
   */
  function SourceCountDataSeries(sourceType, eventType) {
    superClass.call(this, TimelineDataType.SOURCE_COUNT);
    this.sourceType_ = sourceType;
    this.eventType_ = eventType;

    // Map of sources for which we've seen a begin event more recently than an
    // end event.  Each such source has a value of "true".  All others are
    // undefined.
    this.activeSources_ = {};
    // Number of entries in |activeSources_|.
    this.activeCount_ = 0;
  }

  SourceCountDataSeries.prototype = {
    // Inherit the superclass's methods.
    __proto__: superClass.prototype,

    onReceivedLogEntry: function(entry) {
      if (entry.source.type != this.sourceType_ ||
          entry.type != this.eventType_) {
        return;
      }

      if (entry.phase == EventPhase.PHASE_BEGIN) {
        this.onBeginEvent(entry.source.id, entry.time);
        return;
      }
      if (entry.phase == EventPhase.PHASE_END)
        this.onEndEvent(entry.source.id, entry.time);
    },

    /**
     * Called when the source with the specified id begins doing whatever we
     * care about.  If it's not already an active source, we add it to the map
     * and add a data point.
     */
    onBeginEvent: function(id, time) {
      if (this.activeSources_[id])
        return;
      this.activeSources_[id] = true;
      ++this.activeCount_;
      this.addPoint(time, this.activeCount_);
    },

    /**
     * Called when the source with the specified id stops doing whatever we
     * care about.  If it's an active source, we remove it from the map and add
     * a data point.
     */
    onEndEvent: function(id, time) {
      if (!this.activeSources_[id])
        return;
      delete this.activeSources_[id];
      --this.activeCount_;
      this.addPoint(time, this.activeCount_);
    }
  };

  return SourceCountDataSeries;
})();

/**
 * Tracks the number of sockets currently in use.  Needs special handling of
 * SSL sockets, so can't just use a normal SourceCountDataSeries.
 */
var SocketsInUseDataSeries = (function() {
  'use strict';

  var superClass = SourceCountDataSeries;

  /**
   * @constructor
   */
  function SocketsInUseDataSeries() {
    superClass.call(this, EventSourceType.SOCKET, EventType.SOCKET_IN_USE);
  }

  SocketsInUseDataSeries.prototype = {
    // Inherit the superclass's methods.
    __proto__: superClass.prototype,

    onReceivedLogEntry: function(entry) {
      // SSL sockets have two nested SOCKET_IN_USE events.  This is needed to
      // mark SSL sockets as unused after SSL negotiation.
      if (entry.type == EventType.SSL_CONNECT &&
          entry.phase == EventPhase.PHASE_END) {
        this.onEndEvent(entry.source.id, entry.time);
        return;
      }
      superClass.prototype.onReceivedLogEntry.call(this, entry);
    }
  };

  return SocketsInUseDataSeries;
})();

/**
 * Tracks approximate data rate using individual data transfer events.
 * Abstract class, doesn't implement onReceivedLogEntry.
 */
var TransferRateDataSeries = (function() {
  'use strict';

  var superClass = TimelineDataSeries;

  /**
   * @constructor
   */
  function TransferRateDataSeries() {
    superClass.call(this, TimelineDataType.BYTES_PER_SECOND);
  }

  TransferRateDataSeries.prototype = {
    // Inherit the superclass's methods.
    __proto__: superClass.prototype,

    /**
     * Returns the average data rate over each interval, only taking into
     * account transfers that occurred within each interval.
     * TODO(mmenke): Do something better.
     */
    getValuesInternal_: function(startTime, stepSize, count) {
      // Find the first DataPoint after |startTime| - |stepSize|.
      var nextPoint = 0;
      while (nextPoint < this.dataPoints_.length &&
             this.dataPoints_[nextPoint].time < startTime - stepSize) {
        ++nextPoint;
      }

      var values = [];
      var time = startTime;
      for (var i = 0; i < count; ++i) {
        // Calculate total bytes transferred from |time| - |stepSize|
        // to |time|.  We look at the transfers before |time| to give
        // us generally non-varying values for a given time.
        var transferred = 0;
        while (nextPoint < this.dataPoints_.length &&
               this.dataPoints_[nextPoint].time < time) {
          transferred += this.dataPoints_[nextPoint].value;
          ++nextPoint;
        }
        // Calculate bytes per second.
        values[i] = 1000 * transferred / stepSize;
        time += stepSize;
      }
      return values;
    }
  };

  return TransferRateDataSeries;
})();

/**
 * Tracks TCP and UDP transfer rate.
 */
var NetworkTransferRateDataSeries = (function() {
  'use strict';

  var superClass = TransferRateDataSeries;

  /**
   * |tcpEvent| and |udpEvent| are the event types for data transfers using
   * TCP and UDP, respectively.
   * @constructor
   */
  function NetworkTransferRateDataSeries(tcpEvent, udpEvent) {
    superClass.call(this);
    this.tcpEvent_ = tcpEvent;
    this.udpEvent_ = udpEvent;
  }

  NetworkTransferRateDataSeries.prototype = {
    // Inherit the superclass's methods.
    __proto__: superClass.prototype,

    onReceivedLogEntry: function(entry) {
      if (entry.type != this.tcpEvent_ && entry.type != this.udpEvent_)
        return;
      this.addPoint(entry.time, entry.params.byte_count);
    },
  };

  return NetworkTransferRateDataSeries;
})();

/**
 * Tracks disk cache read or write rate.  Doesn't include clearing, opening,
 * or dooming entries, as they don't have clear size values.
 */
var DiskCacheTransferRateDataSeries = (function() {
  'use strict';

  var superClass = TransferRateDataSeries;

  /**
   * @constructor
   */
  function DiskCacheTransferRateDataSeries(eventType) {
    superClass.call(this);
    this.eventType_ = eventType;
  }

  DiskCacheTransferRateDataSeries.prototype = {
    // Inherit the superclass's methods.
    __proto__: superClass.prototype,

    onReceivedLogEntry: function(entry) {
      if (entry.source.type != EventSourceType.DISK_CACHE_ENTRY ||
          entry.type != this.eventType_ ||
          entry.phase != EventPhase.PHASE_END) {
        return;
      }
      // The disk cache has a lot of 0-length writes, when truncating entries.
      // Ignore those.
      if (entry.params.bytes_copied != 0)
        this.addPoint(entry.time, entry.params.bytes_copied);
    }
  };

  return DiskCacheTransferRateDataSeries;
})();