import { ScaleLinear, scaleLinear } from 'd3-scale'
import { curveLinear } from 'd3-shape'
import React from 'react'
import { GestureResponderEvent } from 'react-native'
import { Line } from 'react-native-svg'

import { colors } from '../../Colors'
import { useComponentLayout } from '../../hooks/useComponentLayout'
import { DataStatus } from '../../types/types'
import { notEmpty } from '../../util'
import {
  AxisConfig,
  scaleToTicks,
  YAxis,
  YAxisLabelPadding,
  YAxisWidth,
} from './ornaments/Axis'
import { Grid } from './ornaments/Grid'
import { StripedPattern } from './ornaments/StripedPattern'
import { BarPlot, BarStyle, barStyleFn } from './plots/BarPlot'
import { Curve, LinePlot } from './plots/LinePlot'
import { Figure, useFigure } from './useFigure'

function isBarPlot<T>(plot: PlotConfig<T>): plot is BarPlotConfig<T> {
  return plot.type === 'bar'
}

function isLinePlot<T>(plot: PlotConfig<T>): plot is LinePlotConfig<T> {
  return plot.type === 'line'
}

export type FigureAxisConfig = { right?: AxisConfig; left?: AxisConfig }

export type AxisSide = keyof FigureAxisConfig

interface BarPlotConfig<T> {
  type: 'bar'
  data: T[]
  axisSide: AxisSide
  barStyle?: BarStyle | barStyleFn<T>
  barWidth: number
  color: string
}

interface LinePlotConfig<T> {
  type: 'line'
  data: T[]
  axisSide: AxisSide
  lineStrokeWidth: number
  lineStrokeColor?: string
  color: string
  curve?: Curve
}

export type PlotConfig<T> = BarPlotConfig<T> | LinePlotConfig<T>

// Makes a new ScaleLinear that spans from min to max of input scales
function unionScales(
  scales: ScaleLinear<number, number>[],
  reverse: boolean
): ScaleLinear<number, number> {
  const domains = scales.flatMap((scale) => scale.domain())
  const min = Math.min(...domains)
  const max = Math.max(...domains)
  return scaleLinear().domain(reverse ? [max, min] : [min, max])
}

export function BarAndLineFigure<T>({
  plots,
  axisConfig,
  valueExtractor,
  statusExtractor,
  onPressBar,
  selectedBarIndex,
  grid,
  centered,
}: {
  plots: PlotConfig<T>[]
  axisConfig?: FigureAxisConfig
  valueExtractor: (v: T) => number | null
  statusExtractor?: (item: T) => DataStatus
  onPressBar?: (event: GestureResponderEvent, v: T, i: number) => void
  selectedBarIndex?: number
  grid?: boolean
  centered?: boolean
}): React.ReactElement | null {
  const maxBarWidth =
    plots
      .filter(isBarPlot)
      .map(({ barWidth }) => barWidth)
      .sort()
      .reverse()[0] ?? 0

  const hasLeftPlot = plots.findIndex((plot) => plot.axisSide === 'left') !== -1
  const hasRightPlot =
    plots.findIndex((plot) => plot.axisSide === 'right') !== -1

  const visibleLeftAxis = hasLeftPlot && !(axisConfig?.left?.hidden === true)
  const visibleRightAxis = hasRightPlot && !(axisConfig?.right?.hidden === true)

  const maxLineStrokeWidth =
    plots
      .filter(isLinePlot)
      .map(({ lineStrokeWidth }) => lineStrokeWidth)
      .sort()
      .reverse()[0] ?? 0

  const halfBarWidth = maxBarWidth / 2

  const plotPadding = {
    left: halfBarWidth + (visibleLeftAxis || centered ? YAxisWidth : 0),
    right: halfBarWidth + (visibleRightAxis ? YAxisWidth : 0),
    top: 0 /* gridPadding(hasGrid) // If grid is implemented for this figure */,
    // Plot at the very top of the figure will not get clipped
    bottom: maxLineStrokeWidth / 2 + YAxisLabelPadding(true),
  }

  const hasLegendLabels = {
    rightAxis: axisConfig?.right?.legendLabel !== undefined,
    leftAxis: axisConfig?.left?.legendLabel !== undefined,
  }

  const [onLayout, layout] = useComponentLayout({
    width: 0,
    height: 0,
    x: 0,
    y: 0,
  })

  const figureProps = useFigure({
    size: layout,
    dataSets: plots.map(({ data, axisSide }) => {
      const allNonNullData = data.map(valueExtractor).filter(notEmpty)
      const dataDomain = [
        Math.min(...allNonNullData),
        Math.max(...allNonNullData),
      ]

      const config = axisConfig?.[axisSide]

      const axisMax = config?.bounds?.max ?? dataDomain[1]
      const axisMin = config?.bounds?.min ?? 0
      return {
        domain: config?.reverse ? [axisMax, axisMin] : [axisMin, axisMax],
        dataLength: data.length,
      }
    }),
    plotPadding,
  })

  if (layout == null || figureProps == null) {
    return null
  }

  const { contentSize, padding, scaleX, scaleY } = figureProps

  // Take union of plot scales, grouped by the axis they map to
  const range = [contentSize.height - plotPadding.top, plotPadding.bottom]
  const leftScale = unionScales(
    scaleY.filter((_, i) => plots[i].axisSide === 'left'),
    axisConfig?.left?.reverse === true
  ).range(range)
  const rightScale = unionScales(
    scaleY.filter((_, i) => plots[i].axisSide === 'right'),
    axisConfig?.right?.reverse === true
  ).range(range)

  return (
    <Figure
      onLayout={onLayout}
      size={layout}
      patterns={<StripedPattern />}
      padding={padding}
    >
      {visibleLeftAxis ? (
        <YAxis
          translateX={0}
          ticks={scaleToTicks(leftScale, axisConfig?.left)}
          color={axisConfig?.left?.color ?? colors.offWhite}
          side={hasLegendLabels.leftAxis ? 'right' : 'left'}
        />
      ) : null}
      {grid != null ? (
        <Grid
          translateX={hasLeftPlot || centered ? YAxisWidth : 0}
          size={{
            ...contentSize,
            width:
              contentSize.width -
              (centered || (hasLeftPlot && hasRightPlot)
                ? YAxisWidth * 2
                : hasLeftPlot || hasRightPlot
                ? YAxisWidth
                : 0),
          }}
          ticks={scaleToTicks(scaleY[0], axisConfig?.right ?? axisConfig?.left)} //TODO: make grid configurable per axis
        />
      ) : null}
      {plots.map((plot: PlotConfig<T>, i: number) =>
        plot.type === 'bar' ? (
          <BarPlot
            key={i}
            data={plot.data}
            size={contentSize}
            scaleX={scaleX[i]}
            scaleY={plot.axisSide === 'left' ? leftScale : rightScale}
            valueExtractor={valueExtractor}
            statusExtractor={statusExtractor}
            barStyle={{ ...plot.barStyle, fill: plot.color }}
            barWidth={plot.barWidth}
            onPressBar={onPressBar}
          />
        ) : (
          <LinePlot
            key={i}
            data={plot.data}
            scaleX={scaleX[i]}
            scaleY={plot.axisSide === 'left' ? leftScale : rightScale}
            strokeColor={plot.lineStrokeColor ?? plot.color}
            strokeWidth={plot.lineStrokeWidth}
            valueExtractor={valueExtractor}
            curve={plot.curve ?? curveLinear}
          />
        )
      )}

      {selectedBarIndex != null && selectedBarIndex >= 0 ? (
        <Line
          x={scaleX[0](selectedBarIndex)}
          y1={0}
          y2={contentSize.width}
          stroke={colors.white}
          strokeWidth={2}
        />
      ) : null}
      {visibleRightAxis ? (
        <YAxis
          translateX={layout.width - plotPadding.right}
          ticks={scaleToTicks(rightScale, axisConfig?.right)}
          side={hasLegendLabels.rightAxis ? 'left' : 'right'}
          color={axisConfig?.right?.color ?? colors.offWhite}
        />
      ) : null}
    </Figure>
  )
}
