/**
 * Treemap chart - squarified treemap layout with recursive subdivision
 * @countermeasure/web-components/charts/treemap
 */

import { BaseChart } from '../base'
import type { TreemapChartConfig, TreemapNode } from './types'
import { defaultColors, formatNumber, hexToRgba, roundRect } from '../utils'
import { ChartTooltip } from '../tooltip'

const LABEL_FONT = '11px system-ui, sans-serif'
const LABEL_COLOR = '#ffffff'
const MIN_LABEL_WIDTH = 40
const MIN_LABEL_HEIGHT = 20
const DEFAULT_CELL_GAP = 2

interface LayoutRect {
  x: number
  y: number
  width: number
  height: number
  node: TreemapNode
  color: string
}

export class TreemapChart extends BaseChart {
  private data: TreemapNode
  private showLabels: boolean
  private cellGap: number
  private cellRadius: number
  private tooltipEnabled: boolean
  private tooltipFormat: ((value: number, label: string, series: string) => string) | null
  private tooltip: ChartTooltip | null
  private rects: LayoutRect[]
  private colors: string[]
  private handleMouseMove: ((e: MouseEvent) => void) | null
  private handleMouseLeave: (() => void) | null
  private hoveredIndex: number

  constructor(config: TreemapChartConfig) {
    super(config)

    this.data = normalizeNodeValues(config.data)
    this.showLabels = config.showLabels !== false
    this.cellGap = config.cellGap ?? DEFAULT_CELL_GAP
    this.cellRadius = config.cellRadius ?? 0
    this.tooltipEnabled = config.tooltip?.enabled !== false
    this.tooltipFormat = config.tooltip?.format ?? null
    this.tooltip = null
    this.rects = []
    this.colors = defaultColors()
    this.handleMouseMove = null
    this.handleMouseLeave = null
    this.hoveredIndex = -1

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

    this.render()
  }

  private computeLayout(): void {
    this.rects = []
    const plot = this.plotArea
    const children = this.data.children
    if (children === undefined || children.length === 0) {
      // Single node fills the entire plot
      this.rects.push({
        x: plot.x,
        y: plot.y,
        width: plot.width,
        height: plot.height,
        node: this.data,
        color: this.data.color ?? this.colors[0] ?? '#3b82f6',
      })
      return
    }

    this.squarify(children, plot.x, plot.y, plot.width, plot.height, 0)
  }

  private squarify(
    nodes: TreemapNode[],
    x: number,
    y: number,
    width: number,
    height: number,
    colorOffset: number
  ): void {
    if (nodes.length === 0 || width <= 0 || height <= 0) return

    const totalValue = nodes.reduce((sum, n) => sum + Math.max(0, n.value), 0)
    if (totalValue <= 0) return

    // Sort nodes by value descending for better aspect ratios
    const sorted = [...nodes].sort((a, b) => b.value - a.value)

    this.layoutRow(sorted, x, y, width, height, totalValue, colorOffset)
  }

  private layoutRow(
    nodes: TreemapNode[],
    x: number,
    y: number,
    width: number,
    height: number,
    totalValue: number,
    colorOffset: number
  ): void {
    if (nodes.length === 0 || totalValue <= 0) return

    const isWide = width >= height

    let currentRow: TreemapNode[] = []
    let currentRowValue = 0
    let bestAspectRatio = Infinity
    let processedCount = 0

    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i]
      if (node === undefined) continue

      const testRow = [...currentRow, node]
      const testValue = currentRowValue + Math.max(0, node.value)

      const aspect = this.worstAspectRatio(
        testRow,
        testValue,
        totalValue,
        isWide ? height : width,
        isWide ? width : height
      )

      if (aspect <= bestAspectRatio || currentRow.length === 0) {
        currentRow = testRow
        currentRowValue = testValue
        bestAspectRatio = aspect
        processedCount = i + 1
      } else {
        // Current row is optimal, lay it out and recurse on remaining
        break
      }
    }

    // Lay out the current row
    const rowFraction = currentRowValue / totalValue
    const rowSize = isWide ? width * rowFraction : height * rowFraction

    let offset = 0
    for (let i = 0; i < currentRow.length; i++) {
      const node = currentRow[i]
      if (node === undefined) continue

      const nodeFraction = currentRowValue > 0 ? Math.max(0, node.value) / currentRowValue : 0
      const nodeSize = (isWide ? height : width) * nodeFraction
      const gap = this.cellGap

      const rectX = isWide ? x + gap / 2 : x + offset + gap / 2
      const rectY = isWide ? y + offset + gap / 2 : y + gap / 2
      const rectW = isWide ? rowSize - gap : nodeSize - gap
      const rectH = isWide ? nodeSize - gap : rowSize - gap

      if (rectW > 0 && rectH > 0) {
        const color = node.color ?? this.colors[(colorOffset + i) % this.colors.length] ?? '#3b82f6'

        // If the node has children, recursively subdivide
        if (node.children !== undefined && node.children.length > 0) {
          this.squarify(node.children, rectX, rectY, rectW, rectH, colorOffset + i)
        } else {
          this.rects.push({
            x: rectX,
            y: rectY,
            width: rectW,
            height: rectH,
            node,
            color,
          })
        }
      }

      offset += nodeSize
    }

    // Recurse on remaining nodes
    const remaining = nodes.slice(processedCount)
    if (remaining.length > 0) {
      const remainingValue = totalValue - currentRowValue
      if (isWide) {
        this.layoutRow(
          remaining,
          x + rowSize,
          y,
          width - rowSize,
          height,
          remainingValue,
          colorOffset + processedCount
        )
      } else {
        this.layoutRow(
          remaining,
          x,
          y + rowSize,
          width,
          height - rowSize,
          remainingValue,
          colorOffset + processedCount
        )
      }
    }
  }

  private worstAspectRatio(
    row: TreemapNode[],
    rowValue: number,
    totalValue: number,
    shortSide: number,
    longSide: number
  ): number {
    if (row.length === 0 || totalValue <= 0 || shortSide <= 0 || longSide <= 0) return Infinity

    const rowArea = (rowValue / totalValue) * shortSide * longSide
    const rowLength = rowArea / shortSide

    let worst = 0
    for (const node of row) {
      const nodeArea = (Math.max(0, node.value) / totalValue) * shortSide * longSide
      if (rowLength <= 0 || nodeArea <= 0) continue
      const nodeWidth = nodeArea / rowLength
      const aspect = Math.max(rowLength / nodeWidth, nodeWidth / rowLength)
      if (aspect > worst) worst = aspect
    }

    return worst
  }

  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

      let foundIndex = -1
      for (let i = 0; i < this.rects.length; i++) {
        const r = this.rects[i]
        if (r === undefined) continue
        if (mx >= r.x && mx <= r.x + r.width && my >= r.y && my <= r.y + r.height) {
          foundIndex = i
          break
        }
      }

      if (foundIndex !== this.hoveredIndex) {
        this.hoveredIndex = foundIndex
        this.render()
      }

      if (foundIndex >= 0) {
        const r = this.rects[foundIndex]
        if (r !== undefined) {
          let content: string
          if (this.tooltipFormat !== null) {
            content = this.tooltipFormat(r.node.value, r.node.label, '')
          } else {
            content = `<strong>${r.node.label}</strong><br/>Value: ${formatNumber(r.node.value)}`
          }
          this.tooltip?.show(mx, my, content)
        }
      } else {
        this.tooltip?.hide()
      }
    }

    this.handleMouseLeave = (): void => {
      if (this.hoveredIndex !== -1) {
        this.hoveredIndex = -1
        this.render()
      }
      this.tooltip?.hide()
    }

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

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

    const { ctx } = this

    ctx.save()
    for (let i = 0; i < this.rects.length; i++) {
      const r = this.rects[i]
      if (r === undefined) continue

      const isHovered = i === this.hoveredIndex

      // Draw cell background
      ctx.fillStyle = isHovered ? hexToRgba(r.color, 0.8) : r.color

      if (this.cellRadius > 0) {
        roundRect(ctx, r.x, r.y, r.width, r.height, this.cellRadius)
        ctx.fill()
      } else {
        ctx.fillRect(r.x, r.y, r.width, r.height)
      }

      // Hover border
      if (isHovered) {
        ctx.strokeStyle = '#ffffff'
        ctx.lineWidth = 2
        if (this.cellRadius > 0) {
          roundRect(ctx, r.x, r.y, r.width, r.height, this.cellRadius)
          ctx.stroke()
        } else {
          ctx.strokeRect(r.x, r.y, r.width, r.height)
        }
      }

      // Draw label if cell is large enough
      if (this.showLabels && r.width >= MIN_LABEL_WIDTH && r.height >= MIN_LABEL_HEIGHT) {
        ctx.fillStyle = LABEL_COLOR
        ctx.font = LABEL_FONT
        ctx.textAlign = 'left'
        ctx.textBaseline = 'top'
        ctx.fillText(r.node.label, r.x + 4, r.y + 4, r.width - 8)

        // Show value below label if space allows
        if (r.height >= MIN_LABEL_HEIGHT * 2) {
          ctx.fillStyle = 'rgba(255,255,255,0.8)'
          ctx.fillText(formatNumber(r.node.value), r.x + 4, r.y + 18, r.width - 8)
        }
      }
    }
    ctx.restore()
  }

  public update(data: TreemapNode): void {
    this.data = data
    this.hoveredIndex = -1
    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()
  }
}

/**
 * Recursively fill in `value` for non-leaf nodes whose authored value is
 * `<= 0`. Without this, a parent with `{value: 0, children: [...]}` gets
 * zero allocated area in the squarified layout and its entire subtree
 * is silently dropped. Returns a deep copy with values normalized.
 */
function normalizeNodeValues(node: TreemapNode): TreemapNode {
  const next: TreemapNode = { ...node }
  if (node.children && node.children.length > 0) {
    next.children = node.children.map(normalizeNodeValues)
    if (!Number.isFinite(node.value) || node.value <= 0) {
      next.value = next.children.reduce((sum, c) => sum + Math.max(0, c.value), 0)
    }
  }
  return next
}
