import type {Context} from './context.ts';
import type {RGBAImage, AlphaImage} from '../util/image.ts';
import {premultiplyAlpha} from '../util/image.ts';

export type TextureFormat = WebGLRenderingContextBase['RGBA'] | WebGLRenderingContextBase['ALPHA'];
export type TextureFilter = WebGLRenderingContextBase['LINEAR'] | WebGLRenderingContextBase['LINEAR_MIPMAP_NEAREST'] | WebGLRenderingContextBase['NEAREST'];
export type TextureWrap = WebGLRenderingContextBase['REPEAT'] | WebGLRenderingContextBase['CLAMP_TO_EDGE'] | WebGLRenderingContextBase['MIRRORED_REPEAT'];

type EmptyImage = {
    width: number;
    height: number;
    data: null;
};

type DataTextureImage = RGBAImage | AlphaImage | EmptyImage;
export type TextureImage = TexImageSource | DataTextureImage;

function hasDataProperty(image: TextureImage): image is DataTextureImage {
    return 'data' in image;
}

/**
 * @internal
 * A `Texture` GL related object
 */
export class Texture {
    context: Context;
    size: [number, number];
    texture: WebGLTexture;
    format: TextureFormat;
    filter: TextureFilter;
    wrap: TextureWrap;
    useMipmap: boolean;

    /** Tracks the original handle to detect corruption after context loss (#2811) */
    private _ownedHandle: WebGLTexture;

    constructor(context: Context, image: TextureImage, format: TextureFormat, options?: {
        premultiply?: boolean;
        useMipmap?: boolean;
    } | null) {
        this.context = context;
        this.format = format;
        this.texture = context.gl.createTexture();
        this._ownedHandle = this.texture;
        this.update(image, options);
    }

    update(image: TextureImage, options?: {
        premultiply?: boolean;
        useMipmap?: boolean;
    } | null, position?: {
        x: number;
        y: number;
    }): void {
        const {width, height} = image as {width: number; height: number};
        const resize = (this.size?.[0] !== width || this.size[1] !== height) && !position;
        const {context} = this;
        const {gl} = context;

        this.useMipmap = Boolean(options?.useMipmap);

        if (resize && this.size && this.format === gl.RGBA) {
            gl.deleteTexture(this.texture);
            this.texture = gl.createTexture();
            this._ownedHandle = this.texture;
            // A fresh handle is back on GL's defaults, so bind() has to re-apply these.
            this.filter = undefined;
            this.wrap = undefined;
        }

        gl.bindTexture(gl.TEXTURE_2D, this.texture);

        context.pixelStoreUnpackFlipY.set(false);
        context.pixelStoreUnpack.set(1);

        const wantPremultiply = this.format === gl.RGBA && (options?.premultiply !== false);

        if (resize) {
            this.size = [width, height];

            if (this.format === gl.RGBA && width > 0 && height > 0) {
                const mipLevels = this.useMipmap ? Math.floor(Math.log2(Math.max(width, height))) + 1 : 1;
                gl.texStorage2D(gl.TEXTURE_2D, mipLevels, gl.RGBA8, width, height);

                if (hasDataProperty(image)) {
                    // #2030: raw data is premultiplied in JS
                    context.pixelStoreUnpackPremultiplyAlpha.set(false);
                    let {data} = image;
                    if (wantPremultiply && data) data = premultiplyAlpha(data);
                    if (data) gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
                } else {
                    context.pixelStoreUnpackPremultiplyAlpha.set(wantPremultiply);
                    gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image);
                }
            } else {
                if (hasDataProperty(image)) {
                    // #2030: raw data is premultiplied in JS
                    context.pixelStoreUnpackPremultiplyAlpha.set(false);
                    this._uploadRawData(image, wantPremultiply, width, height, gl);
                } else {
                    context.pixelStoreUnpackPremultiplyAlpha.set(wantPremultiply);
                    this._uploadDomImage(image, gl);
                }
            }
        } else {
            const {x, y} = position || {x: 0, y: 0};
            if (hasDataProperty(image)) {
                context.pixelStoreUnpackPremultiplyAlpha.set(false);
                this._updateRawData(image, wantPremultiply, x, y, width, height, gl);
            } else {
                context.pixelStoreUnpackPremultiplyAlpha.set(wantPremultiply);
                this._updateDomImage(image, x, y, gl);
            }
        }

        if (this.useMipmap) {
            gl.generateMipmap(gl.TEXTURE_2D);
        }

        context.pixelStoreUnpackFlipY.setDefault();
        context.pixelStoreUnpack.setDefault();
        context.pixelStoreUnpackPremultiplyAlpha.setDefault();
    }

    private _uploadDomImage(image: TexImageSource, gl: WebGL2RenderingContext) {
        gl.texImage2D(gl.TEXTURE_2D, 0, this.format, this.format, gl.UNSIGNED_BYTE, image);
    }

    private _uploadRawData(image: DataTextureImage, wantPremultiply: boolean, width: number, height: number, gl: WebGL2RenderingContext) {
        let {data} = image;
        if (wantPremultiply && data) data = premultiplyAlpha(data);
        gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, gl.UNSIGNED_BYTE, data);
    }

    private _updateDomImage(image: TexImageSource, x: number, y: number, gl: WebGL2RenderingContext) {
        gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, image);
    }

    private _updateRawData(image: DataTextureImage, wantPremultiply: boolean, x: number, y: number, width: number, height: number, gl: WebGL2RenderingContext) {
        let {data} = image;
        if (wantPremultiply && data) data = premultiplyAlpha(data);
        gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
    }

    bind(filter: TextureFilter, wrap: TextureWrap, minFilter?: TextureFilter | null): void {
        const {context} = this;
        const {gl} = context;

        if (this.texture !== this._ownedHandle) {
            this.texture = this._ownedHandle;
        }

        gl.bindTexture(gl.TEXTURE_2D, this.texture);

        if (minFilter === gl.LINEAR_MIPMAP_NEAREST && !this.useMipmap) {
            minFilter = gl.LINEAR;
        }

        if (filter !== this.filter) {
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter || filter);
            this.filter = filter;
        }

        if (wrap !== this.wrap) {
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
            this.wrap = wrap;
        }
    }

    destroy(): void {
        const {gl} = this.context;
        gl.deleteTexture(this.texture);
        this.texture = null;
        this._ownedHandle = null;
    }
}
