/**
 * UI element rendering helper functions for overlays, colorbars, text, shapes, rulers, and graphs.
 * This module provides pure functions for UI rendering calculations and state setup.
 *
 * Related to: colorbar, ruler, text rendering, shapes, graphs, orientation cube, labels
 */

import type { NVLabel3D } from '@/nvlabel'

/**
 * Font metrics for a single character glyph
 */
export interface GlyphMetrics {
    lbwh: number[]
    xadv: number
    uv_lbwh: number[]
}

/**
 * Font metrics data structure
 */
export interface FontMetrics {
    size: number
    distanceRange: number
    mets: Record<number, GlyphMetrics>
}

/**
 * Parameters for text width calculation
 */
export interface TextWidthParams {
    fontMets: FontMetrics
    scale: number
    str: string
}

/**
 * Calculate the pixel width of a text string based on glyph advances.
 * @param params - Parameters containing font metrics, scale, and string
 * @returns Width in pixels
 */
export function calculateTextWidth(params: TextWidthParams): number {
    const { fontMets, scale, str } = params
    if (!str) {
        return 0
    }

    let w = 0
    for (const char of str) {
        const codePoint = char.codePointAt(0)!
        const metric = fontMets.mets[codePoint]
        if (!metric) {
            console.warn(`calculateTextWidth() : Missing font metric for code point ${codePoint} in string "${str}"`)
            continue
        } else {
            w += scale * metric.xadv
        }
    }

    return w
}

/**
 * Parameters for text height calculation
 */
export interface TextHeightParams {
    fontMets: FontMetrics
    scale: number
    str: string
}

/**
 * Calculate the pixel height of a text string based on tallest glyph.
 * @param params - Parameters containing font metrics, scale, and string
 * @returns Height in pixels
 */
export function calculateTextHeight(params: TextHeightParams): number {
    const { fontMets, scale, str } = params
    if (!str) {
        return 0
    }

    let maxH = 0

    for (const char of str) {
        const codePoint = char.codePointAt(0)!
        const metric = fontMets.mets[codePoint]
        if (metric?.lbwh?.[3] > maxH) {
            maxH = metric.lbwh[3]
        }
    }

    return scale * maxH
}

/**
 * Parameters for text position calculations
 */
export interface TextPositionParams {
    xy: number[]
    str: string
    fontPx: number
    scale: number
    canvasWidth: number
}

/**
 * Calculate text position centered below a point with canvas boundary clamping.
 * @param params - Text position parameters
 * @param getTextWidth - Function to get text width for given size and string
 * @returns Adjusted [x, y] position and adjusted scale
 */
export function calculateTextBelowPosition(params: TextPositionParams, getTextWidth: (size: number, str: string) => number): { x: number; y: number; scale: number } {
    const { xy, str, fontPx, canvasWidth } = params
    let scale = params.scale

    let size = fontPx * scale
    let width = getTextWidth(size, str)
    if (width > canvasWidth) {
        scale *= (canvasWidth - 2) / width
        size = fontPx * scale
        width = getTextWidth(size, str)
    }

    let x = xy[0] - 0.5 * getTextWidth(size, str)
    x = Math.max(x, 1) // clamp left edge of canvas
    x = Math.min(x, canvasWidth - width - 1) // clamp right edge of canvas

    return { x, y: xy[1], scale }
}

/**
 * Calculate text position centered above a point with canvas boundary clamping.
 * @param params - Text position parameters
 * @param getTextWidth - Function to get text width for given size and string
 * @returns Adjusted [x, y] position and adjusted scale
 */
export function calculateTextAbovePosition(params: TextPositionParams, getTextWidth: (size: number, str: string) => number): { x: number; y: number; scale: number } {
    const { xy, str, fontPx, canvasWidth } = params
    let scale = params.scale

    let size = fontPx * scale
    let width = getTextWidth(size, str)
    if (width > canvasWidth) {
        scale *= (canvasWidth - 2) / width
        size = fontPx * scale
        width = getTextWidth(size, str)
    }

    let x = xy[0] - 0.5 * getTextWidth(size, str)
    x = Math.max(x, 1) // clamp left edge of canvas
    x = Math.min(x, canvasWidth - width - 1) // clamp right edge of canvas
    const y = xy[1] - size // position above the y coordinate

    return { x, y, scale }
}

/**
 * Calculate text position centered between two points with background rect.
 * @param startXYendXY - Start and end points [x1, y1, x2, y2]
 * @param fontPx - Font pixel size
 * @param scale - Text scale
 * @param getTextWidth - Function to get text width
 * @returns Position data for text and background rect
 */
export function calculateTextBetweenPosition(
    startXYendXY: number[],
    str: string,
    fontPx: number,
    scale: number,
    getTextWidth: (size: number, str: string) => number
): { textX: number; textY: number; rectLTWH: number[] } {
    const size = fontPx * scale
    const w = getTextWidth(size, str)
    const x = (startXYendXY[0] + startXYendXY[2]) * 0.5 - 0.5 * w
    const y = (startXYendXY[1] + startXYendXY[3]) * 0.5 - 0.5 * size

    return {
        textX: x,
        textY: y,
        rectLTWH: [x - 1, y - 1, w + 2, size + 2]
    }
}

/**
 * Determine background color for text between points based on text color brightness.
 * @param color - Text color or null
 * @param crosshairColor - Default crosshair color
 * @returns Background color [r, g, b, a]
 */
export function getTextBetweenBackgroundColor(color: number[] | null, crosshairColor: number[]): number[] {
    const clr = color ?? crosshairColor
    // if color is bright, make rect background dark and vice versa
    if (clr && clr[0] + clr[1] + clr[2] > 0.8) {
        return [0, 0, 0, 0.5]
    }
    return [1, 1, 1, 0.5]
}

/**
 * Parameters for ruler geometry calculation
 */
export interface RulerGeometryParams {
    fovMM: number[]
    ltwh: number[]
    rulerWidth: number
    regionBounds: { x: number; y: number; w: number; h: number }
}

/**
 * Result of ruler geometry calculation
 */
export interface RulerGeometry {
    startXYendXY: [number, number, number, number]
    pix1cm: number
    clipped: boolean
}

/**
 * Calculate ruler geometry for a 10cm ruler on a 2D slice.
 * @param params - Ruler geometry parameters
 * @returns Ruler geometry including start/end points and 1cm pixel size
 */
export function calculateRulerGeometry(params: RulerGeometryParams): RulerGeometry | null {
    const { fovMM, ltwh, rulerWidth, regionBounds } = params

    if (ltwh.length < 4 || fovMM.length < 1) {
        return null
    }

    const frac10cm = 100.0 / fovMM[0]
    const pix10cm = frac10cm * ltwh[2]
    const pix1cm = Math.max(Math.round(pix10cm * 0.1), 2)

    // position ruler horizontally centered in slice, at bottom of slice
    const pixLeft = Math.floor(ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm)
    const pixTop = Math.floor(ltwh[1] + ltwh[3] - pix1cm) + 0.5 * rulerWidth

    // Clip to bounds region
    const clippedLeft = Math.max(regionBounds.x, pixLeft)
    const clippedRight = Math.min(regionBounds.x + regionBounds.w, pixLeft + pix10cm)
    const clippedY = Math.min(regionBounds.y + regionBounds.h, pixTop)

    if (clippedRight <= clippedLeft) {
        return null // fully clipped out
    }

    return {
        startXYendXY: [clippedLeft, clippedY, clippedRight, clippedY],
        pix1cm,
        clipped: clippedLeft > pixLeft || clippedRight < pixLeft + pix10cm
    }
}

/**
 * Determine ruler outline color based on ruler color brightness.
 * @param rulerColor - The ruler color [r, g, b, a]
 * @returns Outline color (black for bright rulers, white for dark)
 */
export function getRulerOutlineColor(rulerColor: number[]): number[] {
    if (rulerColor[0] + rulerColor[1] + rulerColor[2] < 0.8) {
        return [1, 1, 1, 1]
    }
    return [0, 0, 0, 1]
}

/**
 * Calculate tick mark positions for ruler
 * @param startXYendXY - Ruler start/end coordinates
 * @param rulerWidth - Ruler line thickness
 * @returns Array of tick mark line coordinates [x1, y1, x2, y2] for each tick
 */
export function calculateRulerTicks(startXYendXY: number[], rulerWidth: number): number[][] {
    const ticks: number[][] = []
    const w1cm = -0.1 * (startXYendXY[0] - startXYendXY[2])
    const b = startXYendXY[1] - Math.floor(0.5 * rulerWidth)
    const t = Math.floor(b - 0.35 * w1cm)
    const t2 = Math.floor(b - 0.7 * w1cm)

    for (let i = 0; i < 11; i++) {
        let l = startXYendXY[0] + i * w1cm
        l = Math.max(l, startXYendXY[0] + 0.5 * rulerWidth)
        l = Math.min(l, startXYendXY[2] - 0.5 * rulerWidth)
        const xyxy = [l, b, l, i % 5 === 0 ? t2 : t]
        ticks.push(xyxy)
    }

    return ticks
}

/**
 * Parameters for colorbar panel reservation
 */
export interface ColorbarPanelParams {
    canvasHeight: number
    fontPx: number
    regionBounds: { x: number; y: number; w: number; h: number }
    legendPanelWidth: number
}

/**
 * Calculate the reserved colorbar panel area.
 * @param params - Colorbar panel parameters
 * @returns Left-top-width-height array for colorbar panel
 */
export function calculateColorbarPanel(params: ColorbarPanelParams): number[] {
    const { canvasHeight, fontPx, regionBounds, legendPanelWidth } = params

    const fullHt = 3 * fontPx

    // Adjust for legend panel
    const adjustedWidth = regionBounds.w - legendPanelWidth

    return [regionBounds.x, canvasHeight - fullHt, adjustedWidth, fullHt]
}

/**
 * Spacing and tick range result from tickSpacing calculation
 */
export type TickSpacingResult = [spacing: number, ticMin: number, ticMax: number]

/**
 * Calculate tick spacing for colorbar or graph axis.
 * @param min - Minimum value
 * @param max - Maximum value
 * @returns Tuple of [spacing, ticMin, ticMax]
 */
export function calculateTickSpacing(min: number, max: number): TickSpacingResult {
    const range = max - min
    if (range <= 0) {
        return [1, min, max]
    }

    // Calculate order of magnitude
    const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(range)))

    // Try different nice step values
    const niceSteps = [1, 2, 5, 10]
    let bestSpacing = orderOfMagnitude
    let bestTickCount = Math.ceil(range / orderOfMagnitude)

    for (const step of niceSteps) {
        const spacing = orderOfMagnitude * step
        const tickCount = Math.ceil(range / spacing)
        if (tickCount >= 2 && tickCount <= 10 && (bestTickCount < 2 || bestTickCount > 10 || tickCount <= bestTickCount)) {
            bestSpacing = spacing
            bestTickCount = tickCount
        }
    }

    // Calculate nice min/max
    const ticMin = Math.floor(min / bestSpacing) * bestSpacing
    const ticMax = Math.ceil(max / bestSpacing) * bestSpacing

    return [bestSpacing, ticMin, ticMax]
}

/**
 * Format number by dropping trailing zeros.
 * @param x - Number to format
 * @returns Formatted string
 */
export function humanizeNumber(x: number): string {
    return x.toFixed(6).replace(/\.?0*$/, '')
}

/**
 * Parameters for graph layout calculation
 */
export interface GraphLayoutParams {
    graphLTWH: number[]
    fontPx: number
    dpr: number
    regionBounds: { x: number; y: number; w: number; h: number }
    fontMinPx: number
}

/**
 * Calculate graph layout dimensions including margins and plot area.
 * @param params - Graph layout parameters
 * @param min - Data minimum value
 * @param max - Data maximum value
 * @param getTextWidth - Function to calculate text width
 * @returns Graph layout information
 */
export function calculateGraphLayout(
    params: GraphLayoutParams,
    min: number,
    max: number,
    getTextWidth: (size: number, str: string) => number
): {
    plotLTWH: number[]
    fntSize: number
    fntScale: number
    spacing: number
    ticMin: number
    mn: number
    mx: number
    digits: number
} | null {
    const { graphLTWH, fontPx, dpr, regionBounds, fontMinPx } = params

    const [spacing, ticMin, ticMax] = calculateTickSpacing(min, max)
    const digits = Math.max(0, -1 * Math.floor(Math.log(spacing) / Math.log(10)))
    const mn = Math.min(ticMin, min)
    const mx = Math.max(ticMax, max)

    // Font scaling based on region size
    let fntSize = fontPx * 0.7
    const screenWidthPts = regionBounds.w / dpr
    const screenHeightPts = regionBounds.h / dpr
    const screenAreaPts = screenWidthPts * screenHeightPts
    const refAreaPts = 800 * 600

    if (screenAreaPts < refAreaPts) {
        fntSize = 0
    } else {
        fntSize = Math.max(fntSize, fontMinPx)
    }

    const fntScale = fontPx > 0 ? fntSize / fontPx : 1

    // Determine widest label in vertical axis
    let maxTextWid = 0
    if (fntSize > 0) {
        let lineH = ticMin
        while (lineH <= mx) {
            const str = lineH.toFixed(digits)
            const w = getTextWidth(fntSize, str)
            maxTextWid = Math.max(w, maxTextWid)
            lineH += spacing
        }
    }

    const margin = 0.05
    const frameWid = Math.abs(graphLTWH[2])
    const frameHt = Math.abs(graphLTWH[3])

    // Plot is region where lines are drawn
    const plotLTWH = [
        graphLTWH[0] + margin * frameWid + maxTextWid,
        graphLTWH[1] + margin * frameHt,
        graphLTWH[2] - maxTextWid - 2 * margin * frameWid,
        graphLTWH[3] - fntSize - 2.5 * margin * frameHt
    ]

    if (plotLTWH[2] <= 0 || plotLTWH[3] <= 0) {
        return null
    }

    return {
        plotLTWH,
        fntSize,
        fntScale,
        spacing,
        ticMin,
        mn,
        mx,
        digits
    }
}

/**
 * Calculate graph background colors based on background brightness.
 * @param backColor - Background color [r, g, b, a]
 * @param opacity - Graph opacity
 * @returns Graph colors
 */
export function calculateGraphColors(
    backColor: number[],
    opacity: number
): {
    graphBackColor: number[]
    lineColor: number[]
    textColor: number[]
} {
    let graphBackColor = [0.15, 0.15, 0.15, opacity]
    let lineColor = [1, 1, 1, 1]

    if (backColor[0] + backColor[1] + backColor[2] > 1.5) {
        graphBackColor = [0.95, 0.95, 0.95, opacity]
        lineColor = [0, 0, 0, 1]
    }

    const textColor = [...lineColor]
    textColor[3] = 1

    return { graphBackColor, lineColor, textColor }
}

/**
 * Parameters for measurement tool line calculation
 */
export interface MeasurementLineParams {
    startXYendXY: number[]
    distance: number
}

/**
 * Calculate extended line coordinates for measurement tool end caps.
 * @param params - Measurement line parameters
 * @returns Extended origin and terminus points
 */
export function extendMeasurementLine(params: MeasurementLineParams): { origin: number[]; terminus: number[] } {
    const { startXYendXY, distance } = params
    const x0 = startXYendXY[0]
    const y0 = startXYendXY[1]
    const x1 = startXYendXY[2]
    const y1 = startXYendXY[3]

    const x = x0 - x1
    const y = y0 - y1

    if (x === 0 && y === 0) {
        return {
            origin: [x1 + distance, y1],
            terminus: [x1 + distance, y1]
        }
    }

    const c = Math.sqrt(x * x + y * y)
    const dX = (distance * x) / c
    const dY = (distance * y) / c

    return {
        origin: [x0 + dX, y0 + dY], // next to start point
        terminus: [x1 - dX, y1 - dY] // next to end point
    }
}

/**
 * Parameters for bullet margin calculation
 */
export interface BulletMarginParams {
    labels: NVLabel3D[]
    fontPx: number
    getTextHeight: (scale: number, str: string) => number
}

/**
 * Calculate bullet margin width based on widest bullet scale and tallest label height.
 * @param params - Bullet margin parameters
 * @returns Bullet margin width in pixels
 */
export function calculateBulletMarginWidth(params: BulletMarginParams): number {
    const { labels, fontPx, getTextHeight } = params

    if (labels.length === 0) {
        return 0
    }

    const widestBulletScale = labels.length === 1 ? labels[0].style.bulletScale : labels.reduce((a, b) => (a.style.bulletScale! > b.style.bulletScale! ? a : b)).style.bulletScale

    const tallestLabel =
        labels.length === 1
            ? labels[0]
            : labels.reduce((a, b) => {
                  const heightA = getTextHeight(a.style.textScale, a.text) * fontPx * a.style.textScale
                  const heightB = getTextHeight(b.style.textScale, b.text) * fontPx * b.style.textScale
                  return heightA > heightB ? a : b
              })

    const height = getTextHeight(tallestLabel.style.textScale, tallestLabel.text) * fontPx * tallestLabel.style.textScale
    return (widestBulletScale ?? 1) * height
}

/**
 * Parameters for legend panel height calculation
 */
export interface LegendPanelHeightParams {
    labels: NVLabel3D[]
    fontPx: number
    scaling: number
    getTextHeight: (scale: number, str: string) => number
}

/**
 * Calculate total height of legend panel for given labels.
 * @param params - Legend panel height parameters
 * @returns Total height in pixels
 */
export function calculateLegendPanelHeight(params: LegendPanelHeightParams): number {
    const { labels, fontPx, scaling, getTextHeight } = params

    if (labels.length === 0) {
        return 0
    }

    let totalHeight = 0
    const size = fontPx * scaling

    for (const label of labels) {
        const labelSize = fontPx * label.style.textScale
        const textHeight = getTextHeight(labelSize, label.text) * scaling
        totalHeight += textHeight + size / 2
    }

    return totalHeight
}

/**
 * Parameters for legend panel width calculation
 */
export interface LegendPanelWidthParams {
    labels: NVLabel3D[]
    fontPx: number
    getTextWidth: (size: number, str: string) => number
    getBulletMarginWidth: () => number
}

/**
 * Calculate total width of legend panel for given labels.
 * @param params - Legend panel width parameters
 * @returns Total width in pixels
 */
export function calculateLegendPanelWidth(params: LegendPanelWidthParams): number {
    const { labels, fontPx, getTextWidth, getBulletMarginWidth } = params

    if (labels.length === 0) {
        return 0
    }

    let maxWidth = 0
    const bulletMargin = getBulletMarginWidth()

    for (const label of labels) {
        const labelSize = fontPx * label.style.textScale
        const textWidth = getTextWidth(labelSize, label.text)
        const width = textWidth + bulletMargin + labelSize * 1.5
        maxWidth = Math.max(maxWidth, width)
    }

    return maxWidth
}

/**
 * Dotted line segment result
 */
export interface DottedLineSegment {
    startX: number
    startY: number
    endX: number
    endY: number
}

/**
 * Calculate dotted line segments for a given line.
 * @param startXYendXY - Line start and end [x1, y1, x2, y2]
 * @param fontPx - Font pixel size (used for segment sizing)
 * @param scale - Scale factor
 * @returns Array of visible segment coordinates
 */
export function calculateDottedLineSegments(startXYendXY: number[], fontPx: number, scale: number): DottedLineSegment[] {
    const segments: DottedLineSegment[] = []

    // Calculate segment vector
    const segmentX = startXYendXY[2] - startXYendXY[0]
    const segmentY = startXYendXY[3] - startXYendXY[1]
    const totalLength = Math.sqrt(segmentX * segmentX + segmentY * segmentY)

    if (totalLength === 0) {
        return segments
    }

    const size = fontPx * scale
    const segmentLength = size / 2
    const normalizedX = segmentX / totalLength
    const normalizedY = segmentY / totalLength
    const stepX = normalizedX * segmentLength
    const stepY = normalizedY * segmentLength

    let segmentCount = Math.floor(totalLength / segmentLength)
    if (totalLength % segmentLength) {
        segmentCount++
    }

    let currentX = startXYendXY[0]
    let currentY = startXYendXY[1]

    // Draw all segments except for the last one
    for (let i = 0; i < segmentCount - 1; i++) {
        if (i % 2 === 0) {
            // Only draw even segments (creates dotted pattern)
            segments.push({
                startX: currentX,
                startY: currentY,
                endX: currentX + stepX,
                endY: currentY + stepY
            })
        }
        currentX += stepX
        currentY += stepY
    }

    return segments
}

/**
 * Calculate screen pixel range for MSDF font rendering.
 * @param fontPx - Base font pixel size
 * @param scale - Text scale factor
 * @param fontMets - Font metrics data
 * @returns Screen pixel range value (minimum 1.0)
 */
export function calculateScreenPxRange(fontPx: number, scale: number, fontMets: FontMetrics): number {
    const size = fontPx * scale
    const screenPxRange = (size / fontMets.size) * fontMets.distanceRange
    return Math.max(screenPxRange, 1.0) // screenPxRange must never be lower than 1
}

/**
 * Calculate thumbnail dimensions to fit within a region.
 * @param regionWidth - Region width in pixels
 * @param regionHeight - Region height in pixels
 * @param aspectRatio - Image width/height ratio
 * @returns Thumbnail left, top, width, height
 */
export function calculateThumbnailDimensions(regionWidth: number, regionHeight: number, aspectRatio: number): { left: number; top: number; width: number; height: number } {
    let h = regionHeight
    let w = regionHeight * aspectRatio

    if (w > regionWidth) {
        // constrained by width
        h = regionWidth / aspectRatio
        w = regionWidth
    }

    return {
        left: (regionWidth - w) / 2,
        top: (regionHeight - h) / 2,
        width: w,
        height: h
    }
}

/**
 * Calculate orientation cube position and size.
 * @param leftTopWidthHeight - Tile bounds
 * @param effectiveCanvasHeight - Canvas height minus colorbar
 * @param canvasHeight - Full canvas height
 * @returns Position and size for orientation cube, or null if too small
 */
export function calculateOrientationCubePosition(leftTopWidthHeight: number[], effectiveCanvasHeight: number, canvasHeight: number): { x: number; y: number; size: number } | null {
    const sz = 0.05 * Math.min(leftTopWidthHeight[2], leftTopWidthHeight[3])
    if (sz < 5) {
        return null
    }

    let translateUpForColorbar = 0
    if (leftTopWidthHeight[1] === 0) {
        translateUpForColorbar = canvasHeight - effectiveCanvasHeight
    }

    return {
        x: 1.8 * sz + leftTopWidthHeight[0],
        y: translateUpForColorbar + 1.8 * sz + leftTopWidthHeight[1],
        size: sz
    }
}

/**
 * Calculate the stride for graph vertical lines based on data points.
 * @param dataLength - Number of data points
 * @returns Stride value
 */
export function calculateGraphLineStride(dataLength: number): number {
    let stride = 1
    while (dataLength / stride > 20) {
        stride *= 5
    }
    return stride
}
