// Copyright (c) 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.

'use strict';

base.require('base.range');
base.require('tracing.constants');
base.require('tracing.selection');
base.require('tracing.trace_model.slice');

/**
 * @fileoverview Provides the TimingTool class.
 */
base.exportTo('tracing', function() {

  var constants = tracing.constants;

  /**
   * Tool for taking time measurements in the TimelineTrackView using
   * Viewportmarkers.
   * @constructor
   */
  var TimingTool = function(viewport, targetElement) {
    this.viewport = viewport;

    this.rangeStartMarker_ = viewport.createMarker(0);
    this.rangeEndMarker_ = viewport.createMarker(0);
    this.cursorMarker_ = viewport.createMarker(0);
    this.activeMarker_ = this.cursorMarker_;

    // Prepare the event handlers to be added and removed repeatedly.
    this.onMouseMove_ = this.onMouseMove_.bind(this);
    this.onDblClick_ = this.onDblClick_.bind(this);
    this.targetElement_ = targetElement;
  };

  TimingTool.prototype = {

    getWorldXFromEvent_: function(e) {
      var pixelRatio = window.devicePixelRatio || 1;
      var modelTrackContainer = this.viewport.modelTrackContainer;
      var viewX = (e.clientX -
                   modelTrackContainer.offsetLeft -
                   constants.HEADING_WIDTH) * pixelRatio;
      return this.viewport.currentDisplayTransform.xViewToWorld(viewX);
    },

    onEnterTiming: function(e) {
      // Show the cursor marker if it was the active marker, otherwise the two
      // range markers should be left.
      if (this.activeMarker_ === this.cursorMarker_)
        this.viewport.addMarker(this.cursorMarker_);

      this.targetElement_.addEventListener('mousemove', this.onMouseMove_);
      this.targetElement_.addEventListener('dblclick', this.onDblClick_);
    },

    onBeginTiming: function(e) {
      var mouseEvent = e.data;
      var worldX = this.getWorldXFromEvent_(mouseEvent);

      // Check if click was on a range marker that can be moved.
      if (!this.activeMarker_) {
        var marker = this.viewport.findMarkerNear(worldX, 6);
        if (marker === this.rangeStartMarker_ ||
            marker === this.rangeEndMarker_) {
          // Set the clicked marker as active marker so it will be moved.
          this.activeMarker_ = marker;
          marker.selected = true;
          return;
        }
      } else {
        // Otherwise start selecting a new range by hiding the cursor marker and
        // adding the end marker. This is skipped if there was already a range
        // on screen.
        this.viewport.removeMarker(this.cursorMarker_);
        this.viewport.addMarker(this.rangeEndMarker_);
      }

      // Set the range markers to the mouse or snapped position and select them.
      var snapPos = this.getSnappedToEventPosition_(mouseEvent);
      this.updateMarkerToSnapPosition_(this.rangeStartMarker_, snapPos);
      this.updateMarkerToSnapPosition_(this.rangeEndMarker_, snapPos);

      this.rangeStartMarker_.selected = true;
      this.rangeEndMarker_.selected = true;

      // The end marker is the one that is moved.
      this.activeMarker_ = this.rangeEndMarker_;
    },

    onUpdateTiming: function(e) {
      if (!this.activeMarker_ || this.activeMarker_ === this.cursorMarker_)
        return;

      var mouseEvent = e.data;

      // Update the position of the active marker to the cursor position.
      // This is either the end marker when creating a range, or one of the
      // range markers when they are moved.
      var snapPos = this.getSnappedToEventPosition_(mouseEvent);
      this.updateMarkerToSnapPosition_(this.activeMarker_, snapPos);

      // When creating a range, only show the start marker after the range
      // exceeds a certain amount. This prevents a short flicker showing the
      // dimmed areas left and right of the range when clicking.
      if (this.rangeStartMarker_.selected && this.rangeEndMarker_.selected) {
        var rangeX = Math.abs(this.rangeStartMarker_.positionView -
                              this.rangeEndMarker_.positionView);
        if (rangeX >= constants.MIN_MOUSE_SELECTION_DISTANCE)
          this.viewport.addMarker(this.rangeStartMarker_);
      }
    },

    onEndTiming: function(e) {
      var mouseEvent = e.data;

      if (!this.activeMarker_ || !this.activeMarker_.selected)
        return;

      e.consumed = true;

      // Check if a range selection is finished now.
      if (this.rangeStartMarker_.selected && this.rangeEndMarker_.selected) {
        var rangeX = Math.abs(this.rangeStartMarker_.positionView -
                              this.rangeEndMarker_.positionView);

        // The range is only valid when it exceeds the minimum mouse selection
        // distance, otherwise it could have been just a click.
        if (rangeX >= constants.MIN_MOUSE_SELECTION_DISTANCE) {
          this.rangeStartMarker_.selected = false;
          this.rangeEndMarker_.selected = false;
          this.activeMarker_ = null;
        } else {
          // If the range is not valid, hide it and activate the cursor marker.
          this.viewport.removeMarker(this.rangeStartMarker_);
          this.viewport.removeMarker(this.rangeEndMarker_);

          this.viewport.addMarker(this.cursorMarker_);
          this.cursorMarker_.positionWorld =
              this.getWorldXFromEvent_(mouseEvent);
          this.activeMarker_ = this.cursorMarker_;
          e.consumed = false;
        }
        return;
      }

      // Deselect and deactivate a range marker that was moved.
      this.activeMarker_.selected = false;
      this.activeMarker_ = null;
    },

    onExitTiming: function(e) {
      // If there is a selected range the markers are left on screen, but the
      // cursor marker gets removed.
      if (this.activeMarker_ === this.cursorMarker_)
        this.viewport.removeMarker(this.cursorMarker_);

      this.targetElement_.removeEventListener('mousemove', this.onMouseMove_);
      this.targetElement_.removeEventListener('dblclick', this.onDblClick_);
    },

    onMouseMove_: function(e) {
      var worldX = this.getWorldXFromEvent_(e);

      if (this.activeMarker_) {
        // Update the position of the cursor marker.
        if (this.activeMarker_ === this.cursorMarker_) {
          var snapPos = this.getSnappedToEventPosition_(e);
          this.updateMarkerToSnapPosition_(this.cursorMarker_, snapPos);
        }
        return;
      }

      // If there is no active marker then look for a marker close to the cursor
      // and indicate that it can be moved by displaying it selected.
      var marker = this.viewport.findMarkerNear(worldX, 6);
      if (marker === this.rangeStartMarker_ ||
          marker === this.rangeEndMarker_) {
        marker.selected = true;
      } else {
        // Otherwise deselect markers that may have been selected before.
        this.rangeEndMarker_.selected = false;
        this.rangeStartMarker_.selected = false;
      }
    },

    onDblClick_: function(e) {
      var modelTrackContainer = this.viewport.modelTrackContainer;
      var modelTrackContainerRect = modelTrackContainer.getBoundingClientRect();

      var eventWorldX = this.getWorldXFromEvent_(e);
      var y = e.clientY;

      var selection = new tracing.Selection();
      modelTrackContainer.addClosestEventToSelection(
          eventWorldX, Infinity, y, y, selection);

      if (!selection.length)
        return;

      var slice = selection[0];

      if (!(slice instanceof tracing.trace_model.Slice))
        return;

      if (slice.start > eventWorldX || slice.end < eventWorldX)
        return;

      var track = this.viewport.trackForEvent(slice);
      var trackRect = track.getBoundingClientRect();

      var snapPos = {
        x: slice.start,
        y: trackRect.top +
            modelTrackContainer.scrollTop - modelTrackContainerRect.top,
        height: trackRect.height,
        snapped: true
      };
      this.updateMarkerToSnapPosition_(this.rangeStartMarker_, snapPos);
      snapPos.x = slice.end;
      this.updateMarkerToSnapPosition_(this.rangeEndMarker_, snapPos);

      this.viewport.addMarker(this.rangeStartMarker_);
      this.viewport.addMarker(this.rangeEndMarker_);
      this.viewport.removeMarker(this.cursorMarker_);
      this.activeMarker_ = null;
    },

    /**
     * Get the closest position of an event within a vertical range of the mouse
     * position if possible, otherwise use the position of the mouse pointer.
     * @param {MouseEvent} e Mouse event with the current mouse coordinates.
     * @return {
     *   {Number} x, The x coordinate in world space.
     *   {Number} y, The y coordinate in world space.
     *   {Number} height, The height of the event.
     *   {boolean} snapped Whether the coordinates are from a snapped event or
     *     the mouse position.
     * }
     */
    getSnappedToEventPosition_: function(e) {
      var pixelRatio = window.devicePixelRatio || 1;
      var EVENT_SNAP_RANGE = 16 * pixelRatio;

      var modelTrackContainer = this.viewport.modelTrackContainer;
      var modelTrackContainerRect = modelTrackContainer.getBoundingClientRect();

      var viewport = this.viewport;
      var dt = viewport.currentDisplayTransform;
      var worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE);

      var worldX = this.getWorldXFromEvent_(e);
      var mouseY = e.clientY;

      var selection = new tracing.Selection();

      // Look at the track under mouse position first for better performance.
      modelTrackContainer.addClosestEventToSelection(
          worldX, worldMaxDist, mouseY, mouseY, selection);

      // Look at all tracks visible on screen.
      if (!selection.length) {
        modelTrackContainer.addClosestEventToSelection(
            worldX, worldMaxDist,
            modelTrackContainerRect.top, modelTrackContainerRect.bottom,
            selection);
      }

      var minDistX = worldMaxDist;
      var minDistY = Infinity;
      var pixWidth = dt.xViewVectorToWorld(1);

      // Create result object with the mouse coordinates.
      var result = {
        x: worldX,
        y: mouseY - modelTrackContainerRect.top,
        height: 0,
        snapped: false
      };

      var eventBounds = new base.Range();
      for (var i = 0; i < selection.length; i++) {
        var event = selection[i];
        var track = viewport.trackForEvent(event);
        var trackRect = track.getBoundingClientRect();

        eventBounds.reset();
        event.addBoundsToRange(eventBounds);
        var eventX;
        if (Math.abs(eventBounds.min - worldX) <
            Math.abs(eventBounds.max - worldX)) {
          eventX = eventBounds.min;
        } else {
          eventX = eventBounds.max;
        }

        var distX = eventX - worldX;

        var eventY = trackRect.top;
        var eventHeight = trackRect.height;
        var distY = Math.abs(eventY + eventHeight / 2 - mouseY);

        // Prefer events with a closer y position if their x difference is below
        // the width of a pixel.
        if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) &&
            distY < minDistY) {
          minDistX = distX;
          minDistY = distY;

          // Retrieve the event position from the hit.
          result.x = eventX;
          result.y = eventY +
              modelTrackContainer.scrollTop - modelTrackContainerRect.top;
          result.height = eventHeight;
          result.snapped = true;
        }
      }

      return result;
    },

    /**
     * Update the marker to the snapped position.
     * @param {ViewportMarker} marker The marker to be updated.
     * @param {
     *   {Number} x, The new positionWorld of the marker.
     *   {Number} y, The new indicatorY of the marker.
     *   {Number} height, The new indicatorHeight of the marker.
     *   {boolean} snapped Whether the coordinates are from a snapped event or
     *     the mouse position.
     * } snapPos
     */
    updateMarkerToSnapPosition_: function(marker, snapPos) {
      marker.setSnapIndicator(snapPos.snapped, snapPos.y, snapPos.height);
      marker.positionWorld = snapPos.x;
    }
  };

  return {
    TimingTool: TimingTool
  };
});
