import { IllegalStateException } from "../../../../../core/fsm/exceptions/IllegalStateException.js";
import { EntityPathMarker } from "./EntityPathMarker.js";
import { Transform } from "../../../../ecs/transform/Transform.js";
import Vector3 from "../../../../../core/geom/Vector3.js";
import { assert } from "../../../../../core/assert.js";
import { binarySearchHighIndex } from "../../../../../core/collection/array/binarySearchHighIndex.js";
import { EPSILON } from "../../../../../core/math/EPSILON.js";

const scratch_v3 = new Vector3();


/**
 *
 * @param {number} offset
 * @param {EntityPathMarker} marker
 * @returns {number}
 */
function compareOffsetToPathMarker(offset, marker) {
    return offset - marker.offset;
}

export class EntityPath {
    constructor() {
        /**
         *
         * @type {EntityPathStyle}
         */
        this.style = null;

        /**
         *
         * @type {Path}
         */
        this.path = null;

        /**
         *
         * @type {EntityPathMarker[]}
         */
        this.markers = [];
    }

    /**
     *
     * @param {Path} path
     */
    setPath(path) {
        assert.notNull(path, 'path');
        assert.defined(path, 'path');

        this.path = path;
    }

    /**
     *
     * @returns {Path}
     */
    getPath() {
        return this.path;
    }

    /**
     * @param {EntityPathStyle} style
     */
    setStyle(style) {
        this.style = style;
    }

    /**
     *
     * @returns {EntityPathStyle}
     */
    getStyle() {
        return this.style;
    }

    /**
     *
     * @param {number} offset
     * @param {EntityPathMarkerDefinition} def
     * @private
     */
    __build_marker(offset, def) {
        const marker = new EntityPathMarker();

        const blueprint = def.blueprint;

        marker.offset = offset;
        marker.entity = blueprint.build({});
        marker.definition = def;

        this.__position_marker(marker);

        this.markers.push(marker);
    }


    /**
     *
     * @param {EntityPathMarker} marker
     * @private
     */
    __position_marker(marker) {
        /**
         *
         * @type {Transform}
         */
        const transform = marker.entity.getComponent(Transform);

        const offset = marker.offset;

        const path = this.path;

        path.sample(transform.position, offset);

        const def = marker.definition;

        if (def.align) {
            // get next point position to figure out orientation
            if (path.sample(scratch_v3, offset + EPSILON)) {
                scratch_v3.sub(transform.position);

            } else {
                // sample behind
                path.sample(scratch_v3, offset - EPSILON);

                scratch_v3.subVectors(transform.position, scratch_v3);
            }

            scratch_v3.normalize();

            transform.rotation.lookRotation(scratch_v3);
        }

        transform.multiplyTransforms(transform, def.transform);
    }

    updateMarkerPositions() {
        const length = this.path.computeLength();

        const marker_count = this.markers.length;
        const spacing = length / (marker_count - 1);

        let offset = 0;

        for (let i = 0; i < marker_count; i++) {
            const marker = this.markers[i];

            marker.offset = offset;

            this.__position_marker(marker);

            offset += spacing;
        }
    }

    build() {

        if (this.style === null) {
            throw new IllegalStateException(`No style specified`);
        }

        if (this.path === null) {
            throw new IllegalStateException(`No path specified`);
        }

        const path_length = this.path.computeLength();


        // build first marker
        this.__build_marker(0, this.style.marker_start);


        // we're going to justify the marks, for this we are going to determine justified spacing
        const spacing = this.style.spacing;

        const expected_main_marker_count = Math.floor((path_length / spacing) - 2);

        const justified_spacing = (path_length / (expected_main_marker_count + 1));

        let offset = 0;

        for (let i = 0; i < expected_main_marker_count; i++) {

            offset += justified_spacing;

            this.__build_marker(offset, this.style.marker_main);

        }

        this.__build_marker(path_length, this.style.marker_end);
    }

    /**
     *
     * @param {number} offset
     * @param {function(EntityPathMarker)} callback
     * @param {*} [thisArg]
     */
    visitMarkersAfter(offset, callback, thisArg) {

        const markers = this.markers;
        const marker_count = markers.length;

        const start = binarySearchHighIndex(markers, offset, compareOffsetToPathMarker, 0, marker_count - 1);

        for (let i = start; i < marker_count; i++) {
            const marker = markers[i];

            callback.call(thisArg, marker);
        }
    }

    /**
     *
     * @param {number} offset
     * @param {function(EntityPathMarker)} callback
     * @param {*} [thisArg]
     */
    visitMarkersBefore(offset, callback, thisArg) {

        const markers = this.markers;
        const marker_count = markers.length;

        for (let i = 0; i < marker_count; i++) {
            const marker = markers[i];

            if (marker.offset >= offset) {
                return;
            }

            callback.call(thisArg, marker);
        }
    }
}
