/**
 * WebGL utility functions for context initialization and texture management.
 * This module provides pure functions for WebGL operations that can be reused
 * throughout the library without class instantiation overhead.
 */

// Texture unit constants
const TEXTURE3_FONT = 33987
const TEXTURE4_THUMBNAIL = 33988
const TEXTURE5_MATCAP = 33989

/**
 * Initialize WebGL2 context and detect GPU capabilities
 * @param canvas - The canvas element to attach to
 * @param isAntiAlias - Whether to enable anti-aliasing
 * @returns Object containing gl context and GPU limits
 */
export function initGL(canvas: HTMLCanvasElement, isAntiAlias: boolean): { gl: WebGL2RenderingContext; max2D: number; max3D: number } {
    const gl = canvas.getContext('webgl2', {
        alpha: true,
        antialias: isAntiAlias
    })

    if (!gl) {
        throw new Error('Unable to initialize WebGL2. Your browser may not support it.')
    }

    return {
        gl,
        max2D: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
        max3D: gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) as number
    }
}

/**
 * Creates a 3D 1-component uint8 texture on the GPU with given dimensions.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @param isInit - Whether to initialize with zeros
 * @returns The created WebGL texture
 */
export function r8Tex(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], isInit = false): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R8, dims[1], dims[2], dims[3])
    if (isInit) {
        const img8 = new Uint8Array(dims[1] * dims[2] * dims[3])
        gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, dims[1], dims[2], dims[3], gl.RED, gl.UNSIGNED_BYTE, img8)
    }
    return texID
}

/**
 * Creates a 3D 1-component uint8 texture with LINEAR filtering.
 * Used for smooth drawing blur textures that need interpolation.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @returns The created WebGL texture
 */
export function r8TexLinear(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[]): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R8, dims[1], dims[2], dims[3])
    return texID
}

/**
 * Creates or updates a 1-component 16-bit signed integer 3D texture on the GPU.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @param img16 - 16-bit signed integer data
 * @returns The created WebGL texture
 */
export function r16Tex(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], img16: Int16Array): WebGLTexture {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()!
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R16I, dims[1], dims[2], dims[3])
    const nv = dims[1] * dims[2] * dims[3]
    if (img16.length !== nv) {
        img16 = new Int16Array(nv)
    }
    gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, dims[1], dims[2], dims[3], gl.RED_INTEGER, gl.SHORT, img16)

    return texID
}

/**
 * Creates a 3D 1-component float16 texture on the GPU with LINEAR filtering.
 * Used for smoothed drawing surfaces.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @param data - Float32Array data to upload (will be stored as R16F)
 * @returns The created WebGL texture
 */
export function r16fTex(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], data: Float32Array): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.R16F, dims[1], dims[2], dims[3])
    gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, dims[1], dims[2], dims[3], gl.RED, gl.FLOAT, data)
    return texID
}

/**
 * Creates a 2D 4-component (RGBA) uint8 texture on the GPU with optional vertical flip.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height]
 * @param data - Optional RGBA data
 * @param isFlipVertical - Whether to flip texture vertically
 * @returns The created WebGL texture
 */
export function rgbaTex2D(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], data: Uint8Array | null = null, isFlipVertical = true): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_2D, texID)

    // Set texture parameters
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)

    // Allocate storage for the 2D texture
    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, dims[1], dims[2])
    if (data) {
        let drawData = data
        const width = dims[1]
        const height = dims[2]
        if (isFlipVertical) {
            drawData = new Uint8Array(data.length)
            const rowSize = width * 4 // RGBA has 4 bytes per pixel
            for (let y = 0; y < height; y++) {
                const srcStart = y * rowSize
                const destStart = (height - 1 - y) * rowSize
                drawData.set(data.subarray(srcStart, srcStart + rowSize), destStart)
            }
        }
        gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, drawData)
    }
    return texID
}

/**
 * Creates a 3D 4-component (RGBA) uint8 texture on the GPU.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @param isInit - Whether to initialize with zeros
 * @returns The created WebGL texture
 */
export function rgbaTex(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], isInit = false): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGBA8, dims[1], dims[2], dims[3])
    if (isInit) {
        const img8 = new Uint8Array(dims[1] * dims[2] * dims[3] * 4)
        gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, dims[1], dims[2], dims[3], gl.RGBA, gl.UNSIGNED_BYTE, img8)
    }
    return texID
}

/**
 * Create or recreate a 3D RGBA16UI texture on the GPU with given dimensions.
 * @param gl - WebGL2 rendering context
 * @param texID - Existing texture to delete (null for new texture)
 * @param activeID - Texture unit to activate
 * @param dims - Dimensions array [0, width, height, depth]
 * @param isInit - Whether to initialize with zeros
 * @returns The created WebGL texture
 */
export function rgba16Tex(gl: WebGL2RenderingContext, texID: WebGLTexture | null, activeID: number, dims: number[], isInit = false): WebGLTexture | null {
    if (texID) {
        gl.deleteTexture(texID)
    }
    texID = gl.createTexture()
    gl.activeTexture(activeID)
    gl.bindTexture(gl.TEXTURE_3D, texID)
    // Note: cannot be gl.LINEAR for integer textures
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2)
    gl.pixelStorei(gl.PACK_ALIGNMENT, 2)
    gl.texStorage3D(gl.TEXTURE_3D, 1, gl.RGBA16UI, dims[1], dims[2], dims[3])
    if (isInit) {
        const img16 = new Uint16Array(dims[1] * dims[2] * dims[3] * 4)
        gl.texSubImage3D(gl.TEXTURE_3D, 0, 0, 0, 0, dims[1], dims[2], dims[3], gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, img16)
    }
    return texID
}

/**
 * Remove cross-origin attribute from image if its URL is not from the same origin as the current page.
 * @param img - The image element
 * @param url - The image URL
 */
export function requestCORSIfNotSameOrigin(img: HTMLImageElement, url: string): void {
    if (new URL(url, window.location.href).origin !== window.location.origin) {
        img.crossOrigin = ''
    }
}

/**
 * Loads a PNG image from a URL and creates a 4-component (RGBA) uint8 WebGL texture.
 * @param gl - WebGL2 rendering context
 * @param pngUrl - URL of the PNG image
 * @param textureNum - Texture unit number (3=font, 4=thumbnail, 5=matcap)
 * @param fontShader - Font shader (required for textureNum 3)
 * @param bmpShader - Bitmap shader (required for textureNum 4)
 * @param fontTexture - Current font texture reference (will be deleted if not null)
 * @param bmpTexture - Current bitmap texture reference (will be deleted if not null)
 * @param matCapTexture - Current matcap texture reference (will be deleted if not null)
 * @param onBmpTextureLoaded - Callback when bitmap texture loaded with width/height ratio
 * @returns Promise resolving to the created texture
 */
export async function loadPngAsTexture(
    gl: WebGL2RenderingContext,
    pngUrl: string,
    textureNum: number,
    fontShader: { use: (gl: WebGL2RenderingContext) => void; uniforms: Record<string, WebGLUniformLocation> } | null,
    bmpShader: { use: (gl: WebGL2RenderingContext) => void; uniforms: Record<string, WebGLUniformLocation> } | null,
    fontTexture: WebGLTexture | null,
    bmpTexture: WebGLTexture | null,
    matCapTexture: WebGLTexture | null,
    onBmpTextureLoaded?: (widthHeightRatio: number) => void
): Promise<WebGLTexture | null> {
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = (): void => {
            if (!bmpShader) {
                return
            }
            let pngTexture: WebGLTexture | null

            if (textureNum === 4) {
                // Delete old texture if it exists
                if (bmpTexture !== null) {
                    gl.deleteTexture(bmpTexture)
                }
                // Create new texture
                pngTexture = gl.createTexture()
                const bmpTextureWH = img.width / img.height
                gl.activeTexture(TEXTURE4_THUMBNAIL)
                bmpShader.use(gl)
                gl.uniform1i(bmpShader.uniforms.bmpTexture, 4)
                if (onBmpTextureLoaded) {
                    onBmpTextureLoaded(bmpTextureWH)
                }
            } else if (textureNum === 5) {
                // Delete old texture if it exists
                if (matCapTexture !== null) {
                    gl.deleteTexture(matCapTexture)
                }
                // Create new texture
                pngTexture = gl.createTexture()
                gl.activeTexture(TEXTURE5_MATCAP)
            } else {
                // textureNum === 3 (font)
                if (!fontShader) {
                    reject(new Error('Font shader required for texture unit 3'))
                    return
                }
                // Delete old texture if it exists
                if (fontTexture !== null) {
                    gl.deleteTexture(fontTexture)
                }
                // Create new texture
                pngTexture = gl.createTexture()
                fontShader.use(gl)
                gl.activeTexture(TEXTURE3_FONT)
                gl.uniform1i(fontShader.uniforms.fontTexture, 3)
            }

            gl.bindTexture(gl.TEXTURE_2D, pngTexture)
            // Set the parameters so we can render any size image.
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
            // Upload the image into the texture.
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
            resolve(pngTexture)
            // PR1567
            // if (textureNum !== 4 && onDrawScene) {
            //    onDrawScene()
            // }
        }
        img.onerror = reject
        requestCORSIfNotSameOrigin(img, pngUrl)
        img.src = pngUrl
    })
}
