/* eslint-disable no-underscore-dangle, no-param-reassign */
import d3 from 'd3';
import { category21 } from '../javascripts/modules/colors';
import { wrapSvgText } from '../javascripts/modules/utils';

require('./sunburst.css');

// Modified from http://bl.ocks.org/kerryrodden/7090426
function sunburstVis(slice, payload) {
  const container = d3.select(slice.selector);

  // vars with shared scope within this function
  const margin = { top: 10, right: 5, bottom: 10, left: 5 };
  const containerWidth = slice.width();
  const containerHeight = slice.height();
  const breadcrumbHeight = containerHeight * 0.085;
  const visWidth = containerWidth - margin.left - margin.right;
  const visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
  const radius = Math.min(visWidth, visHeight) / 2;

  let colorByCategory = true; // color by category if primary/secondary metrics match
  let maxBreadcrumbs;
  let breadcrumbDims; // set based on data
  let totalSize; // total size of all segments; set after loading the data.
  let colorScale;
  let breadcrumbs;
  let vis;
  let arcs;
  let gMiddleText; // dom handles

  // Helper + path gen functions
  const partition = d3.layout.partition()
    .size([2 * Math.PI, radius * radius])
    .value(function (d) { return d.m1; });

  const arc = d3.svg.arc()
    .startAngle(d => d.x)
    .endAngle(d => d.x + d.dx)
    .innerRadius(function (d) {
      return Math.sqrt(d.y);
    })
    .outerRadius(function (d) {
      return Math.sqrt(d.y + d.dy);
    });

  const formatNum = d3.format('.3s');
  const formatPerc = d3.format('.3p');

  container.select('svg').remove();

  const svg = container.append('svg:svg')
    .attr('width', containerWidth)
    .attr('height', containerHeight);

  function createBreadcrumbs(rawData) {
    const firstRowData = rawData.data[0];
    // -2 bc row contains 2x metrics, +extra for %label and buffer
    maxBreadcrumbs = (firstRowData.length - 2) + 1;
    breadcrumbDims = {
      width: visWidth / maxBreadcrumbs,
      height: breadcrumbHeight * 0.8, // more margin
      spacing: 3,
      tipTailWidth: 10,
    };

    breadcrumbs = svg.append('svg:g')
      .attr('class', 'breadcrumbs')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

    breadcrumbs.append('svg:text')
      .attr('class', 'end-label');
  }

  // Given a node in a partition layout, return an array of all of its ancestor
  // nodes, highest first, but excluding the root.
  function getAncestors(node) {
    const path = [];
    let current = node;
    while (current.parent) {
      path.unshift(current);
      current = current.parent;
    }
    return path;
  }

  // Generate a string that describes the points of a breadcrumb polygon.
  function breadcrumbPoints(d, i) {
    const points = [];
    points.push('0,0');
    points.push(breadcrumbDims.width + ',0');
    points.push(
      breadcrumbDims.width + breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
    points.push(breadcrumbDims.width + ',' + breadcrumbDims.height);
    points.push('0,' + breadcrumbDims.height);
    if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
      points.push(breadcrumbDims.tipTailWidth + ',' + (breadcrumbDims.height / 2));
    }
    return points.join(' ');
  }

  function updateBreadcrumbs(sequenceArray, percentageString) {
    const g = breadcrumbs.selectAll('g')
      .data(sequenceArray, function (d) {
        return d.name + d.depth;
      });

    // Add breadcrumb and label for entering nodes.
    const entering = g.enter().append('svg:g');

    entering.append('svg:polygon')
        .attr('points', breadcrumbPoints)
        .style('fill', function (d) {
          return colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1);
        });

    entering.append('svg:text')
        .attr('x', (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
        .attr('y', breadcrumbDims.height / 4)
        .attr('dy', '0.35em')
        .style('fill', function (d) {
          // Make text white or black based on the lightness of the background
          const col = d3.hsl(colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1));
          return col.l < 0.5 ? 'white' : 'black';
        })
        .attr('class', 'step-label')
        .text(function (d) { return d.name.replace(/_/g, ' '); })
        .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);

    // Set position for entering and updating nodes.
    g.attr('transform', function (d, i) {
      return 'translate(' + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ', 0)';
    });

    // Remove exiting nodes.
    g.exit().remove();

    // Now move and update the percentage at the end.
    breadcrumbs.select('.end-label')
        .attr('x', (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
        .attr('y', breadcrumbDims.height / 2)
        .attr('dy', '0.35em')
        .text(percentageString);

    // Make the breadcrumb trail visible, if it's hidden.
    breadcrumbs.style('visibility', null);
  }

  // Fade all but the current sequence, and show it in the breadcrumb trail.
  function mouseenter(d) {
    const sequenceArray = getAncestors(d);
    const parentOfD = sequenceArray[sequenceArray.length - 2] || null;

    const absolutePercentage = (d.m1 / totalSize).toPrecision(3);
    const conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;

    const absolutePercString = formatPerc(absolutePercentage);
    const conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : '';

    // 3 levels of text if inner-most level, 4 otherwise
    const yOffsets = ['-25', '7', '35', '60'];
    let offsetIndex = 0;

    // If metrics match, assume we are coloring by category
    const metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;

    gMiddleText.selectAll('*').remove();

    gMiddleText.append('text')
      .attr('class', 'path-abs-percent')
      .attr('y', yOffsets[offsetIndex++])
      .text(absolutePercString + ' of total');

    if (conditionalPercString) {
      gMiddleText.append('text')
        .attr('class', 'path-cond-percent')
        .attr('y', yOffsets[offsetIndex++])
        .text(conditionalPercString + ' of parent');
    }

    gMiddleText.append('text')
      .attr('class', 'path-metrics')
      .attr('y', yOffsets[offsetIndex++])
      .text('m1: ' + formatNum(d.m1) + (metricsMatch ? '' : ', m2: ' + formatNum(d.m2)));

    gMiddleText.append('text')
      .attr('class', 'path-ratio')
      .attr('y', yOffsets[offsetIndex++])
      .text((metricsMatch ? '' : ('m2/m1: ' + formatPerc(d.m2 / d.m1))));

    // Reset and fade all the segments.
    arcs.selectAll('path')
      .style('stroke-width', null)
      .style('stroke', null)
      .style('opacity', 0.7);

    // Then highlight only those that are an ancestor of the current segment.
    arcs.selectAll('path')
      .filter(function (node) {
        return (sequenceArray.indexOf(node) >= 0);
      })
      .style('opacity', 1)
      .style('stroke-width', '2px')
      .style('stroke', '#000');

    updateBreadcrumbs(sequenceArray, absolutePercString);
  }

  // Restore everything to full opacity when moving off the visualization.
  function mouseleave() {
    // Hide the breadcrumb trail
    breadcrumbs.style('visibility', 'hidden');

    gMiddleText.selectAll('*').remove();

    // Deactivate all segments during transition.
    arcs.selectAll('path').on('mouseenter', null);

    // Transition each segment to full opacity and then reactivate it.
    arcs.selectAll('path')
      .transition()
      .duration(200)
      .style('opacity', 1)
      .style('stroke', null)
      .style('stroke-width', null)
      .each('end', function () {
        d3.select(this).on('mouseenter', mouseenter);
      });
  }


  function buildHierarchy(rows) {
    const root = {
      name: 'root',
      children: [],
    };

    // each record [groupby1val, groupby2val, (<string> or 0)n, m1, m2]
    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      const m1 = Number(row[row.length - 2]);
      const m2 = Number(row[row.length - 1]);
      const levels = row.slice(0, row.length - 2);
      if (isNaN(m1)) { // e.g. if this is a header row
        continue;
      }
      let currentNode = root;
      for (let level = 0; level < levels.length; level++) {
        const children = currentNode.children || [];
        const nodeName = levels[level];
        // If the next node has the name '0', it will
        const isLeafNode = (level >= levels.length - 1) || levels[level + 1] === 0;
        let childNode;
        let currChild;

        if (!isLeafNode) {
          // Not yet at the end of the sequence; move down the tree.
          let foundChild = false;
          for (let k = 0; k < children.length; k++) {
            currChild = children[k];
            if (currChild.name === nodeName &&
                currChild.level === level) {
// must match name AND level

              childNode = currChild;
              foundChild = true;
              break;
            }
          }
          // If we don't already have a child node for this branch, create it.
          if (!foundChild) {
            childNode = {
              name: nodeName,
              children: [],
              level,
            };
            children.push(childNode);
          }
          currentNode = childNode;
        } else if (nodeName !== 0) {
          // Reached the end of the sequence; create a leaf node.
          childNode = {
            name: nodeName,
            m1,
            m2,
          };
          children.push(childNode);
        }
      }
    }

    function recurse(node) {
      if (node.children) {
        let sums;
        let m1 = 0;
        let m2 = 0;
        for (let i = 0; i < node.children.length; i++) {
          sums = recurse(node.children[i]);
          m1 += sums[0];
          m2 += sums[1];
        }
        node.m1 = m1;
        node.m2 = m2;
      }
      return [node.m1, node.m2];
    }

    recurse(root);
    return root;
  }

  // Main function to draw and set up the visualization, once we have the data.
  function createVisualization(rawData) {
    const tree = buildHierarchy(rawData.data);

    vis = svg.append('svg:g')
      .attr('class', 'sunburst-vis')
      .attr('transform', (
        'translate(' +
          `${(margin.left + (visWidth / 2))},` +
          `${(margin.top + breadcrumbHeight + (visHeight / 2))}` +
        ')'
      ))
      .on('mouseleave', mouseleave);

    arcs = vis.append('svg:g')
      .attr('id', 'arcs');

    gMiddleText = vis.append('svg:g')
      .attr('class', 'center-label');

    // Bounding circle underneath the sunburst, to make it easier to detect
    // when the mouse leaves the parent g.
    arcs.append('svg:circle')
      .attr('r', radius)
      .style('opacity', 0);

    // For efficiency, filter nodes to keep only those large enough to see.
    const nodes = partition.nodes(tree)
      .filter(function (d) {
        return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
      });

    let ext;
    const fd = slice.formData;

    if (fd.metric !== fd.secondary_metric) {
      colorByCategory = false;
      ext = d3.extent(nodes, d => d.m2 / d.m1);
      colorScale = d3.scale.linear()
        .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
        .range(['#00D1C1', 'white', '#FFB400']);
    }

    const path = arcs.data([tree]).selectAll('path')
      .data(nodes)
      .enter()
      .append('svg:path')
      .attr('display', function (d) {
        return d.depth ? null : 'none';
      })
      .attr('d', arc)
      .attr('fill-rule', 'evenodd')
      .style('fill', d => colorByCategory ? category21(d.name) : colorScale(d.m2 / d.m1))
      .style('opacity', 1)
      .on('mouseenter', mouseenter);

    // Get total size of the tree = value of root node from partition.
    totalSize = path.node().__data__.value;
  }
  createBreadcrumbs(payload);
  createVisualization(payload);
}

module.exports = sunburstVis;
