/**
 * Heatmap chart - grid of colored rectangles with intensity mapping
 * @countermeasure/web-components/charts/heatmap
 */

import { BaseChart } from '../base'
import type { HeatmapChartConfig, HeatmapCell } from './types'
import { lerpColor, clamp, formatNumber, roundRect } from '../utils'
import { ChartTooltip } from '../tooltip'

const DEFAULT_COLOR_LOW = '#f0f9ff'
const DEFAULT_COLOR_HIGH = '#1e40af'
const DEFAULT_CELL_GAP = 1
const LABEL_FONT = '11px system-ui, sans-serif'
const VALUE_FONT = '10px system-ui, sans-serif'
const LABEL_COLOR = '#64748b'

export class HeatmapChart extends BaseChart {
  private data: HeatmapCell[]
  private rowLabels: string[]
  private colLabels: string[]
  private colorLow: string
  private colorHigh: string
  private showValues: boolean
  private cellRadius: number
  private cellGap: number
  private tooltipEnabled: boolean
  private tooltipFormat: ((value: number, label: string, series: string) => string) | null
  private tooltip: ChartTooltip | null
  private handleMouseMove: ((e: MouseEvent) => void) | null
  private handleMouseLeave: (() => void) | null
  private valueMin: number
  private valueMax: number

  constructor(config: HeatmapChartConfig) {
    super(config)

    this.data = config.data
    this.rowLabels = config.rowLabels
    this.colLabels = config.colLabels
    this.colorLow = config.colorRange !== undefined ? config.colorRange[0] : DEFAULT_COLOR_LOW
    this.colorHigh = config.colorRange !== undefined ? config.colorRange[1] : DEFAULT_COLOR_HIGH
    this.showValues = config.showValues === true
    this.cellRadius = config.cellRadius ?? 0
    this.cellGap = config.cellGap ?? DEFAULT_CELL_GAP
    this.tooltipEnabled = config.tooltip?.enabled !== false
    this.tooltipFormat = config.tooltip?.format ?? null
    this.tooltip = null
    this.handleMouseMove = null
    this.handleMouseLeave = null
    this.valueMin = 0
    this.valueMax = 0

    this.computeValueRange()

    if (this.tooltipEnabled) {
      this.setupTooltip()
    }

    this.render()
  }

  private computeValueRange(): void {
    if (this.data.length === 0) {
      this.valueMin = 0
      this.valueMax = 1
      return
    }
    let min = Infinity
    let max = -Infinity
    for (const cell of this.data) {
      if (cell.value < min) min = cell.value
      if (cell.value > max) max = cell.value
    }
    this.valueMin = min
    this.valueMax = max === min ? min + 1 : max
  }

  private getCellColor(value: number): string {
    const t = clamp((value - this.valueMin) / (this.valueMax - this.valueMin), 0, 1)
    return lerpColor(this.colorLow, this.colorHigh, t)
  }

  private getCellBounds(): {
    cellWidth: number
    cellHeight: number
    offsetX: number
    offsetY: number
  } {
    const plot = this.plotArea
    const numCols = this.colLabels.length
    const numRows = this.rowLabels.length

    if (numCols === 0 || numRows === 0) {
      return { cellWidth: 0, cellHeight: 0, offsetX: plot.x, offsetY: plot.y }
    }

    const cellWidth = (plot.width - this.cellGap * (numCols - 1)) / numCols
    const cellHeight = (plot.height - this.cellGap * (numRows - 1)) / numRows

    return { cellWidth, cellHeight, offsetX: plot.x, offsetY: plot.y }
  }

  private setupTooltip(): void {
    this.tooltip = new ChartTooltip(this.container)

    this.handleMouseMove = (e: MouseEvent): void => {
      const rect = this.canvas.getBoundingClientRect()
      const mx = e.clientX - rect.left
      const my = e.clientY - rect.top

      const { cellWidth, cellHeight, offsetX, offsetY } = this.getCellBounds()
      if (cellWidth <= 0 || cellHeight <= 0) return

      const col = Math.floor((mx - offsetX) / (cellWidth + this.cellGap))
      const row = Math.floor((my - offsetY) / (cellHeight + this.cellGap))

      if (col < 0 || col >= this.colLabels.length || row < 0 || row >= this.rowLabels.length) {
        this.tooltip?.hide()
        return
      }

      // Verify the mouse is actually within the cell (not in gap)
      const cellX = offsetX + col * (cellWidth + this.cellGap)
      const cellY = offsetY + row * (cellHeight + this.cellGap)
      if (mx < cellX || mx > cellX + cellWidth || my < cellY || my > cellY + cellHeight) {
        this.tooltip?.hide()
        return
      }

      const cell = this.data.find(c => c.row === row && c.col === col)
      const value = cell !== undefined ? cell.value : 0
      const rowLabel = this.rowLabels[row] ?? String(row)
      const colLabel = this.colLabels[col] ?? String(col)

      let content: string
      if (this.tooltipFormat !== null) {
        content = this.tooltipFormat(value, rowLabel, colLabel)
      } else {
        content = `<strong>${rowLabel} / ${colLabel}</strong><br/>Value: ${formatNumber(value)}`
      }

      this.tooltip?.show(mx, my, content)
    }

    this.handleMouseLeave = (): void => {
      this.tooltip?.hide()
    }

    this.canvas.addEventListener('mousemove', this.handleMouseMove)
    this.canvas.addEventListener('mouseleave', this.handleMouseLeave)
  }

  public render(): void {
    this.clear()

    const { ctx } = this
    const plot = this.plotArea
    const numCols = this.colLabels.length
    const numRows = this.rowLabels.length

    if (numCols === 0 || numRows === 0) return

    const { cellWidth, cellHeight, offsetX, offsetY } = this.getCellBounds()

    // Draw column labels on top
    ctx.save()
    ctx.fillStyle = LABEL_COLOR
    ctx.font = LABEL_FONT
    ctx.textAlign = 'center'
    ctx.textBaseline = 'bottom'
    for (let c = 0; c < numCols; c++) {
      const label = this.colLabels[c]
      if (label === undefined) continue
      const cx = offsetX + c * (cellWidth + this.cellGap) + cellWidth / 2
      ctx.fillText(label, cx, plot.y - 4, cellWidth)
    }
    ctx.restore()

    // Draw row labels on left
    ctx.save()
    ctx.fillStyle = LABEL_COLOR
    ctx.font = LABEL_FONT
    ctx.textAlign = 'right'
    ctx.textBaseline = 'middle'
    for (let r = 0; r < numRows; r++) {
      const label = this.rowLabels[r]
      if (label === undefined) continue
      const cy = offsetY + r * (cellHeight + this.cellGap) + cellHeight / 2
      ctx.fillText(label, plot.x - 6, cy)
    }
    ctx.restore()

    // Build a lookup map for cell values
    const cellMap = new Map<string, number>()
    for (const cell of this.data) {
      cellMap.set(`${String(cell.row)},${String(cell.col)}`, cell.value)
    }

    // Draw cells
    ctx.save()
    for (let r = 0; r < numRows; r++) {
      for (let c = 0; c < numCols; c++) {
        const cellX = offsetX + c * (cellWidth + this.cellGap)
        const cellY = offsetY + r * (cellHeight + this.cellGap)
        const value = cellMap.get(`${String(r)},${String(c)}`) ?? 0
        const color = this.getCellColor(value)

        ctx.fillStyle = color

        if (this.cellRadius > 0) {
          roundRect(ctx, cellX, cellY, cellWidth, cellHeight, this.cellRadius)
          ctx.fill()
        } else {
          ctx.fillRect(cellX, cellY, cellWidth, cellHeight)
        }

        // Draw value text in cell
        if (this.showValues) {
          ctx.fillStyle = this.getContrastTextColor(value)
          ctx.font = VALUE_FONT
          ctx.textAlign = 'center'
          ctx.textBaseline = 'middle'
          ctx.fillText(
            formatNumber(value),
            cellX + cellWidth / 2,
            cellY + cellHeight / 2,
            cellWidth - 4
          )
        }
      }
    }
    ctx.restore()
  }

  private getContrastTextColor(value: number): string {
    const t = clamp((value - this.valueMin) / (this.valueMax - this.valueMin), 0, 1)
    return t > 0.5 ? '#ffffff' : '#1e293b'
  }

  public update(data: HeatmapCell[]): void {
    this.data = data
    this.computeValueRange()
    this.render()
  }

  public override destroy(): void {
    if (this.handleMouseMove !== null) {
      this.canvas.removeEventListener('mousemove', this.handleMouseMove)
    }
    if (this.handleMouseLeave !== null) {
      this.canvas.removeEventListener('mouseleave', this.handleMouseLeave)
    }
    this.tooltip?.destroy()
    super.destroy()
  }
}
