/**
 * DragModeManager module for managing drag interaction modes.
 * This module provides pure functions for drag mode management and drag-related calculations.
 *
 * Related to: Drag interactions, pan/zoom, windowing, angle measurement
 */

import { vec4 } from 'gl-matrix'
import { DRAG_MODE } from '../../nvdocument.js'

/**
 * State for active drag mode
 */
export interface ActiveDragModeState {
    activeDragMode: DRAG_MODE
    activeDragButton: number
}

/**
 * Cleared drag mode state
 */
export interface ClearedDragModeState {
    activeDragMode: null
    activeDragButton: null
}

/**
 * Angle measurement state
 */
export interface AngleMeasurementState {
    angleState: 'none' | 'drawing_first_line' | 'drawing_second_line' | 'complete'
    angleFirstLine: [number, number, number, number]
}

/**
 * Parameters for getting current drag mode
 */
export interface GetCurrentDragModeParams {
    activeDragMode: DRAG_MODE | null
    fallbackDragMode: DRAG_MODE
}

/**
 * Parameters for calculating pan/zoom from drag
 */
export interface CalculatePanZoomParams {
    startMM: vec4 | number[]
    endMM: vec4 | number[]
    pan2DxyzmmAtMouseDown: vec4 | number[]
}

/**
 * Result of pan/zoom calculation
 */
export interface PanZoomResult {
    pan2Dxyzmm: [number, number, number, number]
}

/**
 * Parameters for calculating 3D slicer zoom from drag
 */
export interface CalculateSlicer3DZoomParams {
    startY: number
    endY: number
    pan2DxyzmmAtMouseDown: vec4 | number[] | ArrayLike<number>
    currentPan2Dxyzmm: vec4 | number[] | ArrayLike<number>
    crosshairMM: number[] | ArrayLike<number>
    yoke3Dto2DZoom: boolean
}

/**
 * Result of 3D slicer zoom calculation
 */
export interface Slicer3DZoomResult {
    zoom: number
    pan2Dxyzmm: [number, number, number, number]
    volScaleMultiplier?: number
}

/**
 * Parameters for calculating windowing adjustment
 */
export interface CalculateWindowingParams {
    x: number
    y: number
    windowX: number
    windowY: number
    currentCalMin: number
    currentCalMax: number
    globalMin: number
    globalMax: number
    gainFactor: number
}

/**
 * Result of windowing adjustment calculation
 */
export interface WindowingAdjustmentResult {
    calMin: number
    calMax: number
    windowX: number
    windowY: number
}

/**
 * Parameters for calculating intensity range from voxel selection
 */
export interface CalculateIntensityRangeParams {
    xrange: [number, number]
    yrange: [number, number]
    zrange: [number, number]
    dims: number[]
    img: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array
}

/**
 * Result of intensity range calculation
 */
export interface IntensityRangeResult {
    lo: number
    hi: number
    hasVariation: boolean
}

/**
 * Map of string names to DRAG_MODE values
 */
const DRAG_MODE_MAP: Record<string, DRAG_MODE> = {
    none: DRAG_MODE.none,
    contrast: DRAG_MODE.contrast,
    measurement: DRAG_MODE.measurement,
    angle: DRAG_MODE.angle,
    pan: DRAG_MODE.pan,
    slicer3D: DRAG_MODE.slicer3D,
    callbackOnly: DRAG_MODE.callbackOnly,
    roiSelection: DRAG_MODE.roiSelection,
    crosshair: DRAG_MODE.crosshair,
    windowing: DRAG_MODE.windowing
}

/**
 * Parses a string drag mode to DRAG_MODE enum value.
 *
 * @param mode - String mode name or DRAG_MODE enum value
 * @returns The corresponding DRAG_MODE value, or null if unknown string
 */
export function parseDragModeString(mode: string | DRAG_MODE): DRAG_MODE | null {
    if (typeof mode === 'string') {
        const dragMode = DRAG_MODE_MAP[mode]
        return dragMode !== undefined ? dragMode : null
    }
    return mode
}

/**
 * Gets the currently active drag mode or falls back to default.
 *
 * @param params - Parameters containing active and fallback drag modes
 * @returns The effective drag mode
 */
export function getCurrentDragModeValue(params: GetCurrentDragModeParams): DRAG_MODE {
    const { activeDragMode, fallbackDragMode } = params
    if (activeDragMode !== null) {
        return activeDragMode
    }
    return fallbackDragMode
}

/**
 * Creates cleared drag mode state.
 *
 * @returns State with null values for drag mode and button
 */
export function createClearedDragModeState(): ClearedDragModeState {
    return {
        activeDragMode: null,
        activeDragButton: null
    }
}

/**
 * Creates active drag mode state.
 *
 * @param dragMode - The drag mode to set
 * @param button - The mouse button that triggered the drag
 * @returns Active drag mode state
 */
export function createActiveDragModeState(dragMode: DRAG_MODE, button: number): ActiveDragModeState {
    return {
        activeDragMode: dragMode,
        activeDragButton: button
    }
}

/**
 * Calculates min and max voxel indices from an array of two values.
 * Used in selecting intensities with the selection box.
 *
 * @param array - An array of two values
 * @returns An array of two values representing the min and max voxel indices
 * @throws Error if array contains more than two values
 */
export function calculateMinMaxVoxIdx(array: number[]): [number, number] {
    if (array.length > 2) {
        throw new Error('array must not contain more than two values')
    }
    return [Math.floor(Math.min(array[0], array[1])), Math.floor(Math.max(array[0], array[1]))]
}

/**
 * Calculates the angle between two lines in degrees.
 * The intersection point is assumed to be the end of line1 (start of line2).
 *
 * @param line1 - First line as [x0, y0, x1, y1]
 * @param line2 - Second line as [x0, y0, x1, y1]
 * @returns Angle in degrees
 */
export function calculateAngleBetweenLines(line1: number[], line2: number[]): number {
    // For angle measurement, we need to calculate vectors from the intersection point
    // The intersection point is the end of line1 (which is the start of line2)
    const intersectionX = line1[2]
    const intersectionY = line1[3]
    const v1x = line1[0] - intersectionX
    const v1y = line1[1] - intersectionY
    const v2x = line2[2] - intersectionX
    const v2y = line2[3] - intersectionY
    const dot = v1x * v2x + v1y * v2y
    const mag1 = Math.sqrt(v1x * v1x + v1y * v1y)
    const mag2 = Math.sqrt(v2x * v2x + v2y * v2y)
    // Avoid division by zero
    if (mag1 === 0 || mag2 === 0) {
        return 0
    }
    // Calculate angle in radians
    const cosAngle = Math.max(-1, Math.min(1, dot / (mag1 * mag2)))
    const angleRad = Math.acos(cosAngle)
    // Convert to degrees
    const angleDeg = angleRad * (180 / Math.PI)
    return angleDeg
}

/**
 * Creates reset state for angle measurement.
 *
 * @returns Reset angle measurement state
 */
export function createResetAngleMeasurementState(): AngleMeasurementState {
    return {
        angleState: 'none',
        angleFirstLine: [0.0, 0.0, 0.0, 0.0]
    }
}

/**
 * Calculates scaled drag position from canvas coordinates.
 *
 * @param x - X coordinate
 * @param y - Y coordinate
 * @param dpr - Device pixel ratio
 * @returns Scaled [x, y] coordinates
 */
export function calculateDragPosition(x: number, y: number, dpr: number): [number, number] {
    return [x * dpr, y * dpr]
}

/**
 * Calculates pan offset from drag movement.
 *
 * @param params - Parameters for pan/zoom calculation
 * @returns Pan offset result
 */
export function calculatePanZoomFromDrag(params: CalculatePanZoomParams): PanZoomResult {
    const { startMM, endMM, pan2DxyzmmAtMouseDown } = params

    // Calculate the delta between end and start positions
    const v = vec4.create()
    vec4.sub(v, endMM as vec4, startMM as vec4)

    const zoom = pan2DxyzmmAtMouseDown[3]

    return {
        pan2Dxyzmm: [pan2DxyzmmAtMouseDown[0] + zoom * v[0], pan2DxyzmmAtMouseDown[1] + zoom * v[1], pan2DxyzmmAtMouseDown[2] + zoom * v[2], zoom]
    }
}

/**
 * Calculates 3D slicer zoom from drag movement.
 *
 * @param params - Parameters for 3D slicer zoom calculation
 * @returns 3D slicer zoom result
 */
export function calculateSlicer3DZoomFromDrag(params: CalculateSlicer3DZoomParams): Slicer3DZoomResult {
    const { startY, endY, pan2DxyzmmAtMouseDown, currentPan2Dxyzmm, crosshairMM, yoke3Dto2DZoom } = params

    let zoom = pan2DxyzmmAtMouseDown[3]
    const y = endY - startY
    const pixelScale = 0.01
    zoom += y * pixelScale
    zoom = Math.max(zoom, 0.1)
    zoom = Math.min(zoom, 10.0)

    const zoomChange = currentPan2Dxyzmm[3] - zoom

    const result: Slicer3DZoomResult = {
        zoom,
        pan2Dxyzmm: [currentPan2Dxyzmm[0] + zoomChange * crosshairMM[0], currentPan2Dxyzmm[1] + zoomChange * crosshairMM[1], currentPan2Dxyzmm[2] + zoomChange * crosshairMM[2], zoom]
    }

    if (yoke3Dto2DZoom) {
        result.volScaleMultiplier = zoom
    }

    return result
}

/**
 * Calculates windowing (cal_min/cal_max) adjustment from mouse/touch drag.
 *
 * @param params - Parameters for windowing calculation
 * @returns Windowing result with adjusted cal_min and cal_max
 */
export function calculateWindowingAdjustment(params: CalculateWindowingParams): WindowingAdjustmentResult {
    const { x, y, windowX, windowY, currentCalMin, currentCalMax, globalMin, globalMax, gainFactor } = params

    // Ensure gainFactor is finite and non-negative to avoid propagating NaN or inverted behavior.
    let effectiveGainFactor = Number.isFinite(gainFactor as number) ? (gainFactor as number) : 0.5
    if (effectiveGainFactor < 0) {
        effectiveGainFactor = 0
    }

    let mn = currentCalMin
    let mx = currentCalMax

    const deltaY = (y - windowY) * effectiveGainFactor
    const deltaX = (x - windowX) * effectiveGainFactor

    // Adjust level based on vertical movement
    if (deltaY < 0) {
        // increase level if mouse moves up
        mn += Math.abs(deltaY)
        mx += Math.abs(deltaY)
    } else if (deltaY > 0) {
        // decrease level if mouse moves down
        mn -= deltaY
        mx -= deltaY
    }

    // Adjust window width based on horizontal movement
    if (deltaX > 0) {
        // increase window width if mouse moves right
        mn -= deltaX
        mx += deltaX
    } else if (deltaX < 0) {
        // decrease window width if mouse moves left
        mn += Math.abs(deltaX)
        mx -= Math.abs(deltaX)
    }

    // Ensure window width is at least 1
    if (mx - mn < 1) {
        mx = mn + 1
    }

    // Ensure min is not below global min
    if (mn < globalMin) {
        mn = globalMin
    }

    // Ensure max is not above global max
    if (mx > globalMax) {
        mx = globalMax
    }

    // Ensure min is not above max
    if (mn > mx) {
        mn = mx - 1
    }

    return {
        calMin: mn,
        calMax: mx,
        windowX: x,
        windowY: y
    }
}

/**
 * Calculates intensity range (lo/hi) from voxel region selection.
 *
 * @param params - Parameters for intensity range calculation
 * @returns Intensity range result
 */
export function calculateIntensityRangeFromVoxels(params: CalculateIntensityRangeParams): IntensityRangeResult {
    const { xrange, yrange, zrange, dims, img } = params

    let hi = -Number.MAX_VALUE
    let lo = Number.MAX_VALUE

    const xdim = dims[1]
    const ydim = dims[2]

    for (let z = zrange[0]; z < zrange[1]; z++) {
        const zi = z * xdim * ydim
        for (let y = yrange[0]; y < yrange[1]; y++) {
            const yi = y * xdim
            for (let x = xrange[0]; x < xrange[1]; x++) {
                const index = zi + yi + x
                if (lo > img[index]) {
                    lo = img[index]
                }
                if (hi < img[index]) {
                    hi = img[index]
                }
            }
        }
    }

    return {
        lo,
        hi,
        hasVariation: lo < hi
    }
}

/**
 * Adjusts voxel ranges for constant dimensions to ensure at least one iteration.
 *
 * @param startVox - Start voxel coordinates
 * @param endVox - End voxel coordinates
 * @param xrange - X range [min, max]
 * @param yrange - Y range [min, max]
 * @param zrange - Z range [min, max]
 * @returns Adjusted ranges
 */
export function adjustRangesForConstantDimension(
    startVox: ArrayLike<number>,
    endVox: ArrayLike<number>,
    xrange: [number, number],
    yrange: [number, number],
    zrange: [number, number]
): { xrange: [number, number]; yrange: [number, number]; zrange: [number, number] } {
    const newXrange: [number, number] = [...xrange]
    const newYrange: [number, number] = [...yrange]
    const newZrange: [number, number] = [...zrange]

    // For constant dimension, add one so that the for loop runs at least once
    if (startVox[0] - endVox[0] === 0) {
        newXrange[1] = startVox[0] + 1
    } else if (startVox[1] - endVox[1] === 0) {
        newYrange[1] = startVox[1] + 1
    } else if (startVox[2] - endVox[2] === 0) {
        newZrange[1] = startVox[2] + 1
    }

    return { xrange: newXrange, yrange: newYrange, zrange: newZrange }
}

/**
 * Determines if a drag mode should track drag start/end positions.
 *
 * @param dragMode - The current drag mode
 * @returns True if drag positions should be tracked
 */
export function shouldTrackDragPositions(dragMode: DRAG_MODE): boolean {
    return (
        dragMode === DRAG_MODE.contrast ||
        dragMode === DRAG_MODE.measurement ||
        dragMode === DRAG_MODE.pan ||
        dragMode === DRAG_MODE.slicer3D ||
        dragMode === DRAG_MODE.callbackOnly ||
        dragMode === DRAG_MODE.roiSelection ||
        dragMode === DRAG_MODE.angle
    )
}

/**
 * Determines the next angle measurement state based on current state.
 *
 * @param currentState - Current angle measurement state
 * @returns Next state
 */
export function getNextAngleMeasurementState(currentState: 'none' | 'drawing_first_line' | 'drawing_second_line' | 'complete'): 'drawing_first_line' | 'drawing_second_line' | 'complete' | 'none' {
    switch (currentState) {
        case 'none':
            return 'drawing_first_line'
        case 'drawing_first_line':
            return 'drawing_second_line'
        case 'drawing_second_line':
            return 'complete'
        case 'complete':
            return 'drawing_first_line'
        default:
            return 'none'
    }
}

/**
 * Checks if a drag mode is angle measurement mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if angle mode
 */
export function isAngleDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.angle
}

/**
 * Checks if a drag mode is contrast mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if contrast mode
 */
export function isContrastDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.contrast
}

/**
 * Checks if a drag mode is measurement mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if measurement mode
 */
export function isMeasurementDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.measurement
}

/**
 * Checks if a drag mode is pan mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if pan mode
 */
export function isPanDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.pan
}

/**
 * Checks if a drag mode is slicer3D mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if slicer3D mode
 */
export function isSlicer3DDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.slicer3D
}

/**
 * Checks if a drag mode is ROI selection mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if ROI selection mode
 */
export function isRoiSelectionDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.roiSelection
}

/**
 * Checks if a drag mode is callback only mode.
 *
 * @param dragMode - The drag mode to check
 * @returns True if callback only mode
 */
export function isCallbackOnlyDragMode(dragMode: DRAG_MODE): boolean {
    return dragMode === DRAG_MODE.callbackOnly
}
