import {
    ClampToEdgeWrapping,
    DataTexture,
    LinearEncoding,
    LinearFilter,
    LinearMipMapLinearFilter,
    RGBAFormat,
    UnsignedByteType,
    UVMapping
} from "three";
import { MaxRectanglesPacker } from "../../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
import IdPool from "../../../../../core/IdPool.js";
import { formatToChannelCount } from "../../formatToChannelCount.js";
import { AtlasPatch } from "../AtlasPatch.js";

export class WebGLTextureAtlas {
    constructor({
                    format = RGBAFormat,
                    minFilter = LinearMipMapLinearFilter,
                    magFilter = LinearFilter,
                    type = UnsignedByteType,
                    mapping = UVMapping,
                    anisotropy = 4,
                    encoding = LinearEncoding
                }) {
        /**
         *
         * @type {Texture[]}
         * @private
         */
        this.__sources = [];

        /**
         *
         * @type {AtlasPatch[]}
         * @private
         */
        this.__patches = [];

        /**
         *
         * @type {IdPool}
         * @private
         */
        this.__ids = new IdPool();

        /**
         *
         * @type {number}
         * @private
         */
        this.__width = 1;

        /**
         *
         * @type {number}
         * @private
         */
        this.__height = 1;

        /**
         *
         * @type {number}
         * @private
         */
        this.__total_area = 0;

        /**
         *
         * @type {number}
         * @private
         */
        this.__max_size = 16000;

        /**
         *
         * @type {DataTexture}
         * @private
         */
        this.__texture = new DataTexture(new Uint8Array(0), 0, 0, format, type, mapping, ClampToEdgeWrapping, ClampToEdgeWrapping, magFilter, minFilter, anisotropy, encoding);
    }

    /**
     *
     * @param {Texture} source
     * @param {number} source_x
     * @param {number} source_y
     * @param {number} source_width
     * @param {number} source_height
     * @param {number} target_width
     * @param {number} target_height
     */
    add(source, source_x, source_y, source_width, source_height, target_width, target_height) {
        const id = this.__ids.get();

        const patch = new AtlasPatch();
        patch.id = id;

        this.__patches.push(patch);
        this.__sources[id] = source;

        // compute area of the target patch
        const area = target_width * target_height;

        this.__total_area += area;

        return patch;
    }

    layout() {
        // estimate initial size based on the total area
        let power = Math.ceil(Math.log2(this.__total_area));
        let size = 0;

        const aabbs = this.__patches.map(p => p.packing.clone());

        // attempt packing
        for (; ;) {
            size = Math.pow(2, power);

            if (size > this.__max_size) {
                throw new Error(`Failed to pack patches, maximum atlas size exceeded`);
            }

            const packer = new MaxRectanglesPacker(size, size);

            const success = packer.addMany(aabbs);

            if (success) {
                break;
            } else {
                // failed to pack, increase texture size to next power of 2
                power++;
            }

        }

        this.__width = size;
        this.__height = size;

        // copy packed positions back to patches
        aabbs.forEach((aabb, i) => {
            const patch = this.__patches[i];

            patch.packing.copy(aabb);
        });
    }

    /**
     * Paint patches onto the atlas texture
     * NOTE: all patches are assumed to be packed properly at this point
     * @param {WebGLRenderer} renderer
     */
    build(renderer) {
        // check if texture needs to be rebuilt
        const destination_texture = this.__texture;

        if (destination_texture.image.width !== this.__width || destination_texture.image.height !== this.__height) {
            destination_texture.dispose();

            const channel_count = formatToChannelCount(destination_texture.format);

            destination_texture.image.data = new Uint8Array(this.__height * this.__width * channel_count);
            destination_texture.image.height = this.__height;
            destination_texture.image.width = this.__width;
        }

        // mark for update
        destination_texture.needsUpdate = true;

        // paint individual patches
        const patch_count = this.__patches.length;
        for (let i = 0; i < patch_count; i++) {

            const atlas_patch = this.__patches[i];
            const source_texture = this.__sources[i];

            renderer.copyTextureToTexture(atlas_patch.position, source_texture, destination_texture);
        }
    }
}
