(function($) {
  $.fn.highchartTable = function() {
    
    var allowedGraphTypes = ['column', 'line', 'area', 'spline', 'pie'];

    var getCallable = function (table, attribute) {
      var callback = $(table).data(attribute);
      if (typeof callback != 'undefined') {
        var infosCallback = callback.split('.');
        var callable      = window[infosCallback[0]];
        for(var i = 1, infosCallbackLength = infosCallback.length; i < infosCallbackLength; i++) {
          callable = callable[infosCallback[i]];
        }
        return callable;
      }
    };

    this.each(function() {
      var table = $(this);
      var $table = $(table);
      var nbYaxis = 1;

      // Retrieve graph title from the table caption
      var captions   = $('caption', table);
      var graphTitle = captions.length ? $(captions[0]).text() : '';

      var graphContainer;
      if ($table.data('graph-container-before') != 1) {
        // Retrieve where the graph must be displayed from the graph-container attribute
        var graphContainerSelector = $table.data('graph-container');
        if (!graphContainerSelector) {
          throw "graph-container data attribute is mandatory";
        }

        if (graphContainerSelector[0] === '#' || graphContainerSelector.indexOf('..')===-1) {
          // Absolute selector path
          graphContainer = $(graphContainerSelector);
        } else {
          var referenceNode                 = table;
          var currentGraphContainerSelector = graphContainerSelector;

          while (currentGraphContainerSelector.indexOf('..')!==-1) {
            currentGraphContainerSelector = currentGraphContainerSelector.replace(/^.. /, '');
            referenceNode = referenceNode.parent();
          }

          graphContainer = $(currentGraphContainerSelector, referenceNode);
        }
        if (graphContainer.length !== 1) {
          throw "graph-container is not available in this DOM or available multiple times";
        }
        graphContainer = graphContainer[0];
      } else {
        $table.before('<div></div>');
        graphContainer = $table.prev();
        graphContainer = graphContainer[0];
      }

      // Retrieve graph type from graph-type attribute
      var globalGraphType = $table.data('graph-type');
      if (!globalGraphType) {
        throw "graph-type data attribute is mandatory";
      }
      if ($.inArray(globalGraphType, allowedGraphTypes) == -1) {
        throw "graph-container data attribute must be one of " + allowedGraphTypes.join(', ');
      }

      var stackingType = $table.data('graph-stacking');
      if (!stackingType) {
        stackingType = 'normal';
      }

      var dataLabelsEnabled = $table.data('graph-datalabels-enabled');
      var isGraphInverted   = $table.data('graph-inverted') == 1;

      // Retrieve series titles
      var ths            = $('thead th', table);
      var columns        = [];
      var vlines         = [];
      var skippedColumns = 0;
      var graphIsStacked = false;
      ths.each(function(indexTh, th) {
        var $th = $(th);
        var columnScale = $th.data('graph-value-scale');

        var serieGraphType = $th.data('graph-type');
        if($.inArray(serieGraphType, allowedGraphTypes) == -1) {
          serieGraphType = globalGraphType;
        }

        var serieStackGroup = $th.data('graph-stack-group');
        if(serieStackGroup) {
          graphIsStacked = true;
       }

        var serieDataLabelsEnabled = $th.data('graph-datalabels-enabled');
        if (typeof serieDataLabelsEnabled == 'undefined') {
          serieDataLabelsEnabled = dataLabelsEnabled;
        }

        var yaxis = $th.data('graph-yaxis');

        if (typeof yaxis != 'undefined' && yaxis == '1') {
          nbYaxis = 2;
        }

        var isColumnSkipped = $th.data('graph-skip') == 1;
        if (isColumnSkipped)
        {
          skippedColumns = skippedColumns + 1;
        }

        var thGraphConfig = {
          libelle:           $th.text(),
          skip:              isColumnSkipped,
          indexTd:           indexTh - skippedColumns - 1,
          color:             $th.data('graph-color'),
          visible:           !$th.data('graph-hidden'),
          yAxis:             typeof yaxis != 'undefined' ? yaxis : 0,
          dashStyle:         $th.data('graph-dash-style') || 'solid',
          dataLabelsEnabled: serieDataLabelsEnabled == 1,
          dataLabelsColor:   $th.data('graph-datalabels-color') ||  $table.data('graph-datalabels-color')

        };

        var vlinex = $th.data('graph-vline-x');
        if (typeof vlinex == 'undefined') {
          thGraphConfig.scale     = typeof columnScale != 'undefined' ? parseFloat(columnScale) : 1;
          thGraphConfig.graphType = serieGraphType == 'column' && isGraphInverted ? 'bar' : serieGraphType;
          thGraphConfig.stack     = serieStackGroup;
          thGraphConfig.unit      = $th.data('graph-unit');
          columns[indexTh]        = thGraphConfig;
        } else {
          thGraphConfig.x      = vlinex;
          thGraphConfig.height = $th.data('graph-vline-height');
          thGraphConfig.name   = $th.data('graph-vline-name');
          vlines[indexTh]      = thGraphConfig;
        }
      });
      
      var series = [];
      $(columns).each(function(indexColumn, column) {
        if(indexColumn!=0 && !column.skip) {

          var serieConfig = {
            name:      column.libelle + (column.unit ? ' (' + column.unit + ')' : ''),
            data:      [],
            type:      column.graphType,
            stack:     column.stack,
            color:     column.color,
            visible:   column.visible,
            yAxis:     column.yAxis,
            dashStyle: column.dashStyle,
            marker: {
                enabled: false
            },
            dataLabels: {
              enabled: column.dataLabelsEnabled,
              color:   column.dataLabelsColor,
              align:   $table.data('graph-datalabels-align') || (globalGraphType == 'column' && isGraphInverted == 1 ? undefined : 'center')
            }
          };

          if(column.dataLabelsEnabled) {
            var callableSerieDataLabelsFormatter = getCallable(table, 'graph-datalabels-formatter');
            if (callableSerieDataLabelsFormatter) {
              serieConfig.dataLabels.formatter = function () {
                return callableSerieDataLabelsFormatter(this.y);
              };
            }
          }
          series.push(serieConfig);
        }
      });

      $(vlines).each(function(indexColumn, vline) {
        if (typeof vline != 'undefined' && !vline.skip) {
          series.push({
            name:    vline.libelle,
            data:    [{x: vline.x, y:0, name: vline.name}, {x:vline.x, y:vline.height, name: vline.name}],
            type:    'spline',
            color:   vline.color,
            visible: vline.visible,
            marker: {
              enabled: false
            }
          });
        }
      });

      var xValues         = [];
      var callablePoint   = getCallable(table, 'graph-point-callback');
      var isGraphDatetime = $table.data('graph-xaxis-type') == 'datetime';
      
      var rows            = $('tbody:first tr', table);
      rows.each(function(indexRow, row) {
        if (!!$(row).data('graph-skip')) {
          return;
        }

        var tds = $('td', row);
        tds.each(function(indexTd, td) {
          var cellValue;
          var column = columns[indexTd];

          if (column.skip) {
            return;
          }
          var $td = $(td);
          if (indexTd==0) {
            cellValue = $td.text();
            xValues.push(cellValue);
          } else {
            var rawCellValue = $td.text();
            var serie  = series[column.indexTd];

            if (rawCellValue.length==0) {
              if (!isGraphDatetime) {
                serie.data.push(null);
              }
            } else {
              var cleanedCellValue = rawCellValue.replace(/\s/g, '').replace(/,/, '.');
              var eventOptions = {
                 value: cleanedCellValue,
                 rawValue: rawCellValue,
                 td: $td,
                 tr: $(row),
                 indexTd: indexTd,
                 indexTr: indexRow
               }
               $table.trigger('highchartTable.cleanValue', eventOptions);
               cellValue = Math.round(parseFloat(eventOptions.value) * column.scale * 100) / 100;

                var dataGraphX = $td.data('graph-x');

                if (isGraphDatetime) {
                  dataGraphX    = $('td', $(row)).first().text();
                  var date      = parseDate(dataGraphX);
                  dataGraphX    = date.getTime() - date.getTimezoneOffset()*60*1000;
                }

                var tdGraphName = $td.data('graph-name');
                var serieDataItem = {
                  name:   typeof tdGraphName != 'undefined' ? tdGraphName : rawCellValue,
                  y:      cellValue,
                  x:      dataGraphX //undefined if no x defined in table
                };

                if (callablePoint) {
                  serieDataItem.events = {
                    click: function () {
                        return callablePoint(this);
                      }
                  };
                }

                if (column.graphType === 'pie') {
                  if ($td.data('graph-item-highlight')) {
                    serieDataItem.sliced = 1;
                  }
                }

                var tdGraphItemColor = $td.data('graph-item-color');
                if (typeof tdGraphItemColor != 'undefined') {
                  serieDataItem.color =  tdGraphItemColor;
                }

              serie.data.push(serieDataItem);
            }
          }
        });

      });

      var getYaxisAttr = function($table, yAxisNum, name) {
          var oldConvention = $table.data('graph-yaxis-' + yAxisNum + '-' + name);
          if (typeof oldConvention != 'undefined') {
              return oldConvention;
          }

          return $table.data('graph-yaxis' + yAxisNum + '-' + name);
      };

      var yAxisConfig = [];
      var yAxisNum;
      for (yAxisNum=1 ; yAxisNum <= nbYaxis ; yAxisNum++) {
        var yAxisConfigCurrentAxis = {
          title: {
            text: typeof getYaxisAttr($table, yAxisNum, 'title-text') != 'undefined'  ? getYaxisAttr($table, yAxisNum, 'title-text') : null
          },
          max:          typeof getYaxisAttr($table, yAxisNum, 'max') != 'undefined' ? getYaxisAttr($table, yAxisNum, 'max') : null,
          min:          typeof getYaxisAttr($table, yAxisNum, 'min') != 'undefined' ? getYaxisAttr($table, yAxisNum, 'min') : null,
          reversed:     getYaxisAttr($table, yAxisNum, 'reversed') == '1',
          opposite:     getYaxisAttr($table, yAxisNum, 'opposite') == '1',
          tickInterval: getYaxisAttr($table, yAxisNum, 'tick-interval') || null,
          labels: {
            rotation: getYaxisAttr($table, yAxisNum, 'rotation') || 0
          },
          startOnTick: getYaxisAttr($table, yAxisNum, 'start-on-tick') != "0",
          endOnTick:   getYaxisAttr($table, yAxisNum, 'end-on-tick') != "0",
          stackLabels : {
            enabled: getYaxisAttr($table, yAxisNum, 'stacklabels-enabled') == '1'
          },
          gridLineInterpolation: getYaxisAttr($table, yAxisNum, 'grid-line-interpolation') || null
        };

        var callableYAxisFormatter = getCallable(table, 'graph-yaxis-'+yAxisNum+'-formatter-callback');

        if (!callableYAxisFormatter) {
            callableYAxisFormatter = getCallable(table, 'graph-yaxis'+yAxisNum+'-formatter-callback');
        }

        if (callableYAxisFormatter) {
          yAxisConfigCurrentAxis.labels.formatter = function () {
              return callableYAxisFormatter(this.value);
          };
        }

        yAxisConfig.push(yAxisConfigCurrentAxis);
      }

      var defaultColors = [
        '#4572A7',
        '#AA4643',
        '#89A54E',
        '#80699B',
        '#3D96AE',
        '#DB843D',
        '#92A8CD',
        '#A47D7C',
        '#B5CA92'
      ];
      var colors = [];

      var themeColors = typeof Highcharts.theme != 'undefined' && typeof Highcharts.theme.colors != 'undefined' ? Highcharts.theme.colors : [];
      var lineShadow  = $table.data('graph-line-shadow');
      var lineWidth   = $table.data('graph-line-width') || 2;

      var nbOfColors = Math.max(defaultColors.length, themeColors.length);
      for(var i=0; i < nbOfColors; i++) {
        var dataname = 'graph-color-' + (i+1);
        colors.push(typeof $table.data(dataname) != 'undefined' ? $table.data(dataname) : typeof themeColors[i] != 'undefined' ? themeColors[i] : defaultColors[i]);
      }

      var marginTop    = $table.data('graph-margin-top');
      var marginRight  = $table.data('graph-margin-right');
      var marginBottom = $table.data('graph-margin-bottom');
      var marginLeft   = $table.data('graph-margin-left');
      
      var xAxisLabelsEnabled = $table.data('graph-xaxis-labels-enabled');

      var xAxisLabelStyle = {};
      var xAxisLabelFontSize = $table.data('graph-xaxis-labels-font-size');
      
      if (typeof xAxisLabelFontSize != 'undefined')
      {
        xAxisLabelStyle.fontSize = xAxisLabelFontSize; 
      }

      var highChartConfig = {
        colors: colors,
        chart: {
          renderTo:     graphContainer,
          inverted:     isGraphInverted,
          marginTop:    typeof marginTop != 'undefined' ? marginTop : null,
          marginRight:  typeof marginRight != 'undefined' ? marginRight : null,
          marginBottom: typeof marginBottom != 'undefined' ? marginBottom : null,
          marginLeft:   typeof marginLeft != 'undefined' ? marginLeft : null,
          spacingTop:   $table.data('graph-spacing-top') || 10,
          height:       $table.data('graph-height') || null,
          zoomType:     $table.data('graph-zoom-type') || null,
          polar:        $table.data('graph-polar') || null
        },
        title: {
          text: graphTitle
        },
        subtitle: {
          text: $table.data('graph-subtitle-text') || ''
        },
        legend: {
          enabled:     $table.data('graph-legend-disabled') != '1',
          layout:      $table.data('graph-legend-layout') || 'horizontal',
          symbolWidth: $table.data('graph-legend-width') || 30,
          x:           $table.data('graph-legend-x') || 15,
          y:           $table.data('graph-legend-y') || 0
        },
        xAxis: {
          categories:             ($table.data('graph-xaxis-type') != 'datetime') ? xValues : undefined,
          type:                   ($table.data('graph-xaxis-type') == 'datetime') ? 'datetime' :  undefined,
          reversed:               $table.data('graph-xaxis-reversed') == '1',
          opposite:               $table.data('graph-xaxis-opposite') == '1',
          showLastLabel:          typeof $table.data('graph-xaxis-show-last-label') != 'undefined' ? $table.data('graph-xaxis-show-last-label') : true,
          tickInterval:           $table.data('graph-xaxis-tick-interval') || null,
          dateTimeLabelFormats:   { //by default, we display the day and month on the datetime graphs
            second: '%e. %b',
            minute: '%e. %b',
            hour:   '%e. %b',
            day:    '%e. %b',
            week:   '%e. %b',
            month:  '%e. %b',
            year:   '%e. %b'
          },
          labels:
          {
            rotation: $table.data('graph-xaxis-rotation') || undefined,
            align:    $table.data('graph-xaxis-align') || undefined, 
            enabled:  typeof xAxisLabelsEnabled != 'undefined' ? xAxisLabelsEnabled : true,
            style:    xAxisLabelStyle
          },
          startOnTick: $table.data('graph-xaxis-start-on-tick'),
          endOnTick:   $table.data('graph-xaxis-end-on-tick'),
          min: getXAxisMinMax(table, 'min'),
          max: getXAxisMinMax(table, 'max'),
          alternateGridColor: $table.data('graph-xaxis-alternateGridColor') || null,
          title: {
            text: $table.data('graph-xaxis-title-text') || null
          },
          gridLineWidth:     $table.data('graph-xaxis-gridLine-width') || 0,
          gridLineDashStyle: $table.data('graph-xaxis-gridLine-style') || 'ShortDot',
          tickmarkPlacement: $table.data('graph-xaxis-tickmark-placement') || 'between',
          lineWidth:         $table.data('graph-xaxis-line-width') || 0
        },
        yAxis: yAxisConfig,
        tooltip: {
            formatter: function() {
              if ($table.data('graph-xaxis-type') == 'datetime') {
                return '<b>'+ this.series.name +'</b><br/>'+  Highcharts.dateFormat('%e. %b', this.x) +' : '+ this.y;
              } else {
                var xValue = typeof xValues[this.point.x] != 'undefined' ? xValues[this.point.x] : this.point.x;
                if (globalGraphType === 'pie') {
                  return '<strong>' + this.series.name + '</strong><br />' + xValue + ' : '  + this.point.y;
                }
                return '<strong>' + this.series.name + '</strong><br />' + xValue + ' : '  + this.point.name;
              }
            }
        },
        credits: {
          enabled: false
        },
        plotOptions: {
          line: {
            dataLabels: {
              enabled: true
            },
            lineWidth: lineWidth
          },
          area: {
            lineWidth:   lineWidth,
            shadow:      typeof lineShadow != 'undefined' ? lineShadow : true,
            fillOpacity: $table.data('graph-area-fillOpacity') || 0.75
          },
          pie: {
            allowPointSelect: true,
            dataLabels: {
              enabled: true
            },
            showInLegend: $table.data('graph-pie-show-in-legend') == '1',
            size:         '80%'
          },
          series: {
            animation:       false,
            stickyTracking : false,
            stacking:        graphIsStacked ? stackingType : null,
            groupPadding:    $table.data('graph-group-padding') || 0
          }
        },
        series: series,
        exporting: {
            filename: graphTitle.replace(/ /g, '_'),
            buttons: {
              exportButton: {
                menuItems: null,
                onclick: function() {
                  this.exportChart();
                }
              }
            }
          }
      };

      $table.trigger('highchartTable.beforeRender', highChartConfig);
      new Highcharts.Chart(highChartConfig);

    });
    //for fluent api
    return this;
  };
  
  var getXAxisMinMax = function(table, minOrMax) {
    var value = $(table).data('graph-xaxis-'+minOrMax);
    if (typeof value != 'undefined') {
      if ($(table).data('graph-xaxis-type') == 'datetime') {
        var date      = parseDate(value);
        return date.getTime() - date.getTimezoneOffset()*60*1000;
      }
      return value;
    }
    return null;
  };

  var parseDate = function(datetime) {
    var calculatedateInfos  = datetime.split(' ');
    var dateDayInfos        = calculatedateInfos[0].split('-');
    var min                 = null;
    var hour                = null;
    // If hour and minute are available in the datetime string
    if(calculatedateInfos[1]) {
      var dateHourInfos = calculatedateInfos[1].split(':');
      min               =  parseInt(dateHourInfos[0], 10);
      hour              = parseInt(dateHourInfos[1], 10);
    }
    return new Date(parseInt(dateDayInfos[0], 10), parseInt(dateDayInfos[1], 10)-1, parseInt(dateDayInfos[2], 10), min, hour);
  };
  
})(jQuery);