import d3 from "d3";
import cloud from "d3-cloud";
import { each, filter, map, min, max, sortBy, toString } from "lodash";
import React, { useMemo, useState, useEffect } from "react";
import resizeObserver from "@/services/resizeObserver";
import { RendererPropTypes } from "@/visualizations/prop-types";

import "./renderer.less";

function computeWordFrequencies(rows, column) {
  const result = {};

  each(rows, row => {
    const wordsList = toString(row[column]).split(/\s/g);
    each(wordsList, d => {
      result[d] = (result[d] || 0) + 1;
    });
  });

  return result;
}

function getWordsWithFrequencies(rows, wordColumn, frequencyColumn) {
  const result = {};

  each(rows, row => {
    const count = parseFloat(row[frequencyColumn]);
    if (Number.isFinite(count) && count > 0) {
      const word = toString(row[wordColumn]);
      result[word] = count;
    }
  });

  return result;
}

function applyLimitsToWords(words, { wordLength, wordCount }) {
  wordLength.min = Number.isFinite(wordLength.min) ? wordLength.min : null;
  wordLength.max = Number.isFinite(wordLength.max) ? wordLength.max : null;

  wordCount.min = Number.isFinite(wordCount.min) ? wordCount.min : null;
  wordCount.max = Number.isFinite(wordCount.max) ? wordCount.max : null;

  return filter(words, ({ text, count }) => {
    const wordLengthFits =
      (!wordLength.min || text.length >= wordLength.min) && (!wordLength.max || text.length <= wordLength.max);
    const wordCountFits = (!wordCount.min || count >= wordCount.min) && (!wordCount.max || count <= wordCount.max);
    return wordLengthFits && wordCountFits;
  });
}

function prepareWords(rows, options) {
  let result = [];

  if (options.column) {
    if (options.frequenciesColumn) {
      result = getWordsWithFrequencies(rows, options.column, options.frequenciesColumn);
    } else {
      result = computeWordFrequencies(rows, options.column);
    }
    result = sortBy(
      map(result, (count, text) => ({ text, count })),
      [({ count }) => -count, ({ text }) => -text.length] // "count" desc, length("text") desc
    );
  }

  // Add additional attributes
  const counts = map(result, item => item.count);
  const wordSize = d3.scale
    .linear()
    .domain([min(counts), max(counts)])
    .range([10, 100]); // min/max word size
  const color = d3.scale.category20();

  each(result, (item, index) => {
    item.size = wordSize(item.count);
    item.color = color(index);
    item.angle = (index % 2) * 90; // make it stable between renderings
  });

  return applyLimitsToWords(result, {
    wordLength: options.wordLengthLimit,
    wordCount: options.wordCountLimit,
  });
}

function scaleElement(node, container) {
  node.style.transform = null;
  const { width: nodeWidth, height: nodeHeight } = node.getBoundingClientRect();
  const { width: containerWidth, height: containerHeight } = container.getBoundingClientRect();

  const scaleX = containerWidth / nodeWidth;
  const scaleY = containerHeight / nodeHeight;

  node.style.transform = `scale(${Math.min(scaleX, scaleY)})`;
}

function createLayout() {
  const fontFamily = window.getComputedStyle(document.body).fontFamily;

  return (
    cloud()
      // make the area large enough to contain even very long words; word cloud will be placed in the center of the area
      // TODO: dimensions probably should be larger, but `d3-cloud` has some performance issues related to these values
      .size([5000, 5000])
      .padding(3)
      .font(fontFamily)
      .rotate(d => d.angle)
      .fontSize(d => d.size)
      .random(() => 0.5)
  ); // do not place words randomly - use compact layout
}

function render(container, words) {
  container = d3.select(container);
  container.selectAll("*").remove();

  const svg = container.append("svg");
  const g = svg.append("g");
  g.selectAll("text")
    .data(words)
    .enter()
    .append("text")
    .style("font-size", d => `${d.size}px`)
    .style("font-family", d => d.font)
    .style("fill", d => d.color)
    .attr("text-anchor", "middle")
    .attr("transform", d => `translate(${[d.x, d.y]}) rotate(${d.rotate})`)
    .text(d => d.text);

  const svgBounds = svg.node().getBoundingClientRect();
  const gBounds = g.node().getBoundingClientRect();

  svg.attr("width", Math.ceil(gBounds.width)).attr("height", Math.ceil(gBounds.height));
  g.attr("transform", `translate(${svgBounds.left - gBounds.left},${svgBounds.top - gBounds.top})`);

  scaleElement(svg.node(), container.node());
}

export default function Renderer({ data, options }) {
  const [container, setContainer] = useState(null);
  const [words, setWords] = useState([]);
  const layout = useMemo(createLayout, []);

  useEffect(() => {
    layout
      .words(prepareWords(data.rows, options))
      .on("end", w => setWords(w))
      .start();
    return () => layout.on("end", null).stop();
  }, [layout, data, options, setWords]);

  useEffect(() => {
    if (container) {
      render(container, words);
    }
  }, [container, words]);

  useEffect(() => {
    if (container) {
      const unwatch = resizeObserver(container, () => {
        const svg = container.querySelector("svg");
        if (svg) {
          scaleElement(svg, container);
        }
      });
      return unwatch;
    }
  }, [container]);

  return <div className="word-cloud-visualization-container" ref={setContainer} />;
}

Renderer.propTypes = RendererPropTypes;
