/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// No sense updating the overview more often than receiving data from the
// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
// in toolkit/devtools/server/actors/timeline.js
const OVERVIEW_UPDATE_INTERVAL = 200; // ms

const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100; // ms
const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16; // ms

const FRAMERATE_GRAPH_HEIGHT = 45; // px
const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
const MARKERS_GRAPH_ROW_HEIGHT = 11; // px
const MARKERS_GROUP_VERTICAL_PADDING = 5; // px
const MEMORY_GRAPH_HEIGHT = 30; // px

const GRAPH_SCROLL_EVENTS_DRAIN = 50; // ms

/**
 * View handler for the overview panel's time view, displaying
 * framerate, markers and memory over time.
 */
let OverviewView = {
  /**
   * Sets up the view with event binding.
   */
  initialize: Task.async(function *() {
    this._onRecordingStarted = this._onRecordingStarted.bind(this);
    this._onRecordingStopped = this._onRecordingStopped.bind(this);
    this._onRecordingTick = this._onRecordingTick.bind(this);
    this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
    this._onGraphScroll = this._onGraphScroll.bind(this);

    yield this._showFramerateGraph();
    yield this._showMarkersGraph();
    yield this._showMemoryGraph();

    this.framerateGraph.on("mouseup", this._onGraphMouseUp);
    this.framerateGraph.on("scroll", this._onGraphScroll);
    this.markersOverview.on("mouseup", this._onGraphMouseUp);
    this.markersOverview.on("scroll", this._onGraphScroll);
    this.memoryOverview.on("mouseup", this._onGraphMouseUp);
    this.memoryOverview.on("scroll", this._onGraphScroll);

    PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
  }),

  /**
   * Unbinds events.
   */
  destroy: function () {
    this.framerateGraph.off("mouseup", this._onGraphMouseUp);
    this.framerateGraph.off("scroll", this._onGraphScroll);
    this.markersOverview.off("mouseup", this._onGraphMouseUp);
    this.markersOverview.off("scroll", this._onGraphScroll);
    this.memoryOverview.off("mouseup", this._onGraphMouseUp);
    this.memoryOverview.off("scroll", this._onGraphScroll);

    clearNamedTimeout("graph-scroll");
    PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
  },

  /**
   * Sets up the framerate graph.
   */
  _showFramerateGraph: Task.async(function *() {
    this.framerateGraph = new LineGraphWidget($("#time-framerate"), {
      metric: L10N.getStr("graphs.fps")
    });
    this.framerateGraph.fixedHeight = FRAMERATE_GRAPH_HEIGHT;
    yield this.framerateGraph.ready();
  }),

  /**
   * Sets up the markers overivew graph.
   */
  _showMarkersGraph: Task.async(function *() {
    this.markersOverview = new MarkersOverview($("#markers-overview"), TIMELINE_BLUEPRINT);
    this.markersOverview.headerHeight = MARKERS_GRAPH_HEADER_HEIGHT;
    this.markersOverview.rowHeight = MARKERS_GRAPH_ROW_HEIGHT;
    this.markersOverview.groupPadding = MARKERS_GROUP_VERTICAL_PADDING;
    yield this.markersOverview.ready();

    CanvasGraphUtils.linkAnimation(this.framerateGraph, this.markersOverview);
    CanvasGraphUtils.linkSelection(this.framerateGraph, this.markersOverview);
  }),

  /**
   * Sets up the memory overview graph.
   */
  _showMemoryGraph: Task.async(function *() {
    this.memoryOverview = new MemoryOverview($("#memory-overview"));
    this.memoryOverview.fixedHeight = MEMORY_GRAPH_HEIGHT;
    yield this.memoryOverview.ready();

    CanvasGraphUtils.linkAnimation(this.framerateGraph, this.memoryOverview);
    CanvasGraphUtils.linkSelection(this.framerateGraph, this.memoryOverview);
  }),

  /**
   * Method for handling all the set up for rendering the overview graphs.
   *
   * @param number resolution
   *        The fps graph resolution. @see Graphs.jsm
   */
  render: Task.async(function *(resolution) {
    let interval = PerformanceController.getInterval();
    let markers = PerformanceController.getMarkers();
    let memory = PerformanceController.getMemory();
    let timestamps = PerformanceController.getTicks();

    this.markersOverview.setData({ interval, markers });
    this.emit(EVENTS.MARKERS_GRAPH_RENDERED);

    this.memoryOverview.setData({ interval, memory });
    this.emit(EVENTS.MEMORY_GRAPH_RENDERED);

    yield this.framerateGraph.setDataFromTimestamps(timestamps, resolution);
    this.emit(EVENTS.FRAMERATE_GRAPH_RENDERED);

    // Finished rendering all graphs in this overview.
    this.emit(EVENTS.OVERVIEW_RENDERED);
  }),

  /**
   * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
   * and uses data fetched from the controller to render
   * data into all the corresponding overview graphs.
   */
  _onRecordingTick: Task.async(function *() {
    yield this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL);
    this._prepareNextTick();
  }),

  /**
   * Fired when the graph selection has changed. Called by
   * mouseup and scroll events.
   */
  _onSelectionChange: function () {
    if (this.framerateGraph.hasSelection()) {
      let { min: beginAt, max: endAt } = this.framerateGraph.getMappedSelection();
      this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, { beginAt, endAt });
    } else {
      this.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
    }
  },

  /**
   * Listener handling the "mouseup" event for the framerate graph.
   * Fires an event to be handled elsewhere.
   */
  _onGraphMouseUp: function () {
    // Only fire a selection change event if the selection is actually enabled.
    if (this.framerateGraph.selectionEnabled) {
      this._onSelectionChange();
    }
  },

  /**
   * Listener handling the "scroll" event for the framerate graph.
   * Fires a debounced event to be handled elsewhere.
   */
  _onGraphScroll: function () {
    setNamedTimeout("graph-scroll", GRAPH_SCROLL_EVENTS_DRAIN, () => {
      this._onSelectionChange();
    });
  },

  /**
   * Called to refresh the timer to keep firing _onRecordingTick.
   */
  _prepareNextTick: function () {
    // Check here to see if there's still a _timeoutId, incase
    // `stop` was called before the _prepareNextTick call was executed.
    if (this._timeoutId) {
      this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
    }
  },

  /**
   * Called when recording starts.
   */
  _onRecordingStarted: function () {
    this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);

    this.framerateGraph.dropSelection();
    this.framerateGraph.selectionEnabled = false;
    this.markersOverview.selectionEnabled = false;
    this.memoryOverview.selectionEnabled = false;
  },

  /**
   * Called when recording stops.
   */
  _onRecordingStopped: function () {
    clearTimeout(this._timeoutId);
    this._timeoutId = null;

    this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);

    this.framerateGraph.selectionEnabled = true;
    this.markersOverview.selectionEnabled = true;
    this.memoryOverview.selectionEnabled = true;
  }
};

// Decorates the OverviewView as an EventEmitter
EventEmitter.decorate(OverviewView);
