import { useRelativeTime } from '@/clientsettings/vars/relativeTime'
import { Button, ButtonGroup, Divider } from '@blueprintjs/core'
import { axisBottom, axisLeft } from 'd3-axis'
import { scalePoint, scaleTime } from 'd3-scale'
import { select } from 'd3-selection'
import type { FC } from 'react'
import { useContext, useEffect, useMemo, useRef, useState } from 'react'
import ExerciseContext from '../../ExerciseContext'
import useSVGDimensions from '../../useSVGDimensions'
import {
  DATA_ELEMENT_SIZE,
  DEFAULT_PLOT_MARGIN,
  LABEL_NOT_SELECTED_OPACITY,
  LABEL_SELECTED_OPACITY,
  MIN_TO_MS,
  multiFormat,
} from '../../utilities'
import DownloadSnapshot from '../DownloadSnapshot'
import type { ScatterPlotProps, UnresponsivePlotProps } from '../types'
import Brush from './Brush'

const DURATION_PADDING = 0.02

const ScatterPlot: FC<UnresponsivePlotProps<ScatterPlotProps>> = ({
  id,
  width,
  height: componentHeight,
  groupNames,
  selectedGroupName,
  dataRenderer,
  onResetSelection,
  legendItems,
}) => {
  const svgRef = useRef<SVGSVGElement>(null)
  const xAxisRef = useRef<SVGGElement>(null)
  const yAxisRef = useRef<SVGGElement>(null)
  const legendRef = useRef<SVGGElement>(null)

  const { exercise } = useContext(ExerciseContext)
  const timeRelative = useRelativeTime()

  const { width: yAxisWidth } = useSVGDimensions(yAxisRef, [groupNames])
  const { width: legendWidth, height: legendHeight } = useSVGDimensions(
    legendRef,
    [groupNames]
  )

  const margin = {
    left: DEFAULT_PLOT_MARGIN + yAxisWidth,
    right: legendItems
      ? DEFAULT_PLOT_MARGIN + legendWidth
      : DEFAULT_PLOT_MARGIN,
    top: DEFAULT_PLOT_MARGIN,
    bottom: DEFAULT_PLOT_MARGIN,
  }

  const height = Math.max(componentHeight - 30, 0)
  // bounds = area inside the graph axis = calculated by subtracting the margins
  const boundsWidth = Math.max(width - margin.right - margin.left, 0)
  const boundsHeight = Math.max(height - margin.top - margin.bottom, 0)

  const [startTime, setStartTime] = useState(0)
  const [endTime, setEndTime] = useState(0)

  const startTimeInitial = useMemo(
    () =>
      exercise.exerciseStart ? new Date(exercise.exerciseStart).getTime() : 0,
    [exercise]
  )
  const endTimeInitial = useMemo(
    () =>
      startTimeInitial + (exercise.config.exerciseDuration || 0) * MIN_TO_MS,
    [exercise.config.exerciseDuration, startTimeInitial]
  )

  const resetStartEndTime = () => {
    setStartTime(startTimeInitial)
    setEndTime(endTimeInitial)
  }

  useEffect(() => {
    setStartTime(startTimeInitial)
    setEndTime(endTimeInitial)
  }, [startTimeInitial, endTimeInitial])

  const duration = endTime - startTime

  const xScale = useMemo(
    () =>
      scaleTime()
        .domain([
          startTime - duration * DURATION_PADDING,
          endTime + duration * DURATION_PADDING,
        ])
        .range([0, boundsWidth]),
    [startTime, duration, endTime, boundsWidth]
  )
  const xScaleRelative = xScale
    .copy()
    .domain([
      startTime - startTimeInitial - duration * DURATION_PADDING,
      endTime - startTimeInitial + duration * DURATION_PADDING,
    ])

  const yScale = useMemo(
    () =>
      scalePoint()
        .domain(groupNames || [])
        .range([boundsHeight, 0])
        .padding(0.6),
    [boundsHeight, groupNames]
  )

  useEffect(() => {
    if (!xAxisRef.current || !yAxisRef.current) return

    const xAxisGenerator = (
      timeRelative
        ? axisBottom(xScaleRelative).tickValues(
            xScaleRelative.ticks().filter(tick => tick.getTime() >= 0)
          )
        : axisBottom(xScale)
    ).tickFormat(tick => multiFormat(new Date(tick.toString()), timeRelative))

    select(xAxisRef.current).call(xAxisGenerator)

    select(yAxisRef.current)
      .call(axisLeft(yScale))
      // highlight the label of the selected tool
      .selectAll('text')
      .data(groupNames || [])
      .style('opacity', groupName =>
        !selectedGroupName || groupName === selectedGroupName
          ? LABEL_SELECTED_OPACITY
          : LABEL_NOT_SELECTED_OPACITY
      )
  }, [
    groupNames,
    selectedGroupName,
    timeRelative,
    xScale,
    xScaleRelative,
    yScale,
  ])

  const grid = (groupNames || []).map(groupName => (
    <g key={groupName}>
      <line
        x1={0}
        x2={boundsWidth}
        y1={yScale(groupName)}
        y2={yScale(groupName)}
        stroke='#808080'
        opacity={
          (selectedGroupName === groupName
            ? LABEL_SELECTED_OPACITY
            : LABEL_NOT_SELECTED_OPACITY) / 2
        }
      />
    </g>
  ))

  const dotSize = Math.min(
    (boundsHeight / (groupNames || []).length) * 0.5,
    DATA_ELEMENT_SIZE
  )

  const legend = (
    <g
      ref={legendRef}
      transform={`translate(${
        margin.left + boundsWidth + margin.right - legendWidth
      }, \
        ${margin.top + boundsHeight - legendHeight})`}
    >
      {legendItems?.map((item, i) => (
        <g key={i} transform={`translate(0, ${i * 20})`}>
          {item}
        </g>
      ))}
    </g>
  )

  return (
    <>
      <svg
        id={id}
        ref={svgRef}
        style={{ fontFamily: 'sans-serif', display: 'block' }}
        width={width}
        height={height}
      >
        <g
          width={boundsWidth}
          height={boundsHeight}
          transform={`translate(${margin.left}, ${margin.top})`}
        >
          <defs>
            <clipPath id='clip'>
              <rect width={boundsWidth} height={boundsHeight} x={0} y={0} />
            </clipPath>
          </defs>
          {grid}
          <Brush
            setStartTime={setStartTime}
            setEndTime={setEndTime}
            height={boundsHeight}
            width={boundsWidth}
            xScale={xScale}
          />
          <g clipPath='url(#clip)'>
            {dataRenderer({ xScale, yScale, dotSize, height: boundsHeight })}
          </g>
          <g ref={xAxisRef} transform={`translate(0, ${boundsHeight})`} />
          <g ref={yAxisRef} />
        </g>
        {legendItems ? legend : <g />}
      </svg>

      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <ButtonGroup minimal>
          <Button onClick={resetStartEndTime}>Reset zoom</Button>

          {onResetSelection && (
            <>
              <Divider />
              <Button minimal onClick={onResetSelection}>
                Remove selection
              </Button>
            </>
          )}
        </ButtonGroup>

        <DownloadSnapshot elementId={id} />
      </div>
    </>
  )
}

export default ScatterPlot
