// 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 View visualizes TRACE_EVENT events using the
 * tracing.Timeline component and adds in selection summary and control buttons.
 */
base.requireStylesheet('timeline_view');

base.require('timeline_track_view');
base.require('timeline_analysis_view');
base.require('category_filter_dialog');
base.require('filter');
base.require('find_control');
base.require('overlay');
base.require('importer.trace_event_importer');
base.require('importer.linux_perf_importer');
base.require('importer.v8_log_importer');
base.require('settings');

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

  /**
   * View
   * @constructor
   * @extends {HTMLDivElement}
   */
  var TimelineView = tracing.ui.define('div');

  TimelineView.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.classList.add('view');

      // Create individual elements.
      this.titleEl_ = document.createElement('div');
      this.titleEl_.textContent = 'Tracing: ';
      this.titleEl_.className = 'title';

      this.controlDiv_ = document.createElement('div');
      this.controlDiv_.className = 'control';

      this.leftControlsEl_ = document.createElement('div');
      this.leftControlsEl_.className = 'controls';
      this.rightControlsEl_ = document.createElement('div');
      this.rightControlsEl_.className = 'controls';

      var spacingEl = document.createElement('div');
      spacingEl.className = 'spacer';

      this.timelineContainer_ = document.createElement('div');
      this.timelineContainer_.className = 'container';

      var analysisContainer_ = document.createElement('div');
      analysisContainer_.className = 'analysis-container';

      this.analysisEl_ = new tracing.TimelineAnalysisView();

      this.dragEl_ = new DragHandle();
      this.dragEl_.target = analysisContainer_;

      this.findCtl_ = new tracing.FindControl();
      this.findCtl_.controller = new tracing.FindController();

      this.importErrorsButton_ = this.createImportErrorsButton_();
      this.categoryFilterButton_ = this.createCategoryFilterButton_();
      this.categoryFilterButton_.callback =
          this.updateCategoryFilterFromSettings_.bind(this);
      this.metadataButton_ = this.createMetadataButton_();

      // Connect everything up.
      this.rightControls.appendChild(this.importErrorsButton_);
      this.rightControls.appendChild(this.categoryFilterButton_);
      this.rightControls.appendChild(this.metadataButton_);
      this.rightControls.appendChild(this.findCtl_);
      this.controlDiv_.appendChild(this.titleEl_);
      this.controlDiv_.appendChild(this.leftControlsEl_);
      this.controlDiv_.appendChild(spacingEl);
      this.controlDiv_.appendChild(this.rightControlsEl_);
      this.appendChild(this.controlDiv_);

      this.appendChild(this.timelineContainer_);
      this.appendChild(this.dragEl_);

      analysisContainer_.appendChild(this.analysisEl_);
      this.appendChild(analysisContainer_);

      this.rightControls.appendChild(this.createHelpButton_());

      // Bookkeeping.
      this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
      document.addEventListener('keypress', this.onKeypress_.bind(this), true);
    },

    createImportErrorsButton_: function() {
      var dlg = new tracing.ui.Overlay();
      dlg.classList.add('view-import-errors-overlay');
      dlg.autoClose = true;

      var showEl = document.createElement('div');
      showEl.className = 'button view-import-errors-button view-info-button';
      showEl.textContent = 'Import errors!';

      var textEl = document.createElement('div');
      textEl.className = 'info-button-text import-errors-dialog-text';

      var containerEl = document.createElement('div');
      containerEl.className = 'info-button-container' +
          'import-errors-dialog';

      containerEl.textContent = 'Errors occurred during import:';
      containerEl.appendChild(textEl);
      dlg.appendChild(containerEl);

      var that = this;
      function onClick() {
        dlg.visible = true;
        textEl.textContent = that.model.importErrors.join('\n');
      }
      showEl.addEventListener('click', onClick.bind(this));

      function updateVisibility() {
        if (that.model &&
            that.model.importErrors.length)
          showEl.style.display = '';
        else
          showEl.style.display = 'none';
      }
      updateVisibility();
      that.addEventListener('modelChange', updateVisibility);

      return showEl;
    },

    createCategoryFilterButton_: function() {
      // Set by the embedder of the help button that we create in this function.
      var callback;

      var showEl = document.createElement('div');
      showEl.className = 'button view-info-button';
      showEl.textContent = 'Categories';
      showEl.__defineSetter__('callback', function(value) {
        callback = value;
      });


      var that = this;
      function onClick() {
        var dlg = new tracing.CategoryFilterDialog();
        dlg.categories = that.model.categories;
        dlg.settings = that.settings;
        dlg.settings_key = 'categories';
        dlg.settingUpdatedCallback = callback;
        dlg.visible = true;
      }

      function updateVisibility() {
        if (that.model)
          showEl.style.display = '';
        else
          showEl.style.display = 'none';
      }
      updateVisibility();
      that.addEventListener('modelChange', updateVisibility);

      showEl.addEventListener('click', onClick.bind(this));
      return showEl;
    },

    createHelpButton_: function() {
      var dlg = new tracing.ui.Overlay();
      dlg.classList.add('view-help-overlay');
      dlg.autoClose = true;
      dlg.additionalCloseKeyCodes.push('?'.charCodeAt(0));

      var showEl = document.createElement('div');
      showEl.className = 'button view-help-button';
      showEl.textContent = '?';

      var helpTextEl = document.createElement('div');
      helpTextEl.style.whiteSpace = 'pre';
      helpTextEl.style.fontFamily = 'monospace';
      dlg.appendChild(helpTextEl);

      function onClick(e) {
        dlg.visible = true;
        if (this.timeline_)
          helpTextEl.textContent = this.timeline_.keyHelp;
        else
          helpTextEl.textContent = 'No content loaded. For interesting help,' +
              ' load something.';

        // Stop event so it doesn't trigger new click listener on document.
        e.stopPropagation();
        return false;
      }

      showEl.addEventListener('click', onClick.bind(this));

      return showEl;
    },

    createMetadataButton_: function() {
      var dlg = new tracing.ui.Overlay();
      dlg.classList.add('view-metadata-overlay');
      dlg.autoClose = true;

      var showEl = document.createElement('div');
      showEl.className = 'button view-metadata-button view-info-button';
      showEl.textContent = 'Metadata';

      var textEl = document.createElement('div');
      textEl.className = 'info-button-text metadata-dialog-text';

      var containerEl = document.createElement('div');
      containerEl.className = 'info-button-container metadata-dialog';

      containerEl.textContent = 'Metadata Info:';
      containerEl.appendChild(textEl);
      dlg.appendChild(containerEl);

      var that = this;
      function onClick() {
        dlg.visible = true;

        var metadataStrings = [];

        var model = that.model;
        for (var data in model.metadata) {
          metadataStrings.push(JSON.stringify(model.metadata[data].name) +
                               ': ' + JSON.stringify(model.metadata[data].value, undefined, ' '));
        }
        textEl.textContent = metadataStrings.join('\n');
      }
      showEl.addEventListener('click', onClick.bind(this));

      function updateVisibility() {
        if (that.model &&
            that.model.metadata.length)
          showEl.style.display = '';
        else
          showEl.style.display = 'none';
      }
      updateVisibility();
      that.addEventListener('modelChange', updateVisibility);

      return showEl;
    },

    get leftControls() {
      return this.leftControlsEl_;
    },

    get rightControls() {
      return this.rightControlsEl_;
    },

    get title() {
      return this.titleEl_.textContent.substring(
          this.titleEl_.textContent.length - 2);
    },

    set title(text) {
      this.titleEl_.textContent = text + ':';
    },

    set traceData(traceData) {
      this.model = new tracing.Model(traceData);
    },

    get model() {
      if (this.timeline_)
        return this.timeline_.model;
      return undefined;
    },

    set model(model) {
      var modelInstanceChanged = model != this.model;
      var modelValid = model && !model.bounds.isEmpty;

      // Remove old timeline if the model has completely changed.
      if (modelInstanceChanged) {
        this.timelineContainer_.textContent = '';
        if (this.timeline_) {
          this.timeline_.removeEventListener(
              'selectionChange', this.onSelectionChangedBoundToThis_);
          this.timeline_.detach();
          this.timeline_ = undefined;
          this.findCtl_.controller.timeline = undefined;
        }
      }

      // Create new timeline if needed.
      if (modelValid && !this.timeline_) {
        this.timeline_ = new tracing.TimelineTrackView();
        this.timeline_.focusElement =
            this.focusElement_ ? this.focusElement_ : this.parentElement;
        this.timelineContainer_.appendChild(this.timeline_);
        this.findCtl_.controller.timeline = this.timeline_;
        this.timeline_.addEventListener(
            'selectionChange', this.onSelectionChangedBoundToThis_);
        this.updateCategoryFilterFromSettings_();
      }

      // Set the model.
      if (modelValid)
        this.timeline_.model = model;
      base.dispatchSimpleEvent(this, 'modelChange');

      // Do things that are selection specific
      if (modelInstanceChanged)
        this.onSelectionChanged_();
    },

    get timeline() {
      return this.timeline_;
    },

    get settings() {
      if (!this.settings_)
        this.settings_ = new base.Settings();
      return this.settings_;
    },

    /**
     * Sets the element whose focus state will determine whether
     * to respond to keybaord input.
     */
    set focusElement(value) {
      this.focusElement_ = value;
      if (this.timeline_)
        this.timeline_.focusElement = value;
    },

    /**
     * @return {Element} The element whose focused state determines
     * whether to respond to keyboard inputs.
     * Defaults to the parent element.
     */
    get focusElement() {
      if (this.focusElement_)
        return this.focusElement_;
      return this.parentElement;
    },

    /**
     * @return {boolean} Whether the current timeline is attached to the
     * document.
     */
    get isAttachedToDocument_() {
      var cur = this;
      while (cur.parentNode)
        cur = cur.parentNode;
      return cur == this.ownerDocument;
    },

    get listenToKeys_() {
      if (!this.isAttachedToDocument_)
        return;
      if (!this.focusElement_)
        return true;
      if (this.focusElement.tabIndex >= 0)
        return document.activeElement == this.focusElement;
      return true;
    },

    onKeypress_: function(e) {
      if (!this.listenToKeys_)
        return;

      if (event.keyCode == '/'.charCodeAt(0)) { // / key
        this.findCtl_.focus();
        event.preventDefault();
        return;
      } else if (e.keyCode == '?'.charCodeAt(0)) {
        this.querySelector('.view-help-button').click();
        e.preventDefault();
      }
    },

    beginFind: function() {
      if (this.findInProgress_)
        return;
      this.findInProgress_ = true;
      var dlg = tracing.FindControl();
      dlg.controller = new tracing.FindController();
      dlg.controller.timeline = this.timeline;
      dlg.visible = true;
      dlg.addEventListener('close', function() {
        this.findInProgress_ = false;
      }.bind(this));
      dlg.addEventListener('findNext', function() {
      });
      dlg.addEventListener('findPrevious', function() {
      });
    },

    onSelectionChanged_: function(e) {
      var oldScrollTop = this.timelineContainer_.scrollTop;

      var selection = this.timeline_ ?
          this.timeline_.selection :
          new tracing.Selection();
      this.analysisEl_.selection = selection;
      this.timelineContainer_.scrollTop = oldScrollTop;
    },

    updateCategoryFilterFromSettings_: function() {
      if (!this.timeline_)
        return;

      // Get the disabled categories from settings.
      var categories = this.settings.keys('categories');
      var disabledCategories = [];
      for (var i = 0; i < categories.length; i++) {
        if (this.settings.get(categories[i], 'true', 'categories') == 'false')
          disabledCategories.push(categories[i]);
      }

      this.timeline_.categoryFilter =
          new tracing.CategoryFilter(disabledCategories);
    }
  };

  /**
   * Timeline Drag Handle
   * Detects when user clicks handle determines new height of container based
   * on user's vertical mouse move and resizes the target.
   * @constructor
   * @extends {HTMLDivElement}
   * You will need to set target to be the draggable element
   */
  var DragHandle = tracing.ui.define('div');

  DragHandle.prototype = {
    __proto__: HTMLDivElement.prototype,

    decorate: function() {
      this.className = 'drag-handle';
      this.lastMousePosY = 0;
      this.dragAnalysis = this.dragAnalysis.bind(this);
      this.onMouseUp = this.onMouseUp.bind(this);
      this.addEventListener('mousedown', this.onMouseDown);
    },

    dragAnalysis: function(e) {
      // Compute the difference in height position.
      var dy = this.lastMousePosY - e.clientY;
      // If style is not set, start off with computed height.
      if (!this.target.style.height)
        this.target.style.height = window.getComputedStyle(this.target).height;
      // Calculate new height of the container.
      this.target.style.height = parseInt(this.target.style.height) + dy + 'px';
      this.lastMousePosY = e.clientY;
    },

    onMouseDown: function(e) {
      this.lastMousePosY = e.clientY;
      document.addEventListener('mousemove', this.dragAnalysis);
      document.addEventListener('mouseup', this.onMouseUp);
      e.stopPropagation();
      return false;
    },

    onMouseUp: function(e) {
      document.removeEventListener('mousemove', this.dragAnalysis);
      document.removeEventListener('mouseup', this.onMouseUp);
    }
  };

  return {
    TimelineView: TimelineView
  };
});