import { Cache } from "../../../../core/cache/Cache.js";
import { HashMap } from "../../../../core/collection/map/HashMap.js";
import { computeMaterialEquality } from "../../../asset/loaders/material/computeMaterialEquality.js";
import { computeMaterialHash } from "../../../asset/loaders/material/computeMaterialHash.js";
import { ManagedMaterial } from "./ManagedMaterial.js";

export class MaterialManager {
    constructor() {

        /**
         * Stores links from uncompiled (source) materials to managed containers
         * @type {HashMap<Material, ManagedMaterial>}
         * @private
         */
        this.__library = new HashMap({
            keyHashFunction: computeMaterialHash,
            keyEqualityFunction: computeMaterialEquality,
            capacity: 1024
        });

        /**
         * Cache for unused materials, gives them a second chance to be used before being disposed completely
         * @type {Cache<Material, ManagedMaterial>}
         * @private
         */
        this.__cache = new Cache({
            keyHashFunction: computeMaterialHash,
            keyEqualityFunction: computeMaterialEquality,
            maxWeight: 100
        });

        /**
         * Stores links from compiled materials back to the container
         * @type {HashMap<Material, ManagedMaterial>}
         * @private
         */
        this.__reverse_library = new HashMap({
            keyHashFunction: computeMaterialHash,
            keyEqualityFunction: computeMaterialEquality,
            capacity: 1024
        });

        this.__cache.onEvicted.add(this.__dispose_material, this);

        /**
         *
         * @type {AbstractMaterialTransformer[]}
         * @private
         */
        this.__transform_chain = [];
    }

    /**
     * handle material disposal
     * @param {Material} m
     * @param {ManagedMaterial} managed_material
     * @private
     */
    __dispose_material(m, managed_material) {
        managed_material.getMaterial().dispose();
    }

    /**
     *
     * @param {ManagedMaterial} material
     * @private
     */
    __handle_last_reference_removed(material) {
        const compiled_material = material.getMaterial();

        // remove from library
        const source = material.getSource();
        this.__library.delete(source);
        this.__reverse_library.delete(compiled_material);

        // put material into cache
        this.__cache.put(source, material);
    }

    /**
     *
     * @param {Function} klass
     * @return {AbstractMaterialTransformer[]}
     */
    getCompilationSteps(klass){
        return this.__transform_chain.filter(x => x instanceof klass);
    }

    /**
     *
     * @param {AbstractMaterialTransformer} def
     */
    addCompileStep(def) {
        this.__transform_chain.push(def);
    }

    /**
     *
     * @param {AbstractMaterialTransformer} def
     * @returns {boolean}
     */
    removeCompileStep(def) {
        const i = this.__transform_chain.indexOf(def);

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

        this.__transform_chain.splice(i, 1);

        return true;
    }

    /**
     *
     * @param {Material} m
     * @returns {Material}
     * @private
     */
    __compile(m) {
        let r = m;

        const transformers = this.__transform_chain;

        const n = transformers.length;

        for (let i = 0; i < n; i++) {
            const transformer = transformers[i];

            r = transformer.transform(r);
        }

        return r;
    }


    /**
     *
     * @param {Material} source
     * @returns {Reference<Material>}
     */
    obtain(source) {
        // reverse check to see if this is actually an already compiled/managed material
        let material = this.__reverse_library.get(source);

        if (material !== undefined) {
            // already compiled material
            return material.getRef();
        }

        // forward check
        material = this.__library.get(source);

        if (material === undefined) {
            // not found in library, check cache
            material = this.__cache.get(source);

            if (material !== null) {
                // remove from cache
                this.__cache.remove(source);

            } else {
                // material not found in cache
                const mat = this.__compile(source);

                material = new ManagedMaterial(mat);
                material.setSource(source);
            }

            material.onLastReleased.addOne(this.__handle_last_reference_removed, this);

            this.__library.set(source, material);
            this.__reverse_library.set(material.getMaterial(), material);
        }

        return material.getRef();
    }
}
