import { ScaleLinear } from 'd3-scale'
import { processFontFamily } from 'expo-font'
import React from 'react'
import { G, Text } from 'react-native-svg'

import { baseText } from '../../../Styles'
import { formatCountShort } from '../../../util'

interface AxisRange {
  min?: number
  max?: number
}

export interface AxisConfig {
  // The range over which the axis renders labels. This does not have to fully
  // contain "scale", but instead can be used to pad or clip the axis if
  // necessary
  bounds?: AxisRange
  // Instead of using generated labels based on "scale", use provided optional
  // min or max values, e.g. if scale was [0, 100], yielding ticks [0, 50, 100],
  // providing {min: 1} would instead yield ticks [1, 50, 100].
  fixedLabels?: AxisRange
  // Name of the axis displayed to the user
  legendLabel?: string
  // Color of the axis labels and ticks
  color?: string
  // Do not render the axis or ticks
  hidden?: boolean
  // Reverse values along this axis
  reverse?: boolean
}

export type AxisTicks = [number, number][]

export function scaleToTicks(
  scale: ScaleLinear<number, number>,
  config?: AxisConfig
): AxisTicks {
  const domain = scale.domain()
  // Domain _may_ be more than two items, so we just play it safe and use
  // max/min to find the delta instead of deconstructing into [a, b]
  const domainMin = Math.min(...domain)
  const domainMax = Math.max(...domain)
  const delta = Math.abs(domainMax - domainMin)
  // In case the domain is less than the number of ticks, we reduce the number
  // of ticks to avoid situation with fractional ticks, e.g. domain [1, 2] with
  // 5 ticks would yield [1, 1, 2, 2, 2], with the clamp, we get ticks [1, 2]
  const clampedTicks = Math.min(delta, 4)
  const tickValues = scale
    .ticks(clampedTicks)
    .reverse()
    // First tick may be overridden. E.g. when zero is not a valid label for
    // playlist positions, we will use provided minimum tick label instead.
    // Other ticks are still computed for readability by D3.
    .map<[number, number | null]>((tickValue) => {
      if (config?.fixedLabels?.min && tickValue === domainMin) {
        const value = config?.fixedLabels?.min ?? tickValue
        return [value, scale(value) ?? null]
      } else if (config?.fixedLabels?.max && tickValue === domainMax) {
        const value = config?.fixedLabels?.max ?? tickValue
        return [value, scale(value) ?? null]
      } else {
        return [tickValue, scale(tickValue) ?? null]
      }
    })
    .filter((tick): tick is [number, number] => tick[1] !== null)

  return tickValues
}

// Fits "888M" at font size 12 into the gutter and has 2 padding reserved for
// the plot-side.
export const YAxisWidth = 34 + 2

const YAxisLabelFontSize = 12

const labelPadding = 2

export const YAxisLabelPadding = (hasAxisLabels: boolean) =>
  hasAxisLabels ? YAxisLabelFontSize / labelPadding : 0

export const YAxis: React.FC<{
  translateX: number
  ticks: AxisTicks
  side: 'left' | 'right'
  color: string
  // If config is not provided, label defaults will be inferred from "scale"
  config?: AxisConfig
}> = (props) => (
  <G
    translateX={
      props.translateX +
      (props.side === 'right' ? labelPadding : YAxisWidth - labelPadding)
    }
  >
    {props.ticks.map(([value, position], index) => {
      // Label at the bottom of the figure is flush with the container, other
      // labels are centered with the point position
      const isLastTick = index === props.ticks.length - 1

      const fontFamily = processFontFamily(baseText.fontFamily) ?? undefined

      return (
        <G key={index} translateY={position}>
          <Text
            fontSize={YAxisLabelFontSize}
            fill={props.color}
            fontWeight="bold"
            textAnchor={props.side === 'left' ? 'end' : 'start'}
            alignmentBaseline={isLastTick ? 'text-bottom' : 'middle'}
            fontFamily={fontFamily}
          >
            {formatCountShort(value)}
          </Text>
        </G>
      )
    })}
  </G>
)
