import { assert } from "../../../../core/assert.js";
import { ceilPowerOfTwo } from "../../../../core/binary/operations/ceilPowerOfTwo.js";
import { BinaryDataType } from "../../../../core/binary/type/BinaryDataType.js";
import {
    DataType2TypedArrayConstructorMapping
} from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { MaxRectanglesPacker } from "../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
import Vector2 from "../../../../core/geom/Vector2.js";
import IdPool from "../../../../core/IdPool.js";
import { max2 } from "../../../../core/math/max2.js";
import { invokeObjectClone } from "../../../../core/model/object/invokeObjectClone.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { sampler2d_sub_copy_same_item_size } from "../sampler/sampler2d_sub_copy_same_item_size.js";
import { AbstractTextureAtlas } from "./AbstractTextureAtlas.js";
import { AtlasPatch } from "./AtlasPatch.js";
import { AtlasPatchFlag } from "./AtlasPatchFlag.js";

export class TextureAtlas extends AbstractTextureAtlas {
    /**
     *
     * @param {number} [size]
     * @param {BinaryDataType} [data_type]
     * @param {number} [channel_count]
     * @constructor
     */
    constructor(size = 16, data_type = BinaryDataType.Uint8, channel_count = 4) {
        super();

        assert.isNonNegativeInteger(size, 'size');
        assert.enum(data_type, BinaryDataType, 'data_type');
        assert.isNonNegativeInteger(channel_count, 'channel_count');

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

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

        /**
         * @readonly
         * @type {Vector2}
         */
        this.size = new Vector2(size, size);

        const TypeArrayConstructor = DataType2TypedArrayConstructorMapping[data_type];

        /**
         * @private
         * @readonly
         * @type {Sampler2D}
         */
        this.__sampler = new Sampler2D(new TypeArrayConstructor(size * size * channel_count), channel_count, size, size);


        /**
         * @private
         * @type {MaxRectanglesPacker}
         */
        this.packer = new MaxRectanglesPacker(size, size);

        this.on = {
            painted: new Signal(),
            /**
             * @type {Signal<AtlasPatch, Sampler2D>}
             */
            removed: new Signal()
        };

        /**
         *
         * @type {boolean}
         * @private
         */
        this.__needsUpdate = false;
    }

    /**
     *
     * @return {Sampler2D}
     */
    get sampler() {
        return this.__sampler;
    }

    /**
     *
     * @param {Sampler2D} sampler
     * @returns {AtlasPatch|undefined}
     */
    getPatch(sampler) {
        const patches = this.patches;
        const patch_count = patches.length;
        for (let i = 0; i < patch_count; i++) {
            const patch = patches[i];

            if (patch.sampler === sampler) {
                return patch;
            }
        }
    }

    /**
     * Whether or not current state of atlas requires calling {#update}
     * @returns {boolean}
     */
    needsUpdate() {
        return this.__needsUpdate;
    }

    /**
     *
     * @param {AtlasPatch} patch
     * @returns {boolean}
     */
    contains(patch) {
        assert.notNull(patch, 'patch');
        assert.defined(patch, 'patch');
        assert.ok(patch.isAtlasPatch, 'patch argument is not an AtlasPatch');

        return this.patches.includes(patch);
    }

    /**
     * Clear canvas and update patch flags
     */
    erase() {
        //clear all painted patches

        const patches = this.patches;

        const l = patches.length;

        for (let i = 0; i < l; i++) {
            const atlasPatch = patches[i];

            atlasPatch.clearFlag(AtlasPatchFlag.Painted);
        }


        //erase data
        this.__sampler.data.fill(0);

        this.__needsUpdate = true;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {boolean} true if resizing successful, false otherwise
     */
    resize(x, y) {
        assert.isNonNegativeInteger(x, 'x');
        assert.isNonNegativeInteger(y, 'y');

        //check if any patches would be cut
        const patches = this.patches;
        const numPatches = patches.length;

        //update patch UVs
        for (let i = 0; i < numPatches; i++) {
            const patch = patches[i];

            if (patch.getFlag(AtlasPatchFlag.Packed)) {
                //only care about packed patches

                const x1 = patch.packing.x1;
                const y1 = patch.packing.y1;

                if (x1 > x || y1 > y) {
                    // patch bounds would violate the desired atlas extends
                    // Resizing atlas would result in some patches not fitting
                    return false;
                }
            }
        }

        //update patch UVs
        for (let i = 0; i < numPatches; i++) {
            const patch = patches[i];

            if (patch.getFlag(AtlasPatchFlag.Packed)) {
                patch.updateUV(x, y);
            }
        }

        this.size.set(x, y);
        this.packer.resize(x, y);
        this.__sampler.resize(x, y);

        return true;
    }

    /**
     *
     * @param {AtlasPatch} patch
     * @private
     */
    paintPatch(patch) {
        // console.time('TextureAtlas.paintPatch');

        const target = this.__sampler;

        const source = patch.sampler;

        const patch_position = patch.position;
        const patch_size = patch.size;

        sampler2d_sub_copy_same_item_size(
            target,
            source,
            0, 0,
            patch_position.x, patch_position.y, patch_size.x, patch_size.y
        );

        patch.setFlag(AtlasPatchFlag.Painted);
        patch.last_painted_version = source.version; // record version

        // console.timeEnd('TextureAtlas.paintPatch');
    }

    /**
     *
     * @param {AtlasPatch} patch
     * @private
     */
    erasePatch(patch) {
        //erase the patch
        const packing = patch.packing;

        const x0 = packing.x0;
        const y0 = packing.y0;
        const x1 = packing.x1;
        const y1 = packing.y1;

        this.eraseArea(x0, y0, x1, y1);

        // mark as non-painted
        patch.clearFlag(AtlasPatchFlag.Painted);
    }

    /**
     * @private
     * @param {number} x0
     * @param {number} y0
     * @param {number} x1
     * @param {number} y1
     */
    eraseArea(x0, y0, x1, y1) {
        const sampler = this.__sampler;

        sampler.zeroFill(x0, y0, x1 - x0, y1 - y0);
    }

    /**
     *
     * @param {Sampler2D} sampler
     * @param {number} [padding]
     * @return {AtlasPatch}
     */
    add(sampler, padding = 4) {

        assert.notNull(sampler, 'sampler');
        assert.defined(sampler, 'sampler');
        assert.equal(sampler.isSampler2D, true, 'sampler.isSampler2D !== true');

        assert.isNonNegativeInteger(padding, 'padding');

        // console.log('#add');

        const patch = new AtlasPatch();

        patch.id = this.idPool.get();
        patch.sampler = sampler;
        patch.size.set(sampler.width, sampler.height);
        patch.padding = padding;

        // mark patch as belonging to an atlas
        patch.setFlag(AtlasPatchFlag.Attached);

        const padding2 = padding * 2;

        patch.packing.set(0, 0, sampler.width + padding2, sampler.height + padding2);

        this.patches.push(patch);


        this.__needsUpdate = true;

        return patch;
    }

    /**
     * Whether a patch of certain size could be packed without triggering repack or growing the atlas
     * @param {number} w
     * @param {number} h
     * @returns {boolean}
     */
    can_pack(w, h) {
        return this.packer.canAdd(w, h);
    }

    /**
     *
     * @param {AtlasPatch} patch
     * @return {boolean}
     */
    remove(patch) {
        // console.log('#remove');
        const patchIndex = this.patches.indexOf(patch);

        if (patchIndex === -1) {
            //not on the atlas, do nothing
            return false;
        }

        this.patches.splice(patchIndex, 1);
        this.idPool.release(patch.id);

        if (patch.getFlag(AtlasPatchFlag.Painted)) {
            //erase the patch
            this.erasePatch(patch);
        }

        if (patch.getFlag(AtlasPatchFlag.Packed)) {
            this.packer.remove(patch.packing);
        }

        // clear flags
        patch.clearFlag(
            AtlasPatchFlag.Attached
            | AtlasPatchFlag.Packed
            | AtlasPatchFlag.Painted
        );

        this.on.removed.send2(patch, patch.sampler);

        return true;
    }

    /**
     * Re-packs all patches, this is useful for reclaiming fragmented space after extended usage
     * @returns {boolean}
     */
    repack() {
        const patches = this.patches;

        const numPatches = patches.length;

        for (let i = 0; i < numPatches; i++) {
            const patch = patches[i];

            if (patch.getFlag(AtlasPatchFlag.Packed)) {
                patch.clearFlag(AtlasPatchFlag.Packed | AtlasPatchFlag.Packed);

                this.packer.remove(patch.packing);
            }
        }

        this.pack();

        this.__needsUpdate = true;

        return true;
    }

    /**
     * Pack any patches that are not packed yet
     * @private
     * @returns {boolean}
     */
    pack() {
        const patches = this.patches;

        const numPatches = patches.length;
        let i, l;

        const additions = [];

        for (i = 0; i < numPatches; i++) {
            const patch = patches[i];

            if (!patch.getFlag(AtlasPatchFlag.Packed)) {
                additions.push(patch.packing);
            }
        }

        //perform additions
        if (additions.length <= 0) {
            //nothing to pack
            return true;
        }

        //add all at once, this allows packer to optimize order internally to achieve better results
        if (this.packer.addMany(additions)) {
            //all packed, we're done
        } else {

            //packing failed, lets try a fresh re-pack
            const repacker = new MaxRectanglesPacker(this.size.x, this.size.y);

            // Clone existing placements to make sure our attempt can be reverted
            const originalPlacements = this.packer.boxes.map(invokeObjectClone);

            // Add packed boxes
            Array.prototype.push.apply(additions, this.packer.boxes);

            const repackSuccessful = repacker.addMany(additions);

            if (!repackSuccessful) {
                // repack failed

                for (i = 0, l = originalPlacements.length; i < l; i++) {
                    const originalPlacement = originalPlacements[i];

                    const box = this.packer.boxes[i];

                    //restore packing
                    box.copy(originalPlacement);
                }

                return false;
            }

            //repack succeeded, actualize placements
            for (i = 0, l = originalPlacements.length; i < l; i++) {
                const source = this.packer.boxes[i];

                const originalPlacement = originalPlacements[i];


                if (source.equals(originalPlacement)) {
                    //same position retained
                    // console.log("patch retained");
                } else {
                    //find patch

                    const atlasPatch = this.patches.find(p => p.packing === source);

                    assert.defined(atlasPatch, "atlasPatch");

                    //clear our original patch area
                    this.eraseArea(originalPlacement.x0, originalPlacement.y0, originalPlacement.x1, originalPlacement.y1);

                    //mark patch for re-paint
                    atlasPatch.clearFlag(AtlasPatchFlag.Painted);

                    // console.log("patch erased");
                }

                //replace packing
            }


            //mark all patches for a repaint
            // this.erase();

            //replace packer with the new one
            this.packer = repacker;
        }

        for (i = 0; i < numPatches; i++) {
            const patch = patches[i];

            //mark patch as packed
            patch.setFlag(AtlasPatchFlag.Packed);

            patch.updatePositionFromPacking(this.size.x, this.size.y);
        }


        return true;
    }

    /**
     * @private
     */
    paint() {
        // console.time('TextureAtlas.paint');
        const patches = this.patches;

        const l = patches.length;

        let paintCount = 0;

        for (let i = 0; i < l; i++) {
            const patch = patches[i];

            if (
                !patch.getFlag(AtlasPatchFlag.Painted)
                || patch.last_painted_version !== patch.sampler.version
            ) {

                this.paintPatch(patch);

                paintCount++;

            }
        }

        // console.timeEnd('TextureAtlas.paint');

        if (paintCount > 0) {
            //notify that atlas was painted
            this.on.painted.send0();
        }
    }

    /**
     *
     */
    update() {
        if (!this.__needsUpdate) {
            //no update required
            return;
        }

        // console.group("TextureAtlas.update");
        // console.time('TextureAtlas.update');

        const maxPower = 14;

        const initial_p_x = max2(0, Math.log2(ceilPowerOfTwo(this.size.x)));
        const initial_p_y = max2(0, Math.log2(ceilPowerOfTwo(this.size.y)));

        let power_x = initial_p_x;
        let power_y = initial_p_y;

        while (!this.pack()) {
            //packing failed, grow canvas

            for (; ;) {
                if (power_x < power_y) {
                    power_x++;
                } else {
                    power_y++;
                }

                if (power_x > maxPower || power_y > maxPower) {
                    throw new Error(`Packing failed, could not pack ${this.patches.length} into ${Math.pow(2, maxPower)} resolution texture. Initial powers: ${initial_p_x}, ${initial_p_y}`);
                }

                const size_x = Math.pow(2, power_x);
                const size_y = Math.pow(2, power_y);

                if (this.resize(size_x, size_y)) {
                    break;
                }
            }
        }

        this.paint();

        this.__needsUpdate = false;

        // console.timeEnd('TextureAtlas.update');
        // console.groupEnd("TextureAtlas.update");

    }

    /**
     * Drops all the data
     */
    reset() {
        this.__sampler.data.fill(0);

        this.patches = [];

        this.packer.clear();

        this.idPool.reset();
    }
}
