define(function (require) {
  return function LineChartFactory(Private) {
    var d3 = require('d3');
    var _ = require('lodash');
    var $ = require('jquery');
    var errors = require('ui/errors');

    var PointSeriesChart = Private(require('ui/vislib/visualizations/_point_series_chart'));
    var TimeMarker = Private(require('ui/vislib/visualizations/time_marker'));

    /**
     * Line Chart Visualization
     *
     * @class LineChart
     * @constructor
     * @extends Chart
     * @param handler {Object} Reference to the Handler Class Constructor
     * @param el {HTMLElement} HTML element to which the chart will be appended
     * @param chartData {Object} Elasticsearch query results for this specific chart
     */
    _.class(LineChart).inherits(PointSeriesChart);
    function LineChart(handler, chartEl, chartData) {
      if (!(this instanceof LineChart)) {
        return new LineChart(handler, chartEl, chartData);
      }

      LineChart.Super.apply(this, arguments);

      // Line chart specific attributes
      this._attr = _.defaults(handler._attr || {}, {
        interpolate: 'linear',
        xValue: function (d) { return d.x; },
        yValue: function (d) { return d.y; }
      });
    }

    /**
     * Adds Events to SVG circle
     *
     * @method addCircleEvents
     * @param element{D3.UpdateSelection} Reference to SVG circle
     * @returns {D3.Selection} SVG circles with event listeners attached
     */
    LineChart.prototype.addCircleEvents = function (element, svg) {
      var events = this.events;
      var isBrushable = events.isBrushable();
      var brush = isBrushable ? events.addBrushEvent(svg) : undefined;
      var hover = events.addHoverEvent();
      var mouseout = events.addMouseoutEvent();
      var click = events.addClickEvent();
      var attachedEvents = element.call(hover).call(mouseout).call(click);

      if (isBrushable) {
        attachedEvents.call(brush);
      }

      return attachedEvents;
    };

    /**
     * Adds circles to SVG
     *
     * @method addCircles
     * @param svg {HTMLElement} SVG to which rect are appended
     * @param data {Array} Array of object data points
     * @returns {D3.UpdateSelection} SVG with circles added
     */
    LineChart.prototype.addCircles = function (svg, data) {
      var self = this;
      var showCircles = this._attr.showCircles;
      var color = this.handler.data.getColorFunc();
      var xScale = this.handler.xAxis.xScale;
      var yScale = this.handler.yAxis.yScale;
      var ordered = this.handler.data.get('ordered');
      var tooltip = this.tooltip;
      var isTooltip = this._attr.addTooltip;

      var radii = _(data)
      .map(function (series) {
        return _.pluck(series, '_input.z');
      })
      .flattenDeep()
      .reduce(function (result, val) {
        if (result.min > val) result.min = val;
        if (result.max < val) result.max = val;
        return result;
      }, {
        min: Infinity,
        max: -Infinity
      });

      var radiusStep = ((radii.max - radii.min) || (radii.max * 100)) / Math.pow(this._attr.radiusRatio, 2);

      var layer = svg.selectAll('.points')
      .data(data)
      .enter()
        .append('g')
        .attr('class', 'points line');

      var circles = layer
      .selectAll('circle')
      .data(function appendData(data) {
        return data.filter(function (d) {
          return !_.isNull(d.y);
        });
      });

      circles
      .exit()
      .remove();

      function cx(d) {
        if (ordered && ordered.date) {
          return xScale(d.x);
        }
        return xScale(d.x) + xScale.rangeBand() / 2;
      }

      function cy(d) {
        return yScale(d.y);
      }

      function cColor(d) {
        return color(d.label);
      }

      function colorCircle(d) {
        var parent = d3.select(this).node().parentNode;
        var lengthOfParent = d3.select(parent).data()[0].length;
        var isVisible = (lengthOfParent === 1);

        // If only 1 point exists, show circle
        if (!showCircles && !isVisible) return 'none';
        return cColor(d);
      }
      function getCircleRadiusFn(modifier) {
        return function getCircleRadius(d) {
          var margin = self._attr.margin;
          var width = self._attr.width - margin.left - margin.right;
          var height = self._attr.height - margin.top - margin.bottom;
          var circleRadius = (d._input.z - radii.min) / radiusStep;

          return _.min([Math.sqrt((circleRadius || 2) + 2), width, height]) + (modifier || 0);
        };
      }


      circles
      .enter()
        .append('circle')
        .attr('r', getCircleRadiusFn())
        .attr('fill-opacity', (this._attr.drawLinesBetweenPoints ? 1 : 0.7))
        .attr('cx', cx)
        .attr('cy', cy)
        .attr('class', 'circle-decoration')
        .call(this._addIdentifier)
        .attr('fill', colorCircle);

      circles
      .enter()
        .append('circle')
        .attr('r', getCircleRadiusFn(10))
        .attr('cx', cx)
        .attr('cy', cy)
        .attr('fill', 'transparent')
        .attr('class', 'circle')
        .call(this._addIdentifier)
        .attr('stroke', cColor)
        .attr('stroke-width', 0);

      if (isTooltip) {
        circles.call(tooltip.render());
      }

      return circles;
    };

    /**
     * Adds path to SVG
     *
     * @method addLines
     * @param svg {HTMLElement} SVG to which path are appended
     * @param data {Array} Array of object data points
     * @returns {D3.UpdateSelection} SVG with paths added
     */
    LineChart.prototype.addLines = function (svg, data) {
      var self = this;
      var xScale = this.handler.xAxis.xScale;
      var yScale = this.handler.yAxis.yScale;
      var xAxisFormatter = this.handler.data.get('xAxisFormatter');
      var color = this.handler.data.getColorFunc();
      var ordered = this.handler.data.get('ordered');
      var interpolate = (this._attr.smoothLines) ? 'cardinal' : this._attr.interpolate;
      var line = d3.svg.line()
      .defined(function (d) { return !_.isNull(d.y); })
      .interpolate(interpolate)
      .x(function x(d) {
        if (ordered && ordered.date) {
          return xScale(d.x);
        }
        return xScale(d.x) + xScale.rangeBand() / 2;
      })
      .y(function y(d) {
        return yScale(d.y);
      });
      var lines;

      lines = svg
      .selectAll('.lines')
      .data(data)
      .enter()
        .append('g')
        .attr('class', 'pathgroup lines');

      lines.append('path')
      .call(this._addIdentifier)
      .attr('d', function lineD(d) {
        return line(d.values);
      })
      .attr('fill', 'none')
      .attr('stroke', function lineStroke(d) {
        return color(d.label);
      })
      .attr('stroke-width', 2);

      return lines;
    };

    /**
     * Adds SVG clipPath
     *
     * @method addClipPath
     * @param svg {HTMLElement} SVG to which clipPath is appended
     * @param width {Number} SVG width
     * @param height {Number} SVG height
     * @returns {D3.UpdateSelection} SVG with clipPath added
     */
    LineChart.prototype.addClipPath = function (svg, width, height) {
      var clipPathBuffer = 5;
      var startX = 0;
      var startY = 0 - clipPathBuffer;
      var id = 'chart-area' + _.uniqueId();

      return svg
      .attr('clip-path', 'url(#' + id + ')')
      .append('clipPath')
      .attr('id', id)
      .append('rect')
      .attr('x', startX)
      .attr('y', startY)
      .attr('width', width)
      // Adding clipPathBuffer to height so it doesn't
      // cutoff the lower part of the chart
      .attr('height', height + clipPathBuffer);
    };

    /**
     * Renders d3 visualization
     *
     * @method draw
     * @returns {Function} Creates the line chart
     */
    LineChart.prototype.draw = function () {
      var self = this;
      var $elem = $(this.chartEl);
      var margin = this._attr.margin;
      var elWidth = this._attr.width = $elem.width();
      var elHeight = this._attr.height = $elem.height();
      var scaleType = this.handler.yAxis.getScaleType();
      var yMin = this.handler.yAxis.yMin;
      var yScale = this.handler.yAxis.yScale;
      var xScale = this.handler.xAxis.xScale;
      var minWidth = 20;
      var minHeight = 20;
      var startLineX = 0;
      var lineStrokeWidth = 1;
      var addTimeMarker = this._attr.addTimeMarker;
      var times = this._attr.times || [];
      var timeMarker;
      var div;
      var svg;
      var width;
      var height;
      var lines;
      var circles;

      return function (selection) {
        selection.each(function (data) {
          var el = this;

          var layers = data.series.map(function mapSeries(d) {
            var label = d.label;
            return d.values.map(function mapValues(e, i) {
              return {
                _input: e,
                label: label,
                x: self._attr.xValue.call(d.values, e, i),
                y: self._attr.yValue.call(d.values, e, i)
              };
            });
          });

          width = elWidth - margin.left - margin.right;
          height = elHeight - margin.top - margin.bottom;

          if (addTimeMarker) {
            timeMarker = new TimeMarker(times, xScale, height);
          }

          if (self._attr.scale === 'log' && self._invalidLogScaleValues(data)) {
            throw new errors.InvalidLogScaleValues();
          }

          if (width < minWidth || height < minHeight) {
            throw new errors.ContainerTooSmall();
          }

          div = d3.select(el);

          svg = div.append('svg')
          .attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom)
          .append('g')
          .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

          self.addClipPath(svg, width, height);
          if (self._attr.drawLinesBetweenPoints) {
            lines = self.addLines(svg, data.series);
          }
          circles = self.addCircles(svg, layers);
          self.addCircleEvents(circles, svg);
          self.createEndZones(svg);

          var scale = (scaleType === 'log') ? yScale(1) : yScale(0);
          if (scale) {
            svg.append('line')
            .attr('class', 'base-line')
            .attr('x1', startLineX)
            .attr('y1', scale)
            .attr('x2', width)
            .attr('y2', scale)
            .style('stroke', '#ddd')
            .style('stroke-width', lineStrokeWidth);
          }

          if (addTimeMarker) {
            timeMarker.render(svg);
          }

          return svg;
        });
      };
    };

    return LineChart;
  };
});
