'use strict';

var _ = require('lodash');
var times = require('../times');

var DEFAULT_FOCUS = times.hours(3).msecs
  , WIDTH_SMALL_DOTS = 420
  , WIDTH_BIG_DOTS = 800
  , TOOLTIP_TRANS_MS = 100 // milliseconds
  , TOOLTIP_WIDTH = 150 //min-width + padding
;

function init (client, d3) {

  var renderer = {};

  var utils = client.utils;
  var translate = client.translate;

  //chart isn't created till the client gets data, so can grab the var at init
  function chart () {
    return client.chart;
  }

  function focusRangeAdjustment () {
    return client.focusRangeMS === DEFAULT_FOCUS ? 1 : 1 + ((client.focusRangeMS - DEFAULT_FOCUS) / DEFAULT_FOCUS / 8);
  }

  var dotRadius = function(type) {
    var radius = chart().prevChartWidth > WIDTH_BIG_DOTS ? 4 : (chart().prevChartWidth < WIDTH_SMALL_DOTS ? 2 : 3);
    if (type === 'mbg') {
      radius *= 2;
    } else if (type === 'forecast') {
      radius = Math.min(3, radius - 1);
    } else if (type === 'rawbg') {
      radius = Math.min(2, radius - 1);
    }

    return radius / focusRangeAdjustment();
  };

  function tooltipLeft () {
    var windowWidth = $(client.tooltip).parent().parent().width();
    var left = d3.event.pageX + TOOLTIP_WIDTH < windowWidth ? d3.event.pageX : windowWidth - TOOLTIP_WIDTH - 10;
    return left + 'px';
  }

  function hideTooltip () {
    client.tooltip.transition()
      .duration(TOOLTIP_TRANS_MS)
      .style('opacity', 0);
  }

  // get the desired opacity for context chart based on the brush extent
  renderer.highlightBrushPoints = function highlightBrushPoints (data) {
    if (data.mills >= chart().brush.extent()[0].getTime() && data.mills <= chart().brush.extent()[1].getTime()) {
      return chart().futureOpacity(data.mills - client.latestSGV.mills);
    } else {
      return 0.5;
    }
  };

  renderer.bubbleScale = function bubbleScale () {
    // a higher bubbleScale will produce smaller bubbles (it's not a radius like focusDotRadius)
    return (chart().prevChartWidth < WIDTH_SMALL_DOTS ? 4 : (chart().prevChartWidth < WIDTH_BIG_DOTS ? 3 : 2)) * focusRangeAdjustment();
  };

  renderer.addFocusCircles = function addFocusCircles () {
    // get slice of data so that concatenation of predictions do not interfere with subsequent updates
    var focusData = client.entries.slice();

    if (client.sbx.pluginBase.forecastPoints) {
      var shownForecastPoints = _.filter(client.sbx.pluginBase.forecastPoints, function isShown (point) {
        return client.settings.showForecast.indexOf(point.info.type) > -1;
      });
      var maxForecastMills = _.max(_.map(shownForecastPoints, function(point) { return point.mills }));
      // limit lookahead to the same as lookback
      var focusHoursAheadMills = chart().brush.extent()[1].getTime() + client.focusRangeMS;
      maxForecastMills = Math.min(focusHoursAheadMills, maxForecastMills);
      client.forecastTime = maxForecastMills > 0 ? maxForecastMills - client.sbx.lastSGVMills() : 0;
      focusData = focusData.concat(shownForecastPoints);
    }

    // bind up the focus chart data to an array of circles
    // selects all our data into data and uses date function to get current max date
    var focusCircles = chart().focus.selectAll('circle').data(focusData, client.entryToDate);

    function prepareFocusCircles (sel) {
      var badData = [];
      sel.attr('cx', function(d) {
          if (!d) {
            console.error('Bad data', d);
            return chart().xScale(new Date(0));
          } else if (!d.mills) {
            console.error('Bad data, no mills', d);
            return chart().xScale(new Date(0));
          } else {
            return chart().xScale(new Date(d.mills));
          }
        })
        .attr('cy', function(d) {
          var scaled = client.sbx.scaleEntry(d);
          if (isNaN(scaled)) {
            badData.push(d);
            return chart().yScale(utils.scaleMgdl(450));
          } else {
            return chart().yScale(scaled);
          }
        })
        .attr('fill', function(d) {
          return d.type === 'forecast' ? 'none' : d.color;
        })
        .attr('opacity', function(d) {
          return d.noFade ? 100 : chart().futureOpacity(d.mills - client.latestSGV.mills);
        })
        .attr('stroke-width', function(d) {
          return d.type === 'mbg' ? 2 : d.type === 'forecast' ? 2 : 0;
        })
        .attr('stroke', function(d) {
          return (d.type === 'mbg' ? 'white' : d.color);
        })
        .attr('r', function(d) {
          return dotRadius(d.type);
        });

      if (badData.length > 0) {
        console.warn('Bad Data: isNaN(sgv)', badData);
      }

      return sel;
    }

    function focusCircleTooltip (d) {
      if (d.type !== 'sgv' && d.type !== 'mbg' && d.type !== 'forecast') {
        return;
      }

      function getRawbgInfo () {
        var info = {};
        var sbx = client.sbx.withExtendedSettings(client.rawbg);
        if (d.type === 'sgv') {
          info.noise = client.rawbg.noiseCodeToDisplay(d.mgdl, d.noise);
          if (client.rawbg.showRawBGs(d.mgdl, d.noise, client.ddata.cal, sbx)) {
            info.value = utils.scaleMgdl(client.rawbg.calc(d, client.ddata.cal, sbx));
          }
        }
        return info;
      }

      var rawbgInfo = getRawbgInfo();

      client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
      client.tooltip.html('<strong>' + translate('BG') + ':</strong> ' + client.sbx.scaleEntry(d) +
          (d.type === 'mbg' ? '<br/><strong>' + translate('Device') + ': </strong>' + d.device : '') +
          (d.type === 'forecast' && d.forecastType ? '<br/><strong>' + translate('Forecast Type') + ': </strong>' + d.forecastType : '') +
          (rawbgInfo.value ? '<br/><strong>' + translate('Raw BG') + ':</strong> ' + rawbgInfo.value : '') +
          (rawbgInfo.noise ? '<br/><strong>' + translate('Noise') + ':</strong> ' + rawbgInfo.noise : '') +
          '<br/><strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(d.mills)))
        .style('left', tooltipLeft())
        .style('top', (d3.event.pageY + 15) + 'px');
    }

    // if already existing then transition each circle to its new position
    prepareFocusCircles(focusCircles.transition());

    // if new circle then just display
    prepareFocusCircles(focusCircles.enter().append('circle'))
      .on('mouseover', focusCircleTooltip)
      .on('mouseout', hideTooltip);

    focusCircles.exit().remove();
  };

  renderer.addTreatmentCircles = function addTreatmentCircles () {
    function treatmentTooltip (d) {
      return '<strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(d.mills)) + '<br/>' +
        (d.eventType ? '<strong>' + translate('Treatment type') + ':</strong> ' + translate(client.careportal.resolveEventName(d.eventType)) + '<br/>' : '') +
        (d.reason ? '<strong>' + translate('Reason') + ':</strong> ' + translate(d.reason) + '<br/>' : '') +
        (d.glucose ? '<strong>' + translate('BG') + ':</strong> ' + d.glucose + (d.glucoseType ? ' (' + translate(d.glucoseType) + ')' : '') + '<br/>' : '') +
        (d.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + d.enteredBy + '<br/>' : '') +
        (d.targetTop ? '<strong>' + translate('Target Top') + ':</strong> ' + d.targetTop + '<br/>' : '') +
        (d.targetBottom ? '<strong>' + translate('Target Bottom') + ':</strong> ' + d.targetBottom + '<br/>' : '') +
        (d.duration ? '<strong>' + translate('Duration') + ':</strong> ' + Math.round(d.duration) + ' min<br/>' : '') +
        (d.notes ? '<strong>' + translate('Notes') + ':</strong> ' + d.notes : '');
    }

    function announcementTooltip (d) {
      return '<strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(d.mills)) + '<br/>' +
        (d.eventType ? '<strong>' + translate('Announcement') + '</strong><br/>' : '') +
        (d.notes && d.notes.length > 1 ? '<strong>' + translate('Message') + ':</strong> ' + d.notes + '<br/>' : '') +
        (d.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + d.enteredBy + '<br/>' : '');
    }

    //TODO: filter in oref0 instead of here and after most people upgrade take this out
    var openAPSSpam = ['BasalProfileStart', 'ResultDailyTotal', 'BGReceived'];

    //NOTE: treatments with insulin or carbs are drawn by drawTreatment()
    // bind up the focus chart data to an array of circles
    var treatCircles = chart().focus.selectAll('treatment-dot').data(client.ddata.treatments.filter(function(treatment) {

      var notCarbsOrInsulin = !treatment.carbs && !treatment.insulin;
      var notTempOrProfile = !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType);

      var notes = treatment.notes || '';
      var enteredBy = treatment.enteredBy || '';

      var notOpenAPSSpam = enteredBy.indexOf('openaps://') === -1 || _.isUndefined(_.find(openAPSSpam, function startsWith (spam) {
        return notes.indexOf(spam) === 0;
      }));

      return notCarbsOrInsulin && !treatment.duration && notTempOrProfile && notOpenAPSSpam;
    }));

    function prepareTreatCircles (sel) {
      function strokeColor (d) {
        var color = 'white';
        if (d.isAnnouncement) {
          color = 'orange';
        } else if (d.glucose) {
          color = 'grey';
        }
        return color;
      }

      function fillColor (d) {
        var color = 'grey';
        if (d.isAnnouncement) {
          color = 'orange';
        } else if (d.glucose) {
          color = 'red';
        }
        return color;
      }

      sel.attr('cx', function(d) {
          return chart().xScale(new Date(d.mills));
        })
        .attr('cy', function(d) {
          return chart().yScale(client.sbx.scaleEntry(d));
        })
        .attr('r', function() {
          return dotRadius('mbg');
        })
        .attr('stroke-width', 2)
        .attr('stroke', strokeColor)
        .attr('fill', fillColor);

      return sel;
    }

    // if already existing then transition each circle to its new position
    prepareTreatCircles(treatCircles.transition());

    // if new circle then just display
    prepareTreatCircles(treatCircles.enter().append('circle'))
      .on('mouseover', function(d) {
        client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
        client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d))
          .style('left', tooltipLeft())
          .style('top', (d3.event.pageY + 15) + 'px');
      })
      .on('mouseout', hideTooltip);

    var durationTreatments = client.ddata.treatments.filter(function(treatment) {
      return !treatment.carbs && !treatment.insulin && treatment.duration &&
        !_.includes(['Temp Basal', 'Profile Switch', 'Combo Bolus', 'Temporary Target'], treatment.eventType);
    });

    //use the processed temp target so there are no overlaps
    durationTreatments = durationTreatments.concat(client.ddata.tempTargetTreatments);

    // treatments with duration
    var treatRects = chart().focus.selectAll('.g-duration').data(durationTreatments);

    function fillColor (d) {
      // this is going to be updated by Event Type
      var color = 'grey';
      if (d.eventType === 'Exercise') {
        color = 'Violet';
      } else if (d.eventType === 'Note') {
        color = 'Salmon';
      } else if (d.eventType === 'Temporary Target') {
        color = 'lightgray';
      }
      return color;
    }

    function rectHeight (d) {
      var height = 20;
      if (d.targetTop && d.targetTop > 0 && d.targetBottom && d.targetBottom > 0) {
        height = Math.max(5, d.targetTop - d.targetBottom);
      }
      return height;
    }

    function rectTranslate (d) {
      var top = 50;
      if (d.eventType === 'Temporary Target') {
        top = d.targetTop === d.targetBottom ? d.targetTop + rectHeight(d) : d.targetTop;
      }
      return 'translate(' + chart().xScale(new Date(d.mills)) + ',' + chart().yScale(utils.scaleMgdl(top)) + ')';
    }
    // if already existing then transition each rect to its new position
    treatRects.transition()
      .attr('transform', rectTranslate);

    chart().focus.selectAll('.g-duration-rect').transition()
      .attr('width', function(d) {
        return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills));
      });

    chart().focus.selectAll('.g-duration-text').transition()
      .attr('transform', function(d) {
        return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills))) / 2 + ',' + 10 + ')';
      });

    // if new rect then just display
    var gs = treatRects.enter().append('g')
      .attr('class', 'g-duration')
      .attr('transform', rectTranslate)
      .on('mouseover', function(d) {
        client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
        client.tooltip.html(d.isAnnouncement ? announcementTooltip(d) : treatmentTooltip(d))
          .style('left', tooltipLeft())
          .style('top', (d3.event.pageY + 15) + 'px');
      })
      .on('mouseout', hideTooltip);

    gs.append('rect')
      .attr('class', 'g-duration-rect')
      .attr('width', function(d) {
        return chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills));
      })
      .attr('height', rectHeight)
      .attr('rx', 5)
      .attr('ry', 5)
      .attr('opacity', .2)
      .attr('fill', fillColor);

    gs.append('text')
      .attr('class', 'g-duration-text')
      .style('font-size', 15)
      .attr('fill', 'white')
      .attr('text-anchor', 'middle')
      .attr('dy', '.35em')
      .attr('transform', function(d) {
        return 'translate(' + (chart().xScale(new Date(d.mills + times.mins(d.duration).msecs)) - chart().xScale(new Date(d.mills))) / 2 + ',' + 10 + ')';
      })
      .text(function(d) {
        if (d.eventType === 'Temporary Target') {
          return '';
        }
        return d.notes || d.eventType;
      });
  };

  renderer.addContextCircles = function addContextCircles () {
    // bind up the context chart data to an array of circles
    var contextCircles = chart().context.selectAll('circle').data(client.entries);

    function prepareContextCircles (sel) {
      var badData = [];
      sel.attr('cx', function(d) { return chart().xScale2(new Date(d.mills)); })
        .attr('cy', function(d) {
          var scaled = client.sbx.scaleEntry(d);
          if (isNaN(scaled)) {
            badData.push(d);
            return chart().yScale2(utils.scaleMgdl(450));
          } else {
            return chart().yScale2(scaled);
          }
        })
        .attr('fill', function(d) { return d.color; })
        .style('opacity', function(d) { return renderer.highlightBrushPoints(d) })
        .attr('stroke-width', function(d) { return d.type === 'mbg' ? 2 : 0; })
        .attr('stroke', function() { return 'white'; })
        .attr('r', function(d) { return d.type === 'mbg' ? 4 : 2; });

      if (badData.length > 0) {
        console.warn('Bad Data: isNaN(sgv)', badData);
      }

      return sel;
    }

    // if already existing then transition each circle to its new position
    prepareContextCircles(contextCircles.transition());

    // if new circle then just display
    prepareContextCircles(contextCircles.enter().append('circle'));

    contextCircles.exit().remove();
  };

  function calcTreatmentRadius (treatment, opts, carbratio) {
    var CR = treatment.CR || carbratio || 20;
    var carbsOrInsulin = CR;
    if (treatment.carbs) {
      carbsOrInsulin = treatment.carbs;
    } else if (treatment.insulin) {
      carbsOrInsulin = treatment.insulin * CR;
    }

    // R1 determines the size of the treatment dot
    var R1 = Math.sqrt(carbsOrInsulin) / opts.scale
      , R2 = R1
      // R3/R4 determine how far from the treatment dot the labels are placed
      , R3 = R1 + 8 / opts.scale
      , R4 = R1 + 25 / opts.scale;

    return {
      R1: R1
      , R2: R2
      , R3: R3
      , R4: R4
      , isNaN: isNaN(R1) || isNaN(R3) || isNaN(R3)
    };
  }

  function prepareArc (treatment, radius) {
    var arc_data = [
      // white carb half-circle on top
      { 'element': '', 'color': 'white', 'start': -1.5708, 'end': 1.5708, 'inner': 0, 'outer': radius.R1 }
      , { 'element': '', 'color': 'transparent', 'start': -1.5708, 'end': 1.5708, 'inner': radius.R2, 'outer': radius.R3 },
      // blue insulin half-circle on bottom
      { 'element': '', 'color': '#0099ff', 'start': 1.5708, 'end': 4.7124, 'inner': 0, 'outer': radius.R1 },
      // these form a very short transparent arc along the bottom of an insulin treatment to position the label
      // these used to be semicircles from 1.5708 to 4.7124, but that made the tooltip target too big
      { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R3 }
      , { 'element': '', 'color': 'transparent', 'start': 3.1400, 'end': 3.1432, 'inner': radius.R2, 'outer': radius.R4 }
    ];

    arc_data[0].outlineOnly = !treatment.carbs;
    arc_data[2].outlineOnly = !treatment.insulin;

    if (treatment.carbs > 0) {
      arc_data[1].element = Math.round(treatment.carbs) + ' g';
    }

    if (treatment.protein > 0) {
      arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.protein) + ' g';
    }

    if (treatment.fat > 0) {
      arc_data[1].element = arc_data[1].element + " / " + Math.round(treatment.fat) + ' g';
    }

    if (treatment.foodType) {
      arc_data[1].element = arc_data[1].element + " " + treatment.foodType;
    }

    if (treatment.insulin > 0) {
      var dosage_units = '' + Math.round(treatment.insulin * 100) / 100;

      var unit_of_measurement = ' U'; // One international unit of insulin (1 IU) is shown as '1 U'
      var enteredBy = '' + treatment.enteredBy;

      if (treatment.insulin < 1 && !treatment.carbs && enteredBy.indexOf('openaps') > -1) { // don't show the unit of measurement for insulin boluses < 1 without carbs (e.g. oref0 SMB's). Otherwise lot's of small insulin only dosages are often unreadable
        unit_of_measurement = '';
        // remove leading zeros to avoid overlap with adjacent boluses
        dosage_units = (dosage_units + "").replace(/^0/, "");
      }

      arc_data[3].element = dosage_units + unit_of_measurement;
    }

    if (treatment.status) {
      arc_data[4].element = translate(treatment.status);
    }

    var arc = d3.svg.arc()
      .innerRadius(function(d) {
        return 5 * d.inner;
      })
      .outerRadius(function(d) {
        return 5 * d.outer;
      })
      .endAngle(function(d) {
        return d.start;
      })
      .startAngle(function(d) {
        return d.end;
      });

    return {
      data: arc_data
      , svg: arc
    };
  }

  function isInRect (x, y, rect) {
    return !(x < rect.x || x > rect.x + rect.width || y < rect.y || y > rect.y + rect.height);
  }

  function appendTreatments (treatment, arc) {

    function boluscalcTooltip (treatment) {
      if (!treatment.boluscalc) {
        return '';
      }
      var html = '<hr>';
      html += (treatment.boluscalc.othercorrection ? '<strong>' + translate('Other correction') + ':</strong> ' + parseFloat(treatment.boluscalc.othercorrection).toFixed(2) + 'U<br/>' : '');
      html += (treatment.boluscalc.profile ? '<strong>' + translate('Profile used') + ':</strong> ' + treatment.boluscalc.profile + '<br/>' : '');
      if (treatment.boluscalc.foods && treatment.boluscalc.foods.length) {
        html += '<table><tr><td><strong>' + translate('Food') + '</strong></td></tr>';
        for (var fi = 0; fi < treatment.boluscalc.foods.length; fi++) {
          var f = treatment.boluscalc.foods[fi];
          html += '<tr>';
          html += '<td>' + f.name + '</td>';
          html += '<td>' + (f.portion * f.portions).toFixed(1) + ' ' + f.unit + '</td>';
          html += '<td>(' + (f.carbs * f.portions).toFixed(1) + ' g)</td>';
          html += '</tr>';
        }
        html += '</table>';
      }
      return html;
    }

    function treatmentTooltip () {
      client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
      client.tooltip.html('<strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(treatment.mills)) + '<br/>' + '<strong>' + translate('Treatment type') + ':</strong> ' + translate(client.careportal.resolveEventName(treatment.eventType)) + '<br/>' +
          (treatment.carbs ? '<strong>' + translate('Carbs') + ':</strong> ' + treatment.carbs + '<br/>' : '') +
          (treatment.protein ? '<strong>' + translate('Protein') + ':</strong> ' + treatment.protein + '<br/>' : '') +
          (treatment.fat ? '<strong>' + translate('Fat') + ':</strong> ' + treatment.fat + '<br/>' : '') +
          (treatment.absorptionTime > 0 ? '<strong>' + translate('Absorption Time') + ':</strong> ' + (Math.round(treatment.absorptionTime / 60.0 * 10) / 10) + 'h' + '<br/>' : '') +
          (treatment.insulin ? '<strong>' + translate('Insulin') + ':</strong> ' + treatment.insulin + '<br/>' : '') +
          (treatment.enteredinsulin ? '<strong>' + translate('Combo Bolus') + ':</strong> ' + treatment.enteredinsulin + 'U, ' + treatment.splitNow + '% : ' + treatment.splitExt + '%, ' + translate('Duration') + ': ' + treatment.duration + '<br/>' : '') +
          (treatment.glucose ? '<strong>' + translate('BG') + ':</strong> ' + treatment.glucose + (treatment.glucoseType ? ' (' + translate(treatment.glucoseType) + ')' : '') + '<br/>' : '') +
          (treatment.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + treatment.enteredBy + '<br/>' : '') +
          (treatment.notes ? '<strong>' + translate('Notes') + ':</strong> ' + treatment.notes : '') +
          boluscalcTooltip(treatment)
        )
        .style('left', tooltipLeft())
        .style('top', (d3.event.pageY + 15) + 'px');
    }

    var newTime;
    var deleteRect = { x: 0, y: 0, width: 0, height: 0 };
    var insulinRect = { x: 0, y: 0, width: 0, height: 0 };
    var carbsRect = { x: 0, y: 0, width: 0, height: 0 };
    var operation;
    renderer.drag = d3.behavior.drag()
      .on('dragstart', function() {
        //console.log(treatment);
        var windowWidth = $(client.tooltip).parent().parent().width();
        var left = d3.event.x + TOOLTIP_WIDTH < windowWidth ? d3.event.x : windowWidth - TOOLTIP_WIDTH - 10;
        client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9)
          .style('left', left + 'px')
          .style('top', (d3.event.pageY ? d3.event.pageY + 15 : 40) + 'px');

        deleteRect = {
          x: 0
          , y: 0
          , width: 50
          , height: chart().yScale(chart().yScale.domain()[0])
        };
        chart().drag.append('rect')
          .attr({
            class: 'drag-droparea'
            , x: deleteRect.x
            , y: deleteRect.y
            , width: deleteRect.width
            , height: deleteRect.height
            , fill: 'red'
            , opacity: 0.4
            , rx: 10
            , ry: 10
          });
        chart().drag.append('text')
          .attr({
            class: 'drag-droparea'
            , x: deleteRect.x + deleteRect.width / 2
            , y: deleteRect.y + deleteRect.height / 2
            , 'font-size': 15
            , 'font-weight': 'bold'
            , fill: 'red'
            , 'text-anchor': 'middle'
            , dy: '.35em'
            , transform: 'rotate(-90 ' + (deleteRect.x + deleteRect.width / 2) + ',' + (deleteRect.y + deleteRect.height / 2) + ')'
          })
          .text(translate('Remove'));

        if (treatment.insulin && treatment.carbs) {
          carbsRect = {
            x: 0
            , y: 0
            , width: chart().charts.attr('width')
            , height: 50
          };
          insulinRect = {
            x: 0
            , y: chart().yScale(chart().yScale.domain()[0]) - 50
            , width: chart().charts.attr('width')
            , height: 50
          };
          chart().drag.append('rect')
            .attr({
              class: 'drag-droparea'
              , x: carbsRect.x
              , y: carbsRect.y
              , width: carbsRect.width
              , height: carbsRect.height
              , fill: 'white'
              , opacity: 0.4
              , rx: 10
              , ry: 10
            });
          chart().drag.append('text')
            .attr({
              class: 'drag-droparea'
              , x: carbsRect.x + carbsRect.width / 2
              , y: carbsRect.y + carbsRect.height / 2
              , 'font-size': 15
              , 'font-weight': 'bold'
              , fill: 'white'
              , 'text-anchor': 'middle'
              , dy: '.35em'
            })
            .text(translate('Move carbs'));
          chart().drag.append('rect')
            .attr({
              class: 'drag-droparea'
              , x: insulinRect.x
              , y: insulinRect.y
              , width: insulinRect.width
              , height: insulinRect.height
              , fill: '#0099ff'
              , opacity: 0.4
              , rx: 10
              , ry: 10
            });
          chart().drag.append('text')
            .attr({
              class: 'drag-droparea'
              , x: insulinRect.x + insulinRect.width / 2
              , y: insulinRect.y + insulinRect.height / 2
              , 'font-size': 15
              , 'font-weight': 'bold'
              , fill: '#0099ff'
              , 'text-anchor': 'middle'
              , dy: '.35em'
            })
            .text(translate('Move insulin'));
        }

        chart().basals.attr('display', 'none');

        operation = 'Move';
      })
      .on('drag', function() {
        //console.log(d3.event);
        client.tooltip.transition().style('opacity', .9);
        var x = Math.min(Math.max(0, d3.event.x), chart().charts.attr('width'));
        var y = Math.min(Math.max(0, d3.event.y), chart().focusHeight);

        operation = 'Move';
        if (isInRect(x, y, deleteRect) && isInRect(x, y, insulinRect)) {
          operation = 'Remove insulin';
        } else if (isInRect(x, y, deleteRect) && isInRect(x, y, carbsRect)) {
          operation = 'Remove carbs';
        } else if (isInRect(x, y, deleteRect)) {
          operation = 'Remove';
        } else if (isInRect(x, y, insulinRect)) {
          operation = 'Move insulin';
        } else if (isInRect(x, y, carbsRect)) {
          operation = 'Move carbs';
        }

        newTime = new Date(chart().xScale.invert(x));
        var minDiff = times.msecs(newTime.getTime() - treatment.mills).mins.toFixed(0);
        client.tooltip.html(
          '<b>' + translate('Operation') + ':</b> ' + translate(operation) + '<br>' +
          '<b>' + translate('New time') + ':</b> ' + newTime.toLocaleTimeString() + '<br>' +
          '<b>' + translate('Difference') + ':</b> ' + (minDiff > 0 ? '+' : '') + minDiff + ' ' + translate('mins')
        );

        chart().drag.selectAll('.arrow').remove();
        chart().drag.append('line')
          .attr({
            'class': 'arrow'
            , 'marker-end': 'url(#arrow)'
            , 'x1': chart().xScale(new Date(treatment.mills))
            , 'y1': chart().yScale(client.sbx.scaleEntry(treatment))
            , 'x2': x
            , 'y2': y
            , 'stroke-width': 2
            , 'stroke': 'white'
          });

      })
      .on('dragend', function() {
        var newTreatment;
        chart().drag.selectAll('.drag-droparea').remove();
        hideTooltip();
        switch (operation) {
          case 'Move':
            if (window.confirm(translate('Change treatment time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) {
              client.socket.emit(
                'dbUpdate', {
                  collection: 'treatments'
                  , _id: treatment._id
                  , data: { created_at: newTime.toISOString() }
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
          case 'Remove insulin':
            if (window.confirm(translate('Remove insulin from treatment ?'))) {
              client.socket.emit(
                'dbUpdateUnset', {
                  collection: 'treatments'
                  , _id: treatment._id
                  , data: { insulin: 1 }
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
          case 'Remove carbs':
            if (window.confirm(translate('Remove carbs from treatment ?'))) {
              client.socket.emit(
                'dbUpdateUnset', {
                  collection: 'treatments'
                  , _id: treatment._id
                  , data: { carbs: 1 }
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
          case 'Remove':
            if (window.confirm(translate('Remove treatment ?'))) {
              client.socket.emit(
                'dbRemove', {
                  collection: 'treatments'
                  , _id: treatment._id
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
          case 'Move insulin':
            if (window.confirm(translate('Change insulin time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) {
              client.socket.emit(
                'dbUpdateUnset', {
                  collection: 'treatments'
                  , _id: treatment._id
                  , data: { insulin: 1 }
                }
              );
              newTreatment = _.cloneDeep(treatment);
              delete newTreatment._id;
              delete newTreatment.NSCLIENT_ID;
              delete newTreatment.carbs;
              newTreatment.created_at = newTime.toISOString();
              client.socket.emit(
                'dbAdd', {
                  collection: 'treatments'
                  , data: newTreatment
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
          case 'Move carbs':
            if (window.confirm(translate('Change carbs time to %1 ?', { params: [newTime.toLocaleTimeString()] }))) {
              client.socket.emit(
                'dbUpdateUnset', {
                  collection: 'treatments'
                  , _id: treatment._id
                  , data: { carbs: 1 }
                }
              );
              newTreatment = _.cloneDeep(treatment);
              delete newTreatment._id;
              delete newTreatment.NSCLIENT_ID;
              delete newTreatment.insulin;
              newTreatment.created_at = newTime.toISOString();
              client.socket.emit(
                'dbAdd', {
                  collection: 'treatments'
                  , data: newTreatment
                }
                , function callback (result) {
                  console.log(result);
                  chart().drag.selectAll('.arrow').transition().duration(5000).style('opacity', 0).remove();
                }
              );
            } else {
              chart().drag.selectAll('.arrow').remove();
            }
            break;
        }
        chart().basals.attr('display', '');
      });

    var treatmentDots = chart().focus.selectAll('treatment-insulincarbs')
      .data(arc.data)
      .enter()
      .append('g')
      .attr('class', 'draggable-treatment')
      .attr('transform', 'translate(' + chart().xScale(new Date(treatment.mills)) + ', ' + chart().yScale(client.sbx.scaleEntry(treatment)) + ')')
      .on('mouseover', treatmentTooltip)
      .on('mouseout', hideTooltip);
    if (client.editMode) {
      treatmentDots
        .style('cursor', 'move')
        .call(renderer.drag);
    }

    treatmentDots.append('path')
      .attr('class', 'path')
      .attr('fill', function(d) {
        return d.outlineOnly ? 'transparent' : d.color;
      })
      .attr('stroke-width', function(d) {
        return d.outlineOnly ? 1 : 0;
      })
      .attr('stroke', function(d) {
        return d.color;
      })
      .attr('id', function(d, i) {
        return 's' + i;
      })
      .attr('d', arc.svg);

    return treatmentDots;
  }

  function appendLabels (treatmentDots, arc, opts) {
    // labels for carbs and insulin
    if (opts.showLabels) {
      var label = treatmentDots.append('g')
        .attr('class', 'path')
        .attr('id', 'label')
        .style('fill', 'white');

      // reduce the treatment label font size to make it readable with SMB
      var fontBaseSize = (opts.treatments >= 30) ? 40 : 50 - Math.floor((25 - opts.treatments) / 30 * 10);

      label.append('text')
        .style('font-size', fontBaseSize / opts.scale)
        .style('text-shadow', '0px 0px 10px rgba(0, 0, 0, 1)')
        .attr('text-anchor', 'middle')
        .attr('dy', '.35em')
        .attr('transform', function(d) {
          d.outerRadius = d.outerRadius * 2.1;
          d.innerRadius = d.outerRadius * 2.1;
          return 'translate(' + arc.svg.centroid(d) + ')';
        })
        .text(function(d) {
          return d.element;
        });
    }
  }

  renderer.drawTreatments = function drawTreatments (client) {

    var treatmentCount = 0;
    chart().focus.selectAll('.draggable-treatment').remove();

    _.forEach(client.ddata.treatments, function eachTreatment (d) {
      if (Number(d.insulin) > 0 || Number(d.carbs) > 0) { treatmentCount += 1; }
    });

    // add treatment bubbles
    _.forEach(client.ddata.treatments, function eachTreatment (d) {
      renderer.drawTreatment(d, {
        scale: renderer.bubbleScale()
        , showLabels: true
        , treatments: treatmentCount
      }, client.sbx.data.profile.getCarbRatio(new Date()));
    });
  }

  renderer.drawTreatment = function drawTreatment (treatment, opts, carbratio) {
    if (!treatment.carbs && !treatment.insulin) {
      return;
    }

    //when the tests are run window isn't available
    var innerWidth = window && window.innerWidth || -1;
    // don't render the treatment if it's not visible
    if (Math.abs(chart().xScale(new Date(treatment.mills))) > innerWidth) {
      return;
    }

    var radius = calcTreatmentRadius(treatment, opts, carbratio);
    if (radius.isNaN) {
      console.warn('Bad Data: Found isNaN value in treatment', treatment);
      return;
    }

    var arc = prepareArc(treatment, radius);
    var treatmentDots = appendTreatments(treatment, arc);
    appendLabels(treatmentDots, arc, opts);
  }

  renderer.addBasals = function addBasals (client) {

    var mode = client.settings.extendedSettings.basal.render;
    var profile = client.sbx.data.profile;
    var linedata = [];
    var notemplinedata = [];
    var basalareadata = [];
    var tempbasalareadata = [];
    var comboareadata = [];
    var from = chart().brush.extent()[0].getTime();
    var to = Math.max(chart().brush.extent()[1].getTime(), client.sbx.time) + client.forecastTime;

    var date = from;
    var lastbasal = 0;

    if (!profile.activeProfileToTime(from)) {
      window.alert(translate('Wrong profile setting.\nNo profile defined to displayed time.\nRedirecting to profile editor to create new profile.'));
      try {
        window.location.href = '/profile';
      } catch (err) {
        //doesn't work when running tests, so catch and ignore
      }
      return;
    }

    while (date <= to) {
      var basalvalue = profile.getTempBasal(date);
      if (!_.isEqual(lastbasal, basalvalue)) {
        linedata.push({ d: date, b: basalvalue.totalbasal });
        notemplinedata.push({ d: date, b: basalvalue.basal });
        if (basalvalue.combobolustreatment && basalvalue.combobolustreatment.relative) {
          tempbasalareadata.push({ d: date, b: basalvalue.tempbasal });
          basalareadata.push({ d: date, b: 0 });
          comboareadata.push({ d: date, b: basalvalue.totalbasal });
        } else if (basalvalue.treatment) {
          tempbasalareadata.push({ d: date, b: basalvalue.totalbasal });
          basalareadata.push({ d: date, b: 0 });
          comboareadata.push({ d: date, b: 0 });
        } else {
          tempbasalareadata.push({ d: date, b: 0 });
          basalareadata.push({ d: date, b: basalvalue.totalbasal });
          comboareadata.push({ d: date, b: 0 });
        }
      }
      lastbasal = basalvalue;
      date += times.mins(1).msecs;
    }

    var toTempBasal = profile.getTempBasal(to);

    linedata.push({ d: to, b: toTempBasal.totalbasal });
    notemplinedata.push({ d: to, b: toTempBasal.basal });
    basalareadata.push({ d: to, b: toTempBasal.basal });
    tempbasalareadata.push({ d: to, b: toTempBasal.totalbasal });
    comboareadata.push({ d: to, b: toTempBasal.totalbasal });

    var max_linedata = d3.max(linedata, function(d) { return d.b; });
    var max_notemplinedata = d3.max(notemplinedata, function(d) { return d.b; });
    var max = Math.max(max_linedata, max_notemplinedata) * ('icicle' === mode ? 1 : 1.1);
    chart().maxBasalValue = max;
    chart().yScaleBasals.domain('icicle' === mode ? [0, max] : [max, 0]);

    chart().basals.selectAll('g').remove();
    chart().basals.selectAll('.basalline').remove().data(linedata);
    chart().basals.selectAll('.notempline').remove().data(notemplinedata);
    chart().basals.selectAll('.basalarea').remove().data(basalareadata);
    chart().basals.selectAll('.tempbasalarea').remove().data(tempbasalareadata);
    chart().basals.selectAll('.comboarea').remove().data(comboareadata);

    var valueline = d3.svg.line()
      .interpolate('step-after')
      .x(function(d) { return chart().xScaleBasals(d.d); })
      .y(function(d) { return chart().yScaleBasals(d.b); });

    var area = d3.svg.area()
      .interpolate('step-after')
      .x(function(d) { return chart().xScaleBasals(d.d); })
      .y0(chart().yScaleBasals(0))
      .y1(function(d) { return chart().yScaleBasals(d.b); });

    var g = chart().basals.append('g');

    g.append('path')
      .attr('class', 'line basalline')
      .attr('stroke', '#0099ff')
      .attr('stroke-width', 1)
      .attr('fill', 'none')
      .attr('d', valueline(linedata))

    g.append('path')
      .attr('class', 'line notempline')
      .attr('stroke', '#0099ff')
      .attr('stroke-width', 1)
      .attr('stroke-dasharray', ('3, 3'))
      .attr('fill', 'none')
      .attr('d', valueline(notemplinedata))

    g.append('path')
      .attr('class', 'area basalarea')
      .datum(basalareadata)
      .attr('fill', '#0099ff')
      .attr('fill-opacity', .1)
      .attr('stroke-width', 0)
      .attr('d', area);

    g.append('path')
      .attr('class', 'area tempbasalarea')
      .datum(tempbasalareadata)
      .attr('fill', '#0099ff')
      .attr('fill-opacity', .2)
      .attr('stroke-width', 1)
      .attr('d', area);

    g.append('path')
      .attr('class', 'area comboarea')
      .datum(comboareadata)
      .attr('fill', 'url(#hash)')
      .attr('fill-opacity', .2)
      .attr('stroke-width', 1)
      .attr('d', area);

    _.forEach(client.ddata.tempbasalTreatments, function eachTemp (t) {
      // only if basal and focus interval overlap and there is a chance to fit
      if (t.duration && t.mills < to && t.mills + times.mins(t.duration).msecs > from) {
        var text = g.append('text')
          .attr('class', 'tempbasaltext')
          .style('font-size', 15)
          .attr('fill', '#0099ff')
          .attr('text-anchor', 'middle')
          .attr('dy', '.35em')
          .attr('x', chart().xScaleBasals((Math.max(t.mills, from) + Math.min(t.mills + times.mins(t.duration).msecs, to)) / 2))
          .attr('y', 10)
          .text((t.percent ? (t.percent > 0 ? '+' : '') + t.percent + '%' : '') + (isNaN(t.absolute) ? '' : Number(t.absolute).toFixed(2) + 'U') + (t.relative ? 'C: +' + t.relative + 'U' : ''));
        // better hide if not fit
        if (text.node().getBBox().width > chart().xScaleBasals(t.mills + times.mins(t.duration).msecs) - chart().xScaleBasals(t.mills)) {
          text.attr('display', 'none');
        }
      }
    });

    client.chart.basals.attr('display', !mode || 'none' === mode ? 'none' : '');
  };

  renderer.addTreatmentProfiles = function addTreatmentProfiles (client) {
    if (client.profilefunctions.listBasalProfiles().length < 2) {
      return; // do not visualize profiles if there is only one
    }

    function profileTooltip (d) {
      return '<strong>' + translate('Time') + ':</strong> ' + client.formatTime(new Date(d.mills)) + '<br/>' +
        (d.eventType ? '<strong>' + translate('Treatment type') + ':</strong> ' + translate(client.careportal.resolveEventName(d.eventType)) + '<br/>' : '') +
        (d.endprofile ? '<strong>' + translate('End of profile') + ':</strong> ' + d.endprofile + '<br/>' : '') +
        (d.profile ? '<strong>' + translate('Profile') + ':</strong> ' + d.profile + '<br/>' : '') +
        (d.duration ? '<strong>' + translate('Duration') + ':</strong> ' + d.duration + translate('mins') + '<br/>' : '') +
        (d.enteredBy ? '<strong>' + translate('Entered By') + ':</strong> ' + d.enteredBy + '<br/>' : '') +
        (d.notes ? '<strong>' + translate('Notes') + ':</strong> ' + d.notes : '');
    }

    // calculate position of profile on left side
    var from = chart().brush.extent()[0].getTime();
    var to = chart().brush.extent()[1].getTime();
    var mult = (to - from) / times.hours(24).msecs;
    from += times.mins(20 * mult).msecs;

    var mode = client.settings.extendedSettings.basal.render;
    var data = client.ddata.profileTreatments.slice();
    data.push({
      //eventType: 'Profile Switch'
      profile: client.profilefunctions.activeProfileToTime(from)
      , mills: from
      , first: true
    });

    _.forEach(client.ddata.profileTreatments, function eachTreatment (d) {
      if (d.duration && !d.cuttedby) {
        data.push({
          cutting: d.profile
          , profile: client.profilefunctions.activeProfileToTime(times.mins(d.duration).msecs + d.mills + 1)
          , mills: times.mins(d.duration).msecs + d.mills
          , end: true
        });
      }
    });

    var treatProfiles = chart().basals.selectAll('.g-profile').data(data);

    var topOfText = ('icicle' === mode ? chart().maxBasalValue + 0.05 : -0.05);

    var generateText = function(t) {
      var sign = t.first ? '▲▲▲' : '▬▬▬';
      var ret;
      if (t.cutting) {
        ret = sign + '    ' + t.cutting + '    ' + '►►►' + '    ' + t.profile + '    ' + sign;
      } else {
        ret = sign + '    ' + t.profile + '    ' + sign;
      }
      return ret;
    };

    treatProfiles.transition().duration(0)
      .attr('transform', function(t) {
        // change text of record on left side
        return 'rotate(-90,' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' +
          'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')';
      }).
    text(generateText);

    treatProfiles.enter().append('text')
      .attr('class', 'g-profile')
      .style('font-size', 15)
      .style('font-weight', 'bold')
      .attr('fill', '#0099ff')
      .attr('text-anchor', 'end')
      .attr('dy', '.35em')
      .attr('transform', function(t) {
        return 'rotate(-90 ' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ') ' +
          'translate(' + chart().xScale(t.mills) + ',' + chart().yScaleBasals(topOfText) + ')';
      })
      .text(generateText)
      .on('mouseover', function(d) {
        client.tooltip.transition().duration(TOOLTIP_TRANS_MS).style('opacity', .9);
        client.tooltip.html(profileTooltip(d))
          .style('left', (d3.event.pageX) + 'px')
          .style('top', (d3.event.pageY + 15) + 'px');
      })
      .on('mouseout', hideTooltip);

    treatProfiles.exit().remove();
  };

  return renderer;
}

module.exports = init;
