
// Chart design based on the recommendations of Stephen Few. Implementation
// based on the work of Clint Ivy, Jamie Love, and Jason Davies.
// http://projects.instantcognition.com/protovis/bulletchart/

nv.models.bullet = function() {
    "use strict";

    //============================================================
    // Public Variables with Default Settings
    //------------------------------------------------------------

    var margin = {top: 0, right: 0, bottom: 0, left: 0}
        , orient = 'left' // TODO top & bottom
        , reverse = false
        , ranges = function(d) { return d.ranges }
        , markers = function(d) { return d.markers ? d.markers : [] }
        , markerLines = function(d) { return d.markerLines ? d.markerLines : [0] }
        , measures = function(d) { return d.measures }
        , rangeLabels = function(d) { return d.rangeLabels ? d.rangeLabels : [] }
        , markerLabels = function(d) { return d.markerLabels ? d.markerLabels : []  }
        , markerLineLabels = function(d) { return d.markerLineLabels ? d.markerLineLabels : []  }
        , measureLabels = function(d) { return d.measureLabels ? d.measureLabels : []  }
        , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.)
        , width = 380
        , height = 30
        , container = null
        , tickFormat = null
        , color = nv.utils.getColor(['#1f77b4'])
        , dispatch = d3.dispatch('elementMouseover', 'elementMouseout', 'elementMousemove')
        , defaultRangeLabels = ["Maximum", "Mean", "Minimum"]
        , legacyRangeClassNames = ["Max", "Avg", "Min"]
        ;

    function sortLabels(labels, values){
        var lz = labels.slice();
        labels.sort(function(a, b){
            var iA = lz.indexOf(a);
            var iB = lz.indexOf(b);
            return d3.descending(values[iA], values[iB]);
        });
    };

    function chart(selection) {
        selection.each(function(d, i) {
            var availableWidth = width - margin.left - margin.right,
                availableHeight = height - margin.top - margin.bottom;

            container = d3.select(this);
            nv.utils.initSVG(container);

            var rangez = ranges.call(this, d, i).slice(),
                markerz = markers.call(this, d, i).slice(),
                markerLinez = markerLines.call(this, d, i).slice().sort(d3.descending),
                measurez = measures.call(this, d, i).slice(),
                rangeLabelz = rangeLabels.call(this, d, i).slice(),
                markerLabelz = markerLabels.call(this, d, i).slice(),
                markerLineLabelz = markerLineLabels.call(this, d, i).slice(),
                measureLabelz = measureLabels.call(this, d, i).slice();

            // Sort labels according to their sorted values
            sortLabels(rangeLabelz, rangez);
            sortLabels(markerLabelz, markerz);
            sortLabels(markerLineLabelz, markerLinez);
            sortLabels(measureLabelz, measurez);

            // sort values descending
            rangez.sort(d3.descending);
            markerz.sort(d3.descending);
            measurez.sort(d3.descending);

            // Setup Scales
            // Compute the new x-scale.
            var x1 = d3.scale.linear()
                .domain( d3.extent(d3.merge([forceX, rangez])) )
                .range(reverse ? [availableWidth, 0] : [0, availableWidth]);

            // Retrieve the old x-scale, if this is an update.
            var x0 = this.__chart__ || d3.scale.linear()
                .domain([0, Infinity])
                .range(x1.range());

            // Stash the new scale.
            this.__chart__ = x1;

            var rangeMin = d3.min(rangez), //rangez[2]
                rangeMax = d3.max(rangez), //rangez[0]
                rangeAvg = rangez[1];

            // Setup containers and skeleton of chart
            var wrap = container.selectAll('g.nv-wrap.nv-bullet').data([d]);
            var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bullet');
            var gEnter = wrapEnter.append('g');
            var g = wrap.select('g');

            for(var i=0,il=rangez.length; i<il; i++){
                var rangeClassNames = 'nv-range nv-range'+i;
                if(i <= 2){
                    rangeClassNames = rangeClassNames + ' nv-range'+legacyRangeClassNames[i];
                }
                gEnter.append('rect').attr('class', rangeClassNames);
            }

            gEnter.append('rect').attr('class', 'nv-measure');

            wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

            var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0)
                w1 = function(d) { return Math.abs(x1(d) - x1(0)) };
            var xp0 = function(d) { return d < 0 ? x0(d) : x0(0) },
                xp1 = function(d) { return d < 0 ? x1(d) : x1(0) };

            for(var i=0,il=rangez.length; i<il; i++){
                var range = rangez[i];
                g.select('rect.nv-range'+i)
                    .attr('height', availableHeight)
                    .attr('width', w1(range))
                    .attr('x', xp1(range))
                    .datum(range)
            }

            g.select('rect.nv-measure')
                .style('fill', color)
                .attr('height', availableHeight / 3)
                .attr('y', availableHeight / 3)
                .attr('width', measurez < 0 ?
                    x1(0) - x1(measurez[0])
                    : x1(measurez[0]) - x1(0))
                .attr('x', xp1(measurez))
                .on('mouseover', function() {
                    dispatch.elementMouseover({
                        value: measurez[0],
                        label: measureLabelz[0] || 'Current',
                        color: d3.select(this).style("fill")
                    })
                })
                .on('mousemove', function() {
                    dispatch.elementMousemove({
                        value: measurez[0],
                        label: measureLabelz[0] || 'Current',
                        color: d3.select(this).style("fill")
                    })
                })
                .on('mouseout', function() {
                    dispatch.elementMouseout({
                        value: measurez[0],
                        label: measureLabelz[0] || 'Current',
                        color: d3.select(this).style("fill")
                    })
                });

            var h3 =  availableHeight / 6;

            var markerData = markerz.map( function(marker, index) {
                return {value: marker, label: markerLabelz[index]}
            });
            gEnter
              .selectAll("path.nv-markerTriangle")
              .data(markerData)
              .enter()
              .append('path')
              .attr('class', 'nv-markerTriangle')
              .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z')
              .on('mouseover', function(d) {
                dispatch.elementMouseover({
                  value: d.value,
                  label: d.label || 'Previous',
                  color: d3.select(this).style("fill"),
                  pos: [x1(d.value), availableHeight/2]
                })

              })
              .on('mousemove', function(d) {
                  dispatch.elementMousemove({
                      value: d.value,
                      label: d.label || 'Previous',
                      color: d3.select(this).style("fill")
                  })
              })
              .on('mouseout', function(d, i) {
                  dispatch.elementMouseout({
                      value: d.value,
                      label: d.label || 'Previous',
                      color: d3.select(this).style("fill")
                  })
              });

            g.selectAll("path.nv-markerTriangle")
              .data(markerData)
              .attr('transform', function(d) { return 'translate(' + x1(d.value) + ',' + (availableHeight / 2) + ')' });

            var markerLinesData = markerLinez.map( function(marker, index) {
                return {value: marker, label: markerLineLabelz[index]}
            });
            gEnter
              .selectAll("path.nv-markerLine")
              .data(markerLinesData)
              .enter()
              .append('line')
              .attr('cursor', '')
              .attr('class', 'nv-markerLine')
              .attr('x1', function(d) { return x1(d.value) })
              .attr('y1', '2')
              .attr('x2', function(d) { return x1(d.value) })
              .attr('y2', availableHeight - 2)
              .on('mouseover', function(d) {
                dispatch.elementMouseover({
                  value: d.value,
                  label: d.label || 'Previous',
                  color: d3.select(this).style("fill"),
                  pos: [x1(d.value), availableHeight/2]
                })

              })
              .on('mousemove', function(d) {
                  dispatch.elementMousemove({
                      value: d.value,
                      label: d.label || 'Previous',
                      color: d3.select(this).style("fill")
                  })
              })
              .on('mouseout', function(d, i) {
                  dispatch.elementMouseout({
                      value: d.value,
                      label: d.label || 'Previous',
                      color: d3.select(this).style("fill")
                  })
              });

            g.selectAll("path.nv-markerLines")
              .data(markerLinesData)
              .attr('transform', function(d) { return 'translate(' + x1(d.value) + ',' + (availableHeight / 2) + ')' });

            wrap.selectAll('.nv-range')
                .on('mouseover', function(d,i) {
                    var label = rangeLabelz[i] || defaultRangeLabels[i];
                    dispatch.elementMouseover({
                        value: d,
                        label: label,
                        color: d3.select(this).style("fill")
                    })
                })
                .on('mousemove', function() {
                    dispatch.elementMousemove({
                        value: measurez[0],
                        label: measureLabelz[0] || 'Previous',
                        color: d3.select(this).style("fill")
                    })
                })
                .on('mouseout', function(d,i) {
                    var label = rangeLabelz[i] || defaultRangeLabels[i];
                    dispatch.elementMouseout({
                        value: d,
                        label: label,
                        color: d3.select(this).style("fill")
                    })
                });
        });

        return chart;
    }

    //============================================================
    // Expose Public Variables
    //------------------------------------------------------------

    chart.dispatch = dispatch;
    chart.options = nv.utils.optionsFunc.bind(chart);

    chart._options = Object.create({}, {
        // simple options, just get/set the necessary values
        ranges:      {get: function(){return ranges;}, set: function(_){ranges=_;}}, // ranges (bad, satisfactory, good)
        markers:     {get: function(){return markers;}, set: function(_){markers=_;}}, // markers (previous, goal)
        measures: {get: function(){return measures;}, set: function(_){measures=_;}}, // measures (actual, forecast)
        forceX:      {get: function(){return forceX;}, set: function(_){forceX=_;}},
        width:    {get: function(){return width;}, set: function(_){width=_;}},
        height:    {get: function(){return height;}, set: function(_){height=_;}},
        tickFormat:    {get: function(){return tickFormat;}, set: function(_){tickFormat=_;}},

        // options that require extra logic in the setter
        margin: {get: function(){return margin;}, set: function(_){
            margin.top    = _.top    !== undefined ? _.top    : margin.top;
            margin.right  = _.right  !== undefined ? _.right  : margin.right;
            margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
            margin.left   = _.left   !== undefined ? _.left   : margin.left;
        }},
        orient: {get: function(){return orient;}, set: function(_){ // left, right, top, bottom
            orient = _;
            reverse = orient == 'right' || orient == 'bottom';
        }},
        color:  {get: function(){return color;}, set: function(_){
            color = nv.utils.getColor(_);
        }}
    });

    nv.utils.initOptions(chart);
    return chart;
};


