"use client"
import React, { useEffect, useMemo, useRef } from "react"
import { useContextSelector } from "use-context-selector"
import { useMergedRef, useMousePosition, useMouseUp } from "../../../hooks"
import { useDateRangeFormatter } from "../../../hooks/useDateRangeFormatter"
import { isMouseWithinElement } from "../../../utils/element"
import { HydrationGuard } from "../../HydrationGuard"
import { useIsLessThanBreakpoint } from "../../Media"
import { ChartContainer } from "../ChartContainer"
import { ChartContext, ChartContextProvider } from "../ChartContext"
import { ChartTooltip, RenderTooltipFn } from "../ChartTooltip"
import { Highcharts, HighchartsReact, RefObject } from "../Highcharts"
import {
  createChartOptions,
  createSeriesOptions,
  createTooltipOptions,
  createXAxisOptions,
  createYAxisOptions,
  type CreateYAxisOptions,
  type CreateXAxisOptions,
  DEFAULT_DATE_TIME_LABEL_FORMATS,
  DEFAULT_HIGHCHART_OPTIONS,
  DEFAULT_MARKER_STYLES,
  OUTLIER_MARKER_STYLES,
  SINGLE_POINT_DATE_LABEL_FORMAT,
} from "../utils"
import { getPointIndex } from "../utils/points"
import { AxisTitleProps, RangeProps } from "../utils/types"
import { getCorrespondingVolumeData } from "./getCorrespondingVolumeData"
import { TimeSeriesChartEmptyState } from "./TimeSeriesChartEmptyState"
import { TimeSeriesChartSkeleton } from "./TimeSeriesChartSkeleton"

export type AccessorProps<T> = {
  getDate: (d: T) => Date
  getValue?: (d: T) => number
  getScatterValue?: (d: T) => number
  getColumnValue?: (d: T) => number
  getIsOutlier?: (d: T) => boolean
}

export type StandaloneColumn<S> = {
  items: S[]
  getDate: (d: S) => Date
  getValue: (d: S) => number
  yAxisMin?: number
  yAxisMax?: number
}

export type TimeSeriesChartProps<T, S = undefined> = AxisTitleProps &
  AccessorProps<T> &
  RangeProps & {
    className?: string
    /**
     * Points in data must be sorted in chronological order for zooming and panning to work.
     */
    data: T[]

    // Use standalone columns for a separate column series at the bottom of the chart for e.g. volume data
    standaloneColumn?: StandaloneColumn<S>

    renderTooltip?: RenderTooltipFn<
      T,
      {
        matchedColumnData?: S
      }
    >
    tooltipDelay?: number
    interactive?: boolean
    lineType?: "spline" | "areaspline" | "line" | "area"
    lineColor?: string
    lineWidth?: number
    markerRadius?: number
    hideAxes?: boolean
    hideYAxis?: boolean
    columnAxisTitle?: string
    singlePointDateLabelFormat?: string
    overrides?: {
      tooltip?: Highcharts.TooltipOptions
      yAxisOptions?: Partial<CreateYAxisOptions>
      xAxisOptions?: Partial<CreateXAxisOptions>
    }
    scatterOptions?: Partial<Highcharts.SeriesScatterOptions> & {
      tooltipDelay?: number
    }
    areaOptions?: Partial<Highcharts.SeriesAreaOptions> & {
      tooltipDelay?: number
    }
    lineOptions?: Partial<Highcharts.SeriesLineOptions> & {
      tooltipDelay?: number
    }
    splineOptions?: Partial<Highcharts.SeriesSplineOptions> & {
      tooltipDelay?: number
    }
    areaSplineOptions?: Partial<Highcharts.SeriesAreasplineOptions> & {
      tooltipDelay?: number
    }
  }

const STANDALONE_COLUMN_NAME = "standalone"

const TimeSeriesChartBase = <T, S>({
  data,
  getColumnValue,
  getScatterValue,
  getIsOutlier,
  standaloneColumn,
  getDate,
  getValue,
  className,
  renderTooltip: RenderTooltip,
  interactive = false,
  lineType = "spline",
  lineWidth = 2,
  hideAxes = false,
  hideYAxis = false,
  xAxisTitle,
  yAxisTitle,
  columnAxisTitle,
  hideAxisTitlesBreakpoint = "sm",
  rangeStart,
  rangeEnd,
  yAxisMin,
  yAxisMax,
  singlePointDateLabelFormat = SINGLE_POINT_DATE_LABEL_FORMAT,
  overrides = { tooltip: {}, yAxisOptions: {}, xAxisOptions: {} },
  scatterOptions,
  lineOptions,
  formatXAxisLabels,
  formatYAxisLabels,
  tooltipDelay,
  splineOptions,
  areaSplineOptions,
  areaOptions,
}: TimeSeriesChartProps<T, S>) => {
  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)
  const chartRef = useRef<HTMLDivElement>(null)
  const mousePosition = useMousePosition({ throttle: 100 })

  const defaultDateFormatter = useDateRangeFormatter(
    rangeStart ?? (data[0] ? getDate(data[0]) : new Date()),
    rangeEnd ??
      (data[data.length - 1] ? getDate(data[data.length - 1]) : new Date()),
  )

  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,
    )
  }, [setXAxisRange, rangeStartTime, rangeEndTime])

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

  const mouseUpRef = useMouseUp(() => setIsPanning(false))
  const mergedRef = useMergedRef(chartRef, mouseUpRef)

  // Doing this because hoveredPoint.index is not reliable when trying to map back to data
  // as highcharts can reorder series under the hood.
  const dataIndexMap = useMemo(() => {
    const scatterData = getScatterValue
      ? data.map((dp, index) => ({
          x: getDate(dp).getTime(),
          y: getScatterValue(dp),
          dataIndex: index,
        }))
      : []

    const lineData = getValue
      ? data.map((dp, index) => ({
          x: getDate(dp).getTime(),
          y: getValue(dp),
          dataIndex: index,
        }))
      : []

    return new Map(
      scatterData.concat(lineData).map(dp => [`${dp.x}-${dp.y}`, dp.dataIndex]),
    )
  }, [data, getScatterValue, getValue, getDate])

  const series = useMemo(() => {
    const series: Highcharts.SeriesOptionsType[] = []

    const columnOptions: Highcharts.SeriesColumnOptions = {
      type: "column",
      color: "rgb(var(--color-bg-additional-3))",
      borderColor: "rgb(var(--color-bg-additional-3))",
      borderWidth: 1,
      borderRadius: 0,
      maxPointWidth: 32,
    }

    if (standaloneColumn) {
      series.push(
        createSeriesOptions({
          ...columnOptions,
          name: STANDALONE_COLUMN_NAME,
          data: standaloneColumn.items.map(dp => [
            standaloneColumn.getDate(dp).getTime(),
            standaloneColumn.getValue(dp),
          ]),
          yAxis: 2,
          states: {
            inactive: {
              opacity: 1,
            },
            hover: {
              enabled: false,
            },
          },
          groupPadding: 0,
          pointPadding: 0,
          borderWidth: 2,
          enableMouseTracking: false,
        }),
      )
    }

    if (getColumnValue) {
      series.push(
        createSeriesOptions({
          ...columnOptions,
          data: data.map(dp => [getDate(dp).getTime(), getColumnValue(dp)]),
          yAxis: 1,
          states: {
            hover: {
              color: "var(--color-bg-additional-3)",
            },
          },
        }),
      )
    }

    if (getScatterValue) {
      series.push(
        createSeriesOptions({
          type: "scatter",
          data: data.map(dp => ({
            x: getDate(dp).getTime(),
            y: getScatterValue(dp),
            marker:
              getIsOutlier && getIsOutlier(dp)
                ? OUTLIER_MARKER_STYLES
                : DEFAULT_MARKER_STYLES,
          })),
          yAxis: 0,
          ...scatterOptions,
        }),
      )
    }

    // Only add line series if getValue is provided
    if (getValue) {
      if (lineType === "areaspline") {
        series.push(
          createSeriesOptions({
            type: "areaspline",
            data: data.map(dp => [getDate(dp).getTime(), getValue(dp)]),
            yAxis: 0,
            marker: {
              enabled: false,
            },
            lineColor: "rgb(var(--color-blue-4))",
            color: {
              linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
              stops: [
                [0, "rgb(var(--color-blue-4) / 50%)"],
                [1, "rgb(var(--color-bg-app))"], // end
              ],
            },
            lineWidth,
            ...areaSplineOptions,
          }),
        )
      } else if (lineType === "area") {
        series.push(
          createSeriesOptions({
            type: "area",
            data: data.map(dp => [getDate(dp).getTime(), getValue(dp)]),
            yAxis: 0,
            marker: {
              enabled: false,
            },
            lineColor: "rgb(var(--color-blue-4))",
            color: {
              linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 },
              stops: [
                [0, "rgb(var(--color-blue-4) / 50%)"],
                [1, "rgb(var(--color-bg-app))"], // end
              ],
            },
            lineWidth,
            ...areaOptions,
          }),
        )
      } else if (lineType === "line") {
        series.push(
          createSeriesOptions({
            type: lineType,
            data: data.map(dp => [getDate(dp).getTime(), getValue(dp)]),
            yAxis: 0,
            color: "rgb(var(--color-blue-4))",
            marker: {
              enabled: false,
            },
            lineWidth,
            ...lineOptions,
          }),
        )
      } else {
        series.push(
          createSeriesOptions({
            type: lineType,
            data: data.map(dp => [getDate(dp).getTime(), getValue(dp)]),
            yAxis: 0,
            color: "rgb(var(--color-blue-4))",
            marker: {
              enabled: false,
            },
            lineWidth,
            ...splineOptions,
          }),
        )
      }
    }

    return series
  }, [
    areaOptions,
    standaloneColumn,
    getColumnValue,
    getScatterValue,
    lineType,
    data,
    getDate,
    scatterOptions,
    getIsOutlier,
    lineWidth,
    areaSplineOptions,
    getValue,
    lineOptions,
    splineOptions,
  ])

  const options: Highcharts.Options = useMemo(() => {
    return {
      ...DEFAULT_HIGHCHART_OPTIONS,
      chart: createChartOptions({
        animation: false,
        interactive,
        spacing: hideAxes
          ? [0, 0, 0, 0]
          : Highcharts.defaultOptions.chart?.spacing,
        events: {
          load() {
            this.xAxis[0].setExtremes(rangeStartTime, rangeEndTime)
            setChart(this)
          },
        },
        marginLeft: hideAxes || hideYAxis ? 0 : undefined,
        marginRight: hideAxes || hideYAxis ? 0 : undefined,
      }),
      yAxis: [
        createYAxisOptions({
          visible: !hideAxes && !hideYAxis,
          axisTitle: hideAxisTitle ? undefined : columnAxisTitle,
          min: yAxisMin,
          max: yAxisMax,
          ceiling: yAxisMax,
          startOnTick: false,
          endOnTick: false,
          labels: {
            align: "left",
            x: -2, // overlap the y-axis slightly over the chart
            ...(formatYAxisLabels
              ? {
                  formatter() {
                    return formatYAxisLabels(this.value)
                  },
                }
              : {}),
          },
          ...overridesRef.current.yAxisOptions,
        }),
        createYAxisOptions({
          opposite: true,
          visible: false,
          axisTitle: hideAxisTitle ? undefined : yAxisTitle,
          min: yAxisMin,
          max: yAxisMax,
          ceiling: yAxisMax,
          startOnTick: false,
          endOnTick: false,
        }),
        createYAxisOptions({
          visible: false,
          axisTitle: undefined,
          height: "30%",
          top: "70%",
          min: standaloneColumn?.yAxisMin,
          max: standaloneColumn?.yAxisMax,
          ceiling: standaloneColumn?.yAxisMax,
          startOnTick: false,
          endOnTick: false,
        }),
      ],
      xAxis: createXAxisOptions({
        type: "datetime",
        visible: !hideAxes,
        axisTitle: hideAxisTitle ? undefined : xAxisTitle,
        dateTimeLabelFormats:
          data.length === 1
            ? {
                ...DEFAULT_DATE_TIME_LABEL_FORMATS,
                // Highcharts uses millisecond date time label format if there is only one point
                millisecond: singlePointDateLabelFormat,
              }
            : DEFAULT_DATE_TIME_LABEL_FORMATS,
        events: {
          afterSetExtremes(event) {
            setIsZoomed(
              event.userMin !== rangeStartTime ||
                event.userMax !== rangeEndTime,
            )
          },
          setExtremes(event) {
            if (event.trigger === "pan") {
              setIsPanning(true)
            }
          },
        },
        labels: {
          formatter() {
            return formatXAxisLabels
              ? formatXAxisLabels(this.value)
              : defaultDateFormatter(new Date(this.value))
          },
        },
        ...overridesRef.current.xAxisOptions,
      }),
      series,
      tooltip: createTooltipOptions({
        formatter() {
          // Never show 0 values
          setHoveredPoint(this.y === 0 ? undefined : this)
          return undefined
        },
        ...overridesRef.current.tooltip,
      }),
    }
  }, [
    interactive,
    hideAxes,
    hideYAxis,
    hideAxisTitle,
    columnAxisTitle,
    yAxisMin,
    yAxisMax,
    formatYAxisLabels,
    yAxisTitle,
    standaloneColumn?.yAxisMin,
    standaloneColumn?.yAxisMax,
    xAxisTitle,
    data.length,
    singlePointDateLabelFormat,
    series,
    rangeStartTime,
    rangeEndTime,
    setChart,
    setIsZoomed,
    setIsPanning,
    formatXAxisLabels,
    defaultDateFormatter,
    setHoveredPoint,
  ])

  const tooltipData =
    hoveredPoint &&
    (data[dataIndexMap.get(`${hoveredPoint.x}-${hoveredPoint.y}`) ?? -1] ??
      data[getPointIndex(hoveredPoint)])

  const resolvedTooltipDelay =
    (hoveredPoint?.series?.type === "scatter"
      ? scatterOptions?.tooltipDelay
      : hoveredPoint?.series?.type === "line"
        ? lineOptions?.tooltipDelay
        : tooltipDelay) ?? tooltipDelay

  useEffect(() => {
    if (!isMouseWithinElement(mousePosition, chartRef.current)) {
      setHoveredPoint(undefined)
    }
  }, [mousePosition, setHoveredPoint])

  return (
    <ChartContainer
      className={className}
      ref={mergedRef}
      onPointerLeave={() => {
        setHoveredPoint(undefined)
      }}
    >
      <HighchartsReact
        highcharts={Highcharts}
        options={options}
        ref={chartComponentRef}
      />
      {tooltipData && RenderTooltip && (
        <ChartTooltip
          content={
            <RenderTooltip
              item={tooltipData}
              matchedColumnData={
                standaloneColumn
                  ? getCorrespondingVolumeData(
                      getDate(tooltipData),
                      standaloneColumn,
                    )
                  : undefined
              }
              point={hoveredPoint}
            />
          }
          delay={resolvedTooltipDelay}
          pointType="marker"
        />
      )}
    </ChartContainer>
  )
}

const TimeSeriesChartWithContext = <T, S = undefined>({
  fallback,
  ...rest
}: TimeSeriesChartProps<T, S> & { fallback?: React.ReactNode }) => {
  return (
    <HydrationGuard
      fallback={
        fallback ?? <TimeSeriesChartSkeleton className={rest.className} />
      }
    >
      <ChartContextProvider>
        <TimeSeriesChartBase {...rest} />
      </ChartContextProvider>
    </HydrationGuard>
  )
}

export const TimeSeriesChart = Object.assign(TimeSeriesChartWithContext, {
  Skeleton: TimeSeriesChartSkeleton,
  EmptyState: TimeSeriesChartEmptyState,
})
