import { LightType } from "../LightType.js";
import { AmbientLight, DirectionalLight, PointLight, SpotLight } from "three";
import { assert } from "../../../../../core/assert.js";
import { light2type } from "./light2type.js";

/**
 *
 * @param {Light} light
 */
function disableLight(light) {
    light.intensity = 0;

    if (typeof light.castShadow !== "undefined") {
        light.castShadow = false;
    }
}

/**
 *
 * @param {LightType} type
 */
function createLight(type) {
    switch (type) {
        case LightType.POINT:
            return new PointLight(0x000000, 0, 0, 1);
        case LightType.AMBIENT:
            return new AmbientLight(0x000000, 0);
        case LightType.DIRECTION:
            return new DirectionalLight(0x000000, 0);
        case LightType.SPOT:
            return new SpotLight(0x000000, 0, 0, 0, 0, 1);
        default:
            throw new Error('Unknown light type: ' + type);
    }
}

/**
 * @template T,U
 * @param {Map<T,U[]>} map
 * @param {T} key
 * @returns U[]
 */
function ensureBucket(map, key) {
    let elements = map.get(key);
    if (elements === undefined) {
        elements = [];
        map.set(key, elements);
    }

    return elements;
}

export class ThreeLightCache {
    constructor() {
        /**
         *
         * @type {Map<LightType, Light[]>}
         * @private
         */
        this.__used = new Map();

        /**
         *
         * @type {Map<LightType, Light[]>}
         * @private
         */
        this.__available = new Map();

        /**
         *
         * @type {null}
         * @private
         */
        this.__scene = null;
    }

    /**
     *
     * @param {LightType} type
     * @returns {Light[]}
     * @private
     */
    __getUsed(type) {
        return ensureBucket(this.__used, type);
    }

    /**
     *
     * @param {LightType} type
     * @returns {Light[]}
     * @private
     */
    __getAvailable(type) {
        return ensureBucket(this.__available, type);
    }

    /**
     *
     * @param {LightType} type
     * @returns {Light}
     * @private
     */
    __create(type) {
        const light = createLight(type);

        if (this.__scene !== null) {
            this.__scene.add(light);
        }

        return light;
    }

    /**
     *
     * @param {Scene} scene Three.js scene
     */
    attach(scene) {
        if (this.__scene === scene) {
            // nothing to do
            return;
        }

        if (this.__scene !== null) {
            this.detach();
        }

        this.__scene = scene;

        // add all lights
        this.__traverseAllLight(scene.add, scene);
    }

    detach() {
        const scene = this.__scene;

        if (scene === null) {
            // not connected
            return;
        }

        // disconnect all lights
        this.__traverseAllLight(scene.remove, scene);

        this.__scene = null;
    }

    /**
     *
     * @param {function(light:(Light|Object3D)):*} visitor
     * @param {*} [thisArg]
     * @private
     */
    __traverseAllLight(visitor, thisArg) {
        for (const [type, lights] of this.__used) {
            lights.forEach(visitor, thisArg);
        }

        for (const [type, lights] of this.__available) {
            lights.forEach(visitor, thisArg);
        }
    }

    /**
     *
     * @param {LightType} type
     * @returns {Light}
     */
    obtain(type) {
        // check for available light of the type
        const available_lights = this.__getAvailable(type);

        let light;
        if (available_lights.length <= 0) {
            // no available lights, create one
            light = this.__create(type);
        } else {
            // found some available lights, take one
            light = available_lights.pop();
        }

        // move to "used" bucket
        const used = this.__getUsed(type);
        used.push(light);

        return light;
    }

    /**
     *
     * @param {Light} light
     */
    release(light) {
        assert.defined(light, 'light');
        assert.notNull(light, 'light');

        const type = light2type(light);

        const used_lights = this.__getUsed(type);

        const index = used_lights.indexOf(light);

        if (index === -1) {
            // light is not in use
            return false;
        }

        // disable light and move it to available
        disableLight(light);

        // removed from "used"
        used_lights.splice(index, 1);

        const available_lights = this.__getAvailable(type);

        // add to "available"
        available_lights.push(light);

        return true;
    }

    /**
     *
     * @param {LightType} type
     * @returns {number}
     */
    countTotalByType(type) {
        const available = this.__getAvailable(type);
        const used = this.__getUsed(type);

        return available.length + used.length;
    }

    /**
     * Pre-allocate up to specified number of lights of a given type. If number of allocated light already matches or exceeds that count, nothing will happen
     * @param {LightType} type
     * @param {number} count
     */
    reserve(type, count) {
        assert.isNonNegativeInteger(count, 'count');

        let i = this.countTotalByType(type);

        for (; i < count; i++) {
            const light = this.__create(type);

            const available = this.__getAvailable(type);
            available.push(light);
        }
    }
}
