import { Frustum } from "three";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { array_remove_first } from "../../../../core/collection/array/array_remove_first.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { read_three_planes_to_array } from "../../../../core/geom/3d/frustum/read_three_planes_to_array.js";
import { frustum_from_camera } from "../../ecs/camera/frustum_from_camera.js";
import { compare_three_objects } from "../../three/compare_three_objects.js";
import { IncrementalDeltaSet } from "../visibility/IncrementalDeltaSet.js";
import { CameraViewFlags } from "./CameraViewFlags.js";

const DEFAULT_FLAGS = CameraViewFlags.Active
    | CameraViewFlags.AdaptivePlaneNear
    | CameraViewFlags.AdaptivePlaneFar;

const scratch_frustum = new Frustum();

const scratch_mat4 = new Float32Array(16);

export class CameraView {
    constructor() {

        /**
         * Human-readable name, used for development/debugging purposes
         * Not guaranteed to be unique, do not rely on it as an ID
         * @type {string}
         */
        this.name = '';

        /**
         * @deprecated
         * @type {null|THREE.Camera}
         * @private
         */
        this.__camera = null;

        /**
         * Frustum planes
         * Format: x,y,z,constant
         * @type {Float32Array}
         */
        this.frustum = new Float32Array(24);

        /**
         * 4x4 projection matrix
         * @type {Float32Array}
         */
        this.projection_matrix = new Float32Array(16);

        /**
         *
         * @type {AABB3}
         */
        this.bounding_box = new AABB3();

        /**
         *
         * @type {IncrementalDeltaSet<THREE.Object3D>}
         */
        this.visible_objects = new IncrementalDeltaSet(compare_three_objects);

        /**
         *
         * @type {number}
         */
        this.flags = DEFAULT_FLAGS;

        this.on = {
            preVisibilityBuild: new Signal(),
            postVisibilityBuild: new Signal(),
            preRender: new Signal()
        };

        /**
         *
         * @type {Array<{transform:function(Float32Array, Float32Array):void, context:*}>}
         */
        this.projection_modifiers = [];
    }

    addProjectionModifier(mod) {
        this.projection_modifiers.push(mod);
    }

    removeProjectionModifier(mod) {
        return array_remove_first(this.projection_modifiers, mod);
    }

    /**
     *
     * @param {THREE.Camera} camera
     */
    set_from_camera(camera) {
        this.__camera = camera;

        // read frustum
        frustum_from_camera(camera, scratch_frustum, false);

        // update view definition
        read_three_planes_to_array(scratch_frustum.planes, this.frustum);

        // read projection matrix
        // TODO

        array_copy(camera.projectionMatrix.elements, 0, this.projection_matrix, 0, 16);
        let ts = this.projection_matrix;
        let td = scratch_mat4;
        for (let i = 0; i < this.projection_modifiers.length; i++) {
            const modifier = this.projection_modifiers[i];

            modifier.transform.call(modifier.context, ts, td);

            const swap = ts;

            ts = td;
            td = swap;
        }

        if (ts !== this.projection_matrix) {
            array_copy(ts, 0, this.projection_matrix, 0, 16);
        }

        array_copy(this.projection_matrix, 0, this.__camera.projectionMatrix.elements, 0, 16);
    }

    /**
     * @deprecated
     * @return {Camera|null}
     */
    get camera() {
        return this.__camera;
    }

    /**
     *
     * @param {RenderLayerManager} layers
     * @param {VisibilityComputer} computer
     */
    build_visibility(layers, computer) {
        // signal
        this.on.preVisibilityBuild.send0();

        this.visible_objects.initializeUpdate();

        computer.output = this.visible_objects;
        computer.view = this;

        computer.build(layers);

        this.visible_objects.finalizeUpdate();

        // signal
        this.on.postVisibilityBuild.send0();
    }


    /**
     *
     * @param {number|CameraViewFlags} flag
     * @returns {void}
     */
    setFlag(flag) {
        this.flags |= flag;
    }

    /**
     *
     * @param {number|CameraViewFlags} flag
     * @returns {void}
     */
    clearFlag(flag) {
        this.flags &= ~flag;
    }

    /**
     *
     * @param {number|CameraViewFlags} flag
     * @param {boolean} value
     */
    writeFlag(flag, value) {
        if (value) {
            this.setFlag(flag);
        } else {
            this.clearFlag(flag);
        }
    }

    /**
     *
     * @param {number|CameraViewFlags} flag
     * @returns {boolean}
     */
    getFlag(flag) {
        return (this.flags & flag) === flag;
    }
}
