import { mat4 } from "gl-matrix";
import { assert } from "../../../../../core/assert.js";
import { BVH } from "../../../../../core/bvh2/bvh3/BVH.js";
import { BvhClient } from "../../../../../core/bvh2/bvh3/BvhClient.js";
import { bvh_query_leaves_ray } from "../../../../../core/bvh2/bvh3/query/bvh_query_leaves_ray.js";
import {
    bvh_query_user_data_overlaps_frustum
} from "../../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_frustum.js";
import { LoadingCache } from "../../../../../core/cache/LoadingCache.js";

import { returnTrue } from "../../../../../core/function/returnTrue.js";
import { strictEquals } from "../../../../../core/function/strictEquals.js";
import { aabb3_matrix4_project } from "../../../../../core/geom/3d/aabb/aabb3_matrix4_project.js";
import { aabb3_raycast } from "../../../../../core/geom/3d/aabb/aabb3_raycast.js";
import {
    aabb3_transformed_compute_plane_side
} from "../../../../../core/geom/3d/aabb/aabb3_transformed_compute_plane_side.js";
import { ray3_array_apply_matrix4 } from "../../../../../core/geom/3d/ray/ray3_array_apply_matrix4.js";
import { ray3_array_compose } from "../../../../../core/geom/3d/ray/ray3_array_compose.js";
import { SurfacePoint3 } from "../../../../../core/geom/3d/SurfacePoint3.js";
import { computeStringHash } from "../../../../../core/primitives/strings/computeStringHash.js";
import { AssetManager } from "../../../../asset/AssetManager.js";
import { GameAssetType } from "../../../../asset/GameAssetType.js";
import { ImageRGBADataLoader } from "../../../../asset/loaders/image/ImageRGBADataLoader.js";
import { AbstractContextSystem } from "../../../../ecs/system/AbstractContextSystem.js";
import { SystemEntityContext } from "../../../../ecs/system/SystemEntityContext.js";
import { Transform } from "../../../../ecs/transform/Transform.js";
import { Reference } from "../../../../reference/v2/Reference.js";
import { Decal as FPDecal } from '../../../render/forward_plus/model/Decal.js';
import { ForwardPlusRenderingPlugin } from "../../../render/forward_plus/plugin/ForwardPlusRenderingPlugin.js";
import { Sampler2D } from "../../../texture/sampler/Sampler2D.js";
import { sampler2d_ensure_uint8_RGBA } from "../../../texture/sampler/sampler2d_ensure_uint8_RGBA.js";
import { Decal } from "./Decal.js";

const placeholder_texture = Sampler2D.uint8(4, 1, 1);
placeholder_texture.data.fill(255);

/**
 * @readonly
 * @type {Float32Array}
 */
const scratch_ray_0 = new Float32Array(6);


/**
 *
 * @type {Float32Array|mat4}
 */
const scratch_m4 = new Float32Array(16);

/**
 *
 * @type {Float32Array}
 */
const AABB_UNIT_CUBE = new Float32Array([
    -0.5, -0.5, -0.5,
    0.5, 0.5, 0.5
]);

class Context extends SystemEntityContext {

    /**
     *
     * @type {FPDecal|null}
     * @private
     */
    __fp_decal = null;

    #bvh_leaf = new BvhClient();

    /**
     * @return {ForwardPlusRenderingPlugin}
     * @private
     */
    getPlugin() {
        return this.system.__fp_plugin.getValue();
    }

    /**
     *
     * @returns {Transform}
     */
    getTransform() {
        return this.components[1];
    }

    /**
     *
     * @returns {Decal}
     */
    getDecalComponent() {
        return this.components[0];
    }

    __update_transform() {
        /**
         *
         * @type {Transform}
         */
        const transform = this.getTransform();

        this.__fp_decal.setTransform(transform.matrix);

        aabb3_matrix4_project(this.#bvh_leaf.bounds, AABB_UNIT_CUBE, transform.matrix);

        this.#bvh_leaf.write_bounds();
    }

    async __load_texture() {

        const decal_spec = this.getDecalComponent();

        const _uri = decal_spec.uri;

        if (decal_spec.__cached_sampler !== null && decal_spec.__cached_uri === _uri) {
            // cached asset is still relevant
            return;
        }

        /**
         *
         * @type {FPDecalSystem}
         */
        const system = this.system;

        /**
         * @type {Sampler2D}
         */
        const loaded_texture = await system.__texture_cache.get(_uri);

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

        decal_spec.__cached_sampler = loaded_texture;
        decal_spec.__cached_uri = _uri;
    }

    rebuild() {
        const plugin = this.getPlugin();
        const lm = plugin.getLightManager();

        if (this.__fp_decal === null || !lm.hasLight(this.__fp_decal)) {
            return;
        }

        lm.requestDataUpdate();
    }

    link() {
        const plugin = this.getPlugin();

        const lm = plugin.getLightManager();

        const fpDecal = new FPDecal();

        this.__fp_decal = fpDecal;

        this.__update_transform();

        const transform = this.getTransform();
        transform.subscribe(this.__update_transform, this);

        const decal_spec = this.getDecalComponent();

        // propagate draw priority onto decal
        Object.defineProperties(fpDecal, {
            'draw_priority': {
                get() {
                    return decal_spec.priority;
                }
            },
            'color': {
                get() {
                    return decal_spec.color;
                }
            }
        });

        decal_spec.color.onChanged.add(this.rebuild, this);

        this.__load_texture().then(() => {

            if (!this.__is_linked) {
                // not linked anymore
                return;
            }

            const sampler = decal_spec.__cached_sampler;

            if (sampler === null) {
                return;
            }

            fpDecal.texture_diffuse = sampler;

            if (this.__fp_decal === fpDecal) {
                lm.addLight(fpDecal);
            }
        });

        this.#bvh_leaf.link(this.system.bvh, this.entity);

        super.link();
    }

    unlink() {

        this.getPlugin()
            .getLightManager()
            .removeLight(this.__fp_decal);

        this.__fp_decal = null;

        const transform = this.getTransform();
        transform.subscribe(this.__update_transform, this);

        const decal_spec = this.getDecalComponent();

        decal_spec.color.onChanged.remove(this.rebuild, this);

        this.#bvh_leaf.unlink();

        super.unlink();
    }
}

// TODO we can reduce memory usage and speed things up considerably by using a queue to wait for assets instead of using promises everywhere

/**
 *
 */
export class FPDecalSystem extends AbstractContextSystem {
    bvh = new BVH();

    /**
     *
     * @param {Engine} engine
     */
    constructor(engine) {
        super(Context);

        this.dependencies = [Decal, Transform];

        /**
         *
         * @type {LoadingCache<string, Sampler2D>}
         * @private
         */
        this.__texture_cache = new LoadingCache({
            load: (key) => {
                return this.__assets.promise(key, GameAssetType.Image).then(asset => {
                    const asset_sampler = asset.create();

                    return sampler2d_ensure_uint8_RGBA(asset_sampler);
                });
            },
            keyHashFunction: computeStringHash,
            keyEqualityFunction: strictEquals
        });

        /**
         *
         * @type {AssetManager}
         * @private
         */
        this.__assets = engine.assetManager;

        /**
         *
         * @type {Engine}
         * @private
         */
        this.__engine = engine;

        /**
         *
         * @type {Reference<ForwardPlusRenderingPlugin>|null}
         * @private
         */
        this.__fp_plugin = Reference.NULL;
    }

    async startup(em) {
        this.__fp_plugin = await this.__engine.plugins.acquire(ForwardPlusRenderingPlugin);

        const am = this.__engine.assetManager;
        if (!am.hasLoaderForType(GameAssetType.Image)) {
            await am.registerLoader(GameAssetType.Image, new ImageRGBADataLoader());
        }


    }

    async shutdown(em) {
        this.__fp_plugin.release();
    }

    /**
     *
     * @param {number[]} result entity IDs
     * @param {number} result_offset
     * @param {number[]} planes
     * @returns {number} number of results
     */
    queryOverlapFrustum(result, result_offset, planes) {
        const ecd = this.entityManager.dataset;

        if (ecd === null) {
            return 0;
        }

        const entities = [];
        const rough_count = bvh_query_user_data_overlaps_frustum(
            entities, 0,
            this.bvh,
            planes
        );

        let result_cursor = result_offset;

        loop_0: for (let i = 0; i < rough_count; i++) {

            const entity = entities[i];

            /**
             *
             * @type {Transform}
             */
            const transform = ecd.getComponent(entity, Transform);

            if (transform === undefined) {
                // this shouldn't happen either
                continue;
            }

            for (let j = 0; j < 6; j++) {
                // test true box against each plane
                const plane_address = j * 4;

                const plane_x = planes[plane_address]
                const plane_y = planes[plane_address + 1]
                const plane_z = planes[plane_address + 2]
                const plane_offset = planes[plane_address + 3]

                const side = aabb3_transformed_compute_plane_side(
                    -0.5, -0.5, -0.5,
                    0.5, 0.5, 0.5,
                    plane_x, plane_y, plane_z, plane_offset,
                    transform.matrix
                );

                if (side < 0) {
                    // completely outside
                    continue loop_0;
                }
            }

            result[result_cursor++] = entity;
        }

        return result_cursor - result_offset;
    }

    /**
     *
     * @param {number} origin_x
     * @param {number} origin_y
     * @param {number} origin_z
     * @param {number} direction_x
     * @param {number} direction_y
     * @param {number} direction_z
     * @param {function(entity:number, component:Decal):boolean} [filter_function]
     * @param {*} [filter_function_context]
     * @returns {{entity:number, component:Decal, contact: SurfacePoint3}[]}
     */
    raycast(
        origin_x, origin_y, origin_z,
        direction_x, direction_y, direction_z,
        filter_function = returnTrue, filter_function_context
    ) {

        const ecd = this.entityManager.dataset;

        if (ecd === null) {
            return [];
        }

        const leaves = [];

        const bvh = this.bvh;

        const hit_count = bvh_query_leaves_ray(bvh, bvh.root, leaves, 0,
            origin_x, origin_y, origin_z,
            direction_x, direction_y, direction_z
        );

        const result = [];
        const temp_hit = new Float32Array(6);


        for (let i = 0; i < hit_count; i++) {
            const node = leaves[i];

            const entity = bvh.node_get_user_data(node);


            /**
             *
             * @type {Decal}
             */
            const decal = ecd.getComponent(entity, Decal);

            if (decal === undefined) {
                // this shouldn't happen
                continue;
            }

            if (!filter_function.call(filter_function_context, entity, decal)) {
                continue;
            }

            /**
             *
             * @type {Transform}
             */
            const transform = ecd.getComponent(entity, Transform);

            if (transform === undefined) {
                // this shouldn't happen either
                continue;
            }

            ray3_array_compose(
                scratch_ray_0,
                origin_x, origin_y, origin_z,
                direction_x, direction_y, direction_z
            );


            // get transform in local space
            mat4.invert(scratch_m4, transform.matrix);

            // transform ray into decal's local space
            ray3_array_apply_matrix4(scratch_ray_0, 0, scratch_ray_0, 0, scratch_m4);

            if (aabb3_raycast(
                temp_hit, 0,
                -0.5, -0.5, -0.5,
                0.5, 0.5, 0.5,

                scratch_ray_0[0], scratch_ray_0[1], scratch_ray_0[2],
                scratch_ray_0[3], scratch_ray_0[4], scratch_ray_0[5],
            )) {
                const contact = new SurfacePoint3();

                contact.fromArray(temp_hit);

                // contact will be the center of decal, and normal will follow the decal normal
                contact.applyMatrix4(transform.matrix);

                result.push({
                    entity,
                    component: decal,
                    contact
                });

            }

        }

        return result;
    }
}
