import { Frustum as ThreeFrustum, } from 'three';
import { assert } from "../../../../core/assert.js";
import { SignalBinding } from "../../../../core/events/signal/SignalBinding.js";
import Quaternion from "../../../../core/geom/Quaternion.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { System } from '../../../ecs/System.js';
import { Transform } from '../../../ecs/transform/Transform.js';
import { threeUpdateTransform } from "../../util/threeUpdateTransform.js";
import { auto_set_camera_clipping_planes } from "./auto_set_camera_clipping_planes.js";
import { build_three_camera_object } from "./build_three_camera_object.js";
import { Camera } from './Camera.js';
import { frustum_from_camera } from "./frustum_from_camera.js";
import { quaternion_invert_orientation } from "./quaternion_invert_orientation.js";
import { set_camera_aspect_ratio } from "./set_camera_aspect_ratio.js";
import { update_camera_transform } from "./update_camera_transform.js";


const scratch_quat = new Quaternion();

/**
 *
 * @param {THREE.Camera} object
 * @param {Quaternion} rotation
 */
export function three_camera_set_transform_rotation(object, rotation) {

    /*
    NOTE: I'm not sure why, but three.js camera points in the opposite direction to normal objects
    See: https://github.com/mrdoob/three.js/blob/412b99a7f26e117ea97f40eb53d010ab81aa3279/src/core/Object3D.js#L282
     */

    quaternion_invert_orientation(scratch_quat, rotation);

    object.quaternion.set(
        scratch_quat.x, scratch_quat.y, scratch_quat.z, scratch_quat.w
    );

    object.rotation.setFromQuaternion(object.quaternion);

    // rotation.__setThreeEuler(camera.object.rotation); // seems unnecessary, based on Object3D.lookAt implementation
    // camera.object.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w);

    object.updateProjectionMatrix();
    threeUpdateTransform(object);
}

export class CameraSystem extends System {
    /**
     *
     * @param {GraphicsEngine} graphics
     * @constructor
     */
    constructor(graphics) {
        super();

        assert.defined(graphics, 'graphics');
        assert.notNull(graphics, 'graphics');
        assert.equal(graphics.isGraphicsEngine, true, 'graphics.isGraphicsEngine !== true');

        this.scene = graphics.scene;

        this.dependencies = [Camera, Transform];

        this.components_used = [
            ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write)
        ];

        /**
         *
         * @type {EntityManager}
         */
        this.entityManager = null;

        /**
         * @type {GraphicsEngine}
         */
        this.graphics = graphics;

        this.entityData = [];


        const self = this;

        /**
         *
         * @param {number} x
         * @param {number} y
         * @private
         */
        this.__handleViewportResize = function (x, y) {
            const em = self.entityManager;
            if (em === null) {
                return;
            }

            const dataset = em.dataset;

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

            dataset.traverseComponents(Camera, function (camera) {
                set_camera_aspect_ratio(camera, x, y);
            });
        };

        this.signalBindings = [];
    }


    /**
     *
     * @param {Transform} transform
     * @param {Camera} camera
     * @param entityId
     */
    link(camera, transform, entityId) {
        if (camera.object === null) {
            camera.object = build_three_camera_object(camera);
        }

        const graphics = this.graphics;

        const viewportSize = graphics.viewport.size;

        set_camera_aspect_ratio(camera, viewportSize.x, viewportSize.y);

        this.scene.add(camera.object);

        function synchronizePosition(x, y, z) {
            assert.isNumber(x, 'x');
            assert.isNumber(y, 'y');
            assert.isNumber(z, 'z');

            camera.object.position.set(x, y, z);
            update_camera_transform(camera);
        }

        function synchronizeRotation() {
            const rotation = transform.rotation;

            three_camera_set_transform_rotation(camera.object, rotation);
        }

        function rebuild() {
            camera.object = build_three_camera_object(camera);
            set_camera_aspect_ratio(camera, viewportSize.x, viewportSize.y);

            const position = transform.position;

            synchronizePosition(position.x, position.y, position.z);
            synchronizeRotation();

            synchronizeActiveState(camera.active.getValue());
        }

        function synchronizeActiveState(v) {
            if (v) {
                graphics.camera = camera.object;
            } else {
                //active camera disabled
            }
        }

        const position = transform.position;
        const rotation = transform.rotation;

        const signalBindings = [
            new SignalBinding(position.onChanged, synchronizePosition),
            new SignalBinding(rotation.onChanged, synchronizeRotation),
            new SignalBinding(camera.projectionType.onChanged, rebuild),
            new SignalBinding(camera.active.onChanged, synchronizeActiveState),
            new SignalBinding(camera.fov.onChanged, rebuild)
        ];

        signalBindings.forEach(b => b.link());

        this.entityData[entityId] = signalBindings;


        synchronizePosition(position.x, position.y, position.z);
        synchronizeRotation(rotation.x, rotation.y, rotation.z, rotation.w);
        synchronizeActiveState(camera.active.getValue());

        assert.notEqual(camera.object, null, 'Camera object must not be null (invariant)');
    }

    /**
     *
     * @param {Transform} transform
     * @param {Camera} camera
     * @param entityId
     */
    unlink(camera, transform, entityId) {
        const data = this.entityData;
        if (data.hasOwnProperty(entityId)) {
            const entityData = data[entityId];

            if (entityData !== void 0) {
                entityData.forEach(b => b.unlink());
            }

            delete data[entityId];
        }

        this.scene.remove(camera.object);
    }

    async startup(entityManager) {
        this.entityManager = entityManager;

        const graphics = this.graphics;

        graphics.viewport.size.onChanged.add(this.__handleViewportResize);


        const visibilityConstructionPreHook = new SignalBinding(graphics.on.visibilityConstructionStarted, function () {
            const em = entityManager;

            const layers = graphics.layers;


            const dataset = em.dataset;

            /**
             *
             * @param {Camera} c
             */
            function visitCameraEntity(c) {
                if (c.object === null) {
                    return;
                }

                if (!c.active.getValue()) {
                    return;
                }

                if (c.object.updateProjectionMatrix !== undefined) {
                    c.object.updateProjectionMatrix();
                    c.object.updateMatrix();
                    c.object.updateMatrixWorld(true);
                }

                if (c.autoClip === true) {
                    auto_set_camera_clipping_planes(c, layers);
                } else {
                    c.object.far = c.clip_far;
                    c.object.near = c.clip_near;
                }
            }

            if (dataset !== null) {
                dataset.traverseComponents(Camera, visitCameraEntity);
            }
        });

        visibilityConstructionPreHook.link();

        this.signalBindings.push(visibilityConstructionPreHook);

    }

    async shutdown(entityManager) {
        this.graphics.viewport.size.onChanged.remove(this.__handleViewportResize);

        this.signalBindings.forEach(sb => sb.unlink());

        this.signalBindings.splice(0, this.signalBindings.length);
    }

    /**
     *
     * @param {EntityComponentDataset} ecd
     * @param {function(Camera, entity:number)} visitor
     */
    static traverseActiveCameras(ecd, visitor) {

        ecd.traverseComponents(Camera, function (c, entity) {
            if (c.active.getValue()) {
                visitor(c, entity);
            }
        });
    }

    /**
     *
     * @param {EntityComponentDataset} ecd
     * @returns {{component:(Camera|undefined), entity: number}}
     */
    static getFirstActiveCamera(ecd) {
        const r = {
            entity: -1,
            component: undefined
        };

        this.traverseActiveCameras(ecd, (c, e) => {
            r.entity = e;
            r.component = c;
        });

        return r;
    }

    /**
     *
     * @param {EntityComponentDataset} ecd
     * @param {function(ThreeFrustum[])} callback
     */
    static getActiveFrustums(ecd, callback) {
        const frustums = [];

        if (ecd !== null) {
            ecd.traverseComponents(Camera, function (c) {
                if (c.active.getValue()) {
                    const camera = c.object;

                    if (camera !== null) {
                        const frustum = new ThreeFrustum();
                        frustum_from_camera(camera, frustum);
                        frustums.push(frustum);
                    }

                }
            });
        }

        callback(frustums);
    }
}


