import { AxisLeft, AxisBottom } from "@visx/axis";
import { localPoint } from "@visx/event";
import { GridRows, GridColumns } from "@visx/grid";
import { Group } from "@visx/group";
import { LegendOrdinal } from "@visx/legend";
import { Point } from "@visx/point";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { BarStack, BarStackHorizontal, Line } from "@visx/shape";
import { SeriesPoint } from "@visx/shape/lib/types";
import { withTooltip } from "@visx/tooltip";
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip";
import React from "react";
import { Color } from "utils/Color";

import { TooltipWrapper } from "./TooltipWrapper";

import styles from "./StackedBarGraph.module.css";

interface StackedBarData {
  category: string;
  [key: string]: number | string;
}

type TooltipData = {
  bar: SeriesPoint<StackedBarData>;
  key: string;
  index: number;
  height: number;
  width: number;
  x: number;
  y: number;
  color: string;
};

export type StackedBarProps = {
  data: StackedBarData[];
  /** Width of the entire graph, in pixels, greater than 10.  */
  width: number;
  /** Height of the entire graph, in pixels. */
  height: number;
  /** Names of the groups appearing in the legend */
  keys: string[];
  /** Colours for each key */
  colorRange: string[];
  /** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */
  margin: { top: number; left: number };
  /** Number of ticks for the value axis */
  numTicksValueAxis?: number;
  /** Distance between the left axis labels and the start of the lines of the graph, in px. */
  axisLeftOffset?: number;
  /** Distance between the bottom axis and the bottom of the container of the graph, in px. */
  axisBottomOffset?: number;
  /** Distance between the right side of the graph and the legend, in px. */
  legendLeftOffset?: number;
  /** Distance between the top of the graph and the legend, in px. */
  legendTopOffset?: number;
  /** Width of the lines in the graph, in px. */
  strokeWidth?: number;
  /** Length of the dashes and the gaps in the graph, in px. */
  strokeDashArray?: string;
  /** Padding between each bar in the stacked bar graph, from 0 to 1 */
  scalePadding?: number;
  /** Margin for each item in the legend */
  itemMargin?: string;
  /** Factor multiplied with an offset to center the labels of the category-axis depending on the width/height of the graph.
   * >1 for width/height <600 and <1 for width/height >600 (vertical=width/horizontal=height) */
  categoryAxisLeftFactor?: number;
};

let tooltipTimeout: number;

export const StackedBarGraphVertical = withTooltip<
  StackedBarProps,
  TooltipData
>(
  ({
    data,
    width,
    height,
    keys,
    colorRange,
    margin,
    scalePadding = 0.3,
    numTicksValueAxis = 6,
    axisLeftOffset = 40,
    axisBottomOffset = 40,
    strokeWidth = 2.5,
    strokeDashArray = "10,4",
    legendLeftOffset = 40,
    legendTopOffset = 40,
    itemMargin = "15px 0 0 0",
    categoryAxisLeftFactor = 1,
    tooltipOpen,
    tooltipLeft,
    tooltipTop,
    tooltipData,
    hideTooltip,
    showTooltip,
  }: StackedBarProps & WithTooltipProvidedProps<TooltipData>) => {
    const yTotals = data.reduce((allTotals, currCategory) => {
      const yTotal = keys.reduce((categoryTotal, k) => {
        categoryTotal += currCategory[k] as number;
        return categoryTotal;
      }, 0);
      allTotals.push(yTotal);
      return allTotals;
    }, [] as number[]);

    const TICK_LABEL_FONT_WEIGHT = 800;

    // accessors
    const getCategory = (d: StackedBarData) => d.category;

    // scales
    const categoryScale = scaleBand<string>({
      domain: data.map(getCategory),
      padding: scalePadding,
    });
    const valueScale = scaleLinear<number>({
      domain: [0, Math.max(...yTotals)],
      nice: true,
    });
    const colorScale = scaleOrdinal<string, string>({
      domain: keys,
      range: colorRange,
    });

    // bounds
    const xMax = width;
    const yMax = height - margin.top - axisBottomOffset;

    categoryScale.rangeRound([0, xMax - axisLeftOffset]);
    valueScale.range([yMax, 0]);

    return width < 10 ? null : (
      <div className={styles.container}>
        <svg width={width} height={height}>
          <Group top={margin.top} left={margin.left}>
            <GridRows
              scale={valueScale}
              width={xMax}
              height={yMax}
              left={axisLeftOffset}
              numTicks={numTicksValueAxis}
              stroke={Color.tertiaryBackground}
              strokeWidth={strokeWidth}
              strokeDasharray={strokeDashArray}
            />
            <GridColumns
              scale={categoryScale}
              height={yMax}
              left={axisLeftOffset}
              offset={categoryScale.bandwidth() / 2}
              stroke={Color.tertiaryBackground}
              strokeWidth={strokeWidth}
              strokeDasharray={strokeDashArray}
            />
            <Group left={axisLeftOffset}>
              <BarStack<StackedBarData, string>
                data={data}
                keys={keys}
                x={getCategory}
                xScale={categoryScale}
                yScale={valueScale}
                color={colorScale}
              >
                {(barStacks) =>
                  barStacks.map((barStack) =>
                    barStack.bars.map((bar) => (
                      <rect
                        className={styles.barStack}
                        key={`bar-stack-${barStack.index}-${bar.index}`}
                        x={bar.x}
                        y={bar.y}
                        height={bar.height}
                        width={bar.width / 2}
                        fill={bar.color}
                        onMouseLeave={() => {
                          tooltipTimeout = window.setTimeout(() => {
                            hideTooltip();
                          }, 300);
                        }}
                        onMouseMove={(event) => {
                          if (tooltipTimeout) clearTimeout(tooltipTimeout);
                          const eventSvgCoords = localPoint(event);
                          const left = bar.x + bar.width / 2;
                          showTooltip({
                            tooltipData: bar,
                            tooltipTop: eventSvgCoords?.y,
                            tooltipLeft: left,
                          });
                        }}
                      />
                    ))
                  )
                }
              </BarStack>
            </Group>
            <Line
              fill={Color.tertiaryBackground}
              to={new Point({ x: axisLeftOffset, y: 0 })}
              from={new Point({ x: axisLeftOffset, y: yMax })}
              stroke={Color.tertiaryBackground}
              strokeWidth={strokeWidth}
              strokeDasharray={strokeDashArray}
            />
            <AxisBottom
              top={yMax}
              scale={categoryScale}
              left={
                ((categoryScale.bandwidth() * 100) / width) *
                categoryAxisLeftFactor
              }
              hideTicks
              hideAxisLine
              labelProps={{
                fontSize: `${10 / 16}rem`,
              }}
              tickLabelProps={() => ({
                fill: Color.label,
                fontWeight: TICK_LABEL_FONT_WEIGHT,
              })}
            />
            <AxisLeft
              scale={valueScale}
              top={5}
              numTicks={numTicksValueAxis}
              hideAxisLine
              labelProps={{
                fontSize: `${10 / 16}rem`,
              }}
              tickLabelProps={() => {
                return {
                  fill: Color.label,
                  fontWeight: TICK_LABEL_FONT_WEIGHT,
                };
              }}
            />
          </Group>
        </svg>
        <div
          className={styles.legend}
          style={{ left: width + legendLeftOffset, top: legendTopOffset }}
        >
          <LegendOrdinal
            scale={colorScale}
            direction="column"
            itemMargin={itemMargin}
          />
        </div>

        {tooltipOpen && tooltipData ? (
          <TooltipWrapper
            top={tooltipTop}
            left={tooltipLeft}
            header={tooltipData.key}
          >
            <p>{tooltipData.bar.data[tooltipData.key]}</p>
            <p>{getCategory(tooltipData.bar.data)}</p>
          </TooltipWrapper>
        ) : null}
      </div>
    );
  }
);

export const StackedBarGraphHorizontal = withTooltip<
  StackedBarProps,
  TooltipData
>(
  ({
    data,
    width,
    height,
    keys,
    colorRange,
    margin,
    scalePadding = 0.3,
    numTicksValueAxis = 6,
    axisLeftOffset = 40,
    axisBottomOffset = 40,
    strokeWidth = 2.5,
    strokeDashArray = "10,4",
    legendLeftOffset = 40,
    legendTopOffset = 40,
    itemMargin = "15px 0 0 0",
    categoryAxisLeftFactor = 1,
    tooltipOpen,
    tooltipLeft,
    tooltipTop,
    tooltipData,
    hideTooltip,
    showTooltip,
  }: StackedBarProps & WithTooltipProvidedProps<TooltipData>) => {
    const yTotals = data.reduce((allTotals, currCategory) => {
      const yTotal = keys.reduce((categoryTotal, k) => {
        categoryTotal += currCategory[k] as number;
        return categoryTotal;
      }, 0);
      allTotals.push(yTotal);
      return allTotals;
    }, [] as number[]);

    const TICK_LABEL_FONT_WEIGHT = 800;

    // accessors
    const getCategory = (d: StackedBarData) => d.category;

    // scales
    const valueScale = scaleLinear<number>({
      domain: [0, Math.max(...yTotals)],
      nice: true,
    });
    const categoryScale = scaleBand<string>({
      domain: data.map(getCategory),
      padding: scalePadding,
    });
    const colorScale = scaleOrdinal<string, string>({
      domain: keys,
      range: colorRange,
    });

    // bounds
    const xMax = width;
    const yMax = height - margin.top - axisBottomOffset;

    categoryScale.rangeRound([yMax, 0]);
    valueScale.range([0, xMax - axisLeftOffset]);

    return width < 10 ? null : (
      <div className={styles.container}>
        <svg width={width} height={height}>
          <Group top={margin.top} left={margin.left}>
            <GridRows
              scale={categoryScale}
              width={xMax}
              height={yMax}
              offset={categoryScale.bandwidth() / 2}
              left={axisLeftOffset}
              stroke={Color.tertiaryBackground}
              strokeWidth={strokeWidth}
              strokeDasharray={strokeDashArray}
            />
            <GridColumns
              scale={valueScale}
              height={yMax}
              numTicks={numTicksValueAxis}
              left={axisLeftOffset}
              stroke={Color.tertiaryBackground}
              strokeWidth={strokeWidth}
              strokeDasharray={strokeDashArray}
            />
            <Group left={axisLeftOffset}>
              <BarStackHorizontal<StackedBarData, string>
                data={data}
                keys={keys}
                y={getCategory}
                xScale={valueScale}
                yScale={categoryScale}
                color={colorScale}
              >
                {(barStacks) =>
                  barStacks.map((barStack) =>
                    barStack.bars.map((bar) => (
                      <rect
                        className={styles.barStack}
                        key={`bar-stack-${barStack.index}-${bar.index}`}
                        x={bar.x}
                        y={bar.y}
                        height={bar.height / 2}
                        width={bar.width}
                        fill={bar.color}
                        onMouseLeave={() => {
                          tooltipTimeout = window.setTimeout(() => {
                            hideTooltip();
                          }, 300);
                        }}
                        onMouseMove={(event) => {
                          if (tooltipTimeout) clearTimeout(tooltipTimeout);
                          const eventSvgCoords = localPoint(event);
                          const left = bar.x + bar.width / 2;
                          showTooltip({
                            tooltipData: bar,
                            tooltipTop: eventSvgCoords?.y,
                            tooltipLeft: left,
                          });
                        }}
                      />
                    ))
                  )
                }
              </BarStackHorizontal>
            </Group>
            <AxisBottom
              top={yMax}
              scale={valueScale}
              left={axisLeftOffset}
              numTicks={numTicksValueAxis}
              hideAxisLine
              hideTicks
              labelProps={{
                fontSize: `${10 / 16}rem`,
              }}
              tickLabelProps={() => ({
                fill: Color.label,
                fontWeight: TICK_LABEL_FONT_WEIGHT,
              })}
            />
            <AxisLeft
              top={
                -((categoryScale.bandwidth() * 100) / width) *
                categoryAxisLeftFactor
              }
              scale={categoryScale}
              hideAxisLine
              hideTicks
              labelProps={{
                fontSize: `${10 / 16}rem`,
              }}
              tickLabelProps={() => {
                return {
                  fill: Color.label,
                  fontWeight: TICK_LABEL_FONT_WEIGHT,
                };
              }}
            />
          </Group>
        </svg>
        <div
          className={styles.legend}
          style={{ left: width + legendLeftOffset, top: legendTopOffset }}
        >
          <LegendOrdinal
            scale={colorScale}
            direction="column"
            itemMargin={itemMargin}
          />
        </div>

        {tooltipOpen && tooltipData ? (
          <TooltipWrapper
            top={tooltipTop}
            left={tooltipLeft}
            header={tooltipData.key}
          >
            <p>{tooltipData.bar.data[tooltipData.key]}</p>
            <p>{getCategory(tooltipData.bar.data)}</p>
          </TooltipWrapper>
        ) : null}
      </div>
    );
  }
);
