import { assert } from "../../../../core/assert.js";

/**
 *
 * @type {FrameBuffer[]}
 */
const scratch_buffer_array = [];

export class FrameBufferManager {
    constructor() {
        /**
         *
         * @type {FrameBuffer[]}
         */
        this.buffers = [];

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

        /**
         *
         * @type {FrameBuffer[]}
         */
        this.orderedBuffers = [];

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

    /**
     *
     * @param {WebGLRenderer} renderer
     */
    initialize(renderer) {
        assert.notEqual(renderer, undefined, 'renderer is undefined');
        assert.notEqual(renderer, null, 'renderer is null');

        for (const b of this.buffers) {
            b.initialize(renderer);

            //expect render target to be set
            if (typeof b.renderTarget !== 'object') {
                throw new Error(`renderTarget was expected to be of type 'object', instead was '${typeof b.renderTarget}'`)
            }

            if (b.renderTarget.isWebGLRenderTarget !== true) {
                throw new Error(`renderTarget.isWebGLRenderTarget must be true, instead was '${b.renderTarget.isWebGLRenderTarget}'`);
            }
        }
    }

    build() {
        this.orderedBuffers = this.computeRenderOrder();

        this.needsRebuild = false;
    }

    /**
     *
     * @returns {FrameBuffer[]}
     */
    computeRenderOrder() {
        const openSet = this.buffers.slice();

        const resolved_id_map = {};

        const result = [];

        /**
         *
         * @param {FrameBuffer} buffer
         */
        function dependenciesSatisfied(buffer) {
            const dependencies = buffer.dependencies;
            for (let i = 0; i < dependencies.length; i++) {
                const id = dependencies[i];

                if (resolved_id_map[id] === undefined) {
                    //buffer has not been assigned yet
                    return false;
                }
            }

            //all dependencies satisfied
            return true;
        }


        let openSetCount = openSet.length;
        while (openSetCount > 0) {
            //remember number of buffers in the open set
            const openSetCountBefore = openSetCount;

            for (let i = 0; i < openSetCount; i++) {
                const frameBuffer = openSet[i];
                if (dependenciesSatisfied(frameBuffer)) {
                    result.push(frameBuffer);
                    openSet.splice(i, 1);

                    resolved_id_map[frameBuffer.id] = frameBuffer;

                    //update iteration variables
                    i--;
                    openSetCount--;
                }
            }

            if (openSetCountBefore === openSetCount) {
                //no buffers allocated in this pass
                throw new Error(`dependencies could not be satisfied for ${openSetCount} buffers`);
            }
        }

        return result;
    }

    /**
     *
     * @param {string} id
     * @returns {FrameBuffer|undefined}
     */
    getById(id) {
        assert.isString(id, 'id');

        return this.buffers.find(function (buffer) {
            return buffer.id === id;
        });
    }

    /**
     *
     * @param {FrameBuffer} buffer
     */
    add(buffer) {
        const existing = this.getById(buffer.id);

        if (existing !== undefined) {
            throw new Error(`Failed to add buffer. Another buffer with id='${buffer.id}' already exists`);
        }

        this.buffers.push(buffer);

        //mark for rebuild
        this.needsRebuild = true;
    }

    update() {
        if (this.needsRebuild) {
            this.build();
        }
    }

    /**
     *
     * @param {FrameBuffer[]} result
     * @param {FrameBuffer} dependant
     * @private
     */
    __populate_dependencies(result, dependant) {
        const dependency_ids = dependant.dependencies;

        const dependency_count = dependency_ids.length;

        for (let i = 0; i < dependency_count; i++) {
            const dependencyId = dependency_ids[i];

            const dependency = this.getById(dependencyId);

            result[i] = dependency;
        }
    }

    /**
     *
     * @param {WebGLRenderer} renderer
     * @param {Camera} camera
     * @param {Scene} scene
     */
    render(renderer, camera, scene) {
        this.update();

        const buffers = this.orderedBuffers;

        const numBuffers = buffers.length;

        for (let i = 0; i < numBuffers; i++) {
            const buffer = buffers[i];

            if (buffer.referenceCount > 0) {

                this.__populate_dependencies(scratch_buffer_array, buffer);

                buffer.render(renderer, camera, scene, scratch_buffer_array);

            }
        }
    }

    /**
     *
     * @param {number} value
     */
    setPixelRatio(value) {
        this.pixelRatio = value;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    setSize(x, y) {
        const buffers = this.buffers;

        const numBuffers = buffers.length;

        const pixelRatio = this.pixelRatio;

        for (let i = 0; i < numBuffers; i++) {
            const buffer = buffers[i];
            buffer.setSize(x * pixelRatio, y * pixelRatio);
        }
    }
}

