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

// This file contains common utilities to find video/audio elements on a page
// and collect metrics for each.

(function() {
  // MediaMetric class responsible for collecting metrics on a media element.
  // It attaches required event listeners in order to collect different metrics.
  function MediaMetricBase(element) {
    checkElementIsNotBound(element);
    this.metrics = {};
    this.id = '';
    this.element = element;
  }

  MediaMetricBase.prototype.getMetrics = function() {
    return this.metrics;
  };

  MediaMetricBase.prototype.getSummary = function() {
    return {
      'id': this.id,
      'metrics': this.getMetrics()
    };
  };

  function HTMLMediaMetric(element) {
    MediaMetricBase.prototype.constructor.call(this, element);
    // Set the basic event handlers for HTML5 media element.
    var metric = this;
    function onVideoLoad(event) {
      // If a 'Play' action is performed, then playback_timer != undefined.
      if (metric.playbackTimer == undefined)
        metric.playbackTimer = new Timer();
    }
    // For the cases where autoplay=true, and without a 'play' action, we want
    // to start playbackTimer at 'play' or 'loadedmetadata' events.
    this.element.addEventListener('play', onVideoLoad);
    this.element.addEventListener('loadedmetadata', onVideoLoad);
    this.element.addEventListener('playing', function(e) {
        metric.onPlaying(e);
      });
    this.element.addEventListener('ended', function(e) {
        metric.onEnded(e);
      });
    this.setID();

    // Listen to when a Telemetry actions gets called.
    this.element.addEventListener('willPlay', function (e) {
        metric.onWillPlay(e);
      }, false);
    this.element.addEventListener('willSeek', function (e) {
        metric.onWillSeek(e);
      }, false);
    this.element.addEventListener('willLoop', function (e) {
        metric.onWillLoop(e);
      }, false);
  }

  HTMLMediaMetric.prototype = new MediaMetricBase();
  HTMLMediaMetric.prototype.constructor = HTMLMediaMetric;

  HTMLMediaMetric.prototype.setID = function() {
    if (this.element.id)
      this.id = this.element.id;
    else if (this.element.src)
      this.id = this.element.src.substring(this.element.src.lastIndexOf("/")+1);
    else
      this.id = 'media_' + window.__globalCounter++;
  };

  HTMLMediaMetric.prototype.onWillPlay = function(e) {
    this.playbackTimer = new Timer();
  };

  HTMLMediaMetric.prototype.onWillSeek = function(e) {
    var seekLabel = '';
    if (e.seekLabel)
      seekLabel = '_' + e.seekLabel;
    var metric = this;
    var onSeeked = function(e) {
        metric.appendMetric('seek' + seekLabel, metric.seekTimer.stop())
        e.target.removeEventListener('seeked', onSeeked);
      };
    this.seekTimer = new Timer();
    this.element.addEventListener('seeked', onSeeked);
  };

  HTMLMediaMetric.prototype.onWillLoop = function(e) {
    var loopTimer = new Timer();
    var metric = this;
    var loopCount = e.loopCount;
    var onEndLoop = function(e) {
        var actualDuration = loopTimer.stop();
        var idealDuration = metric.element.duration * loopCount;
        var avg_loop_time = (actualDuration - idealDuration) / loopCount;
        metric.metrics['avg_loop_time'] =
            Math.round(avg_loop_time * 1000) / 1000;
        e.target.removeEventListener('endLoop', onEndLoop);
      };
    this.element.addEventListener('endLoop', onEndLoop);
  };

  HTMLMediaMetric.prototype.appendMetric = function(metric, value) {
    if (!this.metrics[metric])
      this.metrics[metric] = [];
    this.metrics[metric].push(value);
  }

  HTMLMediaMetric.prototype.onPlaying = function(event) {
    // Playing event can fire more than once if seeking.
    if (!this.metrics['time_to_play'])
      this.metrics['time_to_play'] = this.playbackTimer.stop();
  };

  HTMLMediaMetric.prototype.onEnded = function(event) {
    var time_to_end = this.playbackTimer.stop() - this.metrics['time_to_play'];
    // TODO(shadi): Measure buffering time more accurately using events such as
    // stalled, waiting, progress, etc. This works only when continuous playback
    // is used.
    this.metrics['buffering_time'] = time_to_end - this.element.duration * 1000;
  };

  HTMLMediaMetric.prototype.getMetrics = function() {
    var decodedFrames = this.element.webkitDecodedFrameCount;
    var droppedFrames = this.element.webkitDroppedFrameCount;
    // Audio media does not report decoded/dropped frame count
    if (decodedFrames != undefined)
      this.metrics['decoded_frame_count'] = decodedFrames;
    if (droppedFrames != undefined)
      this.metrics['dropped_frame_count'] = droppedFrames;
    this.metrics['decoded_video_bytes'] =
        this.element.webkitVideoDecodedByteCount || 0;
    this.metrics['decoded_audio_bytes'] =
        this.element.webkitAudioDecodedByteCount || 0;
    return this.metrics;
  };

  function MediaMetric(element) {
    if (element instanceof HTMLMediaElement)
      return new HTMLMediaMetric(element);
    throw new Error('Unrecognized media element type.');
  }

  function Timer() {
    this.start_ = 0;
    this.start();
  }

  Timer.prototype = {
    start: function() {
      this.start_ = getCurrentTime();
    },

    stop: function() {
      // Return delta time since start in millisecs.
      return Math.round((getCurrentTime() - this.start_) * 1000) / 1000;
    }
  };

  function checkElementIsNotBound(element) {
    if (!element)
      return;
    if (getMediaMetric(element))
      throw new Error('Can not create MediaMetric for same element twice.');
  }

  function getMediaMetric(element) {
    for (var i = 0; i < window.__mediaMetrics.length; i++) {
      if (window.__mediaMetrics[i].element == element)
        return window.__mediaMetrics[i];
    }
    return null;
  }

  function createMediaMetricsForDocument() {
    // Searches for all video and audio elements on the page and creates a
    // corresponding media metric instance for each.
    var mediaElements = document.querySelectorAll('video, audio');
    for (var i = 0; i < mediaElements.length; i++)
      window.__mediaMetrics.push(new MediaMetric(mediaElements[i]));
  }

  function getCurrentTime() {
    if (window.performance)
      return (performance.now ||
              performance.mozNow ||
              performance.msNow ||
              performance.oNow ||
              performance.webkitNow).call(window.performance);
    else
      return Date.now();
  }

  function getAllMetrics() {
    // Returns a summary (info + metrics) for all media metrics.
    var metrics = [];
    for (var i = 0; i < window.__mediaMetrics.length; i++)
      metrics.push(window.__mediaMetrics[i].getSummary());
    return metrics;
  }

  window.__globalCounter = 0;
  window.__mediaMetrics = [];
  window.__getMediaMetric = getMediaMetric;
  window.__getAllMetrics = getAllMetrics;
  window.__createMediaMetricsForDocument = createMediaMetricsForDocument;
})();