/**
 * Area chart component
 * @countermeasure/web-components/charts/area
 */

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

const DEFAULT_FILL_OPACITY = 0.2
const DEFAULT_LINE_WIDTH = 2

export class AreaChart extends SeriesChart {
  private readonly config: AreaChartConfig

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

  private computeDataBounds(visible: ResolvedSeries[]): {
    labels: string[]
    yMin: number
    yMax: number
  } {
    const labelsSet = new Set<string>()
    const stacked = this.config.stacked === true

    if (stacked) {
      const stackMap = new Map<string, number>()
      for (const s of visible) {
        for (const d of s.data) {
          const label = String(d.x)
          labelsSet.add(label)
          const current = stackMap.get(label) ?? 0
          stackMap.set(label, current + d.y)
        }
      }
      let yMax = 0
      for (const val of stackMap.values()) {
        if (val > yMax) yMax = val
      }
      return {
        labels: Array.from(labelsSet),
        yMin: 0,
        yMax: this.config.yAxis?.max ?? yMax,
      }
    }

    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 smooth = this.config.smooth === true
    const showLine = this.config.showLine !== false
    const fillOpacity = this.config.fillOpacity ?? DEFAULT_FILL_OPACITY
    const stacked = this.config.stacked === true

    // Compute stacked baselines if needed
    const baselineMap = new Map<string, number>()

    // Draw each visible series (in reverse order for stacking so first series is on top)
    const drawOrder = stacked ? [...visible].reverse() : visible

    for (const s of drawOrder) {
      if (s.data.length === 0) continue

      const points: { px: number; py: number; baseY: number }[] = []
      for (const d of s.data) {
        const label = String(d.x)
        const px = xScale(label) + bandOffset
        const stackBase = stacked ? (baselineMap.get(label) ?? 0) : 0
        const totalY = stackBase + d.y
        const py = yScale(totalY)
        const baseY = stacked ? yScale(stackBase) : plot.y + plot.height
        points.push({ px, py, baseY })
      }

      if (points.length === 0) continue
      const firstPoint = points[0]
      if (firstPoint === undefined) continue

      // Draw filled area
      this.ctx.beginPath()
      this.ctx.moveTo(firstPoint.px, firstPoint.baseY)
      this.ctx.lineTo(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)
          }
        }
      }

      // Close the area path by going back along the baseline
      const lastPoint = points[points.length - 1]
      if (lastPoint !== undefined) {
        this.ctx.lineTo(lastPoint.px, lastPoint.baseY)
      }

      // Walk baseline back for stacked areas
      if (stacked) {
        for (let i = points.length - 1; i >= 0; i--) {
          const pt = points[i]
          if (pt !== undefined) {
            this.ctx.lineTo(pt.px, pt.baseY)
          }
        }
      }

      this.ctx.closePath()
      this.ctx.fillStyle = hexToRgba(s.color, fillOpacity)
      this.ctx.fill()

      // Draw line on top of area
      if (showLine && points.length > 1) {
        this.ctx.beginPath()
        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 = DEFAULT_LINE_WIDTH
        this.ctx.lineJoin = 'round'
        this.ctx.lineCap = 'round'
        this.ctx.stroke()
      }

      // Update stacking baselines
      if (stacked) {
        for (const d of s.data) {
          const label = String(d.x)
          const current = baselineMap.get(label) ?? 0
          baselineMap.set(label, current + d.y)
        }
      }
    }

    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 { AreaChartConfig } from './types'
