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

    var ErrorHandler = Private(require('ui/vislib/lib/_error_handler'));

    /**
     * Appends y axis to the visualization
     *
     * @class YAxis
     * @constructor
     * @param args {{el: (HTMLElement), yMax: (Number), _attr: (Object|*)}}
     */
    _.class(YAxis).inherits(ErrorHandler);
    function YAxis(args) {
      this.el = args.el;
      this.scale = null;
      this.domain = [args.yMin, args.yMax];
      this.yAxisFormatter = args.yAxisFormatter;
      this._attr = args._attr || {};
    }

    /**
     * Renders the y axis
     *
     * @method render
     * @return {D3.UpdateSelection} Renders y axis to visualization
     */
    YAxis.prototype.render = function () {
      d3.select(this.el).selectAll('.y-axis-div').call(this.draw());
    };

    YAxis.prototype._isPercentage = function () {
      return (this._attr.mode === 'percentage');
    };

    YAxis.prototype._isUserDefined = function () {
      return (this._attr.setYExtents);
    };

    YAxis.prototype._isYExtents = function () {
      return (this._attr.defaultYExtents);
    };

    YAxis.prototype._validateUserExtents = function (domain) {
      var self = this;

      return domain.map(function (val) {
        val = parseInt(val, 10);

        if (isNaN(val)) throw new Error(val + ' is not a valid number');
        if (self._isPercentage() && self._attr.setYExtents) return val / 100;
        return val;
      });
    };

    YAxis.prototype._getExtents = function (domain) {
      var min = domain[0];
      var max = domain[1];

      if (this._isUserDefined()) return this._validateUserExtents(domain);
      if (this._isYExtents()) return domain;
      if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale.
      if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)];
      return domain;
    };

    YAxis.prototype._throwCustomError = function (message) {
      throw new Error(message);
    };

    YAxis.prototype._throwLogScaleValuesError = function () {
      throw new errors.InvalidLogScaleValues();
    };

    /**
     * Returns the appropriate D3 scale
     *
     * @param fnName {String} D3 scale
     * @returns {*}
     */
    YAxis.prototype._getScaleType = function (fnName) {
      if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt'
      fnName = fnName || 'linear';

      if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function');

      return d3.scale[fnName]();
    };

    /**
     * Return the domain for log scale, i.e. the extent of the log scale.
     * Log scales must begin at 1 since the log(0) = -Infinity
     *
     * @param scale
     * @param yMin
     * @param yMax
     * @returns {*[]}
     */
    YAxis.prototype._logDomain = function (min, max) {
      if (min < 0 || max < 0) return this._throwLogScaleValuesError();
      return [1, max];
    };

    /**
     * Creates the d3 y scale function
     *
     * @method getYScale
     * @param height {Number} DOM Element height
     * @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
     */
    YAxis.prototype.getYScale = function (height) {
      var scale = this._getScaleType(this._attr.scale);
      var domain = this._getExtents(this.domain);

      this.yScale = scale
      .domain(domain)
      .range([height, 0]);

      if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined
      // Prevents bars from going off the chart when the y extents are within the domain range
      if (this._attr.type === 'histogram') this.yScale.clamp(true);
      return this.yScale;
    };

    YAxis.prototype.getScaleType = function () {
      return this._attr.scale;
    };

    YAxis.prototype.tickFormat = function () {
      var isPercentage = this._attr.mode === 'percentage';
      if (isPercentage) return d3.format('%');
      if (this.yAxisFormatter) return this.yAxisFormatter;
      return d3.format('n');
    };

    YAxis.prototype._validateYScale = function (yScale) {
      if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale);
    };

    /**
     * Creates the d3 y axis function
     *
     * @method getYAxis
     * @param height {Number} DOM Element height
     * @returns {D3.Svg.Axis|*} D3 yAxis function
     */
    YAxis.prototype.getYAxis = function (height) {
      var yScale = this.getYScale(height);
      this._validateYScale(yScale);

      // Create the d3 yAxis function
      this.yAxis = d3.svg.axis()
        .scale(yScale)
        .tickFormat(this.tickFormat(this.domain))
        .ticks(this.tickScale(height))
        .orient('left');

      return this.yAxis;
    };

    /**
     * Create a tick scale for the y axis that modifies the number of ticks
     * based on the height of the wrapping DOM element
     * Avoid using even numbers in the yTickScale.range
     * Causes the top most tickValue in the chart to be missing
     *
     * @method tickScale
     * @param height {Number} DOM element height
     * @returns {number} Number of y axis ticks
     */
    YAxis.prototype.tickScale = function (height) {
      var yTickScale = d3.scale.linear()
      .clamp(true)
      .domain([20, 40, 1000])
      .range([0, 3, 11]);

      return Math.ceil(yTickScale(height));
    };

    /**
     * Renders the y axis to the visualization
     *
     * @method draw
     * @returns {Function} Renders y axis to visualization
     */
    YAxis.prototype.draw = function () {
      var self = this;
      var margin = this._attr.margin;
      var mode = this._attr.mode;
      var isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette');

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

          var div = d3.select(el);
          var width = $(el).parent().width();
          var height = $(el).height();
          var adjustedHeight = height - margin.top - margin.bottom;

          // Validate whether width and height are not 0 or `NaN`
          self.validateWidthandHeight(width, adjustedHeight);

          var yAxis = self.getYAxis(adjustedHeight);

          // The yAxis should not appear if mode is set to 'wiggle' or 'silhouette'
          if (!isWiggleOrSilhouette) {
            // Append svg and y axis
            var svg = div.append('svg')
            .attr('width', width)
            .attr('height', height);

            svg.append('g')
            .attr('class', 'y axis')
            .attr('transform', 'translate(' + (width - 2) + ',' + margin.top + ')')
            .call(yAxis);

            var container = svg.select('g.y.axis').node();
            if (container) {
              var cWidth = Math.max(width, container.getBBox().width);
              svg.attr('width', cWidth);
              svg.select('g')
              .attr('transform', 'translate(' + (cWidth - 2) + ',' + margin.top + ')');
            }
          }
        });
      };
    };

    return YAxis;
  };
});
