/*
 * Copyright (c) 2015-2018, IGN France.
 * Copyright (c) 2018-2026, Giro3D team.
 * SPDX-License-Identifier: MIT
 */

import {
    NearestFilter,
    RGBAFormat,
    UnsignedByteType,
    type Texture,
    type WebGLRenderer,
} from 'three';

import type ColorMap from '../core/ColorMap';

import Rect from '../core/Rect';
import WebGLComposer from './composition/WebGLComposer';

/**
 * Combines color map textures into a single one.
 * This is necessary to avoid consuming too many texture units.
 */
class ColorMapAtlas {
    private readonly _renderer: WebGLRenderer;
    private readonly _colorMaps: Map<ColorMap, { offset: number; texture: string }> = new Map();
    private _texture: Texture | null = null;
    private _dirty = false;
    private _disposed = false;

    /**
     * @param renderer - The renderer
     */
    public constructor(renderer: WebGLRenderer) {
        this._renderer = renderer;
    }

    /**
     * Adds a color map to the atlas.
     *
     * @param colorMap - The color map.
     */
    public add(colorMap: ColorMap): void {
        this._colorMaps.set(colorMap, { offset: 0, texture: '' });
        this._dirty = true;
    }

    /**
     * Removes a color map from the atlas.
     *
     * @param colorMap - The color map.
     */
    public remove(colorMap: ColorMap): void {
        this._colorMaps.delete(colorMap);
        this._dirty = true;
    }

    public forceUpdate(): void {
        this._dirty = true;
    }

    public update(): void {
        // The atlas should be re-rendered if any colormap texture has changed.
        for (const [colorMap, info] of this._colorMaps.entries()) {
            const texture = colorMap.getTexture();
            if (texture.uuid !== info.texture) {
                this._dirty = true;
                break;
            }
        }
    }

    private createTexture(): void {
        this._texture?.dispose();
        this._texture = null;

        if (this._colorMaps.size === 0) {
            return;
        }

        // The atlas width is the width of the biggest color map.
        const atlasWidth = Math.max(...[...this._colorMaps.keys()].map(c => c.colors.length));
        // Use 3 pixels in height per color map, to avoid filtering artifacts when using
        // tightly packed 1-pixel textures.
        const atlasHeight = this._colorMaps.size * 3;

        const atlas = new WebGLComposer({
            extent: new Rect(0, 1, 0, 1),
            width: atlasWidth,
            height: atlasHeight,
            webGLRenderer: this._renderer,
            minFilter: NearestFilter,
            magFilter: NearestFilter,
            reuseTexture: false,
            textureDataType: UnsignedByteType,
            pixelFormat: RGBAFormat,
        });

        const height = 1 / this._colorMaps.size;
        let yMin = 0;

        for (const [colorMap, info] of this._colorMaps.entries()) {
            const yMax = yMin + height;

            // Each color map will be rendered as an horizontal stripe of width 100% and height 1/N
            // of the atlas height, where N is the number of color maps to pack into the atlas.
            const rect = new Rect(0, 1, yMin, yMax);

            const texture = colorMap.getTexture();
            atlas.draw(colorMap.getTexture(), rect);

            // The offset lies right in the middle pixel of the stripe.
            info.offset = rect.centerY;
            info.texture = texture.uuid;
            yMin = yMax;
        }

        this._texture = atlas.render();
        this._texture.name = 'ColorMapAtlas';
        atlas.dispose();
        this._dirty = false;
    }

    /**
     * Gets the atlas texture.
     */
    public get texture(): Texture | null {
        if (this._dirty) {
            this.createTexture();
        }
        return this._texture;
    }

    /**
     * Gets the vertical offset for the specified color map.
     *
     * @param colorMap - The color map.
     * @returns The offset.
     */
    public getOffset(colorMap: ColorMap): number | undefined {
        if (this._dirty) {
            this.createTexture();
        }
        return this._colorMaps.get(colorMap)?.offset;
    }

    public dispose(): void {
        if (this._disposed) {
            return;
        }
        this._disposed = true;
        this._texture?.dispose();
        this._colorMaps.clear();
    }
}

export default ColorMapAtlas;
