import Quaternion from "../../../../core/geom/Quaternion.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { min2 } from "../../../../core/math/min2.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 Path from '../components/Path.js';
import PathFollower from './PathFollower.js';
import { PathFollowerEventType } from "./PathFollowerEventType.js";
import { PathFollowerFlags } from "./PathFollowerFlags.js";

const v3_forward = new Vector3();
const v3_temp1 = new Vector3();
const v3_temp2 = new Vector3();
const scratch_quaternion = new Quaternion();
/**
 *
 * @param {PathFollower} pathFollower
 * @param {Path} path
 * @param {Transform} transform
 * @param {number} timeDelta
 */
function performStep(pathFollower, path, transform, timeDelta) {
    let remainingDistance = pathFollower.speed.getValue() * timeDelta;
    const path_length = path.length;

    while (remainingDistance > 0) {

        let stepLimit = pathFollower.maxMoveDistance;

        if (stepLimit <= 0) {
            console.warn('pathFollower.maxMoveDistance <= 0, defaulting to 1');

            stepLimit = 1;
        }

        const stepDistance = min2(remainingDistance, stepLimit);

        remainingDistance -= stepDistance;

        /**
         *
         * @type {Vector3}
         */
        const nextPosition = v3_temp1;

        const next_offset = pathFollower.position + stepDistance;
        const next_offset_truncated = next_offset % path_length;

        if (next_offset > path_length && !pathFollower.getFlag(PathFollowerFlags.Loop)) {
            // we're not looping
            remainingDistance = 0;
            path.sample(nextPosition, path_length);

            pathFollower.setFlag(PathFollowerFlags.Finished);
            pathFollower.position = path_length;
        } else {
            path.sample(nextPosition, next_offset_truncated);
            pathFollower.position = next_offset_truncated;
        }

        const position = transform.position;

        if (!pathFollower.getFlag(PathFollowerFlags.WritePositionX)) {
            nextPosition.setX(position.x);
        }
        if (!pathFollower.getFlag(PathFollowerFlags.WritePositionY)) {
            nextPosition.setY(position.y);
        }
        if (!pathFollower.getFlag(PathFollowerFlags.WritePositionZ)) {
            nextPosition.setZ(position.z);
        }

        if (!nextPosition.equals(position)) {
            const oldPosition = position;

            const alignment_x = pathFollower.getFlag(PathFollowerFlags.WriteRotationX);
            const alignment_y = pathFollower.getFlag(PathFollowerFlags.WriteRotationY);
            const alignment_z = pathFollower.getFlag(PathFollowerFlags.WriteRotationZ);

            if (alignment_x || alignment_y || alignment_z) {

                // TODO we can extract tangent and normal from the path itself, no need to do euler approximation

                //compute old facing direction vector
                v3_forward.copy(Vector3.forward);

                v3_forward.applyQuaternion(transform.rotation);

                const positionDelta = v3_temp2;

                positionDelta.subVectors(nextPosition, oldPosition);

                if (!alignment_x) {
                    positionDelta.x = v3_forward.x;
                }
                if (!alignment_y) {
                    positionDelta.y = v3_forward.y;
                }
                if (!alignment_z) {
                    positionDelta.z = v3_forward.z;
                }

                positionDelta.normalize();

                const angularLimit = pathFollower.rotationSpeed.getValue() * timeDelta;

                // console.log("Angular limit:", angularLimit, positionDelta.toJSON());

                scratch_quaternion.lookRotation(positionDelta);
                transform.rotation.rotateTowards(scratch_quaternion,angularLimit);
            }

            position.copy(nextPosition);
        }

    }
}

class PathFollowingSystem extends System {
    constructor() {
        super();

        this.dependencies = [PathFollower];

        this.components_used = [
            ResourceAccessSpecification.from(PathFollower, ResourceAccessKind.Read | ResourceAccessKind.Write),
            ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read | ResourceAccessKind.Write),
            ResourceAccessSpecification.from(Path, ResourceAccessKind.Read),
        ];

        /**
         *
         * @type {number}
         * @private
         */
        this.__time_delta = 0;
    }

    /**
     *
     * @param {PathFollower} pathFollower
     * @param {Transform} transform
     * @param {Path} path
     * @param {number} entity Entity ID
     */
    __visit_entity(pathFollower, transform, path, entity) {

        if (!pathFollower.getFlag(PathFollowerFlags.Active)) {
            //follower is not active, skip
            return;
        }

        if (path.isEmpty()) {
            //path is empty, do nothing
            return;
        }

        performStep(pathFollower, path, transform, this.__time_delta);

        if (pathFollower.getFlag(PathFollowerFlags.Finished)) {
            //deactivate
            pathFollower.clearFlag(PathFollowerFlags.Active);

            const dataset = this.entityManager.dataset
            dataset.sendEvent(entity, PathFollowerEventType.EndReached);
        }
    }

    /**
     *
     * @param {number} timeDelta Time in seconds
     */
    update(timeDelta) {
        this.__time_delta = timeDelta;
        const entityManager = this.entityManager;

        const dataset = entityManager.dataset;

        if (dataset !== null) {
            dataset.traverseEntities([PathFollower, Transform, Path], this.__visit_entity, this);
        }
    }
}


export default PathFollowingSystem;
