/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

var App = require('app');
var string_utils = require('utils/string_utils');
var dateUtils = require('utils/date/date');

/**
 * @class
 *
 * This is a view which GETs data from a URL and shows it as a time based line
 * graph. Time is shown on the X axis with data series shown on Y axis. It
 * optionally also has the ability to auto refresh itself over a given time
 * interval.
 *
 * This is an abstract class which is meant to be extended.
 *
 * Extending classes should override the following:
 * <ul>
 * <li>url - from where the data can be retrieved
 * <li>title - Title to be displayed when showing the chart
 * <li>id - which uniquely identifies this chart in any page
 * <li>seriesTemplate - template used by getData method to process server data
 * </ul>
 *
 * Extending classes could optionally override the following:
 * <ul>
 * <li>#getData(jsonData) - function to map server data into series format
 * ready for export to graph and JSON formats
 * <li>#colorForSeries(series) - function to get custom colors per series
 * </ul>
 *
 * @extends Ember.Object
 * @extends Ember.View
 */
App.ChartLinearTimeView = Ember.View.extend(App.ExportMetricsMixin, {
  templateName: require('templates/main/charts/linear_time'),

  /**
   * The URL from which data can be retrieved.
   *
   * This property must be provided for the graph to show properly.
   *
   * @type String
   * @default null
   */
  url: null,

  /**
   * A unique ID for this chart.
   *
   * @type String
   * @default null
   */
  id: null,

  /**
   * Title to be shown under the chart.
   *
   * @type String
   * @default null
   */
  title: null,

  /**
   * @private
   *
   * @type Rickshaw.Graph
   * @default null
   */
  _graph: null,

  /**
   * Array of classnames for each series (in widget)
   * @type Rickshaw.Graph
   */
  _popupGraph: null,

  /**
   * Array of classnames for each series
   * @type Array
   */
  _seriesProperties: null,

  /**
   * Array of classnames for each series (in widget)
   * @type Array
   */
  _seriesPropertiesWidget: null,


  /**
   * Renderer type
   * See <code>Rickshaw.Graph.Renderer</code> for more info
   * @type String
   */
  renderer: 'area',

  /**
   * Suffix used in DOM-elements selectors
   * @type String
   */
  popupSuffix: '-popup',

  /**
   * Is popup for current graph open
   * @type Boolean
   */
  isPopup: false,

  /**
   * Is graph ready
   * @type Boolean
   */
  isReady: false,

  /**
   * Is popup-graph ready
   * @type Boolean
   */
  isPopupReady: false,

  /**
   * Is data for graph available
   * @type Boolean
   */
  hasData: true,

  /**
   * chart height
   * @type {number}
   * @default 150
   */
  height: 150,

  /**
   * @type {string}
   * @default null
   */
  displayUnit: null,

  /**
   * Object containing information to get metrics data from API response
   * Supported properties:
   * <ul>
   * <li>path - exact path to metrics data in response JSON (required)
   * <li>displayName(name, hostName) - returns display name for metrics
   * depending on property name in response JSON and host name for Flume Agents
   * <li>factor - number that metrics values should be multiplied by
   * <li>flumePropertyName - property name to access certain Flume metrics
   * </ul>
   * @type {Object}
   */
  seriesTemplate: null,

  /**
   * Incomplete metrics requests
   * @type {array}
   * @default []
   */
  runningRequests: [],

  /**
   * Incomplete metrics requests for detailed view
   * @type {array}
   * @default []
   */
  runningPopupRequests: [],

  _containerSelector: Em.computed.format('#{0}-container', 'id'),

  _popupSelector: Em.computed.concat('', '_containerSelector', 'popupSuffix'),

  /**
   * @type {boolean}
   */
  isRequestRunning: function() {
    var requestsArrayName = this.get('isPopup') ? 'runningPopupRequests' : 'runningRequests';
    return this.get(requestsArrayName).mapProperty('ajaxIndex').contains(this.get('ajaxIndex'));
  }.property('runningPopupRequests', 'runningRequests'),

  didInsertElement: function () {
    var self = this;
    this.setYAxisFormatter();
    this.loadData();
    this.registerGraph();
    this.$().parent().on('mouseleave', function () {
      self.set('isExportMenuHidden', true);
    });
    App.tooltip(this.$("[rel='ZoomInTooltip']"), {
      placement: 'left',
      template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner graph-tooltip"></div></div>'
    });
  },

  setCurrentTimeIndex: function () {
    this.set('currentTimeIndex', this.get('parentView.currentTimeRangeIndex'));
  }.observes('parentView.currentTimeRangeIndex'),

  /**
   * Maps server data into series format ready for export to graph and JSON formats
   * @param jsonData
   * @returns {Array}
   */
  getData: function (jsonData) {
    var dataArray = [],
      template = this.get('seriesTemplate'),
      data = Em.get(jsonData, template.path);
    if (data) {
      for (var name in data) {
        var currentData = data[name];
        if (currentData) {
          if (Array.isArray(currentData)) {
            currentData = currentData.slice(0);
          }
          var factor = template.factor,
            displayName = template.displayName ? template.displayName(name) : name;
          if (!Em.isNone(factor)) {
            var dataLength = currentData.length;
            for (var i = dataLength; i--;) {
              currentData[i] = [currentData[i][0] * factor, currentData[i][1]];
            }
          }
          dataArray.push({
            name: displayName,
            data: currentData
          });
        }
      }
    }
    return dataArray;
  },

  /**
   * Maps server data for certain Flume metrics into series format ready for export
   * to graph and JSON formats
   * @param jsonData
   * @returns {Array}
   */
  getFlumeData: function (jsonData) {
    var dataArray = [];
    if (jsonData && jsonData.host_components) {
      jsonData.host_components.forEach(function (hc) {
        var hostName = hc.HostRoles.host_name,
          host = App.Host.find(hostName),
          template = this.get('template'),
          data = Em.get(hc, template.path);
        if (host && host.get('publicHostName')) {
          hostName = host.get('publicHostName');
        }
        if (data) {
          for (var cname in data) {
            var seriesName = template.displayName ? template.displayName(cname, hostName) : cname,
              seriesData = template.flumePropertyName ? data[cname][template.flumePropertyName] : data[cname];
            if (seriesData) {
              var factor = template.factor;
              if (!Em.isNone(factor)) {
                var dataLength = seriesData.length;
                for (var i = dataLength; i--;) {
                  seriesData[i][0] *= factor;
                }
              }
              dataArray.push({
                name: seriesName,
                data: seriesData
              });
            }
          }
        }
      }, this);
    }
    return dataArray;
  },

  /**
   * Function to map data into graph series
   * @param jsonData
   * @returns {Array}
   */
  transformToSeries: function (jsonData) {
    var seriesArray = [],
      seriesData = this.getData(jsonData),
      dataLength = seriesData.length;
    for (var i = 0; i < dataLength; i++) {
      seriesArray.push(this.transformData(seriesData[i].data, seriesData[i].name));
    }
    return seriesArray;
  },

  setExportTooltip: function () {
    if (this.get('isReady')) {
      Em.run.next(this, function () {
        var icon = this.$('.corner-icon');
        if (icon) {
          icon.on('mouseover', function () {
            $(this).closest("[rel='ZoomInTooltip']").trigger('mouseleave');
          });
          App.tooltip(icon.children('.icon-save'), {
            title: Em.I18n.t('common.export')
          });
        }
      });
    }
  }.observes('isReady'),

  willDestroyElement: function () {
    this.$("[rel='ZoomInTooltip']").tooltip('destroy');
    $(this.get('_containerSelector') + ' li.line').off();
    $(this.get('_popupSelector') + ' li.line').off();
    App.ajax.abortRequests(this.get('runningRequests'));
  },

  registerGraph: function () {
    var graph = {
      name: this.get('title'),
      id: this.get('elementId'),
      popupId: this.get('id')
    };
    App.router.get('updateController.graphs').push(graph);
  },

  loadData: function () {
    var self = this,
      isPopup = this.get('isPopup');
    if (this.get('loadGroup') && !isPopup) {
      return App.ChartLinearTimeView.LoadAggregator.add(this, this.get('loadGroup'));
    } else {
      var requestsArrayName = isPopup ? 'runningPopupRequests' : 'runningRequests',
        request = App.ajax.send({
          name: this.get('ajaxIndex'),
          sender: this,
          data: this.getDataForAjaxRequest(),
          success: 'loadDataSuccessCallback',
          error: 'loadDataErrorCallback',
          callback: function () {
            self.set(requestsArrayName, self.get(requestsArrayName).reject(function (item) {
              return item === request;
            }));
          }
        });
      if (request) request.ajaxIndex = this.get('ajaxIndex');
      this.get(requestsArrayName).push(request);
      return request;
    }
  },

  getDataForAjaxRequest: function () {
    var fromSeconds,
      toSeconds,
      hostName = (this.get('content')) ? this.get('content.hostName') : "",
      HDFSService = App.HDFSService.find().objectAt(0),
      nameNodeName = "",
      YARNService = App.YARNService.find().objectAt(0),
      resourceManager = YARNService ? YARNService.get('resourceManager.hostName') : "";
    if (HDFSService) {
      nameNodeName = (HDFSService.get('activeNameNode')) ? HDFSService.get('activeNameNode.hostName') : HDFSService.get('nameNode.hostName');
    }
    if (this.get('currentTimeIndex') === 8 && !Em.isNone(this.get('customStartTime')) && !Em.isNone(this.get('customEndTime'))) {
      // Custom start and end time is specified by user
      toSeconds = this.get('customEndTime') / 1000;
      fromSeconds = this.get('customStartTime') / 1000;
    } else {
      // Preset time range is specified by user
      var timeUnit = this.get('timeUnitSeconds');
      toSeconds = Math.round(App.dateTime() / 1000);
      fromSeconds = toSeconds - timeUnit;
    }
    return {
      toSeconds: toSeconds,
      fromSeconds: fromSeconds,
      stepSeconds: 15,
      hostName: hostName,
      nameNodeName: nameNodeName,
      resourceManager: resourceManager
    };
  },

  loadDataSuccessCallback: function (response) {
    this._refreshGraph(response);
  },

  loadDataErrorCallback: function (xhr, textStatus, errorThrown) {
    if (!xhr.isForcedAbort) {
      this.set('isReady', true);
      if (xhr.readyState == 4 && xhr.status) {
        textStatus = xhr.status + " " + textStatus;
      }
      this._showMessage('warn', this.t('graphs.error.title'), this.t('graphs.error.message').format(textStatus, errorThrown));
      this.setProperties({
        hasData: false,
        isExportButtonHidden: true
      });
    }
  },

  /**
   * Shows a yellow warning message in place of the chart.
   *
   * @param type  Can be any of 'warn', 'error', 'info', 'success'
   * @param title Bolded title for the message
   * @param message String representing the message
   * @param tooltip Tooltip content
   * @type: Function
   */
  _showMessage: function (type, title, message, tooltip) {
    var popupSuffix = this.get('isPopup') ? this.get('popupSuffix') : '';
    var chartOverlay = '#' + this.get('id');
    var chartOverlayId = chartOverlay + '-chart' + popupSuffix;
    var chartOverlayY = chartOverlay + '-yaxis' + popupSuffix;
    var chartOverlayX = chartOverlay + '-xaxis' + popupSuffix;
    var chartOverlayLegend = chartOverlay + '-legend' + popupSuffix;
    var chartOverlayTimeline = chartOverlay + '-timeline' + popupSuffix;
    var tooltipTitle = tooltip ? tooltip : Em.I18n.t('graphs.tooltip.title');
    var chartContent = '';

    var typeClass;
    switch (type) {
      case 'error':
        typeClass = 'alert-error';
        break;
      case 'success':
        typeClass = 'alert-success';
        break;
      case 'info':
        typeClass = 'alert-info';
        break;
      default:
        typeClass = '';
        break;
    }
    $(chartOverlayId + ', ' + chartOverlayY + ', ' + chartOverlayX + ', ' + chartOverlayLegend + ', ' + chartOverlayTimeline).html('');
    chartContent += '<div class=\"alert ' + typeClass + '\">';
    if (title) {
      chartContent += '<strong>' + title + '</strong> ';
    }
    chartContent += message + '</div>';
    $(chartOverlayId).append(chartContent);
    $(chartOverlayId).parent().attr('data-original-title', tooltipTitle);
  },

  /**
   * Transforms the JSON data retrieved from the server into the series
   * format that Rickshaw.Graph understands.
   *
   * The series object is generally in the following format: [ { name :
   * "Series 1", data : [ { x : 0, y : 0 }, { x : 1, y : 1 } ] } ]
   *
   * Extending classes should override this method.
   *
   * @param seriesData
   *          Data retrieved from the server
   * @param displayName
   *          Graph title
   * @type: Function
   *
   */
  transformData: function (seriesData, displayName) {
    if (!Em.isNone(seriesData)) {
      // Is it a string?
      if ("string" == typeof seriesData) {
        seriesData = JSON.parse(seriesData);
      }
      // Is it a number?
      if ("number" == typeof seriesData) {
        // Same number applies to all time.
        var number = seriesData;
        seriesData = [];
        seriesData.push([number, App.dateTime() - (60 * 60)]);
        seriesData.push([number, App.dateTime()]);
      }
      // We have valid data
      var series = {};
      series.name = displayName;
      series.data = [];
      var timeDiff = App.dateTimeWithTimeZone(seriesData[0][1] * 1000) / 1000 - seriesData[0][1];
      for (var index = 0; index < seriesData.length; index++) {
        series.data.push({
          x: seriesData[index][1] + timeDiff,
          y: seriesData[index][0]
        });
      }
      return series;
    }
    return null;
  },

  /**
   * Provides the formatter to use in displaying Y axis.
   *
   * By default, uses the App.ChartLinearTimeView.DefaultFormatter which shows 10K,
   * 300M etc.
   *
   * @type Function
   */
  yAxisFormatter: function (y) {
    return App.ChartLinearTimeView.DefaultFormatter(y);
  },

  /**
   * Sets the formatter to use in displaying Y axis depending on graph unit.
   *
   * @type Function
   */
  setYAxisFormatter: function () {
    var method,
      formatterMap = {
        '%': 'PercentageFormatter',
        '/s': 'CreateRateFormatter',
        'B': 'BytesFormatter',
        'ms': 'TimeElapsedFormatter'
      },
      methodName = formatterMap[this.get('displayUnit')];
    if (methodName) {
      method = (methodName == 'CreateRateFormatter') ?
        App.ChartLinearTimeView.CreateRateFormatter('', App.ChartLinearTimeView.DefaultFormatter) :
        App.ChartLinearTimeView[methodName];
      this.set('yAxisFormatter', method);
    }
  },

  /**
   * Provides the color (in any HTML color format) to use for a particular
   * series.
   * May be redefined in child views
   *
   * @param series
   *          Series for which color is being requested
   * @return color String. Returning null allows this chart to pick a color
   *         from palette.
   * @default null
   * @type Function
   */
  colorForSeries: function (series) {
    return null;
  },

  /**
   * Check whether seriesData is correct data for chart drawing
   * @param {Array} seriesData
   * @return {Boolean}
   */
  checkSeries: function (seriesData) {
    if (!seriesData || !seriesData.length) {
      return false;
    }
    var result = true;
    seriesData.forEach(function (item) {
      if (Em.isNone(Em.get(item, 'data.0.x'))) {
        result = false;
      }
    });
    return result;
  },

  /**
   * @private
   *
   * Refreshes the graph with the latest JSON data.
   *
   * @type Function
   */
  _refreshGraph: function (jsonData, graphView) {
    if (this.get('isDestroyed')) {
      return;
    }
    console.time('_refreshGraph');
    var seriesData = this.transformToSeries(jsonData);

    //if graph opened as modal popup
    var popup_path = $(this.get('_popupSelector'));
    var graph_container = $(this.get('_containerSelector'));
    var seconds = this.get('parentView.graphSeconds');
    var container;
    if (!Em.isNone(seconds)) {
      this.set('timeUnitSeconds', seconds);
      this.set('parentView.graphSeconds', null);
    }
    if (popup_path.length) {
      popup_path.children().each(function () {
        $(this).children().remove();
      });
      this.set('isPopup', true);
    }
    else {
      graph_container.children().each(function () {
        if (!($(this).is('.export-graph-list-container, .corner-icon'))) {
          $(this).children().remove();
        }
      });
    }
    var hasData = this.checkSeries(seriesData);
    var view = graphView || this;
    view.set('isExportButtonHidden', !hasData);
    if (hasData) {
      // Check container exists (may be not, if we go to another page and wait while graphs loading)
      if (graph_container.length) {
        container = $(this.get('_containerSelector'));
        this.draw(seriesData);
        this.set('hasData', true);
        //move yAxis value lower to make them fully visible
        container.find('.y_axis text').attr('y', 8);
        container.attr('data-original-title', Em.I18n.t('graphs.tooltip.title'));
      }
    }
    else {
      this.set('isPopupReady', true);
      this.set('isReady', true);
      //if Axis X time interval is default(60 minutes)
      if (this.get('timeUnitSeconds') === 3600) {
        this._showMessage('info', null, this.t('graphs.noData.message'), this.t('graphs.noData.tooltip.title'));
        this.set('hasData', false);
      }
      else {
        this._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noDataAtTime.message'));
      }
    }
    graph_container = null;
    container = null;
    popup_path = null;
    console.timeEnd('_refreshGraph');
  },

  /**
   * Returns a custom time unit, that depends on X axis interval length, for the graph's X axis.
   * This is needed as Rickshaw's default time X axis uses UTC time, which can be confusing
   * for users expecting locale specific time.
   *
   * If <code>null</code> is returned, Rickshaw's default time unit is used.
   *
   * @type Function
   * @return Rickshaw.Fixtures.Time
   */
  localeTimeUnit: function (timeUnitSeconds) {
    var timeUnit = new Rickshaw.Fixtures.Time(),
      unitName;
    if (timeUnitSeconds < 172800) {
      timeUnit = {
        name: timeUnitSeconds / 240 + ' minute',
        seconds: timeUnitSeconds / 4,
        formatter: function (d) {
          // format locale specific time
          var minutes = dateUtils.dateFormatZeroFirst(d.getMinutes());
          var hours = dateUtils.dateFormatZeroFirst(d.getHours());
          return hours + ":" + minutes;
        }
      };
    } else if (timeUnitSeconds < 1209600) {
      timeUnit = timeUnit.unit('day');
    } else if (timeUnitSeconds < 5184000) {
      timeUnit = timeUnit.unit('week');
    } else if (timeUnitSeconds < 62208000) {
      timeUnit = timeUnit.unit('month');
    } else {
      timeUnit = timeUnit.unit('year');
    }
    return timeUnit;
  },

  /**
   * calculate statistic data for popup legend and set proper colors for series
   * @param {Array} data
   */
  dataPreProcess: function (data) {
    var self = this;
    var palette = new Rickshaw.Color.Palette({scheme: 'munin'});
    // Format series for display
    var series_min_length = 100000000;
    data.forEach(function (series) {
      var displayUnit = self.get('displayUnit');
      var seriesColor = self.colorForSeries(series);
      if (Em.isNone(seriesColor)) {
        seriesColor = palette.color();
      }
      series.color = seriesColor;
      series.stroke = 'rgba(0,0,0,0.3)';
      // calculate statistic data for popup legend
      var avg = 0;
      var min = Number.MAX_VALUE;
      var max = Number.MIN_VALUE;
      var numberOfNotNullValues = 0;
      series.isZero = true;
      for (var i = 0; i < series.data.length; i++) {
        avg += series.data[i]['y'];
        if (series.data[i]['y'] !== null) {
          numberOfNotNullValues++;
        }
        if (!Em.isNone(series.data[i]['y'])) {
          if (series.data[i]['y'] < min) {
            min = series.data[i]['y'];
          }
        }
        if (series.data[i]['y'] > max) {
          max = series.data[i]['y'];
        }
        if (series.data[i]['y'] > 1) {
          series.isZero = false;
        }
      }
      if (self.get('isPopup')) {
        series.name = string_utils.pad(series.name.length > 36 ? series.name.substr(0, 36) + '...' : series.name, 40, '&nbsp;', 2) + '|&nbsp;' +
          string_utils.pad('min', 5, '&nbsp;', 3) +
          string_utils.pad(self.get('yAxisFormatter')(min), 12, '&nbsp;', 3) +
          string_utils.pad('avg', 5, '&nbsp;', 3) +
          string_utils.pad(self.get('yAxisFormatter')(avg / numberOfNotNullValues), 12, '&nbsp;', 3) +
          string_utils.pad('max', 12, '&nbsp;', 3) +
          string_utils.pad(self.get('yAxisFormatter')(max), 5, '&nbsp;', 3);
      }
      if (series.isZero) {
        series.stroke = series.color;
      }
      if (series.data.length < series_min_length) {
        series_min_length = series.data.length;
      }
    });

    // All series should have equal length
    data.forEach(function (series) {
      if (series.data.length > series_min_length) {
        series.data.length = series_min_length;
      }
    });
  },

  draw: function (seriesData) {
    var self = this;
    var isPopup = this.get('isPopup');
    var p = isPopup ? this.get('popupSuffix') : '';

    this.dataPreProcess(seriesData);

    var chartElement = document.querySelector("#" + this.get('id') + "-chart" + p);
    var overlayElement = document.querySelector(+this.get('id') + "-container" + p);
    var xaxisElement = document.querySelector("#" + this.get('id') + "-xaxis" + p);
    var yaxisElement = document.querySelector("#" + this.get('id') + "-yaxis" + p);
    var legendElement = document.querySelector("#" + this.get('id') + "-legend" + p);

    var graphSize = this._calculateGraphSize();

    var _graph = new Rickshaw.GraphReopened({
      height: graphSize.height,
      width: graphSize.width,
      element: chartElement,
      series: seriesData,
      interpolation: 'step-after',
      stroke: true,
      renderer: this.get('renderer'),
      strokeWidth: 'area' === this.get('renderer') ? 1 : 2,
      max: seriesData.everyProperty('isZero') ? 1 : null
    });

    if ('area' === this.get('renderer')) {
      _graph.renderer.unstack = false;
    }

    new Rickshaw.Graph.Axis.Time({
      graph: _graph,
      timeUnit: this.localeTimeUnit(this.get('timeUnitSeconds'))
    });

    new Rickshaw.Graph.Axis.Y({
      tickFormat: this.yAxisFormatter,
      pixelsPerTick: isPopup ? 75 : 40,
      element: yaxisElement,
      orientation: isPopup ? 'left' : 'right',
      graph: _graph
    });

    var legend = new Rickshaw.Graph.Legend({
      graph: _graph,
      element: legendElement,
      description: self.get('description')
    });

    new Rickshaw.Graph.Behavior.Series.Toggle({
      graph: _graph,
      legend: legend
    });

    new Rickshaw.Graph.Behavior.Series.Order({
      graph: _graph,
      legend: legend
    });

    if (!isPopup) {
      $(overlayElement).on('mousemove', function () {
        $(xaxisElement).removeClass('hide');
        $(legendElement).removeClass('hide');
        $(chartElement).children("div").removeClass('hide');
      });
      $(overlayElement).on('mouseout', function () {
        $(legendElement).addClass('hide');
      });
      _graph.onUpdate(function () {
        $(legendElement).addClass('hide');
      });
    }

    this.$().on('remove', function () {
      $(overlayElement).off();
    });

    //show the graph when it's loaded
    _graph.onUpdate(function () {
      self.set('isReady', true);
    });
    _graph.render();

    if (isPopup) {
      new Rickshaw.Graph.HoverDetail({
        graph: _graph,
        yFormatter: function (y) {
          return self.yAxisFormatter(y);
        },
        xFormatter: function (x) {
          return (new Date(x * 1000)).toLocaleTimeString();
        },
        formatter: function (series, x, y, formattedX, formattedY) {
          return formattedY + '<br />' + formattedX;
        }
      });
    }

    _graph = this.updateSeriesInGraph(_graph);
    if (isPopup) {
      //show the graph when it's loaded
      _graph.onUpdate(function () {
        self.set('isPopupReady', true);
      });
      _graph.update();

      var popupSelector = this.get('_popupSelector');
      $(popupSelector + ' li.line').click(function () {
        var series = [];
        $(popupSelector + ' a.action').each(function (index, v) {
          series[index] = v.parentNode.classList;
        });
        self.set('_seriesProperties', series);
      });

      this.set('_popupGraph', _graph);
    }
    else {
      _graph.update();
      var containerSelector = this.get('_containerSelector');
      $(containerSelector + ' li.line').click(function () {
        var series = [];
        $(containerSelector + ' a.action').each(function (index, v) {
          series[index] = v.parentNode.classList;
        });
        self.set('_seriesPropertiesWidget', series);
      });

      this.set('_graph', _graph);
    }
  },

  /**
   * Calculate graph size
   * @returns {{width: number, height: number}}
   * @private
   */
  _calculateGraphSize: function () {
    var isPopup = this.get('isPopup');
    var height = this.get('height');
    var width = 400;
    var diff = 32;

    if (this.get('inWidget')) {
      height = 105; // for widgets view
      diff = 22;
    }
    if (isPopup) {
      height = 180;
      width = 670;
    }
    else {
      // If not in popup, the width could vary.
      // We determine width based on div's size.
      var thisElement = this.get('element');
      if (!Em.isNone(thisElement)) {
        var calculatedWidth = $(thisElement).width();
        if (calculatedWidth > diff) {
          width = calculatedWidth - diff;
        }
      }
    }
    return {
      width: width,
      height: height
    }
  },

  /**
   *
   * @param {Rickshaw.Graph} graph
   * @returns {Rickshaw.Graph}
   */
  updateSeriesInGraph: function (graph) {
    var id = this.get('id');
    var isPopup = this.get('isPopup');
    var popupSuffix = this.get('popupSuffix');
    var _series = isPopup ? this.get('_seriesProperties') : this.get('_seriesPropertiesWidget');
    graph.series.forEach(function (series, index) {
      if (_series && !Em.isNone(_series[index])) {
        if (_series[_series.length - index - 1].length > 1) {
          var s = '#' + id + '-container' + (isPopup ? popupSuffix : '') + ' a.action:eq(' + (_series.length - index - 1) + ')';
          $(s).parent('li').addClass('disabled');
          series.disable();
        }
      }
    });
    return graph;
  },

  showGraphInPopup: function () {
    if (!this.get('hasData') || this.get('isPreview')) {
      return;
    }

    this.set('isPopup', true);
    if (this.get('inWidget') && !this.get('parentView.isClusterMetricsWidget')) {
      this.setProperties({
        currentTimeIndex: this.get('parentView.timeIndex'),
        customStartTime: this.get('parentView.startTime'),
        customEndTime: this.get('parentView.endTime'),
        customDurationFormatted: this.get('parentView.durationFormatted')
      });
    }
    var self = this;

    App.ModalPopup.show({
      bodyClass: Em.View.extend(App.ExportMetricsMixin, App.TimeRangeMixin, {

        containerId: null,
        containerClass: null,
        yAxisId: null,
        yAxisClass: null,
        xAxisId: null,
        xAxisClass: null,
        legendId: null,
        legendClass: null,
        chartId: null,
        chartClass: null,
        titleId: null,
        titleClass: null,

        timeRangeClassName: 'graph-details-time-range',

        isReady: Em.computed.alias('parentView.graph.isPopupReady'),

        currentTimeRangeIndex: self.get('currentTimeIndex'),
        customStartTime: self.get('currentTimeIndex') === 8 ? self.get('customStartTime') : null,
        customEndTime: self.get('currentTimeIndex') === 8 ? self.get('customEndTime') : null,
        customDurationFormatted: self.get('currentTimeIndex') === 8 ? self.get('customDurationFormatted') : null,

        didInsertElement: function () {
          var popupBody = this;
          this._super();
          App.tooltip(this.$('.corner-icon > .icon-save'), {
            title: Em.I18n.t('common.export')
          });
          this.$().closest('.modal').on('click', function (event) {
            if (!($(event.target).is('.corner-icon, .icon-save, .export-graph-list-container, .export-graph-list-container *'))) {
              popupBody.set('isExportMenuHidden', true);
            }
          });
          $('#modal').addClass('modal-graph-line');
          var popupSuffix = this.get('parentView.graph.popupSuffix');
          var id = this.get('parentView.graph.id');
          var idTemplate = id + '-{element}' + popupSuffix;

          this.set('containerId', idTemplate.replace('{element}', 'container'));
          this.set('containerClass', 'chart-container' + popupSuffix);
          this.set('yAxisId', idTemplate.replace('{element}', 'yaxis'));
          this.set('yAxisClass', this.get('yAxisId').replace(popupSuffix, ''));
          this.set('xAxisId', idTemplate.replace('{element}', 'xaxis'));
          this.set('xAxisClass', this.get('xAxisId').replace(popupSuffix, ''));
          this.set('legendId', idTemplate.replace('{element}', 'legend'));
          this.set('legendClass', this.get('legendId').replace(popupSuffix, ''));
          this.set('chartId', idTemplate.replace('{element}', 'chart'));
          this.set('chartClass', this.get('chartId').replace(popupSuffix, ''));
          this.set('titleId', idTemplate.replace('{element}', 'title'));
          this.set('titleClass', this.get('titleId').replace(popupSuffix, ''));
        },

        templateName: require('templates/common/chart/linear_time'),
        /**
         * check is time paging feature is enable for graph
         */
        isTimePagingEnable: function () {
          return !self.get('isTimePagingDisable');
        }.property(),

        rightArrowVisible: function () {
          // Time range is neither custom nor the least possible preset
          return (this.get('isReady') && this.get('parentView.currentTimeIndex') != 0 &&
            this.get('parentView.currentTimeIndex') != 8);
        }.property('isReady', 'parentView.currentTimeIndex'),

        leftArrowVisible: function () {
          // Time range is neither custom nor the largest possible preset
          return (this.get('isReady') && this.get('parentView.currentTimeIndex') < 7);
        }.property('isReady', 'parentView.currentTimeIndex'),

        exportGraphData: function (event) {
          this.set('isExportMenuHidden', true);
          var ajaxIndex = this.get('parentView.graph.ajaxIndex'),
            targetView = ajaxIndex ? this.get('parentView.graph') : self.get('parentView');
          targetView.exportGraphData({
            context: event.context
          });
        },

        setTimeRange: function (event) {
          var index = event.context.index,
            callback = this.get('parentView').reloadGraphByTime.bind(this.get('parentView'), index);
          this._super(event, callback, self);

          // Preset time range is specified by user
          if (index !== 8) {
            callback();
          }
        }
      }),
      header: this.get('title'),
      /**
       * App.ChartLinearTimeView
       */
      graph: self,

      secondary: null,

      onPrimary: function () {
        var targetView = Em.isNone(self.get('parentView.currentTimeRangeIndex')) ? self.get('parentView.parentView') : self.get('parentView');
        self.setProperties({
          currentTimeIndex: targetView.get('currentTimeRangeIndex'),
          customStartTime: targetView.get('customStartTime'),
          customEndTime: targetView.get('customEndTime'),
          customDurationFormatted: targetView.get('customDurationFormatted'),
          isPopup: false
        });
        App.ajax.abortRequests(this.get('graph.runningPopupRequests'));
        this._super();
      },

      onClose: function () {
        this.onPrimary();
      },
      /**
       * move graph back by time
       * @param event
       */
      switchTimeBack: function (event) {
        var index = this.get('currentTimeIndex');
        // 7 - number of last preset time state
        if (index < 7) {
          this.reloadGraphByTime(++index);
        }
      },
      /**
       * move graph forward by time
       * @param event
       */
      switchTimeForward: function (event) {
        var index = this.get('currentTimeIndex');
        if (index) {
          this.reloadGraphByTime(--index);
        }
      },
      /**
       * reload graph depending on the time
       * @param index
       */
      reloadGraphByTime: function (index) {
        this.set('childViews.firstObject.currentTimeRangeIndex', index);
        this.set('currentTimeIndex', index);
        self.setProperties({
          currentTimeIndex: index,
          isPopupReady: false
        });
        App.ajax.abortRequests(this.get('graph.runningPopupRequests'));
      },
      currentTimeIndex: self.get('currentTimeIndex'),

      currentTimeState: function () {
        return self.get('timeStates').objectAt(this.get('currentTimeIndex'));
      }.property('currentTimeIndex')

    });
    Em.run.next(function () {
      self.loadData();
      self.set('isPopupReady', false);
    });
  },
  reloadGraphByTime: function () {
    Em.run.once(this, function () {
      this.loadData();
    });
  }.observes('timeUnitSeconds', 'customStartTime', 'customEndTime'),

  timeStates: [
    {name: Em.I18n.t('graphs.timeRange.hour'), seconds: 3600},
    {name: Em.I18n.t('graphs.timeRange.twoHours'), seconds: 7200},
    {name: Em.I18n.t('graphs.timeRange.fourHours'), seconds: 14400},
    {name: Em.I18n.t('graphs.timeRange.twelveHours'), seconds: 43200},
    {name: Em.I18n.t('graphs.timeRange.day'), seconds: 86400},
    {name: Em.I18n.t('graphs.timeRange.week'), seconds: 604800},
    {name: Em.I18n.t('graphs.timeRange.month'), seconds: 2592000},
    {name: Em.I18n.t('graphs.timeRange.year'), seconds: 31104000},
    {name: Em.I18n.t('common.custom'), seconds: 0}
  ],
  // should be set by time range control dropdown list when create current graph
  currentTimeIndex: 0,
  customStartTime: null,
  customEndTime: null,
  customDurationFormatted: null,
  setCurrentTimeIndexFromParent: function () {
    // 8 index corresponds to custom start and end time selection
    var targetView = !Em.isNone(this.get('parentView.currentTimeRangeIndex')) ? this.get('parentView') : this.get('parentView.parentView'),
      index = targetView.get('currentTimeRangeIndex'),
      customStartTime = (index === 8) ? targetView.get('customStartTime') : null,
      customEndTime = (index === 8) ? targetView.get('customEndTime'): null,
      customDurationFormatted = (index === 8) ? targetView.get('customDurationFormatted'): null;
    this.setProperties({
      currentTimeIndex: index,
      customStartTime: customStartTime,
      customEndTime: customEndTime,
      customDurationFormatted: customDurationFormatted
    });
    if (index !== 8 || targetView.get('customStartTime') && targetView.get('customEndTime')) {
      App.ajax.abortRequests(this.get('runningRequests'));
      if (this.get('inWidget') && !this.get('parentView.isClusterMetricsWidget')) {
        this.set('parentView.isLoaded', false);
      } else {
        this.set('isReady', false);
      }
    }
  }.observes('parentView.parentView.currentTimeRangeIndex', 'parentView.currentTimeRangeIndex', 'parentView.parentView.customStartTime', 'parentView.customStartTime', 'parentView.parentView.customEndTime', 'parentView.customEndTime'),
  timeUnitSeconds: 3600,
  timeUnitSecondsSetter: function () {
    var index = this.get('currentTimeIndex'),
      startTime = this.get('customStartTime'),
      endTime = this.get('customEndTime'),
      durationFormatted = this.get('customDurationFormatted'),
      seconds;
    if (index !== 8) {
      // Preset time range is specified by user
      seconds = this.get('timeStates').objectAt(this.get('currentTimeIndex')).seconds;
    } else if (!Em.isNone(startTime) && !Em.isNone(endTime)) {
      // Custom start and end time is specified by user
      seconds = (endTime - startTime) / 1000;
    }
    if (!Em.isNone(seconds)) {
      this.set('timeUnitSeconds', seconds);
      if (this.get('inWidget') && !this.get('parentView.isClusterMetricsWidget')) {
        this.get('parentView').setProperties({
          graphSeconds: seconds,
          timeIndex: index,
          startTime: startTime,
          endTime: endTime,
          durationFormatted: durationFormatted
        });
      }
    }
  }.observes('currentTimeIndex', 'customStartTime', 'customEndTime')

});

/**
 * A formatter which will turn a number into computer storage sizes of the
 * format '23 GB' etc.
 *
 * @type {Function}
 * @return {string}
 */
App.ChartLinearTimeView.BytesFormatter = function (y) {
  if (0 == y) {
    return '0 B';
  }
  var value = Rickshaw.Fixtures.Number.formatBase1024KMGTP(y);
  if (!y) {
    return '0 B';
  }
  if ("number" == typeof value) {
    value = String(value);
  }
  if ("string" == typeof value) {
    value = value.replace(/\.\d(\d+)/, function ($0, $1) { // Remove only 1-digit after decimal part
      return $0.replace($1, '');
    });
    // Either it ends with digit or ends with character
    value = value.replace(/(\d$)/, '$1 '); // Ends with digit like '120'
    value = value.replace(/([a-zA-Z]$)/, ' $1'); // Ends with character like
    // '120M'
    value = value + 'B'; // Append B to make B, MB, GB etc.
  }
  return value;
};

/**
 * A formatter which will turn a number into percentage display like '42%'
 *
 * @type {Function}
 * @param {number} percentage
 * @return {string}
 */
App.ChartLinearTimeView.PercentageFormatter = function (percentage) {
  return percentage ?
    percentage.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + '%' :
    '0 %';
};

/**
 * A formatter which will turn a number into percentage display like '42%'
 *
 * @type {Function}
 * @param {number} value
 * @param {string} displayUnit
 * @return {string}
 */
App.ChartLinearTimeView.DisplayUnitFormatter = function (value, displayUnit) {
  return value ?
    value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + " " + displayUnit :
    '0 ' + displayUnit;
};

/**
 * A formatter which will turn elapsed time into display time like '50 ms',
 * '5s', '10 m', '3 hr' etc. Time is expected to be provided in milliseconds.
 *
 * @type {Function}
 * @return {string}
 */
App.ChartLinearTimeView.TimeElapsedFormatter = function (millis) {
  if (!millis) {
    return '0 ms';
  }
  if ('number' == Em.typeOf(millis)) {
    var seconds = millis > 1000 ? Math.round(millis / 1000) : 0;
    var minutes = seconds > 60 ? Math.round(seconds / 60) : 0;
    var hours = minutes > 60 ? Math.round(minutes / 60) : 0;
    var days = hours > 24 ? Math.round(hours / 24) : 0;
    if (days > 0) {
      return days + ' d';
    }
    if (hours > 0) {
      return hours + ' hr';
    }
    if (minutes > 0) {
      return minutes + ' m';
    }
    if (seconds > 0) {
      return seconds + ' s';
    }
    return millis.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + ' ms';
  }
  return millis;
};

/**
 * The default formatter which uses Rickshaw.Fixtures.Number.formatKMBT
 * which shows 10K, 300M etc.
 *
 * @type {Function}
 * @return {string|number}
 */
App.ChartLinearTimeView.DefaultFormatter = function (y) {
  if (!y) {
    return 0;
  }
  var value = Rickshaw.Fixtures.Number.formatKMBT(y);
  if ('' == value) return '0';
  value = String(value);
  var c = value[value.length - 1];
  return isNaN(parseInt(c)) ?
  parseFloat(value.substr(0, value.length - 1)).toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + c :
    parseFloat(value).toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
};


/**
 * Creates and returns a formatter that can convert a 'value'
 * to 'value units/s'.
 *
 * @param unitsPrefix Prefix which will be used in 'unitsPrefix/s'
 * @param valueFormatter  Value itself will need further processing
 *        via provided formatter. Ex: '10M requests/s'. Generally
 *        should be App.ChartLinearTimeView.DefaultFormatter.
 * @return {Function}
 */
App.ChartLinearTimeView.CreateRateFormatter = function (unitsPrefix, valueFormatter) {
  var suffix = " " + unitsPrefix + "/s";
  return function (value) {
    value = valueFormatter(value) + suffix;
    return value;
  };
};

Rickshaw.GraphReopened = function (a) {
  Rickshaw.Graph.call(this, a);
};

//reopened in order to exclude "null" value from validation
Rickshaw.GraphReopened.prototype = Object.create(Rickshaw.Graph.prototype, {
  validateSeries: {
    value: function (series) {

      if (!(series instanceof Array) && !(series instanceof Rickshaw.Series)) {
        var seriesSignature = Object.prototype.toString.apply(series);
        throw "series is not an array: " + seriesSignature;
      }

      var pointsCount;

      series.forEach(function (s) {

        if (!(s instanceof Object)) {
          throw "series element is not an object: " + s;
        }
        if (!(s.data)) {
          throw "series has no data: " + JSON.stringify(s);
        }
        if (!(s.data instanceof Array)) {
          throw "series data is not an array: " + JSON.stringify(s.data);
        }

        pointsCount = pointsCount || s.data.length;

        if (pointsCount && s.data.length != pointsCount) {
          throw "series cannot have differing numbers of points: " +
          pointsCount + " vs " + s.data.length + "; see Rickshaw.Series.zeroFill()";
        }
      })
    },
    enumerable: true,
    configurable: false,
    writable: false
  }
});

//show no line if point is "null"
Rickshaw.Graph.Renderer.Line.prototype.seriesPathFactory = function () {

  var graph = this.graph;

  return d3.svg.line()
    .x(function (d) {
      return graph.x(d.x)
    })
    .y(function (d) {
      return graph.y(d.y)
    })
    .defined(function (d) {
      return d.y != null;
    })
    .interpolate(this.graph.interpolation).tension(this.tension);
};

//show no area if point is null
Rickshaw.Graph.Renderer.Stack.prototype.seriesPathFactory = function () {

  var graph = this.graph;

  return d3.svg.area()
    .x(function (d) {
      return graph.x(d.x)
    })
    .y0(function (d) {
      return graph.y(d.y0)
    })
    .y1(function (d) {
      return graph.y(d.y + d.y0)
    })
    .defined(function (d) {
      return d.y != null;
    })
    .interpolate(this.graph.interpolation).tension(this.tension);
};


/**
 * aggregate requests to load metrics by component name
 * requests can be added via add method
 * input example:
 * {
 *   data: request,
 *   context: this,
 *   startCallName: this.getServiceComponentMetrics,
 *   successCallback: this.getMetricsSuccessCallback,
 *   completeCallback: function () {
 *     requestCounter--;
 *     if (requestCounter === 0) this.onMetricsLoaded();
 *   }
 * }
 * @type {Em.Object}
 */
App.ChartLinearTimeView.LoadAggregator = Em.Object.create({
  /**
   * @type {Array}
   */
  requests: [],

  /**
   * @type {number|null}
   */
  timeoutId: null,

  /**
   * time interval within which calls get collected
   * @type {number}
   * @const
   */
  BULK_INTERVAL: 1000,

  /**
   * add request
   * every {{BULK_INTERVAL}} requests get collected, aggregated and sent to server
   *
   * @param {object} context
   * @param {object} requestData
   */
  add: function (context, requestData) {
    var self = this;

    requestData.context = context;
    this.get('requests').push(requestData);
    if (Em.isNone(this.get('timeoutId'))) {
      this.set('timeoutId', window.setTimeout(function () {
        self.runRequests(self.get('requests'));
        self.get('requests').clear();
        clearTimeout(self.get('timeoutId'));
        self.set('timeoutId', null);
      }, this.get('BULK_INTERVAL')));
    }
  },

  /**
   * return requests which grouped into bulks
   * @param {Array} requests
   * @returns {object} bulks
   */
  groupRequests: function (requests) {
    var bulks = {};

    requests.forEach(function (request) {
      var id = request.name;

      if (Em.isNone(bulks[id])) {
        bulks[id] = {
          name: request.name,
          fields: request.fields.slice(),
          context: request.context
        };
        bulks[id].subRequests = [{
          context: request.context
        }];
      } else {
        bulks[id].fields.pushObjects(request.fields);
        bulks[id].subRequests.push({
          context: request.context
        });
      }
    }, this);
    return bulks;
  },

  /**
   * run aggregated requests
   * @param {Array} requests
   */
  runRequests: function (requests) {
    var bulks = this.groupRequests(requests);
    var self = this;

    for (var id in bulks) {
      (function (_request) {
        var fields = self.formatRequestData(_request);
        var hostName = (_request.context.get('content')) ? _request.context.get('content.hostName') : "";
        var xhr = App.ajax.send({
          name: _request.name,
          sender: _request.context,
          data: {
            fields: fields,
            hostName: hostName
          }
        });

        xhr.done(function (response) {
          console.time('==== runRequestsDone');
          _request.subRequests.forEach(function (subRequest) {
            subRequest.context._refreshGraph.call(subRequest.context, response);
          }, this);
          console.timeEnd('==== runRequestsDone');
        }).fail(function (jqXHR, textStatus, errorThrown) {
          if (!jqXHR.isForcedAbort) {
            _request.subRequests.forEach(function (subRequest) {
              subRequest.context.loadDataErrorCallback.call(subRequest.context, jqXHR, textStatus, errorThrown);
            }, this);
          }
        }).always(function () {
          _request.context.set('runningRequests', _request.context.get('runningRequests').reject(function (item) {
            return item === xhr;
          }));
        });
        _request.context.get('runningRequests').push(xhr);
      })(bulks[id]);
    }
  },

  /**
   *
   * @param {object} request
   * @returns {number[]}
   */
  formatRequestData: function (request) {
    var fromSeconds, toSeconds;
    if (request.context.get('currentTimeIndex') === 8 && !Em.isNone(request.context.get('customStartTime')) && !Em.isNone(request.context.get('customEndTime'))) {
      // Custom start and end time is specified by user
      toSeconds = request.context.get('customEndTime') / 1000;
      fromSeconds = request.context.get('customStartTime') / 1000;
    } else {
      var timeUnit = request.context.get('timeUnitSeconds');
      toSeconds = Math.round(App.dateTime() / 1000);
      fromSeconds = toSeconds - timeUnit;
    }
    var fields = request.fields.uniq().map(function (field) {
      return field + "[" + (fromSeconds) + "," + toSeconds + "," + 15 + "]";
    });

    return fields.join(",");
  }
});
