/**
 * Licensed 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.
 */

/* global Rickshaw */
/*
  Draw line chart in d3.

  To use, a div is required with the data attributes
  data-chart-type="line_chart", data-data.

  data-chart-type - REQUIRED(string) must be "line_chart" so chart gets initialized
  data-data - REQUIRED(string or json):
    string: url for the json data for the chart
    json: json stringified object for the chart data
  data-form-selector - Optional(string) jQuery selector of Forms that controls this chart
  data-legend-selector - Optional(string) jQuery selector of div element that will display legend
  data-smoother-selector - Optional(string) jQuery selector of TODO(lsmola)
  data-slider-selector - Optional(string) jQuery selector of TODO(lsmola)


  If used in popup, initialization must be made manually e.g.:

  if (typeof horizon.d3_line_chart !== 'undefined') {
    horizon.d3_line_chart.init("div[data-chart-type='line_chart']");
  }


  Example:
  <div id="line_chart"
    data-chart-type="line_chart"
    data-data="{% url 'horizon:admin:metering:samples'%}"
    data-form-selector='#linechart_general_form'>
  </div>
  <div id="linea_chart2"
    data-chart-type="line_chart"
    data-data="{% url 'horizon:admin:metering:samples'%}?query=not_popular_data"
    data-form-selector='#linechart_general_form'>
  </div>


  The data format example:
  Url has to return JSON in format:
  {
    "series": [
      {
        "name": "instance-00000005",
        "data": [
          {"y": 171, "x": "2013-08-21T11:22:25"},
          {"y": 172, "x": "2013-08-21T11:22:26"}
        ]
      }, {
        "name": "instance-00000006",
        "data": [
          {"y": 161, "x": "2013-08-21T11:22:25"},
          {"y": 162, "x": "2013-08-21T11:22:26"}
        ]
      }
    ],
    "settings": {}
  }

  Example of line-bar chart sparkline:

  <div class="overview_chart">
    <div class="chart_container">
      <div class="chart"
        data-chart-type="line_chart"
        data-data="/admin/samples?meter=test2"
        data-form-selector='#linechart_general_form'>
      </div>
    </div>
    <div class="bar_chart_container">
      <div class="chart" data-chart-type="overview_bar_chart">
      </div>
    </div>
  </div>

  {
    "series": [
      {
        "name": "instance-00000005",
        "data": [
          {"y": 171, "x": "2013-08-21T11:22:25"},
          {"y": 172, "x": "2013-08-21T11:22:26"}
        ]
      }, {
        "name": "instance-00000006",
        "data": [
          {"y": 161, "x": "2013-08-21T11:22:25"},
          {"y": 162, "x": "2013-08-21T11:22:26"}
        ]
      }
    ],
    "settings": {
      'renderer': 'StaticAxes',
      'yMin': 0,
      'yMax': 100,
      'higlight_last_point': True,
      "auto_size": False, 'auto_resize': False,
      "axes_x" : False, "axes_y" : False,
      'bar_chart_settings': {
        'orientation': 'vertical',
        'used_label_placement': 'left',
        'width': 30,
        'color_scale_domain': [0, 80, 80, 100],
        'color_scale_range': ['#00FE00', '#00FF00', '#FE0000', '#FF0000'],
        'average_color_scale_domain': [0, 100],
        'average_color_scale_range': ['#0000FF', '#0000FF']
      }
    },
    "stats": {
      'average': 20,
      'used': 30,
      'tooltip_average': tooltip_average
    }
  }


  The control Forms:
  There are currently 2 form elements that can be connected to charts and act
  as controls. Elements listen for change event and refresh the chart on change event.
  Chart can be connected to multiple forms via selector, all for data will be sent on
  change.
  Form can be connected to multiple charts, all charts will be refreshed when form
  element changes.
  The firsts rendering of the chart takes data from the connected Forms.

  1. Selectbox
  The data attribute 'data-line-chart-command="select_box_change' needs to be defined on
  select element.

  Example:
  <form class="form-horizontal"
    id="linechart_general_form">

    <div class="form-group">
      <label for="meter" class="control-label col-sm-2">{% trans "Metric" %}:&nbsp;</label>
      <div class="col-sm-10">
        <select data-line-chart-command="select_box_change"
          name="meter" id="meter" class="col-sm-w form-control example">
          {% for meter in meters %}
            <option value="{{ meter }}" data-unit="{{ meter }}">
              {{ meter }}
            </option>
          {% endfor %}
        </select>
      </div>
    </div>
  </form>

  2. Date picker
  The data attribute 'data-line-chart-command="date_picker_change"' needs to be defined on
  input element.

  Example:
  <form class="form-horizontal"
    id="linechart_general_form">
    <div class="form-group" id="date_from">
      <label for="date_from" class="control-label col-sm-2">{% trans "From" %}:&nbsp;</label>
      <div class="col-sm-10">
        <input data-line-chart-command="date_picker_change"
          type="text" id="date_from" name="date_from" class="form-control example"/>
      </div>
    </div>
  </form>

 */

Rickshaw.namespace('Rickshaw.Graph.Renderer.StaticAxes');
Rickshaw.Graph.Renderer.StaticAxes = Rickshaw.Class.create(Rickshaw.Graph.Renderer.Line, {
  name: 'StaticAxes',
  defaults: function($super) {
    return Rickshaw.extend($super(), {
      xMin: undefined,
      xMax: undefined,
      yMin: undefined,
      yMax: undefined
    });
  },
  domain: function($super) {
    var ret = $super();
    var xMin, xMax;
    // If y axis wants to have static range, not based on data
    if (this.yMin !== undefined && this.yMax !== undefined){
      ret.y = [this.yMin, this.yMax];
    }
    // If x axis wants to have static range, not based on data
    if (this.xMin !== undefined && this.xMax !== undefined){
      xMin = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(this.xMin);
      xMin = xMin.getTime() / 1000;
      xMax = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(this.xMax);
      xMax = xMax.getTime() / 1000;

      ret.x = [xMin, xMax];
    }
    return ret;
  }
});

horizon.d3_line_chart = {
  /**
   * A class representing the line chart
   * @param chart_module A context of horizon.d3_line_chart module.
   * @param html_element A html_element containing the chart.
   * @param settings An object containing settings of the chart.
   */
  LineChart: function(chart_module, html_element, settings){
    var self = this;
    var jquery_element = $(html_element);

    self.chart_module = chart_module;
    self.html_element = html_element;
    self.jquery_element = jquery_element;
    self.$spinner = horizon.loader.inline(gettext('Loading')).hide().appendTo(jquery_element);

    /************************************************************************/
    /*********************** Initialization methods *************************/
    /************************************************************************/
    /**
     * Initialize object
     */
    self.init = function() {
      var self = this;
      /* TODO(lsmola) make more configurable init from more sources */
      self.legend_element = $(jquery_element.data('legend-selector')).get(0);
      self.slider_element = $(jquery_element.data('slider-selector')).get(0);

      // Set the data, undefined if it doesn't exist
      self.data = jquery_element.data('data');
      self.url_parameters = jquery_element.data('url_parameters');

      if (typeof self.data === 'string') {
        self.final_url = self.data;
        if (jquery_element.data('form-selector')){
          $(jquery_element.data('form-selector')).each(function(){
            // Add serialized data from all connected forms to url.
            if (self.final_url.indexOf('?') > -1){
              self.final_url += '&' + $(this).serialize();
            } else {
              self.final_url += '?' + $(this).serialize();
            }
          });
        }
      }

      self.color = d3.scale.category10();

      // Self aggregation and statistic attrs
      self.stats = {};
      self.stats.average = 0;
      self.stats.last_value = 0;

      // Load initial settings.
      self.init_settings(settings);
      // Get correct size of chart and the wrapper.
      self.get_size();
    };
    /**
     * Initialize settings of the chart with default values, then applies
     * defined settings of the chart. Settings are obtained either from JSON
     * of the html attribute data-settings, or from init of the charts. The
     * highest priority settings are obtained directly from the JSON data
     * obtained from the server.
     * @param settings An object containing settings of the chart.
     */
    self.init_settings = function(settings) {
      var self = this;

      self.settings = {};
      self.settings.renderer = 'line';
      self.settings.auto_size = true;
      self.settings.axes_x = true;
      self.settings.axes_y = true;
      self.settings.axes_y_label = true;
      self.settings.interpolation = 'linear';
      // Static y axes values
      self.settings.yMin = undefined;
      self.settings.yMax = undefined;
      // Static x axes values
      self.settings.xMin = undefined;
      self.settings.xMax = undefined;
      // Show last point as dot
      self.settings.higlight_last_point = false;

      // Composed charts wrapper
      self.settings.composed_chart_selector = '.overview_chart';
      // Bar chart component
      self.settings.bar_chart_selector = 'div[data-chart-type="overview_bar_chart"]';
      self.settings.bar_chart_settings = undefined;

      // allowed: verbose
      self.hover_formatter = 'verbose';

      /*
        Applying settings. The later application rewrites the previous
        therefore it has bigger priority.
      */

      // Settings defined in the init method of the chart
      if (settings){
        self.apply_settings(settings);
      }

      // Settings defined in the html data-settings attribute
      if (self.jquery_element.data('settings')){
        var inline_settings = self.jquery_element.data('settings');
        self.apply_settings(inline_settings);
      }
    };

    /**
     * Applies passed settings to the chart object. Allowed settings are
     * listed in this method.
     * @param settings An object containing settings of the chart.
     */
    self.apply_settings = function(settings){
      var self = this;

      var allowed_settings = ['renderer', 'auto_size', 'axes_x', 'axes_y',
        'interpolation', 'yMin', 'yMax', 'xMin', 'xMax', 'bar_chart_settings',
        'bar_chart_selector', 'composed_chart_selector',
        'higlight_last_point', 'axes_y_label'];

      jQuery.each(allowed_settings, function(index, setting_name) {
        if (settings[setting_name] !== undefined){
          self.settings[setting_name] = settings[setting_name];
        }
      });
    };

    /**
     * Computes size of the chart from surrounding divs. When
     * settings.auto_size is on, it stretches the chart to bottom of
     * the screen.
     */
    self.get_size = function(){
      /*
        The height will be determined by css or window size,
        I have to hide everything inside that could mess with
        the size, so it is fully determined by outer CSS.
      */
      $(self.html_element).css('height', '');
      $(self.html_element).css('width', '');
      var svg = $(self.html_element).find('svg');
      svg.hide();

      /*
        Width and height of the chart will be taken from chart wrapper,
        that can be styled by css.
      */
      self.width = jquery_element.width();

      // Set either the minimal height defined by CSS.
      self.height = jquery_element.height();
      /*
        Or stretch it to the remaining height of the window if there
        is a place. + some space on the bottom, lets say 30px.
      */
      if (self.settings.auto_size) {
        var auto_height = $(window).height() - jquery_element.offset().top - 30;
        if (auto_height > self.height) {
          self.height = auto_height;
        }
      }

      /* Setting new sizes. It is important when resizing a window.*/
      $(self.html_element).css('height', self.height);
      $(self.html_element).css('width', self.width);
      svg.show();
      svg.css('height', self.height);
      svg.css('width', self.width);
    };

    /************************************************************************/
    /****************************** Initialization **************************/
    /************************************************************************/
    // Init of the object
    self.init();

    /************************************************************************/
    /****************************** Error Message ***************************/
    /************************************************************************/
    // Load the data
    self.error_message = function(error) {
      $(self.html_element).html(error);
      $(self.legend_element).empty();
      // Setting a fix height breaks things when legend is getting
      // bigger.
      $(self.legend_element).css('height', '');
      // FIXME add proper fail message
      horizon.toast.add('error',
        gettext('An error occurred. Please try again later.'));
    };

    /************************************************************************/
    /******************************** Data Load *****************************/
    /************************************************************************/
    // Load the data
    self.load_data = function(data) {

      // Clearing the old chart data.
      self.jquery_element.empty();
      $(self.legend_element).empty();

      self.series = data.series;
      self.stats = data.stats;
      // The highest priority settings are sent with the data.
      self.apply_settings(data.settings);

      if (self.series.length <= 0) {
        $(self.html_element).html(gettext('No data available.'));
        $(self.legend_element).empty();
        // Setting a fix height breaks things when legend is getting
        // bigger.
        $(self.legend_element).css('height', '');
      } else {
        self.render();
      }
    };

    /************************************************************************/
    /****************************** Methods *********************************/
    /************************************************************************/
    /**
     * Obtains the actual chart data and renders the chart again.
     */
    self.refresh = function (){
      var self = this;

      self.start_loading();
      if (typeof self.data === 'string') {
        horizon.ajax.queue({
          url: self.final_url,
          success: function (data) {
            self.load_data(data);
          },
          error: function () {
            self.error_message(gettext('No data available.'));
          },
          complete: function () {
            self.finish_loading();
          }
        });
      } else if (self.data) {
        self.load_data(self.data);
        self.finish_loading();
      } else {
        self.error_message(gettext('No data available.'));
      }
    };

    /**
     * Renders the chart using Rickshaw library.
     */
    self.render = function(){
      var self = this;
      var last_point, last_point_color;

      $.map(self.series, function (serie) {
        serie.color = last_point_color = self.color(serie.name);
        $.map(serie.data, function (statistic) {
          // need to parse each date
          statistic.x = d3.time.format.utc('%Y-%m-%dT%H:%M:%S').parse(statistic.x);
          statistic.x = statistic.x.getTime() / 1000;
          last_point = statistic;
          last_point.color = serie.color;
        });
      });

      var renderer = self.settings.renderer;
      if (renderer === 'StaticAxes'){
        renderer = Rickshaw.Graph.Renderer.StaticAxes;
      }

      // clean the old graph the messy way
      // TODO(lsmola) clena when the Rickshaw issue is gone
      // https://github.com/shutterstock/rickshaw/issues/432
      self.jquery_element.empty();

      var $newGraph = self.jquery_element.clone();
      self.jquery_element.replaceWith($newGraph);
      self.jquery_element = $newGraph;
      self.html_element = self.jquery_element[0];

      // instantiate our graph!
      var graph = new Rickshaw.Graph({
        element: self.html_element,
        width: self.width,
        height: self.height,
        renderer: renderer,
        series: self.series,
        yMin: self.settings.yMin,
        yMax: self.settings.yMax,
        xMin: self.settings.xMin,
        xMax: self.settings.xMax,
        interpolation: self.settings.interpolation
      });

      /*
        TODO(lsmola) add jQuery UI slider to make this work
        if (self.slider_element) {
          var slider = new Rickshaw.Graph.RangeSlider({
            graph: graph,
            element: $(self.slider_element)
          });
        }
      */
      graph.render();

      if (self.hover_formatter === 'verbose'){
        new Rickshaw.Graph.HoverDetail({
          graph: graph,
          formatter: function(series, x, y) {
            if (y % 1 === 0) {
              y = parseInt(y, 10);
            } else {
              y = parseFloat(y).toFixed(2);
            }

            var d = new Date(x * 1000);
            // Convert datetime to YYYY-MM-DD HH:MM:SS GMT
            var datetime_string = d.getUTCFullYear() + "-" +
              ("00" + (d.getUTCMonth() + 1)).slice(-2) + "-" +
              ("00" + d.getUTCDate()).slice(-2) + " " +
              ("00" + d.getUTCHours()).slice(-2) + ":" +
              ("00" + d.getUTCMinutes()).slice(-2) + ":" +
              ("00" + d.getUTCSeconds()).slice(-2) + " GMT";

            var date = '<span class="date">' + datetime_string + '</span>';
            var swatch = '<span class="detail_swatch" style="background-color: ' + series.color + '"></span>';
            return swatch + series.name + ': ' + y + ' ' + series.unit + '<br>' + date;
          }
        });
      }

      if (self.legend_element) {
        var legend = new Rickshaw.Graph.Legend({
          graph: graph,
          element:  self.legend_element
        });

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

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

        new Rickshaw.Graph.Behavior.Series.Highlight({
          graph: graph,
          legend: legend
        });
      }
      if (self.settings.axes_x) {
        var axes_x = new Rickshaw.Graph.Axis.Time({
          graph: graph
        });
        axes_x.render();
      }
      if (self.settings.axes_y) {
        var axes_y_settings = {
          graph: graph
        };
        if (!self.settings.axes_y_label){
          // hiding label of Y axis if setting is set to false
          axes_y_settings.tickFormat = (function () { return ''; });
        }
        var axes_y = new Rickshaw.Graph.Axis.Y(axes_y_settings);
        axes_y.render();
      }

      /*
        Setting a fix height breaks things when chart is refreshed and
        legend is getting bigger.
      */
      $(self.legend_element).css('height', '');

      // Render bar chart
      if (self.stats !== undefined){
        var composed_chart = self.jquery_element.parents(self.settings.composed_chart_selector).first();
        var bar_chart_html = composed_chart.find(self.settings.bar_chart_selector).get(0);

        horizon.d3_bar_chart.refresh(bar_chart_html,
          self.settings.bar_chart_settings,
          self.stats);
      }

      // Render ending dot to last point
      if (self.settings.higlight_last_point){
        if (last_point !== undefined && last_point_color !== undefined){
          graph.vis.append('circle')
            .attr('class', 'used_component')
            .attr('cy', graph.y(last_point.y))
            .attr('cx', graph.x(last_point.x))
            .attr('r', 2)
            .style('fill', last_point_color)
            .style('stroke', last_point_color)
            .style('stroke-width', 2);
        }
      }
    };

    /**
     * Shows chart loader with backdrop. Backdrop is computed to hide
     * the canvas with chart. Loader is computed to be placed in the center.
     * Hides also a block with legend.
     */
    self.start_loading = function () {
      var self = this;

      $(self.html_element).addClass('has-spinner');
      self.$spinner.show();

      // Hide the legend.
      $(self.legend_element).empty().addClass('disabled');

      /*
        TODO(lsmola) a loader for in-line tables spark-lines has to be
        prepared, the parameters of loader could be sent in settings.
      */

    };

    /**
     * Hides the loader and backdrop so the chart will become visible.
     * Shows also the block with legend.
     */
    self.finish_loading = function () {
      var self = this;
      // Showing the legend.
      $(self.legend_element).removeClass('disabled');
      $(self.html_element).removeClass('has-spinner');
      self.$spinner.hide();
    };
  },

  /**
   * Function for initializing of the charts.
   * If settings['auto_resize'] is true, the chart will be refreshed when
   * the size of the window is changed. This option made only sense when
   * the size of the chart and its wrapper is not static.
   * @param selector jQuery selector of charts we want to initialize.
   * @param settings An object containing settings of the chart.
   */
  init: function(selector, settings) {
    var self = this;
    $(selector).each(function() {
      self.refresh(this, settings);
    });

    if (settings !== undefined && settings.auto_resize) {
      /*
        I want to refresh chart on resize of the window, but only
        at the end of the resize. Nice code from mr. Google.
      */
      var rtime = new Date(1, 1, 2000, 12, 0, 0);
      var timeout = false;
      var delta = 400;
      $(window).resize(function() {
        rtime = new Date();
        if (timeout === false) {
          timeout = true;
          setTimeout(resizeend, delta);
        }
      });

      var resizeend = function() {
        if (new Date() - rtime < delta) {
          setTimeout(resizeend, delta);
        } else {
          timeout = false;
          $(selector).each(function() {
            self.refresh(this, settings);
          });
        }
      };
    }

    self.bind_commands(selector, settings);
  },

  /**
   * Function for creating chart objects, saving them for later reuse
   * and calling their refresh method.
   * @param html_element HTML element where the chart will be rendered.
   * @param settings An object containing settings of the chart.
   */
  refresh: function(html_element, settings){
    var chart = new this.LineChart(this, html_element, settings);
    /*
      FIXME save chart objects somewhere so I can use them again when
      e.g. I am switching tabs, or if I want to update them
      via web sockets
      this.charts.add_or_update(chart)
    */
    chart.refresh();
  },

  /**
   * Function for binding controlling commands to the chart. Like changing
   * timespan or various parameters we want to show in the chart. The
   * charts will be refreshed immediately after the form element connected
   * to them is changed.
   * @param selector jQuery selector of charts we are initializing.
   * @param settings An object containing settings of the chart.
   */
  bind_commands: function (selector, settings){
    // connecting controls of the charts
    var select_box_selector = 'select[data-line-chart-command="select_box_change"]';
    var datepicker_selector = 'input[data-line-chart-command="date_picker_change"]';
    var self = this;

    /**
     * Connecting forms to charts it controls. Each chart contains
     * jQuery selector data-form-selector, which defines by which
     * html Forms is a particular chart controlled. This information
     * has to be projected to forms. So when form input is changed,
     * all connected charts are refreshed.
     */
    var connect_forms_to_charts = function(){
      $(selector).each(function() {
        var chart = $(this);
        $(chart.data('form-selector')).each(function(){
          var form = $(this);
          // each form is building a jquery selector for all charts it affects
          var chart_identifier = 'div[data-form-selector="' + chart.data('form-selector') + '"]';
          if (!form.data('charts_selector')){
            form.data('charts_selector', chart_identifier);
          } else {
            form.data('charts_selector', form.data('charts_selector') + ', ' + chart_identifier);
          }
        });
      });
    };

    /**
     * A helper function for delegating form events to charts, causing their
     * refreshing.
     * @param selector jQuery selector of charts we are initializing.
     * @param event_name Event name we want to delegate.
     * @param settings An object containing settings of the chart.
     */
    var delegate_event_and_refresh_charts = function(selector, event_name, settings) {
      $('form').on(selector, event_name, function() {
        /*
          Registering 'any event' on form element by delegating. This way it
          can be easily overridden / enhanced when some special functionality
          needs to be added. Like input element showing/hiding another element
          on some condition will be defined directly on element and can block
          this default behavior.
        */
        var invoker = $(this);
        var form = invoker.parents('form').first();

        $(form.data('charts_selector')).each(function(){
          // refresh the chart connected to changed form
          self.refresh(this, settings);
        });
      });
    };

    /**
     * A helper function for catching change event of form selectboxes
     * connected to charts.
     */
    var bind_select_box_change = function(settings) {
      delegate_event_and_refresh_charts(select_box_selector, 'change', settings);
    };

    /**
     * A helper function for catching changeDate event of form datepickers
     * connected to charts.
     */
    var bind_datepicker_change = function(settings) {
      horizon.datepickers.add(datepicker_selector);
      delegate_event_and_refresh_charts(datepicker_selector, 'changeDate', settings);
    };

    connect_forms_to_charts();
    bind_select_box_change(settings);
    bind_datepicker_change(settings);
  }
};

/* Init the graphs */
horizon.addInitFunction(function () {
  horizon.d3_line_chart.init('div[data-chart-type="line_chart"]', {});
});
