import {
  axisBottom,
  axisLeft,
} from 'd3-axis';
import {
  scaleBand,
  scaleLinear,
  scaleOrdinal,
} from 'd3-scale';
import { Selection } from 'd3-selection';
import merge from 'lodash/merge';

import colorScheme from './colors';
import attrs from './d3/attrs';
import {
  drawGrid,
  gridHeight,
  gridWidth,
  xAxisHeight,
  yAxisWidth,
} from './grid';
import {
  EGroupedBarLayout,
  IAxis,
  IChartAdaptor,
  IHistogramProps,
} from './Histogram';
import tips, { makeTip } from './tip';
import {
  barMargin,
  getBarWidth,
  groupedBarsUseSameXAxisValue,
  groupedMargin,
} from './utils/bars';
import {
  annotationAxisDefaults,
  axis as defaultAxis,
  grid,
} from './utils/defaults';
import {
  appendDomainRange,
  isStacked,
  maxValueCount,
  ticks,
} from './utils/domain';
import {
  onClick,
  onMouseOut,
  onMouseOver,
} from './utils/mouseOver';
import {
  makeGrid,
  makeScales,
  makeSvg,
  sizeSVG,
  TSelection,
} from './utils/svg';
import { DeepPartial } from './utils/types';

export interface IGroupDataItem {
  label: string;
  groupLabel?: string;
  colorRef?: string; // String which can be used to return same colour value
  value: number;
  side?: 'left' | 'right'; // For Tornados
}

export type IGroupData = IGroupDataItem[][];

export const histogramD3 = ((): IChartAdaptor<IHistogramProps> => {
  let svg: Selection<any, any, any, any>;
  let tipContainer;
  let tipContent;
  const y = scaleLinear();
  const x = scaleBand();
  const xAnnotations = scaleBand();
  const innerScaleBand = scaleBand();
  let container: Selection<SVGElement, any, any, any>;
  let dataSets: IGroupData;
  let gridX: TSelection;
  let gridY: TSelection;
  let yAxisContainer: TSelection;
  let xAxisContainer: TSelection;
  let xAnnotationAxisContainer: TSelection;
  let yAnnotationAxisContainer: TSelection;
  let xAxisLabel: any;
  let yAxisLabel: any;
  let percentBarLabel: any;

  const props: IHistogramProps = {
    axis: defaultAxis,
    bar: {
      groupMargin: 0.1,
      margin: 0,
      overlayMargin: 5,
      width: 50,
    },
    className: 'histogram-d3',
    colorScheme,
    annotations: [],
    data: {
      bins: [],
      counts: [],
    },
    delay: 0,
    domain: {
      max: null,
      min: null,
    },
    duration: 400,
    grid,
    groupLayout: EGroupedBarLayout.GROUPED,
    height: 200,
    margin: {
      bottom: 0,
      left: 5,
      right: 0,
      top: 5,
    },
    showBinPercentages: [],
    stacked: false, // Deprecated use groupLayout instead
    stroke: {
      color: '#005870',
      dasharray: '',
      linecap: 'butt',
      width: 0,
    },
    tip: tips,
    tipContainer: 'body',
    tipContentFn: (bins: string[], i: number, d: number): string =>
      bins[i] + '<br />' + d,
    visible: {},
    width: 200,
  };

  const HistogramD3 = {
    /**
     * Initialization
     */
    create(el: Element, newProps: DeepPartial<IHistogramProps> = {}) {
      this.mergeProps(newProps);
      svg = makeSvg(el, svg);
      const { margin, width, height, className } = props;
      sizeSVG(svg, { margin, width, height, className });
      const r = makeTip(props.tipContainer, tipContainer);
      tipContent = r.tipContent;
      tipContainer = r.tipContainer;
      [gridX, gridY] = makeGrid(svg);
      [xAxisContainer, yAxisContainer, xAxisLabel, yAxisLabel, xAnnotationAxisContainer, yAnnotationAxisContainer] = makeScales(svg);
      container = svg
        .append<SVGElement>('g')
        .attr('class', 'histogram-container');

      this.update(el, newProps);
    },

    mergeProps(newProps: DeepPartial<IHistogramProps>) {
      merge(props, newProps);
      if (newProps.data) {
        props.data = newProps.data as IHistogramProps['data'];
      }
      if (newProps.colorScheme) {
        props.colorScheme = newProps.colorScheme;
      }
    },

    /**
     * Draw scales
     */
    drawAxes() {
      const { axis, bar, annotations, domain, groupLayout, stacked, data, margin, height } = props;
      const valuesCount = maxValueCount(data.counts);
      const w = gridWidth(props);
      const h = gridHeight(props);
      const dataLabels = data.counts.map((c) => c.label);
      const annotationsEnabled = annotations && annotations.length === data.bins.length;

      x
        .domain(data.bins)
        .rangeRound([0, w])
        .paddingInner(groupedMargin(bar));

      innerScaleBand
        .domain(groupedBarsUseSameXAxisValue({ groupLayout, stacked }) ? ['main'] : dataLabels)
        .rangeRound([0, x.bandwidth()])
        .paddingInner(barMargin(props.bar));

      const xAxis = axisBottom<string>(x);
      const yAxis = axisLeft<number>(y);

      const axisXAnnotationAllowance: IAxis = {
        ...axis.x,
        tickSize: 5,
      }
      /** X-Axis (label axis) set up */
      ticks({
        axis: xAxis,
        valuesCount,
        axisLength: w,
        axisConfig: annotationsEnabled ? axisXAnnotationAllowance : axis.x,
        scaleBand: x,
        limitByValues: true,
      });

      xAxisContainer
        .attr('transform', 'translate(' + (yAxisWidth(axis) + axis.y.style['stroke-width']) + ',' +
          (height - xAxisHeight(props.axis) - (margin.left * 2)) + ')')
        .call(xAxis);

      /** X-Axis 2 (bottom axis) for annoations if annotations data sent (and match bin length) */
      if (annotations && annotations.length === data.bins.length) {

        xAnnotations
          .domain(data.bins)
          .rangeRound([0, w])
          .paddingInner(groupedMargin(bar));

        const annotationAxis = axisBottom<string>(xAnnotations);

        ticks({
          axis: annotationAxis,
          valuesCount: annotations.length,
          axisLength: w,
          axisConfig: annotationAxisDefaults,
          scaleBand: xAnnotations,
          limitByValues: true,
        });
        // Override the default axis bin labels with the custom annotations 
        annotationAxis.tickFormat((d, i) => annotations[i].value);

        xAnnotationAxisContainer
          .attr('transform', 'translate(' + (yAxisWidth(axis) + axis.y.style['stroke-width']) + ',' + (h + 14) + ')')
          .call(annotationAxis);

        // Annotation Axis styling
        attrs(svg.selectAll('.x-axis-bottom .domain, .x-axis-bottom .tick line'), axis.x.style);
        attrs(svg.selectAll('.x-axis-bottom .tick text'), axis.x.text.style as any);

        // Style the annotations with their specific color
        xAnnotationAxisContainer
          .selectAll('g.tick')
          .select('text')
          .style('fill', (d, i) => annotations[i].color)
          .style('font-size', '0.475rem');

        xAnnotationAxisContainer
          .selectAll('line')
          .style('opacity', 0);

        // Hide the line for the annotations axis
        xAnnotationAxisContainer.call(g => g.select(".domain").remove());

      }
      /** Y-Axis (value axis) set up */
      appendDomainRange({
        data: dataSets,
        domain,
        range: [h, 0],
        scale: y,
        stacked: isStacked({ groupLayout, stacked })
      });

      ticks({
        axis: yAxis,
        valuesCount,
        axisLength: w,
        axisConfig: axis.y,
        scaleBand: y,
      });

      yAxisContainer
        .attr('transform', 'translate(' + yAxisWidth(axis) + ', 0)')
        .transition()
        .call(yAxis);

      attrs(svg.selectAll('.y-axis .domain, .y-axis .tick line'), axis.y.style);
      attrs(svg.selectAll('.y-axis .tick text'), axis.y.text.style as any);

      attrs(svg.selectAll('.x-axis .domain, .x-axis .tick line'), axis.x.style);
      attrs(svg.selectAll('.x-axis .tick text'), axis.x.text.style as any);
    },

    /**
     * Draw a single data set into the chart
     */
    updateChart(
      bins: string[],
      groupData: IGroupData,
    ) {
      const { axis, annotations, data, height, width, margin, delay, duration, tip, groupLayout, showBinPercentages, stacked } = props;

      const stackedOffset = (d: IGroupDataItem, stackIndex: number): number => {
        const thisGroupData = groupData.find((gData) => {
          return gData.find((dx) => dx.label === d.label) !== undefined;
        });
        const oSet = thisGroupData ?
          thisGroupData
            .filter((_, i) => i < stackIndex)
            .reduce((prev, next) => prev + next.value, 0)
          : 0;
        const offset = isStacked({ groupLayout, stacked }) && stackIndex > 0
          ? oSet
          : 0;
        return y(d.value + offset);
      }

      const calculateXPosition = (d: IGroupDataItem, stackIndex: number, offset?: number): number => {
        const totalWidth = innerScaleBand.bandwidth();
        const barWidth = getBarWidth(stackIndex, props.groupLayout, props.bar, innerScaleBand);
        const overlaidXPos = (totalWidth / 2) - (barWidth / 2);
        const finalXPos = (props.groupLayout === EGroupedBarLayout.OVERLAID)
          ? overlaidXPos
          : Number(innerScaleBand(String(d.groupLabel)));
        return offset ? finalXPos + offset : finalXPos;
      }

      const colors = scaleOrdinal(props.colorScheme);
      const gHeight = gridHeight(props);

      const g = container
        .selectAll<SVGElement, {}>('g')
        .data(groupData);

      const bars = g.enter()
        .append<SVGElement>('g')
        .merge(g)
        .attr('transform', (d: any[]) => {
          let xd = x(d[0].label);
          if (xd === undefined) {
            xd = 0;
          }
          const xDelta = yAxisWidth(axis)
            + axis.y.style['stroke-width']
            + xd;
          return `translate(${xDelta}, 0)`;
        })

        .selectAll<SVGElement, {}>('rect')
        .data((d) => d);

      bars
        .enter()
        .append<SVGElement>('rect')
        .attr('height', 0)
        .attr('y', stackedOffset)
        .attr('class', 'bar')
        .on('click', onClick(props.onClick))
        .on('mouseover', onMouseOver({ bins, hover: props.bar.hover, colors, tipContentFn: props.tipContentFn, tipContent, tip, tipContainer }))
        .on('mousemove', () => tip.fx.move(tipContainer))
        .on('mouseout', onMouseOut({ tip, tipContainer, colors }))
        .merge(bars)
        .attr('x', (d: IGroupDataItem, i: number) => calculateXPosition(d, i))
        .attr('width', (d, i) => getBarWidth(i, props.groupLayout, props.bar, innerScaleBand))
        .attr('fill', (d, i) => colors(String(i)))
        .transition()
        .duration(duration)
        .delay(delay)
        .attr('y', stackedOffset)
        // Hide bar's bottom border
        .attr('stroke-dasharray',
          (d: IGroupDataItem, i): string => {
            const currentHeight = gHeight - (y(d.value));
            const barWidth = getBarWidth(i, props.groupLayout, props.bar, innerScaleBand);
            return `${barWidth} 0 ${currentHeight} ${barWidth}`;
          })
        .attr('height', (d: IGroupDataItem): number => gHeight - (y(d.value)));

      // We need to show the bar percentage splits if flag enabled
      if (showBinPercentages) {

        const percents = g.enter()
          .append<SVGElement>('g')
          .merge(g)
          .attr('transform', (d: any[]) => {
            let xd = x(d[0].label);
            if (xd === undefined) {
              xd = 0;
            }
            const xDelta = yAxisWidth(axis)
              + axis.y.style['stroke-width']
              + xd;
            return `translate(${xDelta}, 0)`;
          })

          .selectAll<SVGElement, {}>('text')
          .data((d) => d);

        percents
          .enter()
          .append<SVGElement>('text')
          .attr('class', 'percentage-label')
          .attr('y', stackedOffset)
          .data((d) => d)
          .merge(percents)
          .text((d, i) => {
            // To show the correct percentage split we need to total all the other values in this count set
            const total = groupData.reduce((prev, group) => prev + group[i].value, 0);
            const percentage = d.value === 0 ? 0 : Math.round((d.value / total) * 100);
            return showBinPercentages[i] ? `${percentage}%` : '';
          })
          .style('text-anchor', 'middle')
          .style('font-size', '0.675rem')
          .attr('fill', (d, i) => colors(String(i)))
          .attr('x', (d: IGroupDataItem, i: number) => {
            const barWidthForOffset = getBarWidth(i, props.groupLayout, props.bar, innerScaleBand);
            return calculateXPosition(d, i, barWidthForOffset / 2);
          })
          .attr('dy', -2);
        percents.exit().remove();
      };
      bars.exit().remove();
      g.exit().remove();

      const xText = xAxisLabel
        .selectAll('text')
        .data([axis.x.label]);

      xText.enter().append('text')
        .attr('class', 'x-axis-label')
        .merge(xText)
        .attr('transform',
          'translate(' + (Number(width) / 2) + ' ,' +
          ((height - xAxisHeight(props.axis) - (margin.left * 2)) + axis.x.margin) + ')')
        .style('text-anchor', 'middle')
        .text((d) => d);

      const yText = yAxisLabel
        .selectAll('text')
        .data([axis.y.label]);

      yText.enter().append('text')
        .attr('class', 'y-axis-label')
        .merge(yText)
        .attr('transform', 'rotate(-90)')
        .attr('y', 0)
        .attr('x', 0 - (gHeight / 2 - (margin.top * 2)))
        .attr('dy', '1em')
        .style('text-anchor', 'middle')
        .text((d) => d);
    },

    /**
     * Update chart
     */
    update(el: Element, newProps: DeepPartial<IHistogramProps>) {
      if (!newProps.data) {
        return;
      }
      this.mergeProps(newProps);
      if (!props.data.bins) {
        return;
      }
      const { margin, width, height, className, data, visible } = props;
      sizeSVG(svg, { margin, width, height, className });
      dataSets = [];

      data.counts.forEach((count) => {
        count.data.forEach((value, i) => {
          if (!dataSets[i]) {
            dataSets[i] = [];
          }
          dataSets[i].push({
            groupLabel: count.label,
            label: data.bins[i],
            value: visible[data.bins[i]] !== false && visible[count.label] !== false ? value : 0,
          });
        });
      });

      this.drawAxes();
      drawGrid(x, y, gridX, gridY, props, maxValueCount(data.counts));
      this.updateChart(data.bins, dataSets);
    },

    /**
     * Any necessary clean up
     */
    destroy(el: Element) {
      svg.selectAll('svg > *').remove();
    },
  };
  return HistogramD3;
});
