/**
 * Bar chart component
 * @countermeasure/web-components/charts/bar
 */

import { SeriesChart } from '../series'
import type { ChartSeries, PlotArea, ResolvedSeries } from '../types'
import { scaleLinear, scaleBand, niceScale, drawFilledBar, hitTestRect } from '../utils'
import { drawXAxis, drawYAxis } from '../axis'
import type { BarChartConfig } from './types'

const DEFAULT_BAR_GAP = 0.1
const DEFAULT_BAR_RADIUS = 0

interface BarRect {
  x: number
  y: number
  width: number
  height: number
  color: string
  seriesName: string
  label: string
  value: number
}

export class BarChart extends SeriesChart {
  private readonly config: BarChartConfig
  private barRects: BarRect[]
  private hoveredBar: BarRect | null

  constructor(config: BarChartConfig) {
    super(config)
    this.config = config
    this.barRects = []
    this.hoveredBar = null
    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 = 0
    let yMax = 0
    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
      }
    }

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

  render(): void {
    this.clear()
    this.barRects = []

    const visible = this.getVisibleSeries()
    const { labels, yMin, yMax } = this.computeDataBounds(visible)
    const plot = this.plotArea
    const horizontal = this.config.horizontal === true
    const stacked = this.config.stacked === true
    const tickCount = this.config.yAxis?.tickCount ?? 5
    const nice = niceScale(yMin, yMax, tickCount)
    const barRadius = this.config.barRadius ?? DEFAULT_BAR_RADIUS
    const barGap = this.config.barGap ?? DEFAULT_BAR_GAP

    if (horizontal) {
      this.renderHorizontal(visible, labels, nice, plot, stacked, barGap, barRadius)
    } else {
      this.renderVertical(visible, labels, nice, plot, stacked, barGap, barRadius)
    }

    this.updateSeriesLegend()
  }

  private renderVertical(
    visible: ResolvedSeries[],
    labels: string[],
    nice: { min: number; max: number; step: number },
    plot: PlotArea,
    stacked: boolean,
    barGap: number,
    barRadius: number
  ): void {
    drawXAxis(this.ctx, plot, labels, this.config.xAxis)
    drawYAxis(this.ctx, plot, nice.min, nice.max, this.config.yAxis)

    const xScale = scaleBand(labels, [plot.x, plot.x + plot.width], 4)
    const yScale = scaleLinear([nice.min, nice.max], [plot.y + plot.height, plot.y])
    const bandwidth = xScale.bandwidth()
    const seriesCount = visible.length
    const baseline = yScale(0)

    if (stacked) {
      for (const label of labels) {
        let cumY = 0
        for (const s of visible) {
          const dp = s.data.find(d => String(d.x) === label)
          const value = dp?.y ?? 0
          const barBottom = yScale(cumY)
          const barTop = yScale(cumY + value)
          const barX = xScale(label)
          const barHeight = barBottom - barTop
          const rect: BarRect = {
            x: barX,
            y: barTop,
            width: bandwidth,
            height: barHeight,
            color: s.color,
            seriesName: s.name,
            label,
            value,
          }
          this.barRects.push(rect)
          drawFilledBar(
            this.ctx,
            rect.x,
            rect.y,
            rect.width,
            rect.height,
            barRadius,
            rect.color,
            this.isBarHovered(rect)
          )
          cumY += value
        }
      }
    } else {
      const groupWidth = bandwidth * (1 - barGap)
      const singleBarWidth = seriesCount > 0 ? groupWidth / seriesCount : groupWidth
      const groupOffset = (bandwidth - groupWidth) / 2

      for (const label of labels) {
        for (let si = 0; si < visible.length; si++) {
          const s = visible[si]
          if (s === undefined) continue
          const dp = s.data.find(d => String(d.x) === label)
          const value = dp?.y ?? 0
          const barX = xScale(label) + groupOffset + si * singleBarWidth
          const barY = value >= 0 ? yScale(value) : baseline
          const barH = Math.abs(yScale(value) - baseline)
          const rect: BarRect = {
            x: barX,
            y: barY,
            width: singleBarWidth,
            height: barH,
            color: s.color,
            seriesName: s.name,
            label,
            value,
          }
          this.barRects.push(rect)
          drawFilledBar(
            this.ctx,
            rect.x,
            rect.y,
            rect.width,
            rect.height,
            barRadius,
            rect.color,
            this.isBarHovered(rect)
          )
        }
      }
    }
  }

  private renderHorizontal(
    visible: ResolvedSeries[],
    labels: string[],
    nice: { min: number; max: number; step: number },
    plot: PlotArea,
    stacked: boolean,
    barGap: number,
    barRadius: number
  ): void {
    // For horizontal, x-axis shows values, y-axis shows categories
    drawYAxis(this.ctx, plot, nice.min, nice.max, this.config.xAxis)
    drawXAxis(this.ctx, plot, labels, this.config.yAxis)

    const yScale = scaleBand(labels, [plot.y, plot.y + plot.height], 4)
    const xScale = scaleLinear([nice.min, nice.max], [plot.x, plot.x + plot.width])
    const bandwidth = yScale.bandwidth()
    const seriesCount = visible.length
    const baseline = xScale(0)

    if (stacked) {
      for (const label of labels) {
        let cumX = 0
        for (const s of visible) {
          const dp = s.data.find(d => String(d.x) === label)
          const value = dp?.y ?? 0
          const barLeft = xScale(cumX)
          const barRight = xScale(cumX + value)
          const barY = yScale(label)
          const barWidth = barRight - barLeft
          const rect: BarRect = {
            x: barLeft,
            y: barY,
            width: barWidth,
            height: bandwidth,
            color: s.color,
            seriesName: s.name,
            label,
            value,
          }
          this.barRects.push(rect)
          drawFilledBar(
            this.ctx,
            rect.x,
            rect.y,
            rect.width,
            rect.height,
            barRadius,
            rect.color,
            this.isBarHovered(rect)
          )
          cumX += value
        }
      }
    } else {
      const groupHeight = bandwidth * (1 - barGap)
      const singleBarHeight = seriesCount > 0 ? groupHeight / seriesCount : groupHeight
      const groupOffset = (bandwidth - groupHeight) / 2

      for (const label of labels) {
        for (let si = 0; si < visible.length; si++) {
          const s = visible[si]
          if (s === undefined) continue
          const dp = s.data.find(d => String(d.x) === label)
          const value = dp?.y ?? 0
          const barLeft = value >= 0 ? baseline : xScale(value)
          const barRight = value >= 0 ? xScale(value) : baseline
          const barY = yScale(label) + groupOffset + si * singleBarHeight
          const barW = barRight - barLeft
          const rect: BarRect = {
            x: barLeft,
            y: barY,
            width: barW,
            height: singleBarHeight,
            color: s.color,
            seriesName: s.name,
            label,
            value,
          }
          this.barRects.push(rect)
          drawFilledBar(
            this.ctx,
            rect.x,
            rect.y,
            rect.width,
            rect.height,
            barRadius,
            rect.color,
            this.isBarHovered(rect)
          )
        }
      }
    }
  }

  private isBarHovered(rect: BarRect): boolean {
    return (
      this.hoveredBar !== null &&
      this.hoveredBar.x === rect.x &&
      this.hoveredBar.y === rect.y &&
      this.hoveredBar.width === rect.width &&
      this.hoveredBar.height === rect.height
    )
  }

  protected handleMouseMove(e: MouseEvent): void {
    const rect = this.canvas.getBoundingClientRect()
    const mouseX = e.clientX - rect.left
    const mouseY = e.clientY - rect.top

    let found: BarRect | null = null
    for (const bar of this.barRects) {
      if (hitTestRect(mouseX, mouseY, bar)) {
        found = bar
        break
      }
    }

    const previousHovered = this.hoveredBar
    this.hoveredBar = found

    if (found !== null && this.tooltip !== null) {
      let content: string
      if (this.config.tooltip?.format !== undefined) {
        content = this.config.tooltip.format(found.value, found.label, found.seriesName)
      } else {
        content = `${found.seriesName}: ${String(found.value)}`
      }
      this.tooltip.show(found.x + found.width / 2, found.y, content)
    } else if (this.tooltip !== null) {
      this.tooltip.hide()
    }

    // Re-render if hover state changed
    if (found !== previousHovered) {
      this.render()
    }
  }

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

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

export type { BarChartConfig } from './types'
