import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import {appEvents, contextSrv} from 'app/core/core';
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
import d3 from 'vendor/d3/d3';
import {HeatmapTooltip} from './heatmap_tooltip';
import {mergeZeroBuckets} from './heatmap_data_converter';

let MIN_CARD_SIZE = 1,
    CARD_PADDING = 1,
    CARD_ROUND = 0,
    DATA_RANGE_WIDING_FACTOR = 1.2,
    DEFAULT_X_TICK_SIZE_PX = 100,
    DEFAULT_Y_TICK_SIZE_PX = 50,
    X_AXIS_TICK_PADDING = 10,
    Y_AXIS_TICK_PADDING = 5,
    MIN_SELECTION_WIDTH = 2;

export default function link(scope, elem, attrs, ctrl) {
  let data, timeRange, panel, heatmap;

  // $heatmap is JQuery object, but heatmap is D3
  let $heatmap = elem.find('.heatmap-panel');
  let tooltip = new HeatmapTooltip($heatmap, scope);

  let width, height,
      yScale, xScale,
      chartWidth, chartHeight,
      chartTop, chartBottom,
      yAxisWidth, xAxisHeight,
      cardPadding, cardRound,
      cardWidth, cardHeight,
      colorScale, opacityScale,
      mouseUpHandler;

  let selection = {
    active: false,
    x1: -1,
    x2: -1
  };

  let padding = {left: 0, right: 0, top: 0, bottom: 0},
      margin = {left: 25, right: 15, top: 10, bottom: 20},
      dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;

  ctrl.events.on('render', () => {
    render();
    ctrl.renderingCompleted();
  });

  function setElementHeight() {
    try {
      var height = ctrl.height || panel.height || ctrl.row.height;
      if (_.isString(height)) {
        height = parseInt(height.replace('px', ''), 10);
      }

      height -= 5; // padding
      height -= panel.title ? 24 : 9; // subtract panel title bar

      $heatmap.css('height', height + 'px');

      return true;
    } catch (e) { // IE throws errors sometimes
      return false;
    }
  }

  function getYAxisWidth(elem) {
    let axis_text = elem.selectAll(".axis-y text").nodes();
    let max_text_width = _.max(_.map(axis_text, text => {
      // Use SVG getBBox method
      return text.getBBox().width;
    }));

    return max_text_width;
  }

  function getXAxisHeight(elem) {
    let axis_line = elem.select(".axis-x line");
    if (!axis_line.empty()) {
      let axis_line_position = parseFloat(elem.select(".axis-x line").attr("y2"));
      let canvas_width = parseFloat(elem.attr("height"));
      return canvas_width - axis_line_position;
    } else {
      // Default height
      return 30;
    }
  }

  function addXAxis() {
    scope.xScale = xScale = d3.scaleTime()
      .domain([timeRange.from, timeRange.to])
      .range([0, chartWidth]);

    let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
    let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
    let timeFormat;
    let dashboardTimeZone = ctrl.dashboard.getTimezone();
    if (dashboardTimeZone === 'utc') {
      timeFormat = d3.utcFormat(grafanaTimeFormatter);
    } else {
      timeFormat = d3.timeFormat(grafanaTimeFormatter);
    }

    let xAxis = d3.axisBottom(xScale)
      .ticks(ticks)
      .tickFormat(timeFormat)
      .tickPadding(X_AXIS_TICK_PADDING)
      .tickSize(chartHeight);

    let posY = margin.top;
    let posX = yAxisWidth;
    heatmap.append("g")
      .attr("class", "axis axis-x")
      .attr("transform", "translate(" + posX + "," + posY + ")")
      .call(xAxis);

    // Remove horizontal line in the top of axis labels (called domain in d3)
    heatmap.select(".axis-x").select(".domain").remove();
  }

  function addYAxis() {
    let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
    let tick_interval = tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
    let {y_min, y_max} = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);

    // Rewrite min and max if it have been set explicitly
    y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
    y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;

    // Adjust ticks after Y range widening
    tick_interval = tickStep(y_min, y_max, ticks);
    ticks = Math.ceil((y_max - y_min) / tick_interval);

    let decimalsAuto = getPrecision(tick_interval);
    let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
    // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
    let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
    let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
    ctrl.decimals = decimals;
    ctrl.scaledDecimals = scaledDecimals;

    // Set default Y min and max if no data
    if (_.isEmpty(data.buckets)) {
      y_max = 1;
      y_min = -1;
      ticks = 3;
      decimals = 1;
    }

    data.yAxis = {
      min: y_min,
      max: y_max,
      ticks: ticks
    };

    scope.yScale = yScale = d3.scaleLinear()
      .domain([y_min, y_max])
      .range([chartHeight, 0]);

    let yAxis = d3.axisLeft(yScale)
      .ticks(ticks)
      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
      .tickSizeInner(0 - width)
      .tickSizeOuter(0)
      .tickPadding(Y_AXIS_TICK_PADDING);

    heatmap.append("g")
      .attr("class", "axis axis-y")
      .call(yAxis);

    // Calculate Y axis width first, then move axis into visible area
    let posY = margin.top;
    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
    heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")");

    // Remove vertical line in the right of axis labels (called domain in d3)
    heatmap.select(".axis-y").select(".domain").remove();
  }

  // Wide Y values range and anjust to bucket size
  function wideYAxisRange(min, max, tickInterval) {
    let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
    let y_min, y_max;

    if (tickInterval === 0) {
      y_max = max * dataRangeWidingFactor;
      y_min = min - min * (dataRangeWidingFactor - 1);
      tickInterval = (y_max - y_min) / 2;
    } else {
      y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
      y_min = Math.floor((min - y_widing) / tickInterval) * tickInterval;
    }

    // Don't wide axis below 0 if all values are positive
    if (min >= 0 && y_min < 0) {
      y_min = 0;
    }

    return {y_min, y_max};
  }

  function addLogYAxis() {
    let log_base = panel.yAxis.logBase;
    let {y_min, y_max} = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);

    y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
    y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;

    // Set default Y min and max if no data
    if (_.isEmpty(data.buckets)) {
      y_max = Math.pow(log_base, 2);
      y_min = 1;
    }

    scope.yScale = yScale = d3.scaleLog()
      .base(panel.yAxis.logBase)
      .domain([y_min, y_max])
      .range([chartHeight, 0]);

    let domain = yScale.domain();
    let tick_values = logScaleTickValues(domain, log_base);

    let decimalsAuto = getPrecision(y_min);
    let decimals = panel.yAxis.decimals || decimalsAuto;

    // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
    let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
    let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
    ctrl.decimals = decimals;
    ctrl.scaledDecimals = scaledDecimals;

    data.yAxis = {
      min: y_min,
      max: y_max,
      ticks: tick_values.length
    };

    let yAxis = d3.axisLeft(yScale)
      .tickValues(tick_values)
      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
      .tickSizeInner(0 - width)
      .tickSizeOuter(0)
      .tickPadding(Y_AXIS_TICK_PADDING);

    heatmap.append("g")
      .attr("class", "axis axis-y")
      .call(yAxis);

    // Calculate Y axis width first, then move axis into visible area
    let posY = margin.top;
    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
    heatmap.select(".axis-y").attr("transform", "translate(" + posX + "," + posY + ")");

    // Set first tick as pseudo 0
    if (y_min < 1) {
      heatmap.select(".axis-y").select(".tick text").text("0");
    }

    // Remove vertical line in the right of axis labels (called domain in d3)
    heatmap.select(".axis-y").select(".domain").remove();
  }

  // Adjust data range to log base
  function adjustLogRange(min, max, logBase) {
    let y_min, y_max;

    y_min = data.heatmapStats.minLog;
    if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
      y_min = 1;
    } else {
      y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
    }

    // Adjust max Y value to log base
    y_max = adjustLogMax(data.heatmapStats.max, logBase);

    return {y_min, y_max};
  }

  function adjustLogMax(max, base) {
    return Math.pow(base, Math.ceil(logp(max, base)));
  }

  function adjustLogMin(min, base) {
    return Math.pow(base, Math.floor(logp(min, base)));
  }

  function logScaleTickValues(domain, base) {
    let domainMin = domain[0];
    let domainMax = domain[1];
    let tickValues = [];

    if (domainMin < 1) {
      let under_one_ticks = Math.floor(logp(domainMin, base));
      for (let i = under_one_ticks; i < 0; i++) {
        let tick_value = Math.pow(base, i);
        tickValues.push(tick_value);
      }
    }

    let ticks = Math.ceil(logp(domainMax, base));
    for (let i = 0; i <= ticks; i++) {
      let tick_value = Math.pow(base, i);
      tickValues.push(tick_value);
    }

    return tickValues;
  }

  function tickValueFormatter(decimals, scaledDecimals = null) {
    let format = panel.yAxis.format;
    return function(value) {
      return kbn.valueFormats[format](value, decimals, scaledDecimals);
    };
  }

  function fixYAxisTickSize() {
    heatmap.select(".axis-y")
      .selectAll(".tick line")
      .attr("x2", chartWidth);
  }

  function addAxes() {
    chartHeight = height - margin.top - margin.bottom;
    chartTop = margin.top;
    chartBottom = chartTop + chartHeight;

    if (panel.yAxis.logBase === 1) {
      addYAxis();
    } else {
      addLogYAxis();
    }

    yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
    chartWidth = width - yAxisWidth - margin.right;
    fixYAxisTickSize();

    addXAxis();
    xAxisHeight = getXAxisHeight(heatmap);

    if (!panel.yAxis.show) {
      heatmap.select(".axis-y").selectAll("line").style("opacity", 0);
    }

    if (!panel.xAxis.show) {
      heatmap.select(".axis-x").selectAll("line").style("opacity", 0);
    }
  }

  function addHeatmapCanvas() {
    let heatmap_elem = $heatmap[0];

    width = Math.floor($heatmap.width()) - padding.right;
    height = Math.floor($heatmap.height()) - padding.bottom;

    cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
    cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;

    if (heatmap) {
      heatmap.remove();
    }

    heatmap = d3.select(heatmap_elem)
      .append("svg")
      .attr("width", width)
      .attr("height", height);
  }

  function addHeatmap() {
    addHeatmapCanvas();
    addAxes();

    if (panel.yAxis.logBase !== 1) {
      let log_base = panel.yAxis.logBase;
      let domain = yScale.domain();
      let tick_values = logScaleTickValues(domain, log_base);
      data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
    }

    let cardsData = data.cards;
    let maxValueAuto = data.cardStats.max;
    let maxValue = panel.color.max || maxValueAuto;
    let minValue = panel.color.min || 0;

    colorScale = getColorScale(maxValue, minValue);
    setOpacityScale(maxValue);
    setCardSize();

    let cards = heatmap.selectAll(".heatmap-card").data(cardsData);
    cards.append("title");
    cards = cards.enter().append("rect")
    .attr("x", getCardX)
    .attr("width", getCardWidth)
    .attr("y", getCardY)
    .attr("height", getCardHeight)
    .attr("rx", cardRound)
    .attr("ry", cardRound)
    .attr("class", "bordered heatmap-card")
    .style("fill", getCardColor)
    .style("stroke", getCardColor)
    .style("stroke-width", 0)
    .style("opacity", getCardOpacity);

    let $cards = $heatmap.find(".heatmap-card");
    $cards.on("mouseenter", (event) => {
      tooltip.mouseOverBucket = true;
      highlightCard(event);
    })
    .on("mouseleave", (event) => {
      tooltip.mouseOverBucket = false;
      resetCardHighLight(event);
    });
  }

  function highlightCard(event) {
    let color = d3.select(event.target).style("fill");
    let highlightColor = d3.color(color).darker(2);
    let strokeColor = d3.color(color).brighter(4);
    let current_card = d3.select(event.target);
    tooltip.originalFillColor = color;
    current_card.style("fill", highlightColor)
    .style("stroke", strokeColor)
    .style("stroke-width", 1);
  }

  function resetCardHighLight(event) {
    d3.select(event.target).style("fill", tooltip.originalFillColor)
    .style("stroke", tooltip.originalFillColor)
    .style("stroke-width", 0);
  }

  function getColorScale(maxValue, minValue = 0) {
    let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
    let colorInterpolator = d3[colorScheme.value];
    let colorScaleInverted = colorScheme.invert === 'always' ||
      (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);

    let start = colorScaleInverted ? maxValue : minValue;
    let end = colorScaleInverted ? minValue : maxValue;

    return d3.scaleSequential(colorInterpolator).domain([start, end]);
  }

  function setOpacityScale(maxValue) {
    if (panel.color.colorScale === 'linear') {
      opacityScale = d3.scaleLinear()
      .domain([0, maxValue])
      .range([0, 1]);
    } else if (panel.color.colorScale === 'sqrt') {
      opacityScale = d3.scalePow().exponent(panel.color.exponent)
      .domain([0, maxValue])
      .range([0, 1]);
    }
  }

  function setCardSize() {
    let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
    let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));

    if (panel.yAxis.logBase !== 1) {
      let base = panel.yAxis.logBase;
      let splitFactor = data.yBucketSize || 1;
      yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
    }

    cardWidth = xGridSize - cardPadding * 2;
    cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
  }

  function getCardX(d) {
    let x;
    if (xScale(d.x) < 0) {
      // Cut card left to prevent overlay
      x = yAxisWidth + cardPadding;
    } else {
      x = xScale(d.x) + yAxisWidth + cardPadding;
    }

    return x;
  }

  function getCardWidth(d) {
    let w;
    if (xScale(d.x) < 0) {
      // Cut card left to prevent overlay
      let cutted_width = xScale(d.x) + cardWidth;
      w = cutted_width > 0 ? cutted_width : 0;
    } else if (xScale(d.x) + cardWidth > chartWidth) {
      // Cut card right to prevent overlay
      w = chartWidth - xScale(d.x) - cardPadding;
    } else {
      w = cardWidth;
    }

    // Card width should be MIN_CARD_SIZE at least
    w = Math.max(w, MIN_CARD_SIZE);
    return w;
  }

  function getCardY(d) {
    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
    if (panel.yAxis.logBase !== 1 && d.y === 0) {
      y = chartBottom - cardHeight - cardPadding;
    } else {
      if (y < chartTop) {
        y = chartTop;
      }
    }

    return y;
  }

  function getCardHeight(d) {
    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
    let h = cardHeight;

    if (panel.yAxis.logBase !== 1 && d.y === 0) {
      return cardHeight;
    }

    // Cut card height to prevent overlay
    if (y < chartTop) {
      h = yScale(d.y) - cardPadding;
    } else if (yScale(d.y) > chartBottom) {
      h = chartBottom - y;
    } else if (y + cardHeight > chartBottom) {
      h = chartBottom - y;
    }

    // Height can't be more than chart height
    h = Math.min(h, chartHeight);
    // Card height should be MIN_CARD_SIZE at least
    h = Math.max(h, MIN_CARD_SIZE);

    return h;
  }

  function getCardColor(d) {
    if (panel.color.mode === 'opacity') {
      return panel.color.cardColor;
    } else {
      return colorScale(d.count);
    }
  }

  function getCardOpacity(d) {
    if (panel.color.mode === 'opacity') {
      return opacityScale(d.count);
    } else {
      return 1;
    }
  }

  /////////////////////////////
  // Selection and crosshair //
  /////////////////////////////

  // Shared crosshair and tooltip
  appEvents.on('graph-hover', event => {
    drawSharedCrosshair(event.pos);
  }, scope);

  appEvents.on('graph-hover-clear', () => {
    clearCrosshair();
  }, scope);

  function onMouseDown(event) {
    selection.active = true;
    selection.x1 = event.offsetX;

    mouseUpHandler = function() {
      onMouseUp();
    };

    $(document).one("mouseup", mouseUpHandler);
  }

  function onMouseUp() {
    $(document).unbind("mouseup", mouseUpHandler);
    mouseUpHandler = null;
    selection.active = false;

    let selectionRange = Math.abs(selection.x2 - selection.x1);
    if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
      let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
      let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);

      ctrl.timeSrv.setTime({
        from: moment.utc(timeFrom),
        to: moment.utc(timeTo)
      });
    }

    clearSelection();
  }

  function onMouseLeave() {
    appEvents.emit('graph-hover-clear');
    clearCrosshair();
  }

  function onMouseMove(event) {
    if (!heatmap) { return; }

    if (selection.active) {
      // Clear crosshair and tooltip
      clearCrosshair();
      tooltip.destroy();

      selection.x2 = limitSelection(event.offsetX);
      drawSelection(selection.x1, selection.x2);
    } else {
      emitGraphHoverEvet(event);
      drawCrosshair(event.offsetX);
      tooltip.show(event, data);
    }
  }

  function emitGraphHoverEvet(event) {
    let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
    let y = yScale.invert(event.offsetY);
    let pos = {
      pageX: event.pageX,
      pageY: event.pageY,
      x: x, x1: x,
      y: y, y1: y,
      panelRelY: null
    };

    // Set minimum offset to prevent showing legend from another panel
    pos.panelRelY = Math.max(event.offsetY / height, 0.001);

    // broadcast to other graph panels that we are hovering
    appEvents.emit('graph-hover', {pos: pos, panel: panel});
  }

  function limitSelection(x2) {
    x2 = Math.max(x2, yAxisWidth);
    x2 = Math.min(x2, chartWidth + yAxisWidth);
    return x2;
  }

  function drawSelection(posX1, posX2) {
    if (heatmap) {
      heatmap.selectAll(".heatmap-selection").remove();
      let selectionX = Math.min(posX1, posX2);
      let selectionWidth = Math.abs(posX1 - posX2);

      if (selectionWidth > MIN_SELECTION_WIDTH) {
        heatmap.append("rect")
        .attr("class", "heatmap-selection")
        .attr("x", selectionX)
        .attr("width", selectionWidth)
        .attr("y", chartTop)
        .attr("height", chartHeight);
      }
    }
  }

  function clearSelection() {
    selection.x1 = -1;
    selection.x2 = -1;

    if (heatmap) {
      heatmap.selectAll(".heatmap-selection").remove();
    }
  }

  function drawCrosshair(position) {
    if (heatmap) {
      heatmap.selectAll(".heatmap-crosshair").remove();

      let posX = position;
      posX = Math.max(posX, yAxisWidth);
      posX = Math.min(posX, chartWidth + yAxisWidth);

      heatmap.append("g")
      .attr("class", "heatmap-crosshair")
      .attr("transform", "translate(" + posX + ",0)")
      .append("line")
      .attr("x1", 1)
      .attr("y1", chartTop)
      .attr("x2", 1)
      .attr("y2", chartBottom)
      .attr("stroke-width", 1);
    }
  }

  function drawSharedCrosshair(pos) {
    if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
      let posX = xScale(pos.x) + yAxisWidth;
      drawCrosshair(posX);
    }
  }

  function clearCrosshair() {
    if (heatmap) {
      heatmap.selectAll(".heatmap-crosshair").remove();
    }
  }

  function render() {
    data = ctrl.data;
    panel = ctrl.panel;
    timeRange = ctrl.range;

    if (!setElementHeight() || !data) {
      return;
    }

    // Draw default axes and return if no data
    if (_.isEmpty(data.buckets)) {
      addHeatmapCanvas();
      addAxes();
      return;
    }

    addHeatmap();
    scope.yAxisWidth = yAxisWidth;
    scope.xAxisHeight = xAxisHeight;
    scope.chartHeight = chartHeight;
    scope.chartWidth = chartWidth;
    scope.chartTop = chartTop;
  }

  // Register selection listeners
  $heatmap.on("mousedown", onMouseDown);
  $heatmap.on("mousemove", onMouseMove);
  $heatmap.on("mouseleave", onMouseLeave);
}

function grafanaTimeFormat(ticks, min, max) {
  if (min && max && ticks) {
    let range = max - min;
    let secPerTick = (range/ticks) / 1000;
    let oneDay = 86400000;
    let oneYear = 31536000000;

    if (secPerTick <= 45) {
      return "%H:%M:%S";
    }
    if (secPerTick <= 7200 || range <= oneDay) {
      return "%H:%M";
    }
    if (secPerTick <= 80000) {
      return "%m/%d %H:%M";
    }
    if (secPerTick <= 2419200 || range <= oneYear) {
      return "%m/%d";
    }
    return "%Y-%m";
  }

  return "%H:%M";
}

function logp(value, base) {
  return Math.log(value) / Math.log(base);
}

function getPrecision(num) {
  let str = num.toString();
  let dot_index = str.indexOf(".");
  if (dot_index === -1) {
    return 0;
  } else {
    return str.length - dot_index - 1;
  }
}
