import { mat4 } from "gl-matrix";
import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { bvh_query_depth_range_in_frustum } from "../../../../core/bvh2/bvh3/query/bvh_query_depth_range_in_frustum.js";
import { bvh_query_leaves_generic } from "../../../../core/bvh2/bvh3/query/bvh_query_leaves_generic.js";
import {
    bvh_query_user_data_overlaps_frustum
} from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_frustum.js";
import { BVHQueryIntersectsRay } from "../../../../core/bvh2/bvh3/query/BVHQueryIntersectsRay.js";

import { returnTrue } from "../../../../core/function/returnTrue.js";
import { ray3_array_compose } from "../../../../core/geom/3d/ray/ray3_array_compose.js";
import { SurfacePoint3 } from "../../../../core/geom/3d/SurfacePoint3.js";
import { v3_distance_sqr } from "../../../../core/geom/vec3/v3_distance_sqr.js";
import Task from "../../../../core/process/task/Task.js";
import { TaskSignal } from "../../../../core/process/task/TaskSignal.js";
import TaskState from "../../../../core/process/task/TaskState.js";
import { iteratorTask } from "../../../../core/process/task/util/iteratorTask.js";
import { System } from "../../../ecs/System.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { RuntimeDrawMethodOptimizer } from "./render/optimization/RuntimeDrawMethodOptimizer.js";
import { ShadedGeometryRendererContext } from "./render/ShadedGeometryRendererContext.js";
import { ShadedGeometry } from "./ShadedGeometry.js";


/**
 *
 * @type {SurfacePoint3}
 */
const scratch_point = new SurfacePoint3();

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

/**
 *
 * @param {ShadedGeometrySystem} system
 * @return {Generator}
 */
function* maintenance_generator(system) {
    while (true) { //infinite loop

        const contexts = system.__view_contexts;

        for (const [key, value] of contexts) {

            if (value === undefined) {
                continue;
            }

            yield* value.maintain();

        }

        // a small break between re-runs
        yield;
    }
}

export class ShadedGeometrySystem extends System {
    /**
     *
     * @param {Engine} engine
     */
    constructor(engine) {
        super();
        this.dependencies = [ShadedGeometry, Transform];

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

        /**
         *
         * @type {RenderLayer|null}
         * @private
         */
        this.__render_layer = null;

        /**
         *
         * @type {Map<CameraView, ShadedGeometryRendererContext>}
         * @private
         */
        this.__view_contexts = new Map();

        /**
         *
         * @type {Map<number, function():AbstractRenderAdapter>}
         * @private
         */
        this.__adapter_suppliers = new Map();

        /**
         * Keeps count of usage for each geometry by ID
         * @type {Map<number, number>}
         * @private
         */
        this.__geometry_usage_counters = new Map();

        /**
         *
         * @type {BVH}
         * @private
         */
        this.__bvh_binary = new BVH();

        const optimizer = new RuntimeDrawMethodOptimizer();

        this.__optimization_task = new Task({
            name: 'ShadedGeometry Draw Method optimizer',
            initializer: () => {
                optimizer.__system = this;
            },
            cycleFunction() {
                if (optimizer.step()) {
                    return TaskSignal.Continue;
                } else {
                    return TaskSignal.Yield;
                }
            }
        });

        this.__maintenance_task = iteratorTask(
            'Maintenance',
            maintenance_generator(this),
            TaskSignal.Yield
        );
    }


    /**
     *
     * @returns {BVH}
     */
    get bvh() {
        return this.__bvh_binary;
    }

    /**
     * NOTE: DO NOT MODIFY RESULTS
     * @returns {Map<number,number>}
     */
    getGeometryUsageCounters() {
        return this.__geometry_usage_counters;
    }

    /**
     *
     * @param {number} index
     * @param {function():AbstractRenderAdapter} supplier
     */
    register_render_adapter(index, supplier) {
        assert.isNonNegativeInteger(index, 'index');
        assert.isFunction(supplier, 'supplier');

        const map = this.__adapter_suppliers;

        if (map.has(index)) {
            // slot is already taken
            console.warn(`Render adapter slot '${index}' is already taken, removing old adapter`);
            this.unregister_render_adapter(index);
        }

        map.set(index, supplier);

        this.__view_contexts.forEach((value, key) => {
            const adapter = supplier();

            value.setAdapter(index, adapter);
        });
    }

    /**
     *
     * @param {number} index
     * @return {boolean}
     */
    unregister_render_adapter(index) {
        assert.isNonNegativeInteger(index, 'index');

        const supplier = this.__adapter_suppliers.get(index);

        if (supplier === undefined) {
            // not found
            return false;
        }

        this.__adapter_suppliers.delete(index);

        this.__view_contexts.forEach((value, key) => {
            value.removeAdapter(index);
        });

        return true;
    }

    /**
     *
     * @param {CameraView} view
     * @private
     */
    __handle_view_added(view) {
        const ctx = new ShadedGeometryRendererContext();

        // attach adapters
        this.__adapter_suppliers.forEach((value, key) => {
            ctx.setAdapter(key, value());
        });

        this.__view_contexts.set(view, ctx);
    }

    /**
     *
     * @param {CameraView} view
     * @private
     */
    __handle_view_removed(view) {
        const ctx = this.__view_contexts.get(view);

        this.__view_contexts.delete(view);

        ctx.dispose();
    }

    async startup(entityManager) {

        this.entityManager = entityManager;

        const engine = this.__engine;
        const graphics = engine.graphics;

        const view_list = graphics.views.elements;

        view_list.forEach(this.__handle_view_added, this);
        view_list.on.added.add(this.__handle_view_added, this);
        view_list.on.removed.add(this.__handle_view_removed, this);

        this.__render_layer = graphics.layers.create('shaded-geometry');

        this.__render_layer.buildVisibleSet = ((destination, destination_offset, view) => {
            const ctx = this.__view_contexts.get(view);

            const ecd = entityManager.dataset;

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

            return ctx.collect(destination, destination_offset, graphics.renderer, view, this.__bvh_binary, ecd);
        });

        const bvh = this.__bvh_binary;
        this.__render_layer.compute_depth_range = (result, frustum, nx, ny, nz, c) => {
            bvh_query_depth_range_in_frustum(result, bvh, frustum, nx, ny, nz, c);
        };

        this.__optimization_task.state.set(TaskState.INITIAL);
        engine.executor.run(this.__optimization_task);

        this.__maintenance_task.state.set(TaskState.INITIAL);
        engine.executor.run(this.__maintenance_task);
    }

    async shutdown(entityManager) {

        const engine = this.__engine;
        const graphics = engine.graphics;

        const view_list = graphics.views.elements;

        engine.executor.removeTask(this.__optimization_task);
        engine.executor.removeTask(this.__maintenance_task);

        view_list.forEach(this.__handle_view_removed, this);
        view_list.on.added.remove(this.__handle_view_added, this);
        view_list.on.removed.remove(this.__handle_view_removed, this);

    }

    /**
     *
     * @param {ShadedGeometry} sg
     * @param {Transform} t
     * @param {number} entity
     */
    link(sg, t, entity) {
        sg.__c_transform = t;

        sg.update_bounds();

        t.subscribe(sg.updateTransform, sg);

        // remember entity for lookups
        sg.__entity = entity;

        // insert BVH entry
        sg.__bvh_leaf.link(this.__bvh_binary, entity);

        // update usage count
        const geometry_id = sg.geometry.id;
        const count_existing = this.__geometry_usage_counters.get(geometry_id);
        const count_new = count_existing !== undefined ? count_existing + 1 : 1;

        this.__geometry_usage_counters.set(geometry_id, count_new);
    }

    /**
     *
     * @param {ShadedGeometry} sg
     * @param {Transform} t
     * @param {number} entity
     */
    unlink(sg, t, entity) {
        t.unsubscribe(sg.updateTransform, sg);

        // disconnect BVH
        sg.__bvh_leaf.unlink();

        const geometry_id = sg.geometry.id;
        const count_existing = this.__geometry_usage_counters.get(geometry_id);

        if (count_existing === 1) {

            // last instance
            this.__geometry_usage_counters.delete(geometry_id);

            // release GPU memory
            sg.geometry.dispose();

        } else {
            this.__geometry_usage_counters.set(geometry_id, count_existing - 1);
        }
    }

    /**
     *
     * @param {number[]} result entity IDs
     * @param {number} result_offset offset into result array where to write first result into
     * @param {number[]|ArrayLike<number>} planes
     * @returns {number} number of results
     */
    queryOverlapFrustum(
        result,
        result_offset,
        planes
    ) {
        return bvh_query_user_data_overlaps_frustum(result, result_offset, this.__bvh_binary, planes);
    }

    /**
     *
     * @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, mesh:ShadedGeometry):boolean} [filter_function]
     * @param {*} [filter_function_context]
     * @returns {{entity:number, mesh:ShadedGeometry, contact: SurfacePoint3}[]}
     */
    raycast(
        origin_x, origin_y, origin_z,
        direction_x, direction_y, direction_z,
        filter_function = returnTrue, filter_function_context
    ) {
        const result = [];

        const hits = [];

        const bvh = this.__bvh_binary;

        const hit_count = bvh_query_leaves_generic(hits, 0, bvh, bvh.root, BVHQueryIntersectsRay.from([origin_x, origin_y, origin_z, direction_x, direction_y, direction_z]));

        const ecd = this.entityManager.dataset;

        if (ecd === null) {
            // no ECD set
            return result;
        }

        const sg_component_index = ecd.computeComponentTypeIndex(ShadedGeometry);

        for (let i = 0; i < hit_count; i++) {
            const node_id = hits[i];

            const entity = bvh.node_get_user_data(node_id);

            /**
             *
             * @type {ShadedGeometry}
             */
            const sg = ecd.getComponentByIndex(entity, sg_component_index);

            if (!filter_function.call(filter_function_context, entity, sg)) {
                // filtered out
                continue;
            }

            const m4 = sg.transform;

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

            const geometry_hit_found = sg.query_raycast_nearest(scratch_point, scratch_ray_0, m4);

            if (!geometry_hit_found) {
                continue;
            }

            result.push({
                entity,
                mesh: sg,
                contact: scratch_point.clone()
            });
        }

        return result;
    }


    /**
     *
     * @param {SurfacePoint3} contact
     * @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, mesh:ShadedGeometry):boolean} [filter_function]
     * @param {*} [filter_function_context]
     * @returns {{entity:number, mesh:ShadedGeometry}|undefined}
     */
    raycastNearest(
        contact,
        origin_x, origin_y, origin_z,
        direction_x, direction_y, direction_z,
        filter_function = returnTrue, filter_function_context
    ) {

        let best_distance = Infinity;
        let found_hit = false;
        let best_result = null;

        const hits = [];

        const bvh = this.__bvh_binary;

        const hit_count = bvh_query_leaves_generic(hits, 0, bvh, bvh.root, BVHQueryIntersectsRay.from([origin_x, origin_y, origin_z, direction_x, direction_y, direction_z]));

        const ecd = this.entityManager.dataset;

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

        const sg_component_index = ecd.computeComponentTypeIndex(ShadedGeometry);

        for (let i = 0; i < hit_count; i++) {
            const node_id = hits[i];

            const entity = bvh.node_get_user_data(node_id);

            /**
             *
             * @type {ShadedGeometry}
             */
            const sg = ecd.getComponentByIndex(entity, sg_component_index);

            if (!filter_function.call(filter_function_context, entity, sg)) {
                // filtered out
                continue;
            }

            /**
             *
             * @type {number[]|mat4|null}
             */
            const m4 = sg.transform;

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

            const geometry_hit_found = sg.query_raycast_nearest(scratch_point, scratch_ray_0, m4);

            if (!geometry_hit_found) {
                continue;
            }

            found_hit = true;

            const hit_position = scratch_point.position;

            const distance_sqr = v3_distance_sqr(
                hit_position.x, hit_position.y, hit_position.z,
                origin_x, origin_y, origin_z
            );

            if (distance_sqr < best_distance) {
                contact.copy(scratch_point);
                best_result = sg;
                best_distance = distance_sqr;
            }
        }

        if (found_hit) {
            return {
                entity: best_result.__entity,
                mesh: best_result
            };
        }
    }
}
