import React, {
  useState,
  useEffect,
  useMemo,
  useCallback,
  CSSProperties,
  SVGAttributes,
  useRef,
} from 'react'
import { NodeGroup } from 'react-move'
import { easeCubic } from 'd3-ease'
import { interpolate } from 'd3-interpolate'
import { useAtom } from 'jotai'
import {
  useGG,
  focusNodes,
  unfocusNodes,
  EventArea,
  themeState,
  zoomState,
  xScaleState,
  yScaleState,
  isDate,
  defineGroupAccessor,
  BrushAction,
  usePageVisibility,
} from '@graphique/graphique'
import { Entrance, type GeomAes } from './types'
import { Tooltip } from './tooltip'

export interface GeomProps<Datum> {
  /**
   * **data used by this Geom**
   *
   * This will overwrite top-level `data` passed to `GG` as it relates to mappings defined in `aes`.
   */
  data?: Datum[]
  /**
   * **functional mapping applied to `data` for this Geom**
   *
   * This extends the top-level `aes` passed to `GG`. Any repeated mappings defined here will take precedence within the Geom.
   */
  aes?: GeomAes<Datum>
  /** attributes passed to the underlying SVG elements */
  attr?: SVGAttributes<SVGTextElement>
  /** should this Geom have a tooltip associated with it (_default_: `false`) */
  showTooltip?: boolean
  /** determines what happens when brushing (clicking and dragging) over the drawing area  */
  brushAction?: BrushAction
  /** where elements should start as they enter the drawing area (_default_: `Entrance.BOTTOM`) */
  entrance?: Entrance
  /** should elements be strictly clipped at the bounds of the drawing area (_default_: `false`) */
  isClipped?: boolean
  /** array of keys (of the kind that are generated by `aes.key`) used to programmatically focus associated points */
  focusedKeys?: string[]
  /** styles applied to focused elements */
  focusedStyle?: CSSProperties
  /** styles applied to unfocused elements */
  unfocusedStyle?: CSSProperties
  /** callback called for mousemove events on the drawing area when focusing data */
  onDatumFocus?: (data: Datum[], index: number[]) => void
  /** callback called for click events on the drawing area when selecting focused data */
  onDatumSelection?: (data: Datum[], index: number[]) => void
  /** callback called for mouseleave events on the drawing area */
  onExit?: () => void
  /** should elements enter/update/exit with animated transitions (_default_: `true`) */
  isAnimated?: boolean
}

const GeomLabel = <Datum,>({
  data: localData,
  aes: localAes,
  attr,
  focusedStyle,
  unfocusedStyle,
  focusedKeys = [],
  onDatumFocus,
  onDatumSelection,
  entrance = Entrance.BOTTOM,
  onExit,
  showTooltip = false,
  brushAction,
  isClipped = false,
  isAnimated = true,
}: GeomProps<Datum>) => {
  const { ggState } = useGG<Datum>() || {}
  const { id, data, aes, scales, copiedScales, width, height, margin } =
    ggState || { width: 0 }

  const [theme, setTheme] = useAtom(themeState)
  const [{ xDomain: xZoomDomain, yDomain: yZoomDomain }] = useAtom(zoomState)
  const [{ isFixed: isFixedX }] = useAtom(xScaleState)
  const [{ isFixed: isFixedY }] = useAtom(yScaleState)

  const isVisible = usePageVisibility()

  const baseAttr: SVGAttributes<SVGTextElement> = {
    fillOpacity: 1,
    strokeOpacity: 1,
    strokeWidth: 3,
    dy: 3.8,
  }

  const geomAttr: SVGAttributes<SVGTextElement> = {
    ...baseAttr,
    ...attr,
  }

  const { defaultFill, animationDuration: duration, font } = theme

  const initialGeomData = useMemo(() => localData || data, [data, localData])

  const geomAes = useMemo(() => {
    if (localAes) {
      return {
        ...aes,
        ...localAes,
      }
    }
    return aes
  }, [aes, localAes])

  const group = useMemo(
    () => geomAes && defineGroupAccessor(geomAes),
    [geomAes, defineGroupAccessor],
  )

  const keyAccessor = useCallback(
    (d: Datum) =>
      geomAes?.key
        ? geomAes.key(d)
        : (`${geomAes?.x && geomAes.x(d)}-${geomAes?.y && geomAes.y(d)}-${
            group && group(d)
          }` as string),
    [geomAes, group],
  )

  const getLabel = useMemo(() => {
    if (!geomAes?.label)
      throw new Error('GeomLabel needs a label mapped in `aes.label`')

    return geomAes?.label ?? (() => '')
  }, [geomAes])

  const undefinedX = useMemo(
    () =>
      initialGeomData
        ? initialGeomData.filter(
            (d) =>
              geomAes?.x &&
              (geomAes.x(d) === null ||
                typeof geomAes.x(d) === 'undefined' ||
                (isDate(geomAes.x(d)) &&
                  Number.isNaN(geomAes.x(d)?.valueOf()))),
          )
        : [],
    [initialGeomData, geomAes],
  )
  const undefinedY = useMemo(
    () =>
      initialGeomData
        ? initialGeomData.filter(
            (d) =>
              geomAes?.y &&
              (geomAes.y(d) === null || typeof geomAes.y(d) === 'undefined'),
          )
        : [],
    [initialGeomData],
  )

  const geomData = useMemo(() => {
    const presentData = initialGeomData?.filter(
      (d) =>
        geomAes?.x &&
        geomAes?.x(d) !== null &&
        !(typeof geomAes?.x(d) === 'undefined') &&
        (isDate(geomAes?.x(d))
          ? !Number.isNaN(geomAes?.x(d)?.valueOf())
          : true) &&
        geomAes.y &&
        geomAes.y(d) !== null &&
        !(typeof geomAes.y(d) === 'undefined'),
    )

    const uniqueKeyVals = Array.from(
      new Set(presentData?.map((d) => keyAccessor(d))),
    )

    return uniqueKeyVals.flatMap((k) => {
      const dataWithKey = presentData?.filter((d) => keyAccessor(d) === k)
      if (dataWithKey && dataWithKey.length > 1) {
        return dataWithKey.map((dk: any, i) => ({
          ...dk,
          gg_gen_index: i,
        }))
      }
      return dataWithKey?.flat()
    }) as Datum[]
  }, [initialGeomData, keyAccessor])

  const [firstRender, setFirstRender] = useState(true)
  useEffect(() => {
    const timeout = setTimeout(() => setFirstRender(false), 0)
    return () => clearTimeout(timeout)
  }, [])

  useEffect(() => {
    if (firstRender && undefinedX.length > 0) {
      console.warn(
        `Ignoring ${undefinedX.length} labels with missing x values.`,
      )
    }

    if (firstRender && undefinedY.length > 0) {
      console.warn(
        `Ignoring ${undefinedY.length} labels with missing y values.`,
      )
    }
  }, [firstRender, undefinedX, undefinedY])

  const bottomPos = useMemo(
    () => (height && margin ? height - margin.bottom : undefined),
    [height, margin],
  )

  useEffect(() => {
    setTheme((prev) => ({
      ...prev,
      geoms: {
        ...prev.geoms,
        label: {
          fillOpacity: geomAttr?.style?.fillOpacity || geomAttr?.fillOpacity,
          stroke: geomAttr?.stroke,
          strokeWidth: geomAttr?.style?.strokeWidth || geomAttr?.strokeWidth,
          strokeOpacity:
            geomAttr?.style?.strokeOpacity || geomAttr?.strokeOpacity,
        },
      },
    }))
  }, [attr, setTheme])

  const baseStyles: CSSProperties = {
    transition: 'fill-opacity 200ms',
    fillOpacity: geomAttr.fillOpacity,
    strokeOpacity: geomAttr.strokeOpacity,
    ...geomAttr.style,
  }

  const focusedStyles: CSSProperties = {
    ...baseStyles,
    ...focusedStyle,
  }

  const unfocusedStyles: CSSProperties = {
    ...baseStyles,
    fillOpacity: 0.2,
    strokeOpacity: 0.2,
    ...unfocusedStyle,
  }

  const fill = useMemo(
    () => (d: Datum) =>
      geomAttr.fill ||
      (geomAes?.fill && copiedScales?.fillScale
        ? (copiedScales.fillScale(geomAes.fill(d)) as string | undefined)
        : defaultFill),
    [geomAes, copiedScales, geomAttr, defaultFill],
  )

  const getStroke = useMemo(
    () => (d: Datum) =>
      geomAttr.stroke ||
      (geomAes?.stroke && copiedScales?.strokeScale
        ? (copiedScales.strokeScale(geomAes.stroke(d)) as string | undefined)
        : '#fff'),
    [geomAes, copiedScales, geomAttr],
  )

  const x = useMemo(() => {
    if (scales?.xScale.bandwidth) {
      return (d: Datum) =>
        (scales?.xScale(geomAes?.x && geomAes.x(d)) || 0) +
        scales?.xScale.bandwidth() / 2 +
        0.9
    }
    return (d: Datum) =>
      scales?.xScale && geomAes?.x && (scales.xScale(geomAes.x(d)) || 0)
  }, [scales, geomAes])

  const y = useMemo(() => {
    if (scales?.yScale.bandwidth) {
      return (d: Datum) =>
        (scales?.yScale(geomAes?.y && geomAes.y(d)) || 0) +
        scales?.yScale.bandwidth() / 2
    }
    return (d: Datum) =>
      scales?.yScale && geomAes?.y && (scales.yScale(geomAes.y(d)) || 0)
  }, [scales, geomAes])

  const groupRef = useRef<SVGGElement>(null)
  const texts = groupRef.current?.getElementsByTagName('text')

  const [shouldClip, setShouldClip] = useState(isClipped)
  useEffect(() => {
    if (xZoomDomain?.current || yZoomDomain?.current) {
      setShouldClip(true)
    } else {
      const timeout = setTimeout(() => setShouldClip(isClipped), duration)
      return () => clearTimeout(timeout)
    }
    return undefined
  }, [isFixedX, isFixedY, xZoomDomain?.current, yZoomDomain?.current, duration])

  return (
    <>
      <g
        ref={groupRef}
        clipPath={shouldClip ? `url(#__gg_canvas_${id})` : undefined}
      >
        {!firstRender && isVisible && (
          <NodeGroup
            data={[...geomData]}
            keyAccessor={(d) =>
              geomAes?.key
                ? keyAccessor(d)
                : `${keyAccessor(d)}-${d.gg_gen_index}`
            }
            start={(d) => ({
              x: x(d),
              y: entrance === Entrance.DATA ? y(d) : bottomPos,
              fill: 'transparent',
              stroke: 'transparent',
            })}
            enter={(d) => ({
              x: isAnimated ? [x(d)] : x(d),
              y: isAnimated ? [y(d)] : y(d),
              fill: isAnimated ? [fill(d)] : fill(d),
              stroke: isAnimated ? [getStroke(d)] : getStroke(d),
              timing: { duration, ease: easeCubic },
            })}
            update={(d) => ({
              x: isAnimated ? [x(d)] : x(d),
              y: isAnimated ? [y(d)] : y(d),
              fill: isAnimated ? [fill(d)] : fill(d),
              stroke: isAnimated ? [getStroke(d)] : getStroke(d),
              timing: { duration, ease: easeCubic },
            })}
            leave={() => ({
              fill: isAnimated ? ['transparent'] : 'transparent',
              stroke: isAnimated ? ['transparent'] : 'transparent',
              y: isAnimated ? [bottomPos] : bottomPos,
              timing: { duration, ease: easeCubic },
            })}
            interpolation={(begVal, endVal) => interpolate(begVal, endVal)}
          >
            {(nodes) => (
              <>
                {nodes.map(({ state, key, data: nodeData }) => {
                  let styles = {}
                  if (focusedKeys.includes(key)) styles = focusedStyles
                  if (focusedKeys?.length > 0 && !focusedKeys.includes(key))
                    styles = unfocusedStyles

                  const nodeX = x(nodeData) ?? 0

                  return (
                    <text
                      key={key}
                      // eslint-disable-next-line react/jsx-props-no-spreading
                      {...attr}
                      fillOpacity={state.fillOpacity}
                      strokeOpacity={state.strokeOpacity}
                      stroke={state.stroke}
                      fill={state.fill}
                      strokeWidth={geomAttr.strokeWidth}
                      paintOrder="stroke"
                      pointerEvents="none"
                      textAnchor={
                        geomAttr.textAnchor ??
                        (nodeX > width / 2 ? 'end' : undefined)
                      }
                      dx={geomAttr?.dx ?? (nodeX > width / 2 ? -7 : 7)}
                      dy={geomAttr?.dy}
                      x={state.x}
                      y={state.y}
                      style={{
                        pointerEvents: 'none',
                        fontFamily:
                          geomAttr?.style?.fontFamily ??
                          geomAttr?.fontFamily ??
                          font?.family ??
                          '-apple-system, sans-serif',
                        fontSize: geomAttr?.fontSize ?? 11,
                        fontWeight: geomAttr?.fontWeight ?? 600,
                        strokeLinecap: 'round',
                        strokeLinejoin: 'round',
                        ...baseStyles,
                        ...styles,
                      }}
                      data-testid="__gg_geom_label"
                    >
                      {getLabel(nodeData)}
                    </text>
                  )
                })}
              </>
            )}
          </NodeGroup>
        )}
      </g>
      {(showTooltip || brushAction) && geomAes && (
        <>
          <EventArea
            data={geomData}
            showTooltip={showTooltip}
            brushAction={brushAction}
            aes={geomAes}
            x={x}
            y={y}
            onDatumFocus={onDatumFocus}
            onMouseOver={({ i }: { d: Datum[]; i: number[] }) => {
              const focusedIndexes = geomData.flatMap((gd, fi) =>
                focusedKeys.includes(keyAccessor(gd)) ? fi : [],
              )

              if (texts) {
                focusNodes({
                  nodes: texts,
                  focusedIndex: [...focusedIndexes, ...[i].flat()],
                  focusedStyles,
                  unfocusedStyles,
                })
              }
            }}
            onClick={
              onDatumSelection
                ? ({ d, i }: { d: Datum[]; i: number[] }) => {
                    onDatumSelection(d, i)
                  }
                : undefined
            }
            onMouseLeave={() => {
              if (texts) {
                if (showTooltip) {
                  unfocusNodes({ nodes: texts, baseStyles })
                  if (focusedKeys?.length > 0) {
                    focusNodes({
                      nodes: texts,
                      focusedIndex: geomData.flatMap((d, i) =>
                        focusedKeys.includes(keyAccessor(d)) ? i : [],
                      ),
                      focusedStyles,
                      unfocusedStyles,
                    })
                  }
                }
              }

              if (onExit) onExit()
            }}
          />
          {showTooltip && <Tooltip aes={geomAes} group={group} />}
        </>
      )}
    </>
  )
}

GeomLabel.displayName = 'GeomLabel'
export { GeomLabel }
