import _ from 'lodash';
import { metricToFormatter } from './util/Utils.js';
import React from 'react';
import * as d3 from 'd3';
import './../../css/scatterplot.css';

const defaultSvgWidth = 574;
const defaultSvgHeight = 375;
const margin = { top: 0, right: 0, bottom: 10, left: 0 };
const baseWidth = 8;
const circleRadius = 2 * baseWidth;
const graphPadding = 3 * circleRadius;
const highlightBarWidth = 3 * circleRadius;
const successRateColorScale = d3.scaleQuantize()
  .domain([0, 1])
  .range(["#8B0000", "#FF6347", "#FF4500", "#FFA500","#008000"]);

export default class ScatterPlot extends React.Component {
  constructor(props) {
    super(props);
    this.renderNodeTooltipDatum = this.renderNodeTooltipDatum.bind(this);
    this.state = this.getChartDimensions();
  }

  componentWillMount() {
    this.initializeScales();
  }

  componentDidMount() {
    this.svg = d3.select("." + this.props.containerClassName)
      .append("svg")
      .attr("class", "scatterplot")
      .attr("width", defaultSvgWidth)
      .attr("height", defaultSvgHeight)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    this.xAxis = this.svg.append("g")
      .attr("class", "x-axis")
      .attr("transform", "translate(0," + (this.state.height - graphPadding) + ")");

    this.yAxis = this.svg.append("g")
      .attr("class", "y-axis")
      .attr("transform", "translate(" + this.state.width + ",0)");

    this.sidebar = d3.select(".scatterplot-display")
      .append("div").attr("class", "sidebar-tooltip");

    this.initializeVerticalHighlight();
    this.renderAxisLabels();
    this.updateGraph();
  }

  shouldComponentUpdate(nextProps) {
    if (nextProps.lastUpdated === this.props.lastUpdated) {
      // control whether react re-renders the component
      // only rerender if the input data has changed
      return false;
    }
    return true;
  }

  componentDidUpdate() {
    this.updateGraph();
  }

  initializeVerticalHighlight() {
    this.updateScales(this.props.data);

    // highlight bar to show x position
    this.verticalHighlight = this.svg.append("rect")
      .attr("class", "vertical-highlight")
      .attr("width", highlightBarWidth)
      .attr("height", this.state.height);

    // overlay on which to attach mouse events
    // attach this after all other items are attached otherwise they block mouse events
    this.overlay = this.svg.append("rect")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .attr("class", "overlay")
      .attr("width", this.state.width)
      .attr("height", this.state.height);
    this.overlayNode = d3.select(".overlay").node();

    this.overlayTooltip = this.svg.append("g")
      .attr("class", "overlay-tooltip")
      .attr("height", this.state.height);

    this.highlightFirstDatapoint();
  }

  highlightFirstDatapoint() {
    // when graph is initially loaded / reloaded, set highlight and sidebar to first datapoint
    let firstDatapoint = _.first(this.props.data);
    if (firstDatapoint) {
      let firstLatency = _.get(firstDatapoint, ["latency", "P99"]);
      let firstLatencyX = this.xScale(firstLatency);
      let nearestDatapoints = this.getNearbyDatapoints(firstLatencyX, this.props.data);

      this.verticalHighlight
        .attr("transform", "translate(" + (firstLatencyX - (highlightBarWidth/2)) + ", 0)");
      this.renderSidebarTooltip(nearestDatapoints);
      this.overlayTooltip.text('');
    }
  }

  getChartDimensions() {
    let svgWidth = this.props.width || defaultSvgWidth;
    let svgHeight = this.props.height || defaultSvgHeight;

    let width = svgWidth - margin.left - margin.right;
    let height = svgHeight - margin.top - margin.bottom;

    return {
      svgWidth: svgWidth,
      svgHeight: svgHeight,
      width: width,
      height: height,
      margin: margin
    };
  }

  initializeScales() {
    this.xScale = d3.scaleLinear().range([graphPadding, this.state.width - graphPadding]);
    this.yScale = d3.scaleLinear().range([this.state.height - graphPadding, graphPadding]);
  }

  updateScales(data) {
    this.xScale.domain(d3.extent(data, d => d.latency.P99));
    this.yScale.domain([0, 1]);

    this.updateAxes();
  }

  updateAxes() {
    let xAxis = d3.axisBottom(this.xScale)
      .ticks(5)
      .tickSize(5);
    this.xAxis.call(xAxis);

    let yAxis = d3.axisLeft(this.yScale)
      .ticks(4)
      .tickSize(this.state.width)
      .tickFormat(metricToFormatter["SUCCESS_RATE"]);

    // custom axis styling: https://bl.ocks.org/mbostock/3371592
    let customYAxis = g => {
      g.call(yAxis);
      g.select(".domain").remove();
      g.selectAll(".tick text")
        .attr("x", 4)
        .attr("dx", -10)
        .attr("dy", -4);
    };
    this.yAxis.call(customYAxis);
  }

  renderAxisLabels() {
    // text label for the x axis
    this.svg.append("text")
      .attr("class", "axis-label x-axis-label")
      .attr("y", this.state.height - 12)
      .attr("x", 0)
      .text("p99 Latency (ms)");

    // text label for the y axis
    this.svg.append("text")
      .attr("class", "axis-label y-axis-label")
      .attr("y", 20)
      .attr("x", this.state.width)
      .attr("dx", "-3em")
      .text("Success rate");
  }

  getNearbyDatapoints(x, data) {
    // return nodes that have nearby x-coordinates
    let x0 = this.xScale.invert(x - highlightBarWidth);
    let x1 = this.xScale.invert(x + highlightBarWidth);

    if (x0 === x1) {
      // handle case where all the x points are in one column
      let datapointsX = this.xScale(_.first(data).latency.P99);
      if (Math.abs(x - datapointsX < highlightBarWidth)) {
        return data;
      } else {
        return [];
      }
    } else {
      return _(data).filter(d => {
        return d.latency.P99 <= x1 && d.latency.P99 >= x0;
      }).orderBy('successRate', 'desc').value();
    }
  }

  updateGraph() {
    this.updateScales(this.props.data);

    this.scatterPlot = this.svg.selectAll(".dot")
      .data(this.props.data);

    this.scatterPlot.exit().remove();

    let spNode = this.scatterPlot.node();

    this.scatterPlot
      .enter()
      .append("circle")
      .attr("class", "dot")
      .attr("r", circleRadius)
      .merge(this.scatterPlot) // newfangled d3 'update' selection
      .attr("cx", d => this.xScale(d.latency.P99))
      .attr("cy", d => this.yScale(d.successRate))
      .style("fill", d => successRateColorScale(d.successRate))
      .style("stroke", d => successRateColorScale(d.successRate))
      .on("mousemove", () => {
        if (spNode) {
          let currXPos = d3.mouse(spNode)[0];
          this.positionOverlayHighlightAndTooltip(currXPos);
        }
      });

    this.highlightFirstDatapoint();
    this.overlay
      .on("mousemove", () => {
        let currXPos = d3.mouse(this.overlayNode)[0];
        this.positionOverlayHighlightAndTooltip(currXPos);
      });
  }

  positionOverlayHighlightAndTooltip(currXPos) {
    let nearestDatapoints = this.getNearbyDatapoints(currXPos, this.props.data);
    this.renderOverlayTooltip(nearestDatapoints);
    this.renderSidebarTooltip(nearestDatapoints);
    this.verticalHighlight.attr("transform", "translate(" + (currXPos - highlightBarWidth / 2) + ", 0)");

    let bbox = this.overlayTooltip.node().getBBox();

    let overlayTooltipYPos = 0;
    let firstLabelPosition = _.isEmpty(nearestDatapoints) ? null : this.getTooltipLabelY(nearestDatapoints[0]);
    if (firstLabelPosition + bbox.height > this.state.height - 50) {
      // if there are a bunch of nodes at 0, the labels could extend below the chart
      // translate upward if this is the case
      overlayTooltipYPos -= bbox.height;

      // re-render tooltip labels, squished together
      this.renderOverlayTooltip(nearestDatapoints, true);
    }

    let overlayTooltipXPos = currXPos + highlightBarWidth / 2 + baseWidth;
    if (currXPos > defaultSvgWidth / 2) {
      // display tooltip to the left if we're on the RH side of the graph
      overlayTooltipXPos = currXPos - bbox.width - baseWidth - highlightBarWidth / 2;
    }

    this.overlayTooltip
      .attr("transform", "translate(" + overlayTooltipXPos + ", " + overlayTooltipYPos + ")")
      .raise();
  }

  renderSidebarTooltip(data) {
    let innerHtml = _.map(data, d => this.renderNodeTooltipDatum(d));
    this.sidebar.html(innerHtml.join(''));
  }

  renderOverlayTooltip(data, ignoreNodeSpacing = false) {
    this.overlayTooltip.text('');
    let labelWithPosition = this.computeTooltipLabelPositions(data, ignoreNodeSpacing);

    _.each(labelWithPosition, d => {
      this.overlayTooltip
        .append("text").text(d.name)
        .attr("x", 0)
        .attr("y", d.computedY);
    });
  }

  getTooltipLabelY(datum) {
    // position the tooltip label roughly aligned with the center of the node
    return this.yScale(datum.successRate) + circleRadius / 2 - 5;
  }

  computeTooltipLabelPositions(data, ignoreNodeSpacing = false) {
    // in the case that there are multiple nodes in the highlighted area,
    // try to position each label next to its corresponding node
    // if the nodes are too close together, simply list the node labels
    let positions = _.map(data, d => {
      return {
        name: d.name,
        computedY: this.getTooltipLabelY(d)
      };
    });

    if (_.size(positions) > 1) {
      _.each(positions, (_d, i) => {
        if (i > 0) {
          if (positions[i].computedY - positions[i - 1].computedY < 10) {
            // labels are too close together, don't label at the node Y values
            ignoreNodeSpacing = true;
          }
        }
      });
      if (ignoreNodeSpacing) {
        let basePos = positions[0].computedY;
        _.each(positions, (d, i) => {
          d.computedY = basePos + i * 15;
        });
      }
    }
    return positions;
  }

  renderNodeTooltipDatum(d) {
    let latency = metricToFormatter["LATENCY"](d.latency.P99);
    let sr = metricToFormatter["SUCCESS_RATE"](d.successRate);
    return `<div class="title">${d.name}</div><div>${latency}, ${sr}</div>`;
  }

  render() {
    // d3 selects the passed in container from this.props.containerClassName
    return _.isEmpty(this.props.data) ?
      <div className="clearfix no-data-msg">No data</div> : null;
  }
}
