import { mat4 } from "gl-matrix";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { GraphicsEngine } from "../../../graphics/GraphicsEngine.js";
import { FogOfWarVisibilityPredicate } from "../../fow/FogOfWarVisibilityPredicate.js";
import { System } from '../../System.js';
import { Transform } from '../../transform/Transform.js';
import GUIElement from "../GUIElement.js";
import ViewportPosition from "../position/ViewportPosition.js";
import HeadsUpDisplay from './HeadsUpDisplay.js';
import { HeadsUpDisplayFlag } from "./HeadsUpDisplayFlag.js";

const projection = new Float32Array(16);


/**
 *
 * @param {GraphicsEngine} graphicsEngine
 * @param containerView
 * @constructor
 */
class HeadsUpDisplaySystem extends System {
    dependencies = [HeadsUpDisplay];


    components_used = [
        ResourceAccessSpecification.from(GUIElement, ResourceAccessKind.Read | ResourceAccessKind.Write),
        ResourceAccessSpecification.from(ViewportPosition, ResourceAccessKind.Read | ResourceAccessKind.Write),
    ];

    /**
     *
     * @param {GraphicsEngine} graphicsEngine
     */
    constructor(graphicsEngine) {
        super();

        if (!(graphicsEngine instanceof GraphicsEngine)) {
            throw new TypeError(`graphicsEngine is not an instance of GraphicsEngine`);
        }

        this.graphics = graphicsEngine;

        const visibilityPredicate = new FogOfWarVisibilityPredicate();
        // Because of blur being applied to FOW, sometimes you can see a tile quite clearly, though be unable to interact with it, higher clearance helps with that
        visibilityPredicate.maxClearance = 1;

        /**
         *
         * @type {FogOfWarVisibilityPredicate}
         * @private
         */
        this.__visibility_predicate = visibilityPredicate;
    }

    async shutdown(em) {
        this.graphics.on.preRender.remove(this.synchronizePositions, this);

    }

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

        this.graphics.on.preRender.add(this.synchronizePositions, this);
    }

    /**
     * NOTE: for the sake of speed all of the matrix and vector multiplications have been inlined, readability of this method is pretty bad
     * @param {HeadsUpDisplay} hud
     * @param {Transform} transform
     * @param {ViewportPosition} vp
     * @param {GUIElement} element
     * @private
     */
    __apply_entity_transformation(hud, transform, vp, element) {
        const worldOffset = hud.worldOffset;

        let x = worldOffset.x;
        let y = worldOffset.y;
        let z = worldOffset.z;

        if (hud.getFlag(HeadsUpDisplayFlag.TransformWorldOffset)) {
            //apply scale and rotation
            const transform_scale = transform.scale;

            x *= transform_scale.x;
            y *= transform_scale.y;
            z *= transform_scale.y;

            const q = transform.rotation;

            const qx = q.x;
            const qy = q.y;
            const qz = q.z;
            const qw = q.w;

            // calculate quat * vec
            const ix = qw * x + qy * z - qz * y;
            const iy = qw * y + qz * x - qx * z;
            const iz = qw * z + qx * y - qy * x;
            const iw = -qx * x - qy * y - qz * z;

            // calculate result * inverse quat
            x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
            y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
            z = iz * qw + iw * -qz + ix * -qy - iy * -qx;

        }

        // add transform position to get final world position
        const position = transform.position;

        x += position.x;
        y += position.y;
        z += position.z;

        const worldPositionX = x;
        const worldPositionY = y;
        const worldPositionZ = z;

        // Convert the [-1, 1] screen coordinate into a world coordinate on the near plane

        // apply projection matrix
        const _x = projection[0] * x + projection[4] * y + projection[8] * z + projection[12];
        const _y = projection[1] * x + projection[5] * y + projection[9] * z + projection[13];
        const _z = projection[2] * x + projection[6] * y + projection[10] * z + projection[14];
        const _w = projection[3] * x + projection[7] * y + projection[11] * z + projection[15];

        // apply perspective transform
        const d = 1 / Math.abs(_w);
        x = _x * d;
        y = _y * d;
        z = _z * d;

        const ndcX = (x + 1) / 2;
        const ndcY = (1 - y) / 2;

        vp.position.set(ndcX, ndcY);

        const trackedPositionOutOfBounds = z > 1 || z < -1;

        const visible = (!trackedPositionOutOfBounds || vp.stickToScreenEdge) && this.__visibility_predicate.test(worldPositionX, worldPositionY, worldPositionZ);

        if (!visible) {
            // hide element as it's not in viewport bounds
            vp.enabled.set(false);
            element.visible.set(false);
        } else {

            if (hud.getFlag(HeadsUpDisplayFlag.PerspectiveRotation)) {
                //compute rotation in screen-space

                const w_y_1 = worldPositionY + 1;

                const _up_x = projection[0] * worldPositionX + projection[4] * w_y_1 + projection[8] * worldPositionZ + projection[12];
                const _up_y = projection[1] * worldPositionX + projection[5] * w_y_1 + projection[9] * worldPositionZ + projection[13];
                const _up_w = projection[3] * worldPositionX + projection[7] * w_y_1 + projection[11] * worldPositionZ + projection[15];

                // compute perspective divide
                const d = 1 / Math.abs(_up_w);

                const up_x = _up_x * d;
                const up_y = _up_y * d;

                const up_ndcX = (up_x + 1) / 2;
                const up_ndcY = (1 - up_y) / 2;

                const angle = Math.atan2(up_ndcY - ndcY, up_ndcX - ndcX);

                element.view.rotation.set(angle + Math.PI / 2);
            } else {
                //clear rotation
                element.view.rotation.set(0);
            }

            vp.enabled.set(true);
            //TODO set z-index to ensure that things that are closer to the camera appear on top
        }
    }

    synchronizePositions() {
        const entityManager = this.entityManager;

        /**
         *
         * @type {Camera} three.js camera object
         */
        const camera = this.graphics.camera;

        mat4.multiply(projection, camera.projectionMatrix.elements, camera.matrixWorldInverse.elements);

        const visibilityPredicate = this.__visibility_predicate;

        /**
         * @type {EntityComponentDataset}
         */
        const dataset = entityManager.dataset;
        if (dataset !== null) {
            visibilityPredicate.initialize(camera, dataset);
            dataset.traverseEntities([HeadsUpDisplay, Transform, ViewportPosition, GUIElement], this.__apply_entity_transformation, this);
            visibilityPredicate.finalize();
        }
    };
}

export default HeadsUpDisplaySystem;
