/**
 * Clip plane manager helper functions for 3D clipping plane operations.
 * This module provides pure functions for clip plane calculations and management.
 *
 */

import { sph2cartDeg } from '../rendering/VolumeRenderer.js'

/**
 * Default clip plane values when no clipping is active.
 * The depth value of 2 effectively disables clipping.
 */
export const DEFAULT_CLIP_PLANE: [number, number, number, number] = [0, 0, 0, 2]

/**
 * Default depth/azimuth/elevation values when no clipping is active.
 * Depth of 2 means no clip plane.
 */
export const DEFAULT_DEPTH_AZI_ELEV: [number, number, number] = [2, 0, 0]

/**
 * Threshold for determining if a clip plane is active.
 * If depth < this value, the clip plane is considered active.
 */
export const CLIP_PLANE_ACTIVE_THRESHOLD = 1.8

/**
 * Parameters for converting depth/azimuth/elevation to clip plane format
 */
export interface DepthAziElevToClipPlaneParams {
    depth: number
    azimuth: number
    elevation: number
}

/**
 * Convert depth/azimuth/elevation to clip plane [x, y, z, d] format.
 * Uses spherical to Cartesian conversion with 180-degree azimuth offset.
 * @param params - Depth, azimuth, and elevation values
 * @returns Clip plane as [x, y, z, depth] array
 */
export function depthAziElevToClipPlane(params: DepthAziElevToClipPlaneParams): [number, number, number, number] {
    const { depth, azimuth, elevation } = params
    const v = sph2cartDeg(azimuth + 180, elevation)
    return [v[0], v[1], v[2], depth]
}

/**
 * Parameters for converting depth/azimuth/elevation for setClipPlanes (no 180-degree offset)
 */
export interface DepthAziElevToClipPlaneNoOffsetParams {
    depth: number
    azimuth: number
    elevation: number
}

/**
 * Convert depth/azimuth/elevation to clip plane format without 180-degree offset.
 * Used by setClipPlanes which expects a different orientation convention.
 * @param params - Depth, azimuth, and elevation values
 * @returns Clip plane as [x, y, z, -depth] array (note: depth is negated for shader)
 */
export function depthAziElevToClipPlaneNoOffset(params: DepthAziElevToClipPlaneNoOffsetParams): [number, number, number, number] {
    const { depth, azimuth, elevation } = params
    const n = sph2cartDeg(azimuth, elevation)
    return [n[0], n[1], n[2], -depth] // depth negated for shader
}

/**
 * Parameters for calculating clip plane drag rotation
 */
export interface CalculateClipPlaneDragParams {
    startDepthAziElev: number[]
    dragDeltaX: number
    dragDeltaY: number
}

/**
 * Result of clip plane drag calculation
 */
export interface ClipPlaneDragResult {
    depthAziElev: [number, number, number]
    changed: boolean
}

/**
 * Calculate new depth/azimuth/elevation from drag deltas.
 * Azimuth is adjusted by -deltaX and wrapped to 0-360.
 * Elevation is adjusted by +deltaY (unclamped).
 * @param params - Starting values and drag deltas
 * @returns New depth/azimuth/elevation and whether values changed
 */
export function calculateClipPlaneDrag(params: CalculateClipPlaneDragParams): ClipPlaneDragResult {
    const { startDepthAziElev, dragDeltaX, dragDeltaY } = params

    const newDepthAziElev: [number, number, number] = [startDepthAziElev[0], (startDepthAziElev[1] - dragDeltaX) % 360, startDepthAziElev[2] + dragDeltaY]

    const changed = newDepthAziElev[1] !== startDepthAziElev[1] || newDepthAziElev[2] !== startDepthAziElev[2]

    return {
        depthAziElev: newDepthAziElev,
        changed
    }
}

/**
 * Parameters for ensuring clip plane arrays exist
 */
export interface EnsureClipPlaneArraysParams {
    clipPlanes: number[][] | undefined
    clipPlaneDepthAziElevs: number[][] | undefined
    index: number
}

/**
 * Result of ensuring clip plane arrays
 */
export interface EnsureClipPlaneArraysResult {
    clipPlanes: number[][]
    clipPlaneDepthAziElevs: number[][]
}

/**
 * Ensure clip plane arrays exist and have enough elements for the given index.
 * Creates arrays if they don't exist and fills with default values up to the index.
 * @param params - Current arrays and required index
 * @returns Updated arrays guaranteed to have elements up to index
 */
export function ensureClipPlaneArrays(params: EnsureClipPlaneArraysParams): EnsureClipPlaneArraysResult {
    const { index } = params
    const clipPlanes = params.clipPlanes ? [...params.clipPlanes] : []
    const clipPlaneDepthAziElevs = params.clipPlaneDepthAziElevs ? [...params.clipPlaneDepthAziElevs] : []

    while (clipPlanes.length <= index) {
        clipPlanes.push([...DEFAULT_CLIP_PLANE])
    }
    while (clipPlaneDepthAziElevs.length <= index) {
        clipPlaneDepthAziElevs.push([...DEFAULT_DEPTH_AZI_ELEV])
    }

    return {
        clipPlanes,
        clipPlaneDepthAziElevs
    }
}

/**
 * Check if a clip plane is active (depth below threshold).
 * @param depth - The depth value of the clip plane
 * @param threshold - Optional custom threshold (default: 1.8)
 * @returns True if the clip plane is active
 */
export function isClipPlaneActive(depth: number, threshold = CLIP_PLANE_ACTIVE_THRESHOLD): boolean {
    return depth < threshold
}

/**
 * Parameters for converting multiple depth/azimuth/elevation arrays to clip planes
 */
export interface ConvertMultipleClipPlanesParams {
    depthAziElevs: number[][]
}

/**
 * Result of converting multiple clip planes
 */
export interface ConvertMultipleClipPlanesResult {
    clipPlanes: number[][]
    clipPlaneDepthAziElevs: number[][]
}

/**
 * Convert an array of depth/azimuth/elevation values to clip plane format.
 * Used by setClipPlanes to batch-process multiple clip planes.
 * @param params - Array of depth/azimuth/elevation values
 * @returns Parallel arrays of clip planes and their depth/azi/elev values
 */
export function convertMultipleClipPlanes(params: ConvertMultipleClipPlanesParams): ConvertMultipleClipPlanesResult {
    const { depthAziElevs } = params
    const clipPlanes: number[][] = []
    const clipPlaneDepthAziElevs: number[][] = []

    for (const dae of depthAziElevs) {
        const plane = depthAziElevToClipPlaneNoOffset({
            depth: dae[0],
            azimuth: dae[1],
            elevation: dae[2]
        })
        clipPlanes.push(plane)
        clipPlaneDepthAziElevs.push([...dae])
    }

    return {
        clipPlanes,
        clipPlaneDepthAziElevs
    }
}

/**
 * Parameters for checking if clip plane drag should update the view
 */
export interface ShouldUpdateClipPlaneDragParams {
    isDragging: boolean
    activeClipPlaneDepth: number
    dragStartTileIndex: number
}

/**
 * Check if clip plane drag should update the view.
 * @param params - Dragging state, clip plane depth, and drag start tile index
 * @returns True if clip plane drag should be processed
 */
export function shouldUpdateClipPlaneDrag(params: ShouldUpdateClipPlaneDragParams): boolean {
    const { isDragging, activeClipPlaneDepth, dragStartTileIndex } = params
    return isDragging && isClipPlaneActive(activeClipPlaneDepth) && dragStartTileIndex >= 0
}

/**
 * Create a default clip plane (disabled state).
 * @returns Default clip plane array [0, 0, 0, 2]
 */
export function createDefaultClipPlane(): [number, number, number, number] {
    return [...DEFAULT_CLIP_PLANE]
}

/**
 * Create default depth/azimuth/elevation values (disabled state).
 * @returns Default depth/azi/elev array [2, 0, 0]
 */
export function createDefaultDepthAziElev(): [number, number, number] {
    return [...DEFAULT_DEPTH_AZI_ELEV]
}

/**
 * Parameters for updating a single clip plane at a given index
 */
export interface UpdateClipPlaneAtIndexParams {
    clipPlanes: number[][]
    clipPlaneDepthAziElevs: number[][]
    index: number
    depthAzimuthElevation: number[]
}

/**
 * Result of updating a clip plane at an index
 */
export interface UpdateClipPlaneAtIndexResult {
    clipPlanes: number[][]
    clipPlaneDepthAziElevs: number[][]
    clipPlane: [number, number, number, number]
}

/**
 * Update a clip plane at a specific index.
 * Ensures arrays are large enough, then updates the values.
 * @param params - Current arrays, index, and new depth/azimuth/elevation
 * @returns Updated arrays and the computed clip plane
 */
export function updateClipPlaneAtIndex(params: UpdateClipPlaneAtIndexParams): UpdateClipPlaneAtIndexResult {
    const { index, depthAzimuthElevation } = params

    // Ensure arrays are large enough
    const { clipPlanes, clipPlaneDepthAziElevs } = ensureClipPlaneArrays({
        clipPlanes: params.clipPlanes,
        clipPlaneDepthAziElevs: params.clipPlaneDepthAziElevs,
        index
    })

    // Calculate the clip plane
    const clipPlane = depthAziElevToClipPlane({
        depth: depthAzimuthElevation[0],
        azimuth: depthAzimuthElevation[1],
        elevation: depthAzimuthElevation[2]
    })

    // Update the arrays
    clipPlanes[index] = clipPlane
    clipPlaneDepthAziElevs[index] = [...depthAzimuthElevation]

    return {
        clipPlanes,
        clipPlaneDepthAziElevs,
        clipPlane
    }
}

/**
 * Validate depth/azimuth/elevation input.
 * @param depthAzimuthElevation - Input to validate
 * @returns True if valid (non-empty array)
 */
export function isValidDepthAziElev(depthAzimuthElevation: number[] | undefined | null): boolean {
    return !!depthAzimuthElevation && depthAzimuthElevation.length > 0
}
