/*
 * SonarQube, open source software quality management tool.
 * Copyright (C) 2008-2014 SonarSource
 * mailto:contact AT sonarsource DOT com
 *
 * SonarQube is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * SonarQube 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
window.SonarWidgets = window.SonarWidgets == null ? {} : window.SonarWidgets;

(function () {

  window.SonarWidgets.StackArea = function (container) {

    // Ensure container is html id
    if (container.indexOf('#') !== 0) {
      container = '#' + container;
    }

    this.container = d3.select(container);


    // Set default values
    this._data = [];
    this._metrics = [];
    this._snapshots = [];
    this._colors = [];
    this._width = window.SonarWidgets.StackArea.defaults.width;
    this._height = window.SonarWidgets.StackArea.defaults.height;
    this._margin = window.SonarWidgets.StackArea.defaults.margin;

    // Export global variables
    this.data = function (_) {
      return param.call(this, '_data', _);
    };

    this.metrics = function (_) {
      return param.call(this, '_metrics', _);
    };

    this.snapshots = function (_) {
      return param.call(this, '_snapshots', _);
    };

    this.colors = function (_) {
      return param.call(this, '_colors', _);
    };

    this.width = function (_) {
      return param.call(this, '_width', _);
    };

    this.height = function (_) {
      return param.call(this, '_height', _);
    };

    this.margin = function (_) {
      return param.call(this, '_margin', _);
    };

  };


  window.SonarWidgets.StackArea.prototype.initScales = function() {
    var widget = this,
        colorsLength = widget.colors().length;
    var timeDomain = this.data()
        .map(function(_) {
          return d3.extent(_, function(d) {
            return d.x;
          });
        })
        .reduce(function(p, c) {
          return p.concat(c);
        }, []);

    this.time = d3.time.scale().domain(d3.extent(timeDomain));

    this.y = d3.scale.linear()
        .domain([0, d3.max(this.stackDataTop, function(d) {
          return d.y0 + d.y;
        })])
        .nice();

    this.color = function(i) {
      return widget.colors()[i % colorsLength][0];
    };
  };


  window.SonarWidgets.StackArea.prototype.initAxis = function() {
    this.timeAxis = d3.svg.axis()
        .scale(this.time)
        .orient('bottom')
        .ticks(5);
  };


  window.SonarWidgets.StackArea.prototype.initArea = function() {
    var widget = this;
    this.area = d3.svg.area()
        .x(function(d) { return widget.time(d.x); })
        .y0(function(d) { return widget.y(d.y0); })
        .y1(function(d) { return widget.y(d.y0 + d.y); });

    this.areaLine = d3.svg.line()
        .x(function(d) { return widget.time(d.x); })
        .y(function(d) { return widget.y(d.y0 + d.y); });
  };


  window.SonarWidgets.StackArea.prototype.initInfo = function() {
    var widget = this;
    this.infoWrap
        .attr('class', 'info')
        .attr('transform', trans(0, -60));

    this.infoDate
        .attr('class', 'info-text info-text-bold')
        .attr('transform', trans(0, 0));

    this.infoTotal
        .attr('class', 'info-text info-text-small')
        .attr('transform', trans(0, 18));

    this.infoSnapshot
        .attr('class', 'info-text info-text-small')
        .attr('transform', trans(0, 54));

    this.infoMetrics = [];
    var prevX = 120;
    this.metrics().forEach(function(d, i) {
      var infoMetric = widget.infoWrap.append('g');

      var infoMetricText = infoMetric.append('text')
          .attr('class', 'info-text-small')
          .attr('transform', trans(10, 0))
          .text(widget.metrics()[i]);

      infoMetric.append('circle')
          .attr('transform', trans(0, -4))
          .attr('r', 4)
          .style('fill', function() { return widget.color(i); });

      // Align metric labels
      infoMetric
          .attr('transform', function() {
            return trans(prevX, -1 + (i % 3) * 18);
          });

      widget.infoMetrics.push(infoMetric);

      if (i % 3 === 2) {
        prevX += (infoMetricText.node().getComputedTextLength() + 70);
      }
    });
  };


  window.SonarWidgets.StackArea.prototype.initEvents = function() {
    var widget = this;
    this.events = widget.snapshots()
        .filter(function(d) { return d.e.length > 0; });

    this.gevents = this.gWrap.append('g')
        .attr('class', 'axis events')
        .selectAll('.event-tick')
        .data(this.events);

    this.gevents.enter().append('line')
        .attr('class', 'event-tick')
        .attr('y2', -8);


    this.selectSnapshot = function(cl) {
      var dataX = widget.data()[0][cl].x,
          sx = widget.time(dataX),
          snapshotIndex = null,
          eventIndex = null;

      // Update scanner position
      widget.scanner
          .attr('x1', sx)
          .attr('x2', sx);


      // Update metric labels
      var metricsLines = widget.data().map(function(d, i) {
        var value = d[cl].fy || d[cl].y;
        return widget.metrics()[i] + ': ' + value;
      });

      metricsLines.forEach(function(d, i) {
        widget.infoMetrics[i].select('text').text(d);
      });


      // Update snapshot info
      this.snapshots().forEach(function(d, i) {
        if (d.d - dataX === 0) {
          snapshotIndex = i;
        }
      });

      if (snapshotIndex != null) {
        widget.infoSnapshot
            .text(this.snapshots()[snapshotIndex].e.join(', '));
      }


      // Update info
      widget.infoDate
          .text(moment(widget.data()[0][cl].x).format('LL'));

      var snapshotValue = this.snapshots()[snapshotIndex].fy,
          totalValue = snapshotValue || (widget.stackDataTop[cl].y0 + widget.stackDataTop[cl].y);
      widget.infoTotal
          .text('Total: ' + totalValue);


      // Update event
      this.events.forEach(function(d, i) {
        if (d.d - dataX === 0) {
          eventIndex = i;
        }
      });

      widget.gevents.attr('y2', -8);
      d3.select(widget.gevents[0][eventIndex]).attr('y2', -12);
    };


    // Set event listeners
    this.svg.on('mousemove', function() {
      var mx = d3.mouse(widget.plotWrap.node())[0],
          cl = closest(widget.data()[0], mx, function(d) { return widget.time(d.x); });
      widget.selectSnapshot(cl);
    });
  };


  window.SonarWidgets.StackArea.prototype.render = function () {
    this.svg = this.container.append('svg')
        .attr('class', 'sonar-d3');

    this.gWrap = this.svg.append('g');

    this.gtimeAxis = this.gWrap.append('g')
        .attr('class', 'axis x');

    this.plotWrap = this.gWrap.append('g')
        .attr('class', 'plot');

    this.scanner = this.plotWrap.append('line');

    this.infoWrap = this.gWrap.append('g');
    this.infoDate = this.infoWrap.append('text');
    this.infoSnapshot = this.infoWrap.append('text');
    this.infoTotal = this.infoWrap.append('text');

    this.gWrap
        .attr('transform', trans(this.margin().left, this.margin().top));

    // Configure stack
    this.stack = d3.layout.stack();
    this.stackData = this.stack(this.data());
    this.stackDataTop = this.stackData[this.stackData.length - 1];

    this.initScales();
    this.initAxis();
    this.initArea();

    // Configure scanner
    this.scanner
        .attr('class', 'scanner')
        .attr('y1', 0);

    this.initInfo();
    this.initEvents();
    this.update();

    return this;
  };


  window.SonarWidgets.StackArea.prototype.update = function() {
    var widget = this,
        width = this.container.property('offsetWidth');

    this.width(width > 100 ? width : 100);


    // Update svg canvas
    this.svg
        .attr('width', this.width())
        .attr('height', this.height());


    // Update available size
    this.availableWidth = this.width() - this.margin().left - this.margin().right;
    this.availableHeight = this.height() - this.margin().top - this.margin().bottom;


    // Update scales
    this.time.range([0, this.availableWidth]);
    this.y.range([widget.availableHeight, 0]);


    // Update the axis
    this.gtimeAxis.attr('transform', trans(0, this.availableHeight + this.margin().bottom - 30));
    this.gtimeAxis.transition().call(this.timeAxis);


    // Update area
    this.garea = this.plotWrap.selectAll('.area')
        .data(this.stackData)
        .enter()
        .insert('path', ':first-child')
        .attr('class', 'area')
        .attr('d', function(d) { return widget.area(d); })
        .style('fill', function(d, i) { return widget.color(i); });

    this.gareaLine = this.plotWrap.selectAll('.area-line')
        .data(this.stackData)
        .enter()
        .insert('path')
        .attr('class', 'area-line')
        .attr('d', function(d) { return widget.areaLine(d); })
        .style('fill', 'none')
        .style('stroke', '#808080');


    // Update scanner
    this.scanner.attr('y2', this.availableHeight + 10);


    // Update events
    this.gevents
        .transition()
        .attr('transform', function(d) {
          return trans(widget.time(d.d), widget.availableHeight + 10);
        });


    // Select latest values if this it the first update
    if (!this.firstUpdate) {
      this.selectSnapshot(widget.data()[0].length - 1);

      this.firstUpdate = true;
    }

  };



  window.SonarWidgets.StackArea.defaults = {
    width: 350,
    height: 150,
    margin: { top: 80, right: 10, bottom: 40, left: 40 }
  };



  // Some helper functions

  // Gets or sets parameter
  function param(name, value) {
    if (value == null) {
      return this[name];
    } else {
      this[name] = value;
      return this;
    }
  }

  // Helper for create the translate(x, y) string
  function trans(left, top) {
    return 'translate(' + left + ', ' + top + ')';
  }

  // Helper for find the closest number in array
  function closest(array, number, getter) {
    var cl = null;
    array.forEach(function(value, i) {
      if (cl == null ||
          Math.abs(getter(value) - number) < Math.abs(getter(array[cl]) - number)) {
        cl = i;
      }
    });
    return cl;
  }

})();
