import { assert } from "../../../assert.js";
import BinaryHeap from "../../../collection/heap/BinaryHeap.js";
import { QuadTreeDatum } from "../../2d/quad-tree/QuadTreeDatum.js";
import { QuadTreeNode } from "../../2d/quad-tree/QuadTreeNode.js";
import Vector2 from "../../Vector2.js";
import { costByRemainingArea } from "./cost/costByRemainingArea.js";
import { findBestContainer } from "./findBestContainer.js";
import { packOneBox } from "./packOneBox.js";
import { removeRedundantBoxes } from "./removeRedundantBoxes.js";

/**
 * Packs rectangles into a finite area, packer is incremental and supports both insertions and removals of rectangles.
 * Implementation of "max rectangles" packing algorithm.
 * Useful for packing texture atlases.
 *
 * @see "A Thousand Ways to Pack the Bin - A Practical Approach to Two-Dimensional Rectangle Bin Packing" 2010 Jukka Jylänki
 */
export class MaxRectanglesPacker {
    /**
     *
     * @param {number} width
     * @param {number} height
     */
    constructor(width, height) {
        this.size = new Vector2(width, height);

        /**
         *
         * @type {QuadTreeNode}
         */
        this.free = new QuadTreeNode(0, 0, width, height);

        // initialize a with a free space occupying the entire area
        this.free.add(null, 0, 0, width, height);

        /**
         * Currently held boxes.
         * Managed internally, do not modify.
         * @readonly
         * @type {AABB2[]}
         */
        this.boxes = [];
    }

    /**
     *
     * @param {AABB2} box exact box to remove
     * @returns {boolean} true when box is removed, false if not found
     */
    remove(box) {
        const i = this.boxes.indexOf(box);

        if (i === -1) {
            //not found
            return false;
        }

        this.boxes.splice(i, 1);

        // introduce a free node in the unoccupied space
        this.free.insertDatum(new QuadTreeDatum(box.x0, box.y0, box.x1, box.y1));


        // assert.ok(this.validate());


        return true;
    }

    /**
     *
     * @param {AABB2[]} boxes
     * @returns {number} How many failed to be removed. Each failure represents a box which was not packed in the first place.
     */
    removeMany(boxes) {
        let failures = 0;

        const l = boxes.length;
        for (let i = 0; i < l; i++) {
            const box = boxes[i];

            if (!this.remove(box)) {
                failures++;
            }

        }

        return failures;
    }

    /**
     * Pack a given box.
     * Atomic, if box fails packing - the existing data structure is unchanged.
     * @param {AABB2} box
     * @returns {boolean} true if box was successfully added into the packing, false otherwise
     */
    add(box) {
        const success = packOneBox(box, this.free);

        if (success) {
            this.boxes.push(box);
            removeRedundantBoxes(this.free);
        }

        // assert.ok(this.validate());

        return success;
    }

    /**
     * Tests whether a rectangle of given dimensions can be packed into remaining space
     * Essentially, if this method succeeds - insertion will succeed as well, and if it fails - insertion will fail too
     * @param {number} w
     * @param {number} h
     * @return {boolean}
     */
    canAdd(w, h) {
        return findBestContainer(w, h, this.free, costByRemainingArea) !== null;
    }

    /**
     * Method is transactional, if one box fails to be packed, all fail and packer is reverted to original state
     * @param {AABB2[]} boxes
     * @returns {boolean}
     */
    addMany(boxes) {
        assert.defined(boxes, 'boxes');
        assert.isArray(boxes, 'boxes');

        // assert.ok(this.validate());

        const numBoxes = boxes.length;

        const packed = [];

        /**
         *
         * @param {number} boxIndex
         * @returns {number}
         */
        function scoreBoxByMinSide(boxIndex) {
            const box = boxes[boxIndex];
            return -Math.min(box.width, box.height);
        }

        const heap = new BinaryHeap(scoreBoxByMinSide);

        for (let i = 0; i < numBoxes; i++) {
            heap.push(i);
        }

        for (let i = 0; i < numBoxes; i++) {
            const boxIndex = heap.pop();
            const box = boxes[boxIndex];

            if (!packOneBox(box, this.free)) {
                //remove whatever has been packed
                const removeFailCount = this.removeMany(packed);

                assert.equal(removeFailCount, 0, `Failed to remove ${removeFailCount} boxes`);

                return false;
            } else {
                this.boxes.push(box);
                //box packed successfully
                packed.push(box);
            }
        }

        removeRedundantBoxes(this.free);

        return true;
    }

    /**
     * Re-packs all rectangles. If repack fails - all boxes are removed.
     *
     * @returns {boolean} true if successful, false if there was not enough space found during packing
     */
    repack() {
        // remember existing boxes
        const boxes = this.boxes;

        // reset packer
        this.clear();

        // pack again
        return this.addMany(boxes);
    }

    /**
     * Clear out all the data from the packer
     */
    clear() {
        this.free.clear();
        this.free.insertDatum(new QuadTreeDatum(0, 0, this.size.x, this.size.y));

        this.boxes = [];
    }

    /**
     * Resize the packer canvas, may trigger repacking if new dimensions are smaller than the existing ones
     * @param {number} width
     * @param {number} height
     * @returns {boolean} false if packing fails after resize, true otherwise
     */
    resize(width, height) {
        assert.isNumber(width, 'width');
        assert.isNumber(height, 'height');

        assert.isFinite(width, 'width');
        assert.isFinite(height, 'height');

        assert.greaterThan(width, 0, 'width');
        assert.greaterThan(height, 0, 'height');

        // assert.ok(this.validate());

        const oldWidth = this.size.x;
        const oldHeight = this.size.y;

        this.size.set(width, height);

        if (oldWidth > width || oldHeight > height) {
            // canvas was made smaller in at least one dimension, re-pack is required
            return this.repack();
        }

        //canvas was enlarged, we can simply add new free areas
        if (width > oldWidth) {
            this.free.insertDatum(new QuadTreeDatum(oldWidth, 0, width, height));
        }
        if (height > oldHeight) {
            this.free.insertDatum(new QuadTreeDatum(0, oldHeight, width, height))
        }

        // assert.ok(this.validate());

        return true;
    }

}


