/**
 * metrics client graph manager
 * By Joe Turgeon [http://arithmetric.com]
 * 1/16/2010
 */

var MetricsGraph = function (canvas, setName, indexMin, indexMax) {
  var self = (this === window) ? {} : this;
  self.data = {};
  self.cvs = canvas;
  self.setNames = (typeof setName == 'string') ? [setName] : setName;
  self.indexMin = indexMin;
  self.indexMax = indexMax;
  self.valueMin = false;
  self.valueMax = false;
  self.marginX = 40;
  self.marginY = 20;
  self.scaleX = false;
  self.scaleY = false;
  self.analysis = MetricsDataAnalysis.NONE;
  self.engaged = false;
  self.forcerefresh = false;
  self.onUpdate = false;
  self.setColors = ['#000', '#d00', '#d0b', '#00d', '#0dd', '#0d0', '#dd0', '#e60'];
  self.graphImageData = false;
  self.interactiveState = {};
  self.update();
  return self;
};

MetricsGraph.prototype.checkData = function () {
  if (!this.engaged) {
    var reqdata = [];
    var num = this.setNames.length;
    for (var i = 0; i < num; i++) {
      var sn = this.setNames[i];
      if (this.forcerefresh || !this.data[sn] || !this.data[sn].results || !this.data[sn].stats || this.data[sn].stats.reqIndexMin != this.indexMin || this.data[sn].stats.reqIndexMax != this.indexMax || this.data[sn].analysis != this.analysis) {
        reqdata.push(sn);
      }
    }
    this.forcerefresh = false;
    if (reqdata.length) {
//TODO: data density: range / width should be less than 1 (if not, then reduce the number of points)
      this.engaged = true;
      metricsData.getData(new MetricsDataRequest(this, this.setNames, this.indexMin, this.indexMax, this.analysis));
    }
  }
};

MetricsGraph.prototype.handleData = function (dataObj) {
  this.data = dataObj;
  this.engaged = false;
  this.update();
  if (this.onUpdate && this.onUpdate.update) {
    this.onUpdate.update.call(this.onUpdate);
  }
};

MetricsGraph.prototype.update = function () {
  this.checkData();

  if (this.cvs && this.cvs.getContext && this.cvs.width && this.cvs.height) {
    var ctx = this.cvs.getContext('2d'),
      width = this.cvs.width,
      height = this.cvs.height,
      marginX = this.marginX,
      marginY = this.marginY,
      graphWidth = width - 2 * marginX,
      graphHeight = height - 2 * marginY;

    $(this.cvs).data('graph', this);

    ctx.clearRect(0, 0, width, height);

    if (this.setNames.length && this.data && this.data.stats) {
      var i, x, y, yopen, ylow, yhigh, num, val, txt, numsets, set, setidx, index, datum;

      // define data-dependent graph properties
      var trueValueSpan = Number(this.data.stats.valueMax) - Number(this.data.stats.valueMin);
      this.valueMin = Number(this.data.stats.valueMin) - trueValueSpan * 0.1;
      this.valueMax = Number(this.data.stats.valueMax) + trueValueSpan * 0.1;
      this.scaleX = graphWidth / (this.indexMax - this.indexMin);
      this.scaleY = graphHeight / (this.valueMax - this.valueMin);

      // draw border rectangle
      ctx.lineWidth = 1;
      ctx.strokeStyle = '#444';
      ctx.fillStyle = '#000';

      // draw x-axis ticks, grid, and labels
      var xseg = (this.indexMax - this.indexMin) / 10;
      ctx.font = '10px sans-serif';
      ctx.textBaseline = 'top';
      for (i = 0; i < 11; i++) {
        val = Math.floor((xseg * i) + this.indexMin);
        txt = metricsDate.format_jd(val);
        x = marginX + Math.floor(i * xseg * this.scaleX);
        y = height - marginY;
        ctx.strokeStyle = '#444';
        ctx.beginPath();
        ctx.moveTo(x + 0.5, y + 2.5);
        ctx.lineTo(x + 0.5, y + 0.5);
        ctx.stroke();
        if (i && i < 10) {
          ctx.strokeStyle = '#ddd';
        }
        ctx.beginPath();
        ctx.moveTo(x + 0.5, y + 0.5);
        ctx.lineTo(x + 0.5, marginY + 0.5);
        ctx.stroke();
        if (ctx.textAlign && ctx.fillText) {
          if (i === 0) {
            ctx.textAlign = 'left';
          }
          else if (i == 10) {
            ctx.textAlign = 'right';
          }
          else {
            ctx.textAlign = 'center';
          }
          ctx.fillText(txt, x, y + 3);
        }
      }

      // draw y-axis ticks, grid, and labels
      var yseg = (this.valueMax - this.valueMin) / 10;
      ctx.font = '10px sans-serif';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'middle';
      for (i = 0; i < 11; i++) {
        val = (yseg * i) + this.valueMin;
        x = marginX;
        y = height - marginY - Math.floor(i * yseg * this.scaleY);
        txt = this.formatNumber(val, 3);
        ctx.strokeStyle = '#444';
        ctx.beginPath();
        ctx.moveTo(x - 1.5, y + 0.5);
        ctx.lineTo(x + 0.5, y + 0.5);
        ctx.stroke();
        if (i && i < 10) {
          ctx.strokeStyle = '#ddd';
        }
        ctx.beginPath();
        ctx.moveTo(x + 0.5, y + 0.5);
        ctx.lineTo(marginX + graphWidth + 0.5, y + 0.5);
        ctx.stroke();
        if (ctx.fillText) {
          ctx.fillText(txt, x - 3, y);
        }
      }

      // draw data points
      numsets = this.setNames.length;
      for (setidx = 0; setidx < numsets; setidx++) {
        set = this.setNames[setidx];
        if (this.data[set] && this.data[set].index && this.data[set].results && this.data[set].stats) {
          num = this.data[set].index.length;
          var range = this.indexMax - this.indexMin;
// visual density: num / width should be less than 1 (if not, then reduce the number of points)
// if less than .2, then draw as a line
          var density = num / width;
//          $('#messages').text('density ' + Number(density).toFixed(3) + ': ' + num + ' points within ' + range + ' range to plot over ' + this.width + ' px');
          for (i = 0; i < num; i++) {
            index = this.data[set].index[i];
            datum = this.data[set].results[index];
            if (isNaN(index) || isNaN(datum.value)) {
              continue;
            }
            if (index < this.indexMin || index > this.indexMax) {
              continue;
            }
            x = Math.round((index - this.indexMin) * this.scaleX);
            y = graphHeight - Math.round((datum.value - this.valueMin) * this.scaleY);
            yopen = ylow = yhigh = false;
            if (datum.open) {
              yopen = graphHeight - Math.round((datum.open - this.valueMin) * this.scaleY);
            }
            if (datum.low) {
              ylow = graphHeight - Math.round((datum.low - this.valueMin) * this.scaleY);
            }
            if (datum.high) {
              yhigh = graphHeight - Math.round((datum.high - this.valueMin) * this.scaleY);
            }
// draw candlestick graph
            if (yopen && ylow && yhigh) {
              ctx.lineWidth = 1;
              ctx.strokeStyle = '#000';
              ctx.beginPath();
              ctx.moveTo(x + 0.5 + marginX, ylow + 0.5 + marginY);
              ctx.lineTo(x + 0.5 + marginX, yhigh + 0.5 + marginY);
              ctx.stroke();
              if (yopen == y) {
                ctx.strokeStyle = this.setColors[setidx];
                ctx.beginPath();
                ctx.moveTo(x - 0.5 + marginX, y + 0.5 + marginY);
                ctx.lineTo(x + 1.5 + marginX, y + 0.5 + marginY);
                ctx.stroke();
              }
              else if (yopen > y) {
                ctx.strokeStyle = this.setColors[setidx];
                ctx.clearRect(x - 1 + marginX, yopen + marginY, 3, y - yopen);
                ctx.strokeRect(x - 0.5 + marginX, yopen + 0.5 + marginY, 2, y - yopen - 1);
              }
              else {
                ctx.fillStyle = this.setColors[setidx];
                ctx.fillRect(x - 1 + marginX, yopen + marginY, 3, y - yopen);
              }
            }
// draw continuous line
            else if (density < 0.2) {
              if (i === 0) {
                ctx.strokeStyle = this.setColors[setidx];
                ctx.lineWidth = 1;
                ctx.beginPath();
                ctx.moveTo(x + marginX, y + marginY);
              }
              else {
                ctx.lineTo(x + marginX, y + marginY);
              }
              if (i == (num - 1)) {
                ctx.stroke();
              }
            }
// draw one or two pixel square per point
            else {
              ctx.fillStyle = this.setColors[setidx];
              if (density < 0.5) {
                ctx.fillRect(x + marginX, y + marginY, 2, 2);
              }
              else {
                ctx.fillRect(x + marginX, y + marginY, 1, 1);
              }
            }
          }
        }
      }
    }

    // if this graph has an engaged data request, then mark it as unfinished
    if (this.engaged) {
      ctx.fillStyle = 'rgba(160, 160, 160, 0.6)';
      ctx.fillRect(marginX, marginY, graphWidth, graphHeight);
    }

    this.graphImageData = false;
    this.updateOverlays();
//    $(this.cvs).css({'cursor': 'move'});
//    $(this.cvs).css({'cursor': 'pointer'});
    $(this.cvs).css({'cursor': 'crosshair'});
    $(this.cvs).click(this.handleMouse);
    $(this.cvs).mousemove(this.handleMouse);
    $(this.cvs).mousedown(this.handleMouse);
    $(this.cvs).mouseup(this.handleMouse);
  }
  else {
    $(this.cvs).css({'cursor': 'default'});
  }
};

MetricsGraph.prototype.updateOverlays = function (e) {
  var self = this,
    ctx = self.cvs.getContext('2d'),
    width = self.cvs.width,
    height = self.cvs.height,
    mousex = false, mousey = false, xposval,
    xposdist = false,
    graphx = false,
    cx, i, idx1, idx2, val1, val2, pos1, pos2, set, setval, setdist,
    marginX = self.marginX,
    marginY = self.marginY,
    state = self.interactiveState;
  if (e && e.layerX && e.layerY) {
    mousex = e.layerX;
    mousey = e.layerY;
    xposval = self.canvasToGraph.call(self, mousex).x;
  }
  if (self.engaged || !self.setNames.length) {
    return;
  }
  if (mousex && mousey && mousex > marginX && mousex < (width - marginX) && mousey > marginY && mousey < (height - marginY)) {
    // find closest data index value to mouse location
    for (set in self.data) {
      if (set !== 'stats' && self.data.hasOwnProperty(set) && self.data[set].index && self.data[set].index.length) {
        var index = self.data[set].index;
        var num = index.length;
        for (i = 0; i < num; i++) {
          if (i && index[i - 1] < xposval && index[i] >= xposval) {
            setval = (index[i] - xposval) <= (xposval - index[i - 1]) ? index[i] : index[i - 1];
            setdist = Math.abs(setval - xposval);
            if (xposdist === false || setdist < xposdist) {
              graphx = setval;
              xposdist = setdist;
            }
            break;
          }
        }
      }
    }
    // process mouse events
    if (graphx) {
      if (e.type === 'mousedown') {
        state.point1 = graphx;
        state.point2 = 0;
      }
      else if (e.type === 'mouseup') {
        state.point2 = graphx;
      }
      else if (e.type === 'click') {
      }
    }
  }
  // reset points if out-of-bounds
  if ((state.point1 && (state.point1 < self.indexMin || state.point1 > self.indexMax)) || (state.point2 && (state.point2 < self.indexMin || state.point2 > self.indexMax))) {
    state.point1 = 0;
    state.point2 = 0;
  }
  // update dynamic graphics
  if (self.graphImageData) {
    ctx.putImageData(self.graphImageData, 0, 0);
  }
  else {
    self.graphImageData = ctx.getImageData(0, 0, width, height);
  }
  if (state.point1 && (graphx || state.point2)) {
    ctx.fillStyle = 'rgba(240, 240, 240, 0.5)';
    idx2 = state.point2 || graphx;
    if (state.point1 < idx2) {
      idx1 = state.point1;
    }
    else {
      idx1 = idx2;
      idx2 = state.point1;
    }
    if (idx1 != idx2) {
      pos1 = self.graphToCanvas.call(self, idx1, 0);
      pos2 = self.graphToCanvas.call(self, idx2, 0);
      ctx.fillRect(marginX, marginY, pos1.x - marginX, height - 2 * marginY);
      ctx.fillRect(pos2.x, marginY, width - pos2.x - marginX, height - 2 * marginY);
    }
  }
  ctx.lineWidth = 1;
  if (graphx) {
    cx = self.graphToCanvas.call(self, graphx, 0).x;
    ctx.strokeStyle = '#888';
    ctx.beginPath();
    ctx.moveTo(cx + 0.5, height - marginY + 0.5);
    ctx.lineTo(cx + 0.5, marginY + 0.5);
    ctx.stroke();
    var txt = 'At ' + metricsDate.format_jd(graphx) + ': ';
    for (set in self.data) {
      if (set != 'stats' && self.data.hasOwnProperty(set) && self.data[set].results && self.data[set].results.hasOwnProperty(graphx) && self.data[set].results[graphx].hasOwnProperty('value')) {
        txt += set + ' = ' + this.formatNumber(self.data[set].results[graphx].value) + ' ';
      }
    }
    $('#messages').text(txt);
  }
  if (state.point1 && (state.point2 || graphx)) {
    ctx.strokeStyle = '#f88';
    ctx.fillStyle = '#000';
    ctx.font = '10px sans-serif';
    idx2 = state.point2 || graphx;
    if (state.point1 < idx2) {
      idx1 = state.point1;
    }
    else {
      idx1 = idx2;
      idx2 = state.point1;
    }
    if (idx1 != idx2) {
      for (set in self.data) {
        if (set != 'stats' && self.data.hasOwnProperty(set) && self.data[set].results && self.data[set].results.hasOwnProperty(idx1) && self.data[set].results.hasOwnProperty(idx2)) {
          val1 = Number(self.data[set].results[idx1].value);
          val2 = Number(self.data[set].results[idx2].value);
          if (val1 && val2) {
            pos1 = self.graphToCanvas.call(self, idx1, val1);
            pos2 = self.graphToCanvas.call(self, idx2, val2);
// triangle
            ctx.beginPath();
            ctx.moveTo(pos1.x + 0.5, pos1.y + 0.5);
            ctx.lineTo(pos2.x + 0.5, pos2.y + 0.5);
            if (pos1.y > pos2.y) {
              ctx.lineTo(pos2.x + 0.5, pos1.y + 0.5);
              ctx.lineTo(pos1.x + 0.5, pos1.y + 0.5);
            }
            else if (pos1.y < pos2.y) {
              ctx.lineTo(pos1.x + 0.5, pos2.y + 0.5);
              ctx.lineTo(pos1.x + 0.5, pos1.y + 0.5);
            }
            ctx.stroke();
            if (ctx.fillText) {
              var dtxt = (idx2 - idx1) + ' days';
              var dx = pos1.y > pos2.y ? pos2.x - 2 : pos1.x + 2;
              var dy = pos1.y > pos2.y ? pos1.y : pos2.y;
              ctx.textAlign = pos1.y > pos2.y ? 'right' : 'left';
              ctx.textBaseline = 'top';
              ctx.fillText(dtxt, dx, dy + 2);
              dx = pos1.y > pos2.y ? pos2.x + 2 : pos1.x - 2;
              ctx.textAlign = pos1.y > pos2.y ? 'left' : 'right';
              if (Math.abs(pos2.y - pos1.y) > 50) {
                dtxt = self.formatNumber(val1);
                dy = pos1.y;
                ctx.textBaseline = pos1.y > pos2.y ? 'bottom' : 'top';
                ctx.fillText(dtxt, dx, dy);
                dtxt = self.formatNumber(val2);
                dy = pos2.y;
                ctx.textBaseline = pos2.y > pos1.y ? 'bottom' : 'top';
                ctx.fillText(dtxt, dx, dy);
              }
              dtxt = self.formatNumber(val2 - val1);
              if (val2 > val1) {
                dtxt = '+' + dtxt;
              }
              dy = Math.floor((pos1.y + pos2.y) / 2);
              ctx.textBaseline = 'middle';
              ctx.fillText(dtxt, dx, dy - 6);
              dtxt = self.formatPercent((val2 - val1) / val1);
              if (val2 > val1) {
                dtxt = '+' + dtxt;
              }
              ctx.fillText(dtxt, dx, dy + 6);
            }
/* box with fibonacci steps
            var fibonacciSteps = [.146, .236, .382, .5, .618, .764, .854];
            ctx.beginPath();
            ctx.moveTo(pos1.x + 0.5, pos1.y + 0.5);
            ctx.lineTo(pos1.x + 0.5, pos2.y + 0.5);
            ctx.lineTo(pos2.x + 0.5, pos2.y + 0.5);
            ctx.lineTo(pos2.x + 0.5, pos1.y + 0.5);
            ctx.lineTo(pos1.x + 0.5, pos1.y + 0.5);
            ctx.stroke();
            var valdiff = Math.abs(val2 - val1);
            var vallow = val2 < val1 ? val2 : val1;
            for (i = 0; i < 7; i++) {
              var val = valdiff * fibonacciSteps[i] + vallow;
              var pos = self.graphToCanvas.call(self, 0, val).y;
              ctx.beginPath();
              ctx.moveTo(pos1.x + 0.5, pos + 0.5);
              ctx.lineTo(pos2.x + 0.5, pos + 0.5);
              ctx.stroke();
              if (ctx.fillText) {
                ctx.textAlign = 'left';
                ctx.textBaseline = 'middle';
                var dtxt = self.formatNumber(val);
                ctx.fillText(dtxt, pos2.x + 2, pos + 0.5);
              }
            }
*/
//              txt += ' (Selected: ' + Number(val2 - val1).toFixed(2) + ' / ' + Number((val2 - val1) / val1 * 100).toFixed(2) + '% change from ' + metricsDate.format_jd(state.point1) + ' to ' + metricsDate.format_jd(state.point2) + ') ';
          }
        }
      }
    }
//        $('#messages').text(txt);
  }
};

MetricsGraph.prototype.canvasToGraph = function (cx, cy) {
  var x = 0, y = 0;
  if (this.cvs) {
    x = this.indexMin + Math.round((cx - this.marginX) / this.scaleX);
    y = this.valueMin + Math.round((cy - this.cvs.height + this.marginY) / this.scaleY);
  }
  return {x: x, y: y};
};

MetricsGraph.prototype.graphToCanvas = function (gx, gy) {
  var x = 0, y = 0;
  if (this.cvs) {
    x = this.marginX + Math.round(this.scaleX * (gx - this.indexMin));
    y = this.cvs.height - this.marginY - Math.round((gy - this.valueMin) * this.scaleY);
  }
  return {x: x, y: y};
};

MetricsGraph.prototype.formatNumber = function (val, length) {
  if (!length || length < 3) {
    length = 3;
  }
  if (val >= 1000000000) {
    return Number(val / 1000).toFixed(length - 1) + 'B';
  }
  else if (val >= 1000000) {
    return Number(val / 1000000).toFixed(length - 1) + 'M';
  }
  else if (val >= 1000) {
    return Number(val / 1000).toFixed(length - 1) + 'K';
  }
  else if (val >= 100) {
    return Number(val).toFixed(length - 2);
  }
  else if (val >= 1) {
    return Number(val).toFixed(length - 1);
  }
  else if (val >= 0.001) {
    return Number(val).toFixed(length);
  }
  return Number(val).toPrecision(length);
};

MetricsGraph.prototype.formatPercent = function (val) {
  return Number(val * 100).toFixed(2) + '%';
};

MetricsGraph.prototype.handleMouse = function (e) {
  if (e && e.target) {
    var self = $(e.target).data('graph');
    if (self && self.updateOverlays) {
      self.updateOverlays.call(self, e);
    }
  }
};
