import { assert } from "../../../../core/assert.js";
import { Deque } from "../../../../core/collection/queue/Deque.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { ParentEntity } from "../../../ecs/parent/ParentEntity.js";
import { AbstractContextSystem } from "../../../ecs/system/AbstractContextSystem.js";
import { SystemEntityContext } from "../../../ecs/system/SystemEntityContext.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import Path from "../../../navigation/ecs/components/Path.js";
import { PathEvents } from "../../../navigation/ecs/components/PathEvents.js";
import { RibbonXPlugin } from "../../trail/x/RibbonXPlugin.js";
import { ShadedGeometry } from "../mesh-v2/ShadedGeometry.js";
import { EntityPath } from "./entity/EntityPath.js";
import { PathDisplay } from "./PathDisplay.js";
import { PathDisplayEvents } from "./PathDisplayEvents.js";
import { PathDisplayType } from "./PathDisplayType.js";
import { RibbonPathBuilder } from "./ribbon/RibbonPathBuilder.js";
import { TubePathBuilder } from "./tube/build/TubePathBuilder.js";

const builders = {
    [PathDisplayType.None]: function (style, path, result) {

    },
    /**
     *
     * @param {EntityPathStyle} style
     * @param {Path} _p
     * @param {PathDisplaySystem} system
     * @param {Entity[]} result
     */
    [PathDisplayType.Entity]: function (style, path, system, result) {

        const entity_path = new EntityPath();


        entity_path.setPath(path);
        entity_path.setStyle(style);


        entity_path.build();

        const markers = entity_path.markers;
        const n = markers.length;
        for (let i = 0; i < n; i++) {
            const marker = markers[i];

            result.push(marker.entity);
        }

    },
    /**
     *
     * @param {RibbonPathStyle} style
     * @param {Path} path
     * @param {PathDisplaySystem} system
     * @param {Entity[]} result
     */
    [PathDisplayType.Ribbon]: function (style, path, system, result) {
        const builder = new RibbonPathBuilder();

        builder.setPath(path);
        builder.setStyle(style);
        builder.setPlugin(system.plugin.getValue());


        builder.build(result);

    },
    /**
     *
     * @param {TubePathStyle} style
     * @param {Path} path
     * @param {PathDisplaySystem} system
     * @param {Entity[]} result
     */
    [PathDisplayType.Tube]: function (style, path, system, result) {
        const builder = new TubePathBuilder();

        builder.setPath(path);
        builder.setStyle(style);
        builder.setMaterials(system.engine.graphics.getMaterialManager());
        builder.setAssetManager(system.engine.assetManager);


        builder.build(result);

    }
};

/**
 * Maximum amount of time allowed per system tick to spend on processing update queue
 * in milliseconds
 * @readonly
 * @type {number}
 */
const UPDATE_PROCESSING_BUDGET_MS = 10;

class PathDisplayContext extends SystemEntityContext {
    constructor() {
        super();


        /**
         *
         * @type {Entity[]}
         * @private
         */
        this.__owned_entities = [];
    }

    __build() {
        // clear out owned entities
        const ownedEntities = this.__owned_entities;
        ownedEntities.splice(0, ownedEntities.length);

        // todo check that the main entity still exists before we decide to spawn new entities

        /**
         * @type {PathDisplay}
         */
        const display = this.components[0];
        /**
         * @type {Path}
         */
        const path = this.components[1];

        const specs = display.specs;

        for (let i = 0; i < specs.length; i++) {
            const spec = specs[i];

            const builder = builders[spec.type];

            if (builder === undefined) {
                // builder not found
                console.warn(`No builder for type ${spec.type}`);
                return;
            }

            try {
                builder(spec.style, path, this.system, ownedEntities);
            } catch (e) {
                console.error(`Failed to build path of type '${spec.type}'`);
                console.error(e);
            }
        }

        // ensure that all entities are parented
        const n = ownedEntities.length;
        for (let i = 0; i < n; i++) {
            const e = ownedEntities[i];

            e.add(ParentEntity.from(this.entity));
        }

        this.__build_existing_entities();

        const ecd = this.getDataset();

        // signal completion of build
        ecd.sendEvent(this.entity, PathDisplayEvents.BuildComplete, ownedEntities);
    }

    __build_existing_entities() {

        const ecd = this.getDataset();

        // attach all existing owned entities
        const owned_entities = this.__owned_entities;
        const owned_entity_count = owned_entities.length;

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

            const entity = owned_entities[i];

            entity.build(ecd);
        }
    }

    __destroy_existing_entities() {

        const owned_entities = this.__owned_entities;
        const owned_entity_count = owned_entities.length;

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

            const entity = owned_entities[i];

            entity.destroy();
        }
    }

    rebuild() {
        this.__destroy_existing_entities();
        this.__build();
    }

    request_update() {
        /**
         *
         * @type {PathDisplaySystem}
         */
        const system = this.system;

        system.request_entity_update(this.entity);
    }

    link() {
        super.link();

        this.__build();

        const ecd = this.getDataset();

        const entity = this.entity;

        ecd.addEntityEventListener(entity, PathEvents.Changed, this.request_update, this);
        ecd.addEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this);
    }

    unlink() {
        super.unlink();

        const ecd = this.getDataset();

        const entity = this.entity;

        ecd.removeEntityEventListener(entity, PathEvents.Changed, this.request_update, this);
        ecd.removeEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this);

        /**
         *
         * @type {PathDisplaySystem}
         */
        const system = this.system;

        system.cancel_entity_update(entity);

        // destroy existing owned entities
        this.__destroy_existing_entities();
    }
}

export class PathDisplaySystem extends AbstractContextSystem {
    /**
     *
     * @param {Engine} engine
     */
    constructor(engine) {
        super(PathDisplayContext);

        assert.defined(engine, "engine");

        this.dependencies = [
            PathDisplay,
            Path
        ];

        this.components_used = [
            ResourceAccessSpecification.from(ParentEntity, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(ShadedGeometry, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(Transform, ResourceAccessKind.Create),
        ];

        /**
         *
         * @type {Engine}
         */
        this.engine = engine;

        /**
         *
         * @type {Reference<RibbonXPlugin>}
         */
        this.plugin = null;

        /**
         * Deferred queue of entities slated to be rebuilt
         * @type {Deque<number>}
         * @private
         */
        this.__rebuild_queue = new Deque();

        /**
         *
         * @type {Signal}
         */
        this.onWorkDone = new Signal();
    }

    /**
     * Request that path displays be rebuilt
     * @package
     * @param {number} entity
     */
    request_entity_update(entity) {
        assert.isNonNegativeInteger(entity, 'entity');

        if (!this.__rebuild_queue.has(entity)) {
            this.__rebuild_queue.add(entity);
        }
    }

    /**
     * Cancel any pending updates
     * @package
     * @param {number} entity
     */
    cancel_entity_update(entity) {
        this.__rebuild_queue.remove(entity);
    }

    async startup(entityManager) {
        // bind plugin
        this.plugin = await this.engine.plugins.acquire(RibbonXPlugin);
    }

    async shutdown(entityManager) {

        this.plugin.release();
    }

    update(time_delta) {
        // process update queue

        const queue = this.__rebuild_queue;
        if (queue.isEmpty()) {
            return;
        }

        const t0 = performance.now();

        do {
            // note that we don't need to check if the queue is empty here for the first iteration, as we already checked earlier
            const entity = queue.pop();

            /**
             *
             * @type {PathDisplayContext|undefined}
             */
            const ctx = this.__getEntityContext(entity);

            // check that entity still exists
            if (ctx === undefined) {
                continue;
            }

            ctx.rebuild();

        } while ((performance.now() - t0) < UPDATE_PROCESSING_BUDGET_MS && !queue.isEmpty());

        // notify that some work was done
        this.onWorkDone.send0();
    }
}
