"use client"
import React, { useEffect, useMemo, useRef } from "react"
import { useContextSelector } from "use-context-selector"
import { useIsLessThanBreakpoint } from "../../../components/Media"
import { useMouseUp } from "../../../hooks"
import { HydrationGuard } from "../../HydrationGuard"
import { ChartContainer } from "../ChartContainer"
import { ChartContext, ChartContextProvider } from "../ChartContext"
import { ChartTooltip, RenderTooltipFn } from "../ChartTooltip"
import { Highcharts, HighchartsReact, RefObject } from "../Highcharts"
import {
  DEFAULT_HIGHCHART_OPTIONS,
  createChartOptions,
  createSeriesOptions,
  createTooltipOptions,
  createXAxisOptions,
  createYAxisOptions,
} from "../utils"
import { getPointIndex, getSeriesIndex } from "../utils/points"
import { AxisTitleProps, RangeProps } from "../utils/types"
import { ScatterplotEmptyState } from "./ScatterplotEmptyState"
import { ScatterplotSkeleton } from "./ScatterplotSkeleton"

const SCATTERPLOT_BOOST_THRESHOLD = 5000

export type SeriesProps<T> = Pick<
  Highcharts.SeriesScatterOptions,
  "color" | "name" | "marker"
> & {
  data: readonly T[]
  getX: (d: T) => number
  getY: (d: T) => number
}

export type ScatterplotProps<T> = AxisTitleProps &
  RangeProps & {
    className?: string
    series: SeriesProps<T>[]
    renderTooltip?: RenderTooltipFn<T>
    tooltipDelay?: number
    interactive?: boolean
    onClickPoint?: (
      dataPoint: T,
      event: Highcharts.SeriesClickEventObject,
    ) => unknown
    overrides?: {
      xAxis?: Highcharts.XAxisOptions
    }
    formatYAxisLabels?: (value: number | string) => string
    formatXAxisLabels?: (value: number | string) => string
  }

export const ScatterplotBase = <T,>({
  className,
  series: seriesProp,
  renderTooltip: RenderTooltip,
  interactive = false,
  onClickPoint,
  yAxisTitle,
  xAxisTitle,
  hideAxisTitlesBreakpoint = "sm",
  rangeStart,
  rangeEnd,
  overrides = { xAxis: {} },
  formatXAxisLabels,
  formatYAxisLabels,
  tooltipDelay,
}: ScatterplotProps<T>) => {
  const [
    setChart,
    setIsZoomed,
    setIsPanning,
    setXAxisRange,
    hoveredPoint,
    setHoveredPoint,
  ] = useContextSelector(ChartContext, v => [
    v.setChart,
    v.setIsZoomed,
    v.setIsPanning,
    v.setXAxisRange,
    v.hoveredPoint,
    v.setHoveredPoint,
  ])

  const chartComponentRef = useRef<RefObject>(null)
  const hideAxisTitle = useIsLessThanBreakpoint(hideAxisTitlesBreakpoint)

  useEffect(() => {
    return () => setChart(undefined)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const overridesRef = useRef(overrides)
  useEffect(() => {
    overridesRef.current = overrides
  }, [overrides])

  const rangeStartTime = rangeStart?.getTime()
  const rangeEndTime = rangeEnd?.getTime()

  useEffect(() => {
    setXAxisRange([rangeStartTime, rangeEndTime])
    chartComponentRef.current?.chart.xAxis[0].setExtremes(
      rangeStartTime,
      rangeEndTime,
    )
  }, [rangeStartTime, rangeEndTime, setXAxisRange])

  const mouseClickRef = useMouseUp(() => setIsPanning(false))

  const seriesData = useMemo(
    () => seriesProp.map(({ data }) => data),
    [seriesProp],
  )

  useEffect(() => {
    setHoveredPoint(undefined)
  }, [seriesProp, setHoveredPoint])

  const series = useMemo(() => {
    const seriesData: Highcharts.SeriesOptionsType[] = seriesProp.map(
      ({ data, getX, getY, color, ...rest }, seriesIndex) => {
        const isBoostModeEnabled = data.length >= SCATTERPLOT_BOOST_THRESHOLD

        return createSeriesOptions({
          id: "scatterplot",
          type: "scatter",
          data: data.map((dp, pointIndex) => {
            if (isBoostModeEnabled) {
              // Highcharts boost mode does not support object format points
              return [getX(dp), getY(dp)]
            }
            return {
              name: `${seriesIndex}:${pointIndex}`,
              x: getX(dp),
              y: getY(dp),
            }
          }),
          cursor: onClickPoint ? "pointer" : undefined,
          events: {
            click: event => {
              return onClickPoint?.(data[getPointIndex(event.point)], event)
            },
          },
          boostThreshold: SCATTERPLOT_BOOST_THRESHOLD,
          turboThreshold: SCATTERPLOT_BOOST_THRESHOLD,
          marker: {
            radius: isBoostModeEnabled ? 2 : 4,
          },
          ...(color ? { color } : {}),
          ...rest,
        })
      },
    )

    return seriesData
  }, [onClickPoint, seriesProp])

  const options: Highcharts.Options = useMemo(() => {
    return {
      ...DEFAULT_HIGHCHART_OPTIONS,
      chart: createChartOptions({
        interactive,
        zooming: {
          type: "xy",
        },
        events: {
          load() {
            this.xAxis[0].setExtremes(rangeStartTime, rangeEndTime)
            setChart(this)
          },
        },
      }),
      yAxis: createYAxisOptions({
        axisTitle: hideAxisTitle ? undefined : yAxisTitle,
        labels: formatYAxisLabels
          ? {
              formatter() {
                return formatYAxisLabels(this.value)
              },
            }
          : undefined,
      }),
      xAxis: createXAxisOptions({
        type: "datetime",
        ...overridesRef.current.xAxis,
        axisTitle: hideAxisTitle ? undefined : xAxisTitle,
        events: {
          afterSetExtremes(event) {
            setIsZoomed(
              event.userMin !== rangeStartTime ||
                event.userMax !== rangeEndTime,
            )
          },
          setExtremes(event) {
            if (event.trigger === "pan") {
              setIsPanning(true)
            }
          },
        },
        labels: formatXAxisLabels
          ? {
              formatter() {
                return formatXAxisLabels(this.value)
              },
            }
          : undefined,
      }),
      series,
      tooltip: createTooltipOptions({
        formatter() {
          setHoveredPoint(this)
          return undefined
        },
      }),
    }
  }, [
    formatXAxisLabels,
    formatYAxisLabels,
    hideAxisTitle,
    interactive,
    rangeEndTime,
    rangeStartTime,
    series,
    setChart,
    setHoveredPoint,
    setIsPanning,
    setIsZoomed,
    xAxisTitle,
    yAxisTitle,
  ])

  return (
    <ChartContainer
      className={className}
      ref={mouseClickRef}
      onPointerLeave={() => {
        setHoveredPoint(undefined)
      }}
    >
      <HighchartsReact
        highcharts={Highcharts}
        // Re-render scatterplot when theme changes
        options={options}
        ref={chartComponentRef}
      />
      {hoveredPoint &&
        seriesData[getSeriesIndex(hoveredPoint)]?.[
          getPointIndex(hoveredPoint)
        ] &&
        RenderTooltip && (
          <ChartTooltip
            content={
              <RenderTooltip
                item={
                  seriesData[getSeriesIndex(hoveredPoint)][
                    getPointIndex(hoveredPoint)
                  ]
                }
                point={hoveredPoint}
              />
            }
            delay={tooltipDelay}
            pointType="marker"
          />
        )}
    </ChartContainer>
  )
}

const ScatterplotWithContext = <T,>({
  fallback,
  ...rest
}: ScatterplotProps<T> & { fallback?: React.ReactNode }) => {
  return (
    <HydrationGuard
      fallback={fallback ?? <ScatterplotSkeleton className={rest.className} />}
    >
      <ChartContextProvider>
        <ScatterplotBase {...rest} />
      </ChartContextProvider>
    </HydrationGuard>
  )
}

export const Scatterplot = Object.assign(ScatterplotWithContext, {
  Skeleton: ScatterplotSkeleton,
  EmptyState: ScatterplotEmptyState,
})
