/**
 * Copyright (C) 2018-2022  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

// Creation of a stacked histogram with D3.js for software origin visits history
// Parameters description:
//  - container: selector for the div that will contain the histogram
//  - visitsData: raw swh origin visits data
//  - currentYear: the visits year to display by default
//  - yearClickCallback: callback when the user selects a year through the histogram

import {visitStatusColor} from './utils';

export async function createVisitsHistogram(container, visitsData, currentYear, yearClickCallback) {

  const d3 = await import(/* webpackChunkName: "d3" */ 'utils/d3');

  // remove previously created histogram and tooltip if any
  d3.select(container).select('svg').remove();
  d3.select('div.d3-tooltip').remove();

  // histogram size and margins
  let width = 1000;
  let height = 200;
  const margin = {top: 20, right: 80, bottom: 30, left: 50};

  // create responsive svg
  const svg = d3.select(container)
    .attr('style',
          'padding-bottom: ' + Math.ceil(height * 100 / width) + '%')
    .append('svg')
    .attr('viewBox', '0 0 ' + width + ' ' + height);

  // create tooltip div
  const tooltip = d3.select('body')
    .append('div')
    .attr('class', 'd3-tooltip')
    .style('opacity', 0);

  // update width and height without margins
  width = width - margin.left - margin.right;
  height = height - margin.top - margin.bottom;

  // create main svg group element
  const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

  // create x scale
  const x = d3.scaleTime().rangeRound([0, width]);

  // create y scale
  const y = d3.scaleLinear().range([height, 0]);

  // create ordinal colorscale mapping visit status
  const colors = d3.scaleOrdinal()
    .domain(Object.keys(visitStatusColor))
    .range(Object.values(visitStatusColor));

  // first swh crawls were made in 2015
  const startYear = 2015;
  // set latest display year as the current one
  const now = new Date();
  const endYear = now.getUTCFullYear() + 1;
  const monthExtent = [new Date(Date.UTC(startYear, 0, 1)), new Date(Date.UTC(endYear, 0, 1))];

  // create months bins based on setup extent
  const monthBins = d3.timeMonths(d3.timeMonth.offset(monthExtent[0], -1), monthExtent[1]);
  // create years bins based on setup extent
  const yearBins = d3.timeYears(monthExtent[0], monthExtent[1]);

  // set x scale domain
  x.domain(d3.extent(monthBins));

  // use D3 histogram layout to create a function that will bin the visits by month
  const binByMonth = d3.histogram()
    .value(d => d.date)
    .domain(x.domain())
    .thresholds(monthBins);

  // use D3 nest function to group the visits by status
  const visitsByStatus = d3.groups(visitsData, d => d['status'])
    .sort((a, b) => d3.ascending(a[0], b[0]));

  // prepare data in order to be able to stack visit statuses by month
  const statuses = [];
  const histData = [];
  for (let i = 0; i < monthBins.length; ++i) {
    histData[i] = {};
  }
  visitsByStatus.forEach(entry => {
    statuses.push(entry[0]);
    const monthsData = binByMonth(entry[1]);
    for (let i = 0; i < monthsData.length; ++i) {
      histData[i]['x0'] = monthsData[i]['x0'];
      histData[i]['x1'] = monthsData[i]['x1'];
      histData[i][entry[0]] = monthsData[i];
    }
  });

  // create function to stack visits statuses by month
  const stacked = d3.stack()
    .keys(statuses)
    .value((d, key) => d[key].length);

  // compute the maximum amount of visits by month
  const yMax = d3.max(histData, d => {
    let total = 0;
    for (let i = 0; i < statuses.length; ++i) {
      total += d[statuses[i]].length;
    }
    return total;
  });

  // set y scale domain
  y.domain([0, yMax]);

  // compute ticks values for the y axis (at most 8 ticks)
  const step = Math.floor(yMax / 8) + 1;
  const yTickValues = [];
  for (let i = 0; i <= yMax / step; ++i) {
    yTickValues.push(i * step);
  }
  if (yTickValues.length === 0) {
    for (let i = 0; i <= yMax; ++i) {
      yTickValues.push(i);
    }
  } else if (yMax % step !== 0) {
    yTickValues.push(yMax);
  }

  // add histogram background grid
  g.append('g')
    .attr('class', 'grid')
    .call(d3.axisLeft(y)
      .tickValues(yTickValues)
      .tickSize(-width)
      .tickFormat(''));

  // create one fill only rectangle by displayed year
  // each rectangle will be made visible when hovering the mouse over a year range
  // user will then be able to select a year by clicking in the rectangle

  g.append('g')
    .selectAll('rect')
    .data(yearBins)
    .enter().append('rect')
    .attr('class', d => 'year' + d.getUTCFullYear())
    .attr('fill', 'red')
    .attr('fill-opacity', d => d.getUTCFullYear() === currentYear ? 0.3 : 0)
    .attr('stroke', 'none')
    .attr('x', d => {
      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
      return x(date);
    })
    .attr('y', 0)
    .attr('height', height)
    .attr('width', d => {
      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
      return yearWidth;
    })
    // mouse event callbacks used to show rectangle years
    // when hovering the mouse over the histograms
    .on('mouseover', (event, d) => {
      svg.selectAll('rect.year' + d.getUTCFullYear())
        .attr('fill-opacity', 0.5);
    })
    .on('mouseout', (event, d) => {
      svg.selectAll('rect.year' + d.getUTCFullYear())
        .attr('fill-opacity', 0);
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0.3);
    })
    // callback to select a year after a mouse click
    // in a rectangle year
    .on('click', (event, d) => {
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0);
      svg.selectAll('rect.yearoutline' + currentYear)
        .attr('stroke', 'none');
      currentYear = d.getUTCFullYear();
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0.5);
      svg.selectAll('rect.yearoutline' + currentYear)
        .attr('stroke', 'black');
      yearClickCallback(currentYear);
    });

  // create the stacked histogram of visits
  g.append('g')
    .selectAll('g')
    .data(stacked(histData))
    .enter().append('g')
    .attr('fill', d => colors(d.key))
    .selectAll('rect')
    .data(d => d)
    .enter().append('rect')
    .attr('class', d => 'month' + d.data.x1.getMonth())
    .attr('x', d => x(d.data.x0))
    .attr('y', d => y(d[1]))
    .attr('height', d => y(d[0]) - y(d[1]))
    .attr('width', d => x(d.data.x1) - x(d.data.x0) - 1)
    // mouse event callbacks used to show rectangle years
    // but also to show tooltip when hovering the mouse
    // over the histogram bars
    .on('mouseover', (event, d) => {
      svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
        .attr('fill-opacity', 0.5);
      tooltip.transition()
        .duration(200)
        .style('opacity', 1);
      const ds = d.data.x1.toISOString().substr(0, 7).split('-');
      let tooltipText = '<b>' + ds[1] + ' / ' + ds[0] + ':</b><br/>';
      for (let i = 0; i < statuses.length; ++i) {
        const visitStatus = statuses[i];
        const nbVisits = d.data[visitStatus].length;
        if (nbVisits === 0) continue;
        tooltipText += nbVisits + ' ' + visitStatus + ' visits';
        if (i !== statuses.length - 1) tooltipText += '<br/>';
      }
      tooltip.html(tooltipText)
        .style('left', event.pageX + 15 + 'px')
        .style('top', event.pageY + 'px');
    })
    .on('mouseout', (event, d) => {
      svg.selectAll('rect.year' + d.data.x1.getUTCFullYear())
        .attr('fill-opacity', 0);
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0.3);
      tooltip.transition()
        .duration(500)
        .style('opacity', 0);
    })
    .on('mousemove', (event) => {
      tooltip.style('left', event.pageX + 15 + 'px')
        .style('top', event.pageY + 'px');
    })
    // callback to select a year after a mouse click
    // inside a histogram bar
    .on('click', (event, d) => {
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0);
      svg.selectAll('rect.yearoutline' + currentYear)
        .attr('stroke', 'none');
      currentYear = d.data.x1.getUTCFullYear();
      svg.selectAll('rect.year' + currentYear)
        .attr('fill-opacity', 0.5);
      svg.selectAll('rect.yearoutline' + currentYear)
        .attr('stroke', 'black');
      yearClickCallback(currentYear);
    });

  // create one stroke only rectangle by displayed year
  // that will be displayed on top of the histogram when the user has selected a year
  g.append('g')
    .selectAll('rect')
    .data(yearBins)
    .enter().append('rect')
    .attr('class', d => 'yearoutline' + d.getUTCFullYear())
    .attr('fill', 'none')
    .attr('stroke', d => d.getUTCFullYear() === currentYear ? 'black' : 'none')
    .attr('x', d => {
      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
      return x(date);
    })
    .attr('y', 0)
    .attr('height', height)
    .attr('width', d => {
      const date = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
      return yearWidth;
    });

  // add x axis with a tick for every 1st day of each year
  const xAxis = g.append('g')
    .attr('class', 'axis')
    .attr('transform', 'translate(0,' + height + ')')
    .call(
      d3.axisBottom(x)
        .ticks(d3.timeYear.every(1))
        .tickFormat(d => {
          const year = d.getUTCFullYear();
          if (year >= startYear) {
            return year;
          } else {
            // filter out 2014 tick label
            return '';
          }
        })
    );

  // shift tick labels in order to display them at the middle
  // of each year range
  xAxis.selectAll('text')
    .attr('transform', d => {
      const year = d.getUTCFullYear();
      const date = new Date(Date.UTC(year, 0, 1));
      const yearWidth = x(d3.timeYear.offset(date, 1)) - x(date);
      return 'translate(' + -yearWidth / 2 + ', 0)';
    });

  // add y axis for the number of visits
  g.append('g')
    .attr('class', 'axis')
    .call(d3.axisLeft(y).tickValues(yTickValues));

  // add legend for visit statuses
  const legendGroup = g.append('g')
    .attr('font-family', 'sans-serif')
    .attr('font-size', 10)
    .attr('text-anchor', 'end');

  legendGroup.append('text')
    .attr('x', width + margin.right - 5)
    .attr('y', 9.5)
    .attr('dy', '0.32em')
    .text('visit status:');

  const legend = legendGroup.selectAll('g')
    .data(statuses.slice().reverse())
    .enter().append('g')
    .attr('transform', (d, i) => 'translate(0,' + (i + 1) * 20 + ')');

  legend.append('rect')
    .attr('x', width + 2 * margin.right / 3)
    .attr('width', 19)
    .attr('height', 19)
    .attr('fill', colors);

  legend.append('text')
    .attr('x', width + 2 * margin.right / 3 - 5)
    .attr('y', 9.5)
    .attr('dy', '0.32em')
    .text(d => d);

  // add text label for the y axis
  g.append('text')
    .attr('transform', 'rotate(-90)')
    .attr('y', -margin.left)
    .attr('x', -(height / 2))
    .attr('dy', '1em')
    .style('text-anchor', 'middle')
    .text('Number of visits');
}
