import { useEffect, useMemo, useRef } from 'react';
import chartist from 'chartist';

import type { TransitionInOut2Api } from '../TransitionInOut2';

import useIsLargeScreen from '~/src/hooks/useIsLargeScreen';
import { darkenColor } from '~/src/lib/utils/color';
import measure from '~/src/lib/utils/measure';
import toShortNumber from '~/src/lib/utils/toShortNumber';
import TransitionInOut2 from '../TransitionInOut2';
import { globalStyle } from './style';

const LineChart = ({
  height,
  items,
  labels,
  onFocalPointChange = () => {},
  onVisible = () => {},
}: {
  height?: number;
  labels?: number[];

  items?: {
    items: number[];
    color?: string;
    withPoints: boolean;
    withLine: boolean;
    withArea: boolean;
  }[];

  onVisible?: () => void;
  onFocalPointChange: (params?: { index: number; series?: any }) => void;
}) => {
  const isLargeScreen = useIsLargeScreen();
  const transitionRef = useRef<TransitionInOut2Api>(null);
  const nodeRef = useRef<HTMLDivElement>(null);
  const activeIndexRef = useRef<number>(null);
  const activeSeriesIndexRef = useRef<number>(null);

  const data = useMemo(
    () => ({
      labels,

      series: items?.map(({ items, ...rest }) => ({
        ...rest,
        value: items.map((value, index) => ({ value, meta: index })),
      })),
    }),
    [items, labels]
  );

  useEffect(() => {
    const destroyChart = measure<any>('create chart', () =>
      createChart({
        el: nodeRef.current,
        data,

        options: {
          ...createChartOptions(isLargeScreen),
          height,
        },

        onCreated: async () => {
          await transitionRef.current?.setVisible(true);
          onVisible();
        },
      })
    );

    return () => {
      destroyChart();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data, isLargeScreen]);

  useEffect(() => {
    if (!items) return;

    const onMouseOver = (event) => {
      const target = event.target as HTMLElement | null;
      const isArea = target?.classList.contains('ct-area');

      if (!activeIndexRef.current) return;

      if (isArea) {
        const seriesIndex = target?.getAttribute('data-series');
        if (!seriesIndex) return;

        const color = data.series?.[seriesIndex]?.color as string;
        target!.style.fill = color;

        activeSeriesIndexRef.current = Number(seriesIndex);

        onFocalPointChange({
          series: data.series![activeSeriesIndexRef.current],
          index: activeIndexRef.current,
        });
      }
    };

    const unsetStyles = (selector: string) => {
      const els = nodeRef.current!.querySelectorAll(
        selector
      ) as NodeListOf<SVGLineElement>;

      // remove all inline styles
      [].forEach.call(els, (el) => el.removeAttribute('style'));
    };

    // when the mouse leaves the last hovered element
    const onMouseOut = (event) => {
      const target = event.target as HTMLElement | null;
      const isArea = target?.classList.contains('ct-area');

      if (isArea) {
        target!.style.fill = '';

        activeSeriesIndexRef.current = null;

        if (activeIndexRef.current) {
          onFocalPointChange({
            series: undefined,
            index: activeIndexRef.current,
          });
        }
      }
    };

    // when the mouse leaves the chart element
    const onMouseLeave = () => {
      unsetStyles(
        '.ct-grid.ct-horizontal,.ct-labels .ct-horizontal,.ct-custom-point'
      );

      activeIndexRef.current = null;

      if (onFocalPointChange) {
        onFocalPointChange();
      }
    };

    const onMouseMove = (event) => {
      const gridBox = nodeRef
        .current!.querySelector('.ct-grids')!
        .getBoundingClientRect();

      const xLineEls = nodeRef.current!.querySelectorAll(
        '.ct-grid.ct-horizontal'
      ) as NodeListOf<SVGLineElement>;

      const gridLineInterval = gridBox.width / labels!.length;
      const pointerX = event.clientX;
      const x = pointerX - gridBox.left;

      if (x < 0) return;

      const xIndex = Math.floor(x / gridLineInterval);
      const lineEl = xLineEls[xIndex];
      const changed = xIndex !== activeIndexRef.current;

      if (!changed) return;
      if (!lineEl) return;

      const labelEls = nodeRef.current!.querySelectorAll(
        '.ct-labels .ct-horizontal'
      ) as NodeListOf<SVGLineElement>;

      const pointEls = nodeRef.current!.querySelectorAll(
        '.ct-custom-point'
      ) as NodeListOf<SVGLineElement>;

      const labelEl = labelEls[xIndex];
      const pointEl = pointEls[xIndex];

      activeIndexRef.current = xIndex;

      unsetStyles(
        '.ct-grid.ct-horizontal,.ct-labels .ct-horizontal,.ct-custom-point'
      );

      labelEl.style.color = 'white';
      lineEl.style.stroke = '#888';

      if (pointEl) pointEl.style.visibility = 'visible';

      onFocalPointChange({
        index: activeIndexRef.current,
        series:
          activeSeriesIndexRef.current &&
          data.series![activeSeriesIndexRef.current],
      });
    };

    nodeRef.current?.addEventListener('mouseover', onMouseOver);
    nodeRef.current?.addEventListener('mouseleave', onMouseLeave);
    nodeRef.current?.addEventListener('mouseout', onMouseOut);
    nodeRef.current?.addEventListener('mousemove', onMouseMove);

    return () => {
      nodeRef.current?.removeEventListener('mouseover', onMouseOver);
      nodeRef.current?.removeEventListener('mouseout', onMouseOut);
      nodeRef.current?.removeEventListener('mouseleave', onMouseLeave);
      nodeRef.current?.removeEventListener('mousemove', onMouseMove);
    };
  }, [onFocalPointChange, nodeRef.current, data]);

  return (
    <TransitionInOut2 apiRef={transitionRef} isVisibleInitial={false}>
      <div ref={nodeRef} />
      <style jsx global>
        {globalStyle}
      </style>
      <style jsx global>
        {`
          .ct-grid {
            stroke: rgba(255, 255, 255, 0.1);
          }

          .ct-grid.ct-horizontal {
            stroke: transparent;
          }

          .ct-bar.ct-bar,
          .ct-line.ct-line {
            stroke: #666;
          }

          .ct-line {
            stroke-width: 1px;
          }

          .ct-point.ct-point {
            stroke-width: 6px;
            stroke: rgba(255, 255, 255, 0.5);
            cursor: pointer;
          }

          .ct-custom-point {
            visibility: hidden;
            fill: #bbb;
            r: 3;
          }

          .ct-series .ct-area {
            fill: currentColor;
            fill-opacity: 1;
            transition: fill 300ms;
          }

          .ct-labels,
          .ct-grids {
            pointer-events: none;
          }

          .ct-label {
            color: rgba(255, 255, 255, 0.25);
            font-size: 10px;
          }

          .chartist-tooltip {
            position: absolute;
            color: white;
          }
        `}
      </style>
    </TransitionInOut2>
  );
};

interface ChartDrawEvent {
  series: {
    value: number[];
    withPoints?: boolean;
    color?: string;
    withLine?: boolean;
    withArea?: boolean;
  };

  seriesIndex: number;
  index: number;

  element: {
    replace: (newElement: any) => void;
    remove: () => void;
    getNode: () => SVGElement;
  };

  group: {
    getNode: () => SVGElement;
  };
}

interface ChartDrawPointEvent extends ChartDrawEvent {
  type: 'point';

  x: number;
  y: number;

  value: {
    x: number;
    y: number;
  };
}

interface ChartDrawAreaEvent extends ChartDrawEvent {
  type: 'area';
}

interface ChartDrawLineEvent extends ChartDrawEvent {
  type: 'line';
}

const createChartOptions = (isLargeScreen: boolean) => ({
  showArea: true,
  fullWidth: true,

  chartPadding: {
    top: 10,
    right: 10,
    bottom: 0,
    left: 20,
  },

  axisX: {
    showLabel: true,
    showGrid: true,
    scaleMinSpace: 40,

    labelOffset: {
      x: -2,
      y: 2,
    },

    labelInterpolationFnc: (timestamp, index, items) => {
      const isManyItems = items.length > 28;

      const { day, dayMonth } = formatChartLabel(timestamp);
      const label = isManyItems ? dayMonth : day;

      const isFirst = index === 0;
      const isLast = index === items.length - 1;

      if (isFirst || isLast) return label;

      const IN1 = Math.ceil(items.length / (isLargeScreen ? 14 : 7));
      const IN2 = isLargeScreen ? 1 : 4;

      // Calculate INTERVAL dynamically: show max 14 labels when isManyItems is true
      const INTERVAL = isManyItems ? IN1 : IN2;
      const shouldShow = !(index % INTERVAL);

      return shouldShow ? label : '';
    },
  },

  axisY: {
    showLabel: true,
    showGrid: true,
    offset: 30,
    position: 'end',
    onlyInteger: true,
    scaleMinSpace: 40,

    labelOffset: {
      x: 0,
      y: 5,
    },

    labelInterpolationFnc: (value) => toShortNumber(value),
  },
});

const formatChartLabel = (label: number) => {
  const date = new Date(label);

  // 1. Raw day of month (e.g., "15")
  const dayOfMonth = date.getDate();

  // 2. Day and 3 letter month (e.g., "15 Dec")
  const dayAndMonth = date.toLocaleDateString('en-US', {
    day: 'numeric',
    month: 'short',
  });

  return {
    day: String(dayOfMonth),
    dayMonth: dayAndMonth,
  };
};

const createChart = ({ el, data, options, onCreated }) => {
  const chart = new chartist.Line(el, data, options);

  chart.on('created', () => {
    onCreated(chart);
  });

  chart.on(
    'draw',
    (event: ChartDrawAreaEvent | ChartDrawPointEvent | ChartDrawLineEvent) => {
      const { series } = event;

      // If the draw event was triggered from drawing a point on the line chart
      switch (event.type) {
        case 'point':
          {
            if (!series.withPoints) {
              event.element.remove();
              return;
            }

            // We are creating a new path SVG element that draws a triangle around the point coordinates
            const circle = new chartist.Svg(
              'circle',
              {
                'data-index': event.index,
                'data-value': event.value.y,
                cx: event.x,
                cy: event.y,
              },
              'ct-custom-point'
            );

            // With data.element we get the Chartist SVG wrapper and we can replace the original point drawn by Chartist with our newly created circle
            event.element.replace(circle);
          }

          break;

        case 'area':
          {
            if (series.withArea === false) {
              event.element.remove();
              return;
            }

            if (series.color) {
              const node = event.element.getNode();
              const parentGroupNode = event.group.getNode();

              node.setAttribute('data-series', String(event.seriesIndex));
              parentGroupNode.style.color = darkenColor(series.color, 30)!;
            }

            const node = event.element.getNode();
            node.setAttribute('data-series', String(event.seriesIndex));
          }

          break;

        case 'line':
          {
            if (series.withLine === false) {
              event.element.remove();
              return;
            }
          }

          break;
      }
    }
  );

  // return destroy method
  return () => {
    chart.detach();
  };
};

export default LineChart;
