/**
 * Line chart component
 * @countermeasure/web-components/charts/line
 */

import { SeriesChart } from '../series'
import type { ChartSeries } from '../types'
import { scaleLinear, scaleBand, hexToRgba, niceScale, drawSmoothPath } from '../utils'
import { drawXAxis, drawYAxis } from '../axis'
import type { LineChartConfig } from './types'

const DEFAULT_LINE_WIDTH = 2
const DEFAULT_DOT_RADIUS = 4

export class LineChart extends SeriesChart {
  private readonly config: LineChartConfig

  constructor(config: LineChartConfig) {
    super(config)
    this.config = config
    this.render()
  }

  private computeDataBounds(visible: { data: { x: number | string; y: number }[] }[]): {
    labels: string[]
    yMin: number
    yMax: number
  } {
    const labelsSet = new Set<string>()
    let yMin = Infinity
    let yMax = -Infinity

    for (const s of visible) {
      for (const d of s.data) {
        labelsSet.add(String(d.x))
        if (d.y < yMin) yMin = d.y
        if (d.y > yMax) yMax = d.y
      }
    }

    if (yMin === Infinity) {
      yMin = 0
      yMax = 1
    }

    return {
      labels: Array.from(labelsSet),
      yMin: this.config.yAxis?.min ?? yMin,
      yMax: this.config.yAxis?.max ?? yMax,
    }
  }

  render(): void {
    this.clear()

    const visible = this.getVisibleSeries()
    const { labels, yMin, yMax } = this.computeDataBounds(visible)
    const plot = this.plotArea
    const tickCount = this.config.yAxis?.tickCount ?? 5
    const nice = niceScale(yMin, yMax, tickCount)

    // Draw axes
    drawXAxis(this.ctx, plot, labels, this.config.xAxis)
    drawYAxis(this.ctx, plot, nice.min, nice.max, this.config.yAxis)

    // Create scales
    const xScale = scaleBand(labels, [plot.x, plot.x + plot.width], 0)
    const yScale = scaleLinear([nice.min, nice.max], [plot.y + plot.height, plot.y])
    const bandOffset = xScale.bandwidth() / 2

    const lineWidth = this.config.lineWidth ?? DEFAULT_LINE_WIDTH
    const smooth = this.config.smooth === true
    const showDots = this.config.showDots !== false
    const fillArea = this.config.fillArea === true

    // Draw each visible series
    for (const s of visible) {
      if (s.data.length === 0) continue

      const points: { px: number; py: number }[] = []
      for (const d of s.data) {
        const px = xScale(String(d.x)) + bandOffset
        const py = yScale(d.y)
        points.push({ px, py })
      }

      // Fill area below line
      if (fillArea && points.length > 1) {
        this.ctx.beginPath()
        const firstPoint = points[0]
        if (firstPoint !== undefined) {
          this.ctx.moveTo(firstPoint.px, firstPoint.py)
          if (smooth && points.length > 2) {
            drawSmoothPath(this.ctx, points)
          } else {
            for (let i = 1; i < points.length; i++) {
              const pt = points[i]
              if (pt !== undefined) {
                this.ctx.lineTo(pt.px, pt.py)
              }
            }
          }
          const lastPoint = points[points.length - 1]
          if (lastPoint !== undefined) {
            this.ctx.lineTo(lastPoint.px, plot.y + plot.height)
          }
          this.ctx.lineTo(firstPoint.px, plot.y + plot.height)
          this.ctx.closePath()
          this.ctx.fillStyle = hexToRgba(s.color, 0.15)
          this.ctx.fill()
        }
      }

      // Draw line
      if (points.length > 1) {
        this.ctx.beginPath()
        const firstPoint = points[0]
        if (firstPoint !== undefined) {
          this.ctx.moveTo(firstPoint.px, firstPoint.py)
          if (smooth && points.length > 2) {
            drawSmoothPath(this.ctx, points)
          } else {
            for (let i = 1; i < points.length; i++) {
              const pt = points[i]
              if (pt !== undefined) {
                this.ctx.lineTo(pt.px, pt.py)
              }
            }
          }
        }
        this.ctx.strokeStyle = s.color
        this.ctx.lineWidth = lineWidth
        this.ctx.lineJoin = 'round'
        this.ctx.lineCap = 'round'
        this.ctx.stroke()
      }

      // Draw dots
      if (showDots) {
        for (const pt of points) {
          this.ctx.beginPath()
          this.ctx.arc(pt.px, pt.py, DEFAULT_DOT_RADIUS, 0, Math.PI * 2)
          this.ctx.fillStyle = s.color
          this.ctx.fill()
          this.ctx.strokeStyle = '#ffffff'
          this.ctx.lineWidth = 1.5
          this.ctx.stroke()
        }
      }
    }

    this.updateSeriesLegend()
  }

  protected handleMouseMove(e: MouseEvent): void {
    if (this.tooltip === null) return

    const rect = this.canvas.getBoundingClientRect()
    const mouseX = e.clientX - rect.left
    const mouseY = e.clientY - rect.top
    const plot = this.plotArea

    if (
      mouseX < plot.x ||
      mouseX > plot.x + plot.width ||
      mouseY < plot.y ||
      mouseY > plot.y + plot.height
    ) {
      this.tooltip.hide()
      return
    }

    const visible = this.getVisibleSeries()
    const { labels, yMin, yMax } = this.computeDataBounds(visible)
    const tickCount = this.config.yAxis?.tickCount ?? 5
    const nice = niceScale(yMin, yMax, tickCount)
    const xScale = scaleBand(labels, [plot.x, plot.x + plot.width], 0)
    const yScale = scaleLinear([nice.min, nice.max], [plot.y + plot.height, plot.y])
    const bandOffset = xScale.bandwidth() / 2

    let closestDist = Infinity
    let closestContent = ''
    let closestX = 0
    let closestY = 0

    for (const s of visible) {
      for (const d of s.data) {
        const px = xScale(String(d.x)) + bandOffset
        const py = yScale(d.y)
        const dist = Math.sqrt((mouseX - px) ** 2 + (mouseY - py) ** 2)
        if (dist < closestDist) {
          closestDist = dist
          closestX = px
          closestY = py
          if (this.config.tooltip?.format !== undefined) {
            closestContent = this.config.tooltip.format(d.y, String(d.x), s.name)
          } else {
            closestContent = `${s.name}: ${String(d.y)}`
          }
        }
      }
    }

    if (closestDist < 30) {
      this.tooltip.show(closestX, closestY, closestContent)
    } else {
      this.tooltip.hide()
    }
  }

  protected handleMouseLeave(): void {
    if (this.tooltip !== null) {
      this.tooltip.hide()
    }
  }

  public update(series: ChartSeries[]): void {
    this.series = this.resolveSeries(series)
    this.render()
  }
}

export type { LineChartConfig } from './types'
