/*
  Used for animating and displaying bar information using
  D3js rect.

  Usage:
    In order to have single bars that work with this, you need to have a
    DOM structure like this in your Django template:

    Example:
    <div style="width: 100px; min-width: 100px; height: 20px; min-height: 20px">
      <div class="chart"
        data-chart-type="bar_chart"
        data-tooltip-used='Used'
        data-tooltip-free='Free'
        data-tooltip-average='Average'
        data-settings='{"orientation": "horizontal", "color_scale_range": ["#000060", "#99FFFF"]}'
        data-used="20"
        data-average="30">
      </div>
    </div>

    The available data- attributes are:
      data-tooltip-free, data-tooltip-used, data-tooltip-average OPTIONAL
        Html content of tooltips that will be displayed over this areas.


      data-used="integer" REQUIRED
        1. Integer
          Integer representing the percent used.
        2. Array
          Array of following structure:
            [
              {"tooltip_used": "Popup html 1", "used_instances": "5"},
              {"tooltip_used": "Popup html 2", "used_instances": "15"},....
            ]

          used_instances: Integer representing the percent used.
          tooltip_used: Html that will be displayed in tooltip window over
            this area.

      data-settings="JSON"
        Json with variety of settings described below.

        used-label-placement='string' OPTIONAL
          String determining where the floating label stating number of percent
          will be placed. So far only left is supported.

          width="integer" OPTIONAL
            Integer in pixels. Determines the total width of the bar. Handy when
            we use a used_label, so the bar is not a 100% of the container.

          average="integer" OPTIONAL
            Integer representing the average usage in percent of given
            single-bar.

          auto-scale-selector OPTIONAL
            Jquery selector of bar elements that have Integer
            used attribute. It then takes maximum of these
            values as 100% of the linear scale of the colors.
            So the array representing linear scale interval is set
            automatically.This then maps to color-scale-range.
            (arrays must have the same structure)

          color-scale-range OPTIONAL
            Array representing linear scale interval that is set manually.
            E.g "[0,10]". This then maps to color-scale-range.
            (arrays must have the same structure)

          color-scale-range OPTIONAL
            Array representing linear scale of colors.
            E.g '["#000060", "#99FFFF"]'

          orientation OPTIONAL
            String representing orientation of the bar.Can be "horizontal"
            or "vertical". Default is horizontal.

*/

horizon.d3_bar_chart = {
  /**
   * A class representing the bar 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.
   */
  BarChart: function(chart_module, html_element, settings, data){
    var self = this;
    self.chart_module = chart_module;
    self.html_element = html_element;
    self.jquery_element = $(self.html_element);

    /************************************************************************/
    /*********************** Initialization methods *************************/
    /************************************************************************/
    /**
     * Initialize the object.
     */
    self.init = function(settings, data) {
      var self = this;
      self.data = {};

      self.data.max_value = self.jquery_element.data('max-value');
      if (!self.max_value){
        // Default mas value is 100, representing 100 percent
        self.max_value = 100;
      }

      // Chart data
      self.data.used = self.jquery_element.data('used');
      self.data.average = self.jquery_element.data('average');

      // Tooltips data
      self.data.tooltip_average = self.jquery_element.data('tooltip-average');
      self.data.tooltip_free = self.jquery_element.data('tooltip-free');
      self.data.tooltip_used = self.jquery_element.data('tooltip-used');

      if (data !== undefined){
        if (data.used !== undefined){
          self.data.used = data.used;
        }
        if (data.average !== undefined){
          self.data.average = data.average;
        }
        if (data.tooltip_average !== undefined){
          self.data.tooltip_average = data.tooltip_average;
        }
        if (data.tooltip_free !== undefined){
          self.data.tooltip_free = data.tooltip_free;
        }
        if (data.tooltip_used !== undefined){
          self.data.tooltip_used = data.tooltip_used;
        }
      }

      // Percentage count
      if ($.isArray(self.data.used)){
        self.data.percentage_average = 0; // No average for Multi-bar chart
        self.data.percentage_used = Array();
        self.data.tooltip_used_contents = Array();
        for (var i = 0; i < self.data.used.length; ++i) {
          if (!isNaN(self.max_value) && !isNaN(self.data.used[i].used_instances)) {
            var used = Math.round((self.data.used[i].used_instances / self.max_value) * 100);

            self.data.percentage_used.push(used);
            // for multi-bar chart, tooltip is in the data
            self.data.tooltip_used_contents.push(self.data.used[i].tooltip_used);
          } else { // If NaN self.data.percentage_used is 0

          }
        }
      }
      else {
        if (!isNaN(self.max_value) && !isNaN(self.data.used)) {
          self.data.percentage_used = Math.round((self.data.used / self.max_value) * 100);
        } else { // If NaN self.data.percentage_used is 0
          self.data.percentage_used = 0;
        }

        if (!isNaN(self.max_value) && !isNaN(self.data.average)) {
          self.data.percentage_average = ((self.data.average / self.max_value) * 100);
        } else {
          self.data.percentage_average = 0;
        }
      }

      // Load initial settings.
      self.init_settings(settings);
    };

    /**
     * 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.data.settings = {};

      // Placement of the used label
      self.data.settings.used_label_placement = undefined;
      // Orientation of the Bar chart
      self.data.settings.orientation = 'horizontal';

      // Color scales
      self.data.settings.color_scale_domain = [0,100];
      self.data.settings.color_scale_range = ['#000000', '#0000FF'];

      // Width and height of bar
      self.data.settings.width = self.jquery_element.data('width');
      self.data.settings.height = self.jquery_element.data('height');

      /* 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 = ['orientation', 'used_label_placement',
                              'color_scale_domain', 'color_scale_range',
                              'width', 'height'];

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

    /************************************************************************/
    /****************************** Initialization **************************/
    /************************************************************************/
    // Init the object
    self.init(settings, data);

    /************************************************************************/
    /****************************** Methods *********************************/
    /************************************************************************/
    /**
     * Obtains the actual chart data and renders the chart again.
     */
    self.refresh = function(){
      var self = this;
      // Clear the chart before rendering it
      self.jquery_element.empty();
      self.render();
    };

    /**
     * Renders the chart into html element given in initializer.
     */
    self.render = function() {
      var self = this;

      // Initialize wrapper
      var wrapper = new self.chart_module.Wrapper(self.chart_module, self.html_element, self.data);

      // Initialize Tool-tips
      var tooltip_average = (new self.chart_module.TooltipComponent(wrapper)).render(self.data.tooltip_average);
      var tooltip_free = (new self.chart_module.TooltipComponent(wrapper)).render(self.data.tooltip_free);
      var tooltip_used = (new self.chart_module.TooltipComponent(wrapper)).render(self.data.tooltip_used);

      // Append Unused resources Bar
      (new self.chart_module.UnusedComponent(wrapper)).render(tooltip_free);

      if (wrapper.used_multi()){
        // If UsedComponent is shown as multiple values in one chart
        for (var i = 0; i < wrapper.percentage_used.length; ++i) {
          // FIXME write proper iterator
          wrapper.used_multi_iterator = i;

          /* Use general tool-tip, content of tool-tip will be changed by inner
             used components on their hover. HTML content is taken from JSON sent
             from the server. */
          tooltip_used = (new self.chart_module.TooltipComponent(wrapper)).render('');

          // Append used so it will be shown as multiple values in one chart
          (new self.chart_module.UsedComponent(wrapper)).render(tooltip_used);

          // Compute total value as a start point for next Used bar
          wrapper.total_used_perc += wrapper.percentage_used_value();
          wrapper.total_used_value_in_pixels = (wrapper.w / 100) * wrapper.total_used_perc;
        }

      } else {
        // Used is show as one value it the chart
        (new self.chart_module.UsedComponent(wrapper)).render(tooltip_used);
        // Append average value to Bar
        (new self.chart_module.AverageComponent(wrapper)).render(tooltip_average);
      }
    };
  },
  /**
   * Chart wrapper class renders the main svg element and encapsulate
   * the chart data.
   * @param chart_module Chart module name
   * @param html_element HTML element where the chart will be rendered
   * @param chart_module Data + settings of the chart
   */
  Wrapper: function(chart_module, html_element, data){
    var self = this;
    self.html_element = html_element;
    self.jquery_element = $(html_element);

    // Bar HTML element
    self.bar_html = d3.select(html_element);
    // Bar layout for bar chart
    self.bar = self.bar_html.append('svg:svg')
      .attr('class', 'chart')
      .style('background-color', 'white');

    // Get correct size of chart and the wrapper.
    chart_module.get_size(self.html_element);

    self.data = data;
    // Floating label of used bar placement
    self.used_label_placement = data.settings.used_label_placement;

    // Width and height of the chart itself
    if (data.settings.width !== undefined){
      self.w = parseFloat(data.settings.width);
    } else {
      self.w = parseFloat(self.jquery_element.width());
    }
    if (data.settings.height !== undefined) {
      self.h = parseFloat(data.settings.height);
    } else {
      self.h = parseFloat(self.jquery_element.height());
    }

    /* Start coordinations of the chart and size of the chart wrapper.
       Chart can have other elements next to it, so it doesn't have to
       start at 0.
    */
    self.chart_start_x = 0;
    if (self.data.settings.orientation === 'vertical'){
      if (self.used_label_placement === 'left'){
        self.chart_start_x = 44;
      }
      self.chart_wrapper_w = self.w + self.chart_start_x;
    } else {
      self.chart_wrapper_w = self.w;
    }
    self.chart_wrapper_h = self.h;

    // Basic settigns of the chart
    self.lvl_curve = 3;
    self.bkgrnd = '#F2F2F2';
    self.frgrnd = 'grey';
    self.color_scale_max = 25;

    // Percentage used
    self.percentage_used = data.percentage_used;
    self.total_used_perc = 0; // incremented in render method
    self.total_used_value_in_pixels = 0; // incremented in render method
    self.used_value_in_pixels = 0; // set in UsedComponent
    self.average_value_in_pixels = 0; // set in AverageComponent


    // Percentage average
    self.percentage_average = data.percentage_average;
    self.tooltip_used_contents = data.tooltip_used_contents;

    // Set scales for multi bar chart
    self.usage_color = d3.scale.linear()
      .domain(data.settings.color_scale_domain)
      .range(data.settings.color_scale_range);

    // Border of the chart
    self.border_width = 1;

    // Return true if it renders multiple used percentage in one chart
    self.used_multi = function (){
      return ($.isArray(self.percentage_used));
    };

    // Deals with percentage if there should be multiple in one chart
    self.used_multi_iterator = 0;
    self.percentage_used_value = function(){
      if (self.used_multi()){
        return self.percentage_used[self.used_multi_iterator];
      } else {
        return self.percentage_used;
      }
    };

    // Deals with html tooltips if there should be multiple in one chart
    self.tooltip_used_value = function (){
      if (self.used_multi()){
        return self.tooltip_used_contents[self.used_multi_iterator];
      } else {
        return '';
      }
    };

    // Return true if it chart is oriented horizontally
    self.horizontal_orientation = function (){
      return (self.data.settings.orientation === 'horizontal');
    };
  },
  /**
   * Component rendering part of chart showing 'used', optional labels
   * and optional tool-tip element.
   * the chart data.
   * @param wrapper Wrapper object
   */
  UsedComponent: function(wrapper){
    var self = this;
    self.wrapper = wrapper;

    // FIXME(lsmola) would be good to abstract all attributes and resolve orientation inside
    if (self.wrapper.horizontal_orientation()){
      // Horizontal Bars
      self.wrapper.used_value_in_pixels = (self.wrapper.w / 100) * self.wrapper.percentage_used_value();

      self.y = 0;
      self.x = self.wrapper.total_used_value_in_pixels;
      self.width = 0;
      self.height = self.wrapper.h;
      self.trasition_attr = 'width';
      self.trasition_value = self.wrapper.used_value_in_pixels;
    } else {
      // Vertical Bars
      self.wrapper.used_value_in_pixels = (self.wrapper.h / 100) * self.wrapper.percentage_used_value();

      self.y = self.wrapper.h;
      self.x = self.wrapper.chart_start_x;
      self.width = self.wrapper.w - self.wrapper.border_width;
      self.height = self.wrapper.used_value_in_pixels;
      self.trasition_attr = 'y';
      self.trasition_value = self.wrapper.h - self.wrapper.used_value_in_pixels;
    }

    self.render = function(tooltip){
      self.wrapper.bar.append('rect')
        .attr('class', 'used_component')
        .attr('y', self.y)
        .attr('x', self.x)
        .attr('width', self.width)
        .attr('height', self.height)
        .style('fill', self.wrapper.usage_color(self.wrapper.percentage_used_value()))
        .style('stroke', '#bebebe')
        .style('stroke-width', 0)
        .attr('d', self.wrapper.percentage_used_value())
        .attr('tooltip-used', self.wrapper.tooltip_used_value())
        .on('mouseover', function(d){
          if ($(this).attr('tooltip-used')){
            tooltip.html($(this).attr('tooltip-used'));
          }
          tooltip.style('visibility', 'visible');
        })
        .on('mousemove', function(d){
          tooltip
            .style('top', (event.pageY - 10) + 'px')
            .style('left',(event.pageX + 10) + 'px');
        })
        .on('mouseout', function(d){
          tooltip.style('visibility', 'hidden');
        })
        .transition()
          .duration(500)
          .attr(self.trasition_attr, self.trasition_value);

      if (self.wrapper.used_label_placement === 'left') {
        // Now it works only for vertical bar chart placed left form the chart
        var label_placement_y = self.wrapper.h - self.wrapper.used_value_in_pixels;

        // Make sure the placement will be visible with border values
        if (label_placement_y <= 6){
          label_placement_y = 6;
        } else if (label_placement_y >= (self.wrapper.h - 6)){
          label_placement_y = self.wrapper.h - 6;
        }

        // Append label text
        self.wrapper.bar.append('text')
          .attr('class', 'used_component_label')
          .text(self.wrapper.percentage_used_value() + '%')
          .attr('y', label_placement_y)
          .attr('x', 0)
          .attr('dominant-baseline', 'middle')
          .attr('font-size', 12)
          .transition()
            .duration(500)
            .attr('x', function() {
              if (self.wrapper.percentage_used_value() > 99){
                // If there are two digits, label have to be farther to the bar chart
                return 0;
              }
              else if (self.wrapper.percentage_used_value() > 9){
                // If there are two digits, label have to be farther to the bar chart
                return 4;
              }
              else {
                // If there is only one digit, label can be closer to the bar chart
                return 8;
              }
            });

        // Append little triangle pointing to text
        var poly = [{'x':self.wrapper.chart_start_x - 8, 'y':label_placement_y},
                {'x':self.wrapper.chart_start_x - 3,'y':label_placement_y + 2},
                {'x':self.wrapper.chart_start_x - 3,'y':label_placement_y - 2}
               ];

        self.wrapper.bar.selectAll('polygon')
          .data([poly])
          .enter()
          .append('polygon')
          .attr('points',function(d) {
            return d.map(function(d) {
              return [d.x,d.y].join(',');
            }).join(' ');
          })
          .attr('stroke','black')
          .attr('stroke-width', 2);
      }
    };
  },
  /**
   * Component rendering part of chart showing 'average' and optional
   * tool-tip element.
   * the chart data.
   * @param wrapper Wrapper object
   */
  AverageComponent: function(wrapper){
    var self = this;
    self.wrapper = wrapper;

    // FIXME would be good to abstract all attributes and resolve orientation inside
    if (wrapper.horizontal_orientation()){
      // Horizontal Bars
      self.wrapper.average_value_in_pixels = (self.wrapper.w / 100) * self.wrapper.percentage_average;

      self.y = 1;
      self.x = self.wrapper.average_value_in_pixels;
      self.width = 0;
      self.height = self.wrapper.h;
    } else { // Vertical Bars
      self.wrapper.average_value_in_pixels = (self.wrapper.h / 100) * (100 - self.wrapper.percentage_average);

      self.y = self.wrapper.average_value_in_pixels;
      self.x = self.wrapper.chart_start_x;
      self.width = self.wrapper.w - self.wrapper.border_width;
      self.height = 0;
    }

    self.render = function(tooltip){
      if (self.wrapper.percentage_average > 0) {
        // Only show average when it is bigger than 0
        // A dashed line, so it's pretty
        self.wrapper.bar.append('line')
          .attr('class', 'average_component')
          .attr('y1', self.y)
          .attr('x1', self.x)
          .attr('class', 'average')
          .attr('y2', self.y + self.height)
          .attr('x2', self.x + self.width)
          .style('stroke', 'black')
          .style('stroke-width', 3)
          .style('stroke-dasharray', ('6, 2'))
          .on('mouseover', function(){tooltip.style('visibility', 'visible');})
          .on('mousemove', function(){
            tooltip
              .style('top',(event.pageY-10)+'px')
              .style('left',(event.pageX+10)+'px');
          })
          .on('mouseout', function(){tooltip.style('visibility', 'hidden');});

        // A normal line, so it shows popup even in spaces, it's also bigger so
        // it's easier to show popup
        self.wrapper.bar.append('line')
          .attr('class', 'average_component')
          .attr('y1', self.y)
          .attr('x1', self.x)
          .attr('class', 'average')
          .attr('y2', self.y + self.height)
          .attr('x2', self.x + self.width)
          .style('stroke', 'transparent')
          .style('stroke-width', 5)
          .on('mouseover', function(){tooltip.style('visibility', 'visible');})
          .on('mousemove', function(){
            tooltip
              .style('top',(event.pageY-10)+'px')
              .style('left',(event.pageX+10)+'px');
          })
          .on('mouseout', function(){tooltip.style('visibility', 'hidden');});
      }
    };
  },
  /**
   * Component rendering part of chart showing 'unused' and optional
   * tool-tip element.
   * the chart data.
   * @param wrapper Wrapper object
   */
  UnusedComponent: function(wrapper){
    var self = this;
    self.wrapper = wrapper;

    self.render = function(tooltip_free){
      self.wrapper.bar.append('rect')
        .attr('class', 'unused_component')
        .attr('y', 0)
        .attr('x', self.wrapper.chart_start_x)
        .attr('width', self.wrapper.w)
        .attr('height', self.wrapper.h)
        .attr('rx', self.wrapper.lvl_curve)
        .attr('ry', self.wrapper.lvl_curve)
        .style('fill', self.wrapper.bkgrnd)

        .on('mouseover', function(d){
          tooltip_free.style('visibility', 'visible');
        })
        .on('mousemove', function(d){
          tooltip_free
            .style('top',(event.pageY-10)+'px')
            .style('left',(event.pageX+10)+'px');
        })
        .on('mouseout', function(d){tooltip_free.style('visibility', 'hidden');});

      self.wrapper.bar.append('rect')
        .attr('class', 'unused_component_border')
        .attr('x', self.wrapper.chart_start_x)
        .attr('y', 0)
        .attr('height', self.wrapper.h)
        .attr('width', self.wrapper.w - self.wrapper.border_width) // a space for right border line
        .style('stroke', '#bebebe')
        .style('fill', 'none')
        .style('stroke-width', 1);
    };
  },
  /**
   * Component rendering tool-tip HTML code.
   * the chart data.
   * @param wrapper Wrapper object
   * @return HTML code of tool-tip
   */
  TooltipComponent: function(wrapper){
    var self = this;
    self.wrapper = wrapper;
    self.tooltip_html = self.wrapper.bar_html.append('div');

    self.render = function(html_content){
      var display = 'none';
      if (html_content){
        // Display only when there is some HTML content
        display = 'block';
      }

      return self.tooltip_html
        .attr('class', 'tooltip_detail')
        .style('position', 'absolute')
        .style('z-index', '10')
        .style('visibility', 'hidden')
        .style('display', display)
        .html(html_content);
    };
  },
  /**
   * Function for initializing of the charts.
   * @param selector JQuery selector of charts we want to initialize.
   * @param settings An object containing settings of the chart.
   * @param data An object containing data of the chart.
   */
  init: function(selector, settings, data) {
    var self = this;
    self.bars = $(selector);

    self.bars.each(function() {
      self.refresh(this, settings, data);
    });
  },
  /**
   * 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.
   * @param data An object containing data of the chart.
   */
  refresh: function(html_element, settings, data){
    var chart = new this.BarChart(this, html_element, settings, data);
    // 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 computing size of the chart from the surrounding HTML.
   * @param html_element HTML element where the chart will be rendered.
   */
  get_size: function(html_element){
    /* 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. */
    var jquery_element = $(html_element);
    jquery_element.css('height', '');
    jquery_element.css('width', '');
    var svg = jquery_element.find('svg');
    svg.hide();

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

    // Set either the minimal height defined by CSS.
    var height = jquery_element.height();

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


horizon.addInitFunction(function () {
  horizon.d3_bar_chart.init('div[data-chart-type="bar_chart"]', {}, {});
});
