import { memo, useMemo } from 'react';

import {
  timeDays,
  timeWeeks,
  timeMonths,
  timeMonth,
  timeMonday,
  timeMondays,
} from 'd3-time';
import { scaleLinear } from '@visx/scale';
import { Group } from '@visx/group';

import { weekNumber, dayNumber, lastYear, yearRange } from './calendar';
import { HEATMAP_COLORS } from '../../config';
import { ChartData, valueSelector, labelSelector } from '../../utils/data';
import { call } from '../../utils';
import { TooltipBinder } from 'hooks/useTooltip';

type InnerDatum = {
  label: string;
  value: number;
};

export type CalendarChartSVGProps = {
  data: ChartData[];
  width: number;
  year?: number;
  maxValue?: number;
  colors: string[];
  cellRadius?: number;
  cellFill?: string;
  cellSize: number;
  cellMargin: number;
  monthMargin?: number;
  showMonths: boolean;
  showDays: boolean;
  showYear: boolean;
  labelColor?: string;
  labelFontSize?: number;

  onClick?: (
    data: InnerDatum,
    ev: React.MouseEvent<SVGRectElement, MouseEvent>
  ) => void;
  bindTooltip?: TooltipBinder<ChartData>;
};

export const defaultProps = {
  colors: HEATMAP_COLORS,
  cellFill: 'hsla(214, 14%, 58%, 0.1)',
  cellMargin: 2,
  monthMargin: 0,
  showMonths: true,
  showDays: true,
  showYear: true,
  labelFontSize: 12,
};

const weeksInMonth = (month: Date) => {
  const m = timeMonth.floor(month);
  return timeMondays(timeMonday.floor(m), timeMonth.offset(m, 1)).length;
};

const getDays = (month: Date) => {
  const m = timeMonth.floor(month);
  return timeDays(m, timeMonth.offset(m, 1));
};

function arrayToObject<T>(data: T[], dateSelector: any) {
  const dateset: Record<string, T> = {};
  data.forEach((item) => {
    const date = new Date(dateSelector(item));
    date.setHours(0);
    dateset[String(date)] = item;
  });
  return dateset;
}

const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

type RotateTextProps = React.SVGProps<SVGTextElement> & {
  deg: number;
};

const RotateText = ({ deg, x, y, children, ...props }: RotateTextProps) => {
  return (
    <text x={x} y={y} transform={`rotate(${deg}, ${x}, ${y})`} {...props}>
      {children}
    </text>
  );
};

function CalendarChartSVG({
  width,
  data,
  year,
  maxValue,
  colors,
  cellRadius,
  cellFill,
  cellSize,
  cellMargin,
  monthMargin,
  showMonths,
  showDays,
  showYear,
  labelColor,
  labelFontSize,
  onClick,
  bindTooltip,
}: CalendarChartSVGProps & typeof defaultProps) {
  const range = year ? yearRange(year) : lastYear();
  const monthsRange = timeMonths(...range);

  const marginTop = showMonths ? 24 : 0;
  const marginLeft = showDays ? 40 : 0;
  const marginRight = showYear ? 24 : 0;

  if (!cellSize) {
    const [start, stop] = range;
    const weeks = timeWeeks(start, stop);
    const weeksLength = weeks.length + 1;

    const availableWidth = width - marginLeft - marginRight;

    cellSize =
      (availableWidth - monthMargin * 11 - cellMargin * weeks.length) /
      weeksLength;
  }

  const chartHeight = (cellSize + cellMargin) * 7 - cellMargin;

  const domainMax =
    maxValue !== undefined ? maxValue : Math.max(...data.map(valueSelector));

  const colorScale = scaleLinear({
    range: colors,
    domain: [0, domainMax],
  });

  let currDate: number | null = null;
  let currWeek = -1;
  function getX(d: Date) {
    if (currDate !== weekNumber(d)) {
      currDate = weekNumber(d);
      currWeek += 1;
    }
    return currWeek * cellSize + currWeek * cellMargin;
  }

  function getY(d: Date) {
    return dayNumber(d) * cellSize + dayNumber(d) * cellMargin;
  }

  const dataset = useMemo(() => arrayToObject(data, labelSelector), [
    data,
    labelSelector,
  ]);

  return (
    <svg width={width} height={chartHeight + marginTop}>
      {showDays && (
        <Group top={marginTop + (cellSize + cellMargin / 2) / 2}>
          {DAYS.map((day, i) => {
            return (
              <text
                fill={labelColor}
                x={0}
                y={(cellSize + cellMargin) * i}
                dy=".33em"
                fontSize={labelFontSize}
                textAnchor="start"
                key={i}
              >
                {day}
              </text>
            );
          })}
        </Group>
      )}
      {showYear && (
        <Group top={marginTop} left={width}>
          <RotateText
            deg={90}
            x={-16}
            y={chartHeight / 2}
            textAnchor="middle"
            fontSize={labelFontSize}
          >
            {year}
          </RotateText>
        </Group>
      )}
      <Group left={marginLeft} top={0}>
        {monthsRange.map((date, i) => {
          const monthName = date.toLocaleString(undefined, {
            month: 'short',
          });
          const columns = weeksInMonth(date);
          const width = (cellSize + cellMargin) * columns - cellMargin;

          const days = getDays(date);

          return (
            <Group left={monthMargin * i} key={String(date)}>
              {showMonths && (
                <text
                  x={getX(date) + width / 2}
                  y={14}
                  fontSize={labelFontSize}
                  textAnchor="middle"
                >
                  {monthName}
                </text>
              )}
              <Group top={marginTop}>
                {days.map((date) => {
                  const x = getX(date);
                  const y = getY(date);

                  const datum = dataset[String(date)];
                  const value = datum ? valueSelector(datum) : 0;
                  const label = date.toLocaleDateString();

                  const fill = value ? colorScale(value) : cellFill;

                  const handleClick: React.MouseEventHandler<SVGRectElement> = (
                    ev
                  ) => {
                    onClick!(datum, ev);
                  };
                  const tooltipProps = call(bindTooltip, { ...datum, label });

                  return (
                    <rect
                      {...tooltipProps}
                      onClick={onClick && handleClick}
                      // className
                      width={cellSize}
                      height={cellSize}
                      x={x}
                      y={y}
                      fill={fill}
                      rx={cellRadius}
                      key={date.toString()}
                    />
                  );
                })}
              </Group>
            </Group>
          );
        })}
      </Group>
    </svg>
  );
}

CalendarChartSVG.defaultProps = defaultProps;

export default memo(CalendarChartSVG);
