/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function(){

/* global d3: true, nv: true, D3Funnel: true, RadarChart: true, GaugeChart: true  */

"use strict";

var ui = angular.module('axelor.ui');

ui.ChartCtrl = ChartCtrl;
ui.ChartCtrl.$inject = ['$scope', '$element', '$http', 'ActionService'];

function ChartCtrl($scope, $element, $http, ActionService) {

  var views = $scope._views;
  var view = $scope.view = views.chart;

  var viewChart = null;
  var searchScope = null;
  var clickHandler = null;
  var actionHandler = null;

  var loading = false;
  var unwatch = null;

  function refresh() {

    if (viewChart && searchScope && $scope.searchFields && !searchScope.isValid()) {
      return;
    }

    var context = $scope._context || {};
    if ($scope.getContext) {
      context = _.extend({}, $scope.getContext(), context);
    }

    if (searchScope) {
      context = _.extend({}, context, searchScope.getContext());
    }

    context = _.extend({}, context, { _domainAction: $scope._viewAction });
    loading = true;

    var params = {
      data: context
    };

    if (viewChart) {
      params.fields = ['dataset'];
    }

    return $http.post('ws/meta/chart/' + view.name, params).then(function(response) {
      var res = response.data;
      var data = res.data;
      var isInitial = viewChart === null;

      if (viewChart === null) {
        viewChart = data;
        if (data.config && data.config.onClick) {
          clickHandler = ActionService.handler($scope, $element, {
            action: data.config.onClick
          });
        }
        if (data.config && data.config.onAction) {
          actionHandler = ActionService.handler($scope, $element, {
            action: data.config.onAction
          });
        }
        if (data.config && data.config.onActionTitle) {
          $scope.actionTitle = data.config.onActionTitle;
        }
      } else {
        data = _.extend({}, viewChart, data);
      }

      if ($scope.searchFields === undefined && data.search) {
        $scope.searchFields = data.search;
        $scope.searchInit = data.onInit;
        $scope.usingSQL = data.usingSQL;
      } else {
        $scope.render(data);
        if (isInitial) {
          refresh(); // force loading data
        }
      }
      loading = false;
    }, function () {
      loading = false;
    });
  }

  $scope.setSearchScope = function (formScope) {
    searchScope = formScope;
  };

  $scope.hasAction = function () {
    return !!actionHandler;
  };

  $scope.handleAction = function (data) {
    if (actionHandler) {
      actionHandler._getContext = function () {
        return _.extend({}, { _data: data }, {
          _model: $scope._model || 'com.axelor.meta.db.MetaView',
          _chart: view.name
        });
      };
      actionHandler.handle();
    }
  };

  $scope.handleClick = function (e) {
    if (clickHandler) {
      clickHandler._getContext = function () {
        return _.extend({}, e.data.raw, {
          _model: $scope._model || 'com.axelor.meta.db.MetaView',
          _chart: view.name
        });
      };
      clickHandler.handle();
    }
  };

  $scope.onRefresh = function(force) {
    if (unwatch || loading) {
      return;
    }

    // in case of onInit
    if ($scope.searchInit && !(searchScope||{}).record && !force) {
      return;
    }

    unwatch = $scope.$watch(function chartRefreshWatch() {
      if ($element.is(":hidden")) {
        return;
      }
      unwatch();
      unwatch = null;
      refresh();
    });
  };

  $scope.render = function(data) {

  };

  // refresh to load chart
  $scope.onRefresh();
}

ChartFormCtrl.$inject = ['$scope', '$element', 'ViewService', 'DataSource'];
function ChartFormCtrl($scope, $element, ViewService, DataSource) {

  $scope._dataSource = DataSource.create('com.axelor.meta.db.MetaView');

  ui.FormViewCtrl.call(this, $scope, $element);

  $scope.setEditable();
  $scope.setSearchScope($scope);

  function fixFields(fields) {
    _.each(fields, function(field){
      if (field.type == 'reference') {
        field.type = 'MANY_TO_ONE';
        field.canNew = false;
        field.canEdit = false;
      }

      if (field.type)
        field.type = field.type.toUpperCase();
      else
        field.type = 'STRING';
    });
    return fields;
  }

  var unwatch = $scope.$watch('searchFields', function chartSearchFieldsWatch(fields) {
    if (!fields) {
      return;
    }
    unwatch();

    var meta = { fields: fixFields(fields) };
    var view = {
      type: 'form',
      items: [{
        type: 'panel',
        noframe: true,
        items: _.map(meta.fields, function (item) {
          var props = _.extend({}, item, {
            showTitle: false,
            placeholder: item.title || item.autoTitle
          });
          if (item.multiple && (item.target || item.selection)) {
            item.widget = item.target ? "TagSelect" : "MultiSelect";
          }
          return props;
        })
      }]
    };

    ViewService.process(meta, view);

    view.onLoad = $scope.searchInit;

    $scope.fields = meta.fields;
    $scope.schema = view;
    $scope.schema.loaded = true;

    var interval;

    function reload() {
      $scope.$parent.onRefresh();
      $scope.$applyAsync();
    }

    function delayedReload() {
      clearTimeout(interval);
      interval = setTimeout(reload, 500);
    }

    function onNewOrEdit() {
      if ($scope.$events.onLoad) {
        $scope.$events.onLoad().then(delayedReload);
      }
    }

    var __getContext = $scope.getContext;
    $scope.getContext = function () {
      var ctx = __getContext.call(this);
      _.each(meta.fields, function (item) {
        if (item.multiple && (item.target || item.selection)) {
          var value = ctx[item.name];
          if (_.isArray(value)) value = _.pluck(value, "id");
          if (_.isString(value)) value = value.split(/\s*,\s*/g);
          ctx[item.name] = value;
        } else if (item.target && $scope.usingSQL) {
          var value = ctx[item.name];
          if (value) {
            ctx[item.name] = value.id;
          }
        }
      });
      return ctx;
    };

    $scope.$on('on:new', onNewOrEdit);
    $scope.$on('on:edit', onNewOrEdit);

    $scope.$watch('record', function chartSearchRecordWatch(record) {
      if (interval === undefined) {
        interval = null;
        return;
      }
      if ($scope.isValid()) delayedReload();
    }, true);

    $scope.$watch('$events.onLoad', function chartOnLoadWatch(handler) {
      if (handler) {
        handler().then(delayedReload);
      }
    });
  });
}

function $conv(value, type) {
  if (!value && type === 'text') return 'N/A';
  if (!value) return 0;
  if (_.isNumber(value)) return value;
  if (/^(-)?\d+(\.\d+)?$/.test(value)) {
    return +value;
  }
  return value;
}

function applyXY(chart, data) {
  var type = data.xType;
  chart.y(function (d) { return d.y; });
  if (type == "date") {
    return chart.x(function (d) { return moment(d.x).toDate(); });
  }
  return chart.x(function (d) { return d.x; });
}

var themes = {

  // default
  d3: d3.scale.category10().range(),

  // material
  material: [
    '#f44336', // Red
    '#E91E63', // Pink
    '#9c27b0', // Purple
    '#673ab7', // Deep Purple
    '#3f51b5', // Indigo
    '#2196F3', // Blue
    '#03a9f4', // Light Blue
    '#00bcd4', // Cyan
    '#009688', // Teal
    '#4caf50', // Green
    '#8bc34a', // Light Green
    '#cddc39', // Lime
    '#ffeb3b', // Yellow
    '#ffc107', // Amber
    '#ff9800', // Orange
    '#ff5722', // Deep Orange
    '#795548', // Brown
    '#9e9e9e', // Grey
    '#607d8b', // Blue Grey
  ],

  // chart.js
  chartjs: [
    '#ff6384', '#ff9f40', '#ffcd56', '#4bc0c0',
    '#36a2eb', '#9966ff', '#c9cbcf',
  ],

  // echart - roma
  roma: [
    '#E01F54','#001852','#f5e8c8','#b8d2c7','#c6b38e',
      '#a4d8c2','#f3d999','#d3758f','#dcc392','#2e4783',
      '#82b6e9','#ff6347','#a092f1','#0a915d','#eaf889',
      '#6699FF','#ff6666','#3cb371','#d5b158','#38b6b6',
  ],

  // echart - macarons
  macarons: [
      '#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80',
      '#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa',
      '#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050',
      '#59678c','#c9ab00','#7eb00a','#6f5553','#c14089',
  ]
};

function colors(names, shades, type) {
  var given = themes[names] ? themes[names] : names;
  given = given || themes.material;
  given = _.isArray(given) ? given : given.split(',');
  if (given && shades > 1) {
    var n = Math.max(0, Math.min(+(shades) || 4, 4));
    return _.flatten(given.map(function (c) {
      return _.first(_.range(0, n + 1).map(d3.scale.linear().domain([0, n + 1]).range([c, 'white'])), n);
    }));
  }
  return given;
}

var CHARTS = {};

function PlusData(series, data) {
  var result = _.chain(data.dataset)
  .groupBy(data.xAxis)
  .map(function (group, name) {
    var value = 0;
    _.each(group, function (item) {
      value += $conv(item[series.key]);
    });
    var raw = {};
    if (group[0]) {
      raw[data.xAxis] = name;
      raw[series.key] = value;
      raw[data.xAxis + 'Id'] = group[0][data.xAxis + 'Id'];
    }
    return {
      x: name === 'null' ? 'N/A' : name,
      y: value,
      raw: raw
    };
   }).value();

  return result;
}

function PlotData(series, data) {
  var ticks = _.chain(data.dataset).pluck(data.xAxis).unique().map(function (v) { return $conv(v, data.xType); }).value();
  var groupBy = series.groupBy;
  var datum = [];

  _.chain(data.dataset).groupBy(groupBy)
  .map(function (group, groupName) {
    var name = groupBy ? groupName : null;
    var values = _.map(group, function (item) {
      var x = $conv(item[data.xAxis], data.xType) || 0;
      var y = $conv(item[series.key] || name || 0);
      return { x: x, y: y, raw: item };
    });

    var my = _.pluck(values, 'x');
    var missing = _.difference(ticks, my);
    if (ticks.length === missing.length) {
      return;
    }

    _.each(missing, function(x) {
      values.push({ x: x, y: 0 });
    });

    values = _.sortBy(values, 'x');

    datum.push({
      key: name || series.title,
      type: series.type,
      values: values
    });
  });

  return datum;
}

function PieChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlusData(series, data);
  var config = data.config || {};

  var chart = nv.models.pieChart()
    .showLabels(false)
    .height(null)
    .width(null)
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; });

  if (series.type === "donut") {
    chart.donut(true)
       .donutRatio(0.40);
  }

  if (_.toBoolean(config.percent)) {
    chart.showLabels(true)
      .labelType("percent")
      .labelThreshold(0.05);
  }

  d3.select(element[0])
    .datum(datum)
    .transition().duration(1200).call(chart);

  chart.pie.dispatch.on('elementClick', function (e) {
    scope.handleClick(e);
  });

  return chart;
}

CHARTS.pie = PieChart;
CHARTS.donut = PieChart;

function DBarChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlusData(series, data);

  datum = [{
    key: data.title,
    values: datum
  }];

  var chart = nv.models.discreteBarChart()
      .x(function(d) { return d.x; })
      .y(function(d) { return d.y; })
      .staggerLabels(true)
      .showValues(true);

  d3.select(element[0])
    .datum(datum)
    .transition().duration(500).call(chart);

  chart.discretebar.dispatch.on('elementClick', function (e) {
    scope.handleClick(e);
  });

  return chart;
}

function BarChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlotData(series, data);

  var chart = nv.models.multiBarChart()
    .reduceXTicks(false);

  chart.multibar.hideable(true);
  chart.stacked(data.stacked);

  d3.select(element[0])
    .datum(datum)
    .transition().duration(500).call(chart);

  chart.multibar.dispatch.on('elementClick', function (e) {
    scope.handleClick(e);
  });

  return chart;
}

function HBarChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlotData(series, data);

  var chart = nv.models.multiBarHorizontalChart();

  chart.stacked(data.stacked);

  d3.select(element[0])
    .datum(datum)
    .transition().duration(500).call(chart);

  chart.multibar.dispatch.on('elementClick', function (e) {
    scope.handleClick(e);
  });

  return chart;
}

function FunnelChart(scope, element, data) {

  if(!data.dataset){
    return;
  }

  var chart = new D3Funnel(element[0]);
  var w = element.width();
  var h = element.height();
  var config = _.extend({}, data.config);
  var props = {
      fillType: 'gradient',
      hoverEffects: true,
      dynamicArea: true,
      animation: 200};

  if(config.width){
    props.width = w*config.width/100;
  }
  if(config.height){
    props.height = h*config.height/100;
  }

  var series = _.first(data.series) || {};
  var opts = [];
  _.each(data.dataset, function(dat){
    opts.push([dat[data.xAxis],($conv(dat[series.key])||0)]);
  });
  chart.draw(opts, props);

  chart.update = function(){};

  return chart;
}

CHARTS.bar = BarChart;
CHARTS.dbar = DBarChart;
CHARTS.hbar = HBarChart;
CHARTS.funnel = FunnelChart;

function LineChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlotData(series, data);

  var chart = nv.models.lineChart()
    .showLegend(true)
    .showYAxis(true)
    .showXAxis(true);

  applyXY(chart, data);

  d3.select(element[0])
    .datum(datum)
    .transition().duration(500).call(chart);

  return chart;
}

function AreaChart(scope, element, data) {

  var series = _.first(data.series);
  var datum = PlotData(series, data);

  var chart = nv.models.stackedAreaChart();

  applyXY(chart, data);

  d3.select(element[0])
    .datum(datum)
    .transition().duration(500).call(chart);

  return chart;
}

CHARTS.line = LineChart;
CHARTS.area = AreaChart;

function RadarCharter(scope, element, data) {

  var result = _.map(data.dataset, function(item) {
    return _.map(data.series, function(s) {
      var title = s.title || s.key,
        value = item[s.key];
      return {
        axis: title,
        value: $conv(value) || 0
      };
    });
  });

  var id = _.uniqueId('_radarChart'),
    parent = element.parent();

  parent.attr('id', id)
      .addClass('radar-chart')
      .empty();

  var size = Math.min(parent.innerWidth(), parent.innerHeight());

  RadarChart.draw('#'+id, result, {
    w: size,
    h: size
  });

  parent.children('svg')
    .css('width', 'auto')
    .css('margin', 'auto')
    .css('margin-top', 10);

  return null;
}

function GaugeCharter(scope, element, data) {

  var config = data.config,
    min = +(config.min) || 0,
    max = +(config.max) || 100,
    value = 0;

  var item = _.first(data.dataset),
    series = _.first(data.series),
    key = series.key || data.xAxis;

  if (item) {
    value = item[key] || value;
  }

  var w = element.width();
  var h = element.height();

  var parent = element.hide().parent();

  parent.children('svg').remove();
  parent.append(element);

  var chart = GaugeChart(parent[0], {
    size: 300,
    clipWidth: 300,
    clipHeight: h,
    ringWidth: 60,
    minValue: min,
    maxValue: max,
    transitionMs: 4000
  });

  chart.render();
  chart.update(value);

  parent.children('svg:last')
    .css('display', 'block')
    .css('width', 'auto')
    .css('margin', 'auto')
    .css('margin-top', 0);
}

function TextChart(scope, element, data) {

  var config = _.extend({
    strong: true,
    shadow: false,
    fontSize: 22
  }, data.config);

  var values = _.first(data.dataset) || {};
  var series = _.first(data.series) || {};

  var value = values[series.key];

  if (config.format) {
    value = _t(config.format, value);
  }

  var svg = d3.select(element.empty()[0]);
  var text = svg.append("svg:text")
    .attr("x", "50%")
    .attr("y", "50%")
    .attr("dy", ".3em")
      .attr("text-anchor", "middle")
    .text(value);

  if (config.color) text.attr("fill", config.color);
  if (config.fontSize) text.style("font-size", config.fontSize);
  if (_.toBoolean(config.strong)) text.style("font-weight", "bold");
  if (_.toBoolean(config.shadow)) text.style("text-shadow", "0 1px 2px rgba(0, 0, 0, .5)");
}

CHARTS.text = TextChart;
CHARTS.radar = RadarCharter;
CHARTS.gauge = GaugeCharter;

function Chart(scope, element, data) {

  var type = null;
  var config = data.config || {};

  for(var i = 0 ; i < data.series.length ; i++) {
    type = data.series[i].type;
    if (type === "bar" && !data.series[i].groupBy) type = "dbar";
    if (type === "pie" || type === "dbar" || type === "radar" || type === "gauge") {
      break;
    }
  }

  if (type === "pie" && data.series.length > 1) {
    return;
  }

  if (type !== "radar" && data.series.length > 1) {
    type = "multi";
  }

  // clean up last instance
  (function () {
    var chart = element.off('adjustSize').empty().data('chart');
    if (chart && chart.tooltip && chart.tooltip.id) {
      d3.select('#' + chart.tooltip.id()).remove();
    }
  })();

  nv.addGraph(function generate() {

    var noData = _t('No records found.');
    if (data.dataset && data.dataset.stacktrace) {
      noData = data.dataset.message;
      data.dataset = [];
    }

    var maker = CHARTS[type] || CHARTS.bar || function () {};
    var chart = maker(scope, element, data);

    if (!chart) {
      return;
    }

    // series scale attribute
    var series = _.first(data.series);
    var scale = series && series.scale;

    // format as integer if no scale is specified
    // and data has integer series values
    if (!isInteger(scale) && hasIntegerValues(data)) {
      scale = 0;
    }

    if (isInteger(scale)) {
      var format = '.' + scale + 'f';
      chart.yAxis && chart.yAxis.tickFormat(d3.format(format));
      chart.valueFormat && chart.valueFormat(d3.format(format));
    }

    if (chart.color) {
      chart.color(colors(config.colors, config.shades, type));
    }

    if (chart.noData) {
      chart.noData(noData);
    }
    if(chart.controlLabels) {
      chart.controlLabels({
        grouped: _t('Grouped'),
        stacked: _t('Stacked'),
        stream: _t('Stream'),
        expanded: _t('Expanded'),
        stack_percent: _t('Stack %')
      });
    }

    var tickFormats = {
      "date" : function (d) {
        var f = config.xFormat;
        return moment(d).format(f || 'YYYY-MM-DD');
      },
      "month" : function(d) {
        var v = "" + d;
        var f = config.xFormat;
        if (v.indexOf(".") > -1) return "";
        if (_.isString(d) && /^(\d+)$/.test(d)) {
          d = parseInt(d);
        }
        if (_.isNumber(d)) {
          return moment([moment().year(), d - 1, 1]).format(f || "MMM");
        }
        if (_.isString(d) && d.indexOf('-') > 0) {
          return moment(d).format(f || 'MMM, YYYY');
        }
        return d;
      },
      "year" : function(d) {
        return moment([moment().year(), d - 1, 1]).format("YYYY");
      },
      "number": d3.format(',f'),
      "decimal": d3.format(',.1f'),
      "text": function(d) { return d; }
    };

    var tickFormat = tickFormats[data.xType];
    if (chart.xAxis && tickFormat) {
      chart.xAxis
        .rotateLabels(-45)
        .tickFormat(tickFormat);
    }

    if (chart.yAxis && data.yTitle) {
      chart.yAxis.axisLabel(data.yTitle);
    }

    var margin = data.xType === 'date' ? { 'bottom': 65 } : null;
    ['top', 'left', 'bottom', 'right'].forEach(function (side) {
      var key = 'margin-' + side;
      var val = parseInt(config[key]);
      if (val) {
        (margin||(margin={}))[side] = val;
      }
    });
    if (chart.margin && margin) {
      chart.margin(margin);
    }

    var lastWidth = 0;
    var lastHeight = 0;

    function adjust() {

      if (!element[0] || element.parent().is(":hidden")) {
        return;
      }

      var rect = element[0].getBoundingClientRect();
      var w = rect.width,
        h = rect.height;

      if (w === lastWidth && h === lastHeight) {
        return;
      }

      lastWidth = w;
      lastHeight = h;

      chart.update();
    }

    element.data('chart', chart);
    scope.$onAdjust(adjust, 100);
    setTimeout(chart.update, 10);

    return chart;
  });
}

function hasIntegerValues(data) {
  var series = _.first(data.series);
  var dataset = _.first(data.dataset);
  return series && dataset && isInteger(dataset[series.key]);
}

function isInteger(n) {
  return (n ^ 0) === n;
}

var directiveFn = function(){
  return {
    controller: ChartCtrl,
    link: function(scope, element, attrs) {

      var svg = element.children('svg');
      var form = element.children('.chart-controls');

      function doExport(data) {
        var dataset = data.dataset || [];
        var header = [];
        _.each(dataset, function (item) {
          header = _.unique(_.flatten([header, _.keys(item)]));

        });

        var content = "data:text/csv;charset=utf-8," + header.join(';') + '\n';

        dataset.forEach(function (item) {
          var row = header.map(function (key) {
            var val = item[key];
            if (val === undefined || val === null) {
              val = '';
            }
            return '"' + (''+val).replace(/"/g, '""') + '"';
          });
          content += row.join(';') + '\n';
        });

        var name = (data.title || 'export').toLowerCase();
        ui.download(encodeURI(content), _.underscored(name) + '.csv');
      }

      scope.render = function(data) {
        if (element.is(":hidden")) {
          return;
        }
        setTimeout(function () {
          svg.height(element.height() - form.height()).width('100%');
          if (!scope.dashlet || !scope.dashlet.title) {
            scope.title = data.title;
          }
          Chart(scope, svg, data);
          var canExport = data && _.isArray(data.dataset);
          scope.canExport = function () {
            return canExport;
          };
          scope.onExport = function () {
            doExport(data);
          };
          scope.onAction = function () {
            scope.handleAction(data && data.dataset);
          };
          return;
        });
      };

      function onNewOrEdit() {
        if (scope.searchInit && scope.searchFields) {
          return;
        }
        scope.onRefresh(true);
      }

      scope.$on('on:new', onNewOrEdit);
      scope.$on('on:edit', onNewOrEdit);
    },
    replace: true,
    template:
    '<div class="chart-container" style="background-color: white; ">'+
      '<div ui-chart-form></div>'+
      '<svg></svg>'+
    '</div>'
  };
};

ui.directive('uiChartForm', function () {

  return {
    scope: true,
    controller: ChartFormCtrl,
    link: function (scope, element, attrs, ctrls) {

    },
    replace: true,
    template:
      "<div class='chart-controls'>" +
        "<div ui-view-form x-handler='this'></div>" +
      "</div>"
  };
});


ui.directive('uiViewChart', directiveFn);
ui.directive('uiPortletChart', directiveFn);

})();
