import { Hierarchy, MathUtils, } from "@intelligentgraphics/ig.gfx.core";
import { GhostInsertMode, WorldLocalConverter, } from "@intelligentgraphics/ig.gfx.planning";
import { Rule } from "@intelligentgraphics/ig.gfx.multiarticle";
import { valueOrDefault } from "@intelligentgraphics/ig.utilities";
import BaseLogger from "./Logger";
import { findMultiArticleConfiguration } from "./MultiArticleExecutor";
const Logger = BaseLogger.createSubLogger("GhostModeManager");
const AttachPointsLogger = Logger.createSubLogger("AttachPoints");
export class GhostModeManager {
    constructor(session, plannerManager, options = {}) {
        this.session = session;
        this.targets = new Map();
        this.articleActions = new Map();
        /**
         * Creates a createCopy method for the ghost mode based on an article catalog item
         *
         * @private
         */
        this.createArticleCopyFactory = (articleId) => async (transformation, actionRules = [], snapTo) => {
            let configuration;
            if (actionRules && actionRules.length && snapTo !== undefined) {
                const converter = new WorldLocalConverter(snapTo);
                const snapPos = converter.toLocal(transformation.position);
                const ruleParams = Object.assign({}, Rule.createAdditionalRuleParameters({
                    context: snapTo,
                    hasBottomNeighbor: false,
                    hasTopNeighbor: false,
                    hasLeftNeighbor: false,
                    hasRightNeighbor: false,
                    relativeSnapPosition: snapPos,
                }), this.componentConfiguration);
                configuration = {};
                for (const rule of actionRules) {
                    const action = Rule.evalActionRule(rule, ruleParams);
                    if (action.isActive && action.property !== undefined) {
                        configuration[action.property] = action.value;
                    }
                }
            }
            Logger.info(`Creating a new article`, {
                articleId,
                transformation,
                configuration,
            });
            if (snapTo && this.options.applySnapToConfiguration) {
                const rootGfxId = Hierarchy.getRootGfxId(snapTo);
                const article = this.session.getArticleByGfxId(rootGfxId);
                if (article !== undefined) {
                    configuration = {
                        ...article.configuration,
                        ...configuration,
                    };
                }
            }
            const comId = await this.createArticle(this.templateArticle.source, articleId, configuration, {
                position: transformation.position,
                rotation: {
                    x: MathUtils.radToDeg(transformation.rotation.x),
                    y: MathUtils.radToDeg(transformation.rotation.y),
                    z: MathUtils.radToDeg(transformation.rotation.z),
                },
            });
            this.articleActions.set(comId, {
                actionRules,
                snapTo: snapTo,
                comId,
            });
        };
        /**
         * Called after a ghost mode has been stopped
         *
         * @private
         */
        this.cleanupGhostMode = () => {
            if (this.ghostInsertModeInstance) {
                this.ghostInsertModeInstance = undefined;
            }
            this.componentConfiguration = undefined;
            this.onGhostModeEnd();
            if (this.templateArticle) {
                this.removeArticle(this.templateArticle.comId);
            }
        };
        this.options = {
            applySnapToConfiguration: valueOrDefault(options.applySnapToConfiguration, true),
        };
        plannerManager.hooks.prepareForUpdate.tap("GhostInsertMode", article => this.onPrepareForUpdate(article));
        plannerManager.hooks.articleUpdateDone.tap("GhostInsertMode", article => this.onUpdateDone(article));
        plannerManager.hooks.sceneUpdateDone.tap("GhostInsertMode", () => this.executeArticleActions());
    }
    getTemplateArticleInstance() {
        return this.templateArticle;
    }
    stop() {
        if (this.ghostInsertModeInstance) {
            this.ghostInsertModeInstance.stop();
        }
    }
    async createArticle(source, articleId, initialConfiguration, initialPlacement) {
        const catalogManager = this.session.getCatalogManagerForSourceId(source);
        if (catalogManager === undefined) {
            throw new Error("The default implementation of GhostModeManager.createArticle requires a catalog manager to be registered for the specified sourceId");
        }
        const articleItem = await catalogManager.getArticle(articleId);
        if (articleItem === undefined) {
            throw new Error(`Could not fetch catalog article item from id ${articleId}`);
        }
        return this.session.createArticleFromCatalogItem({
            source,
            catalogItem: articleItem,
            initialConfiguration,
            initialPlacement,
        });
    }
    removeArticle(comId) {
        this.session.setArticleGfxId(comId, undefined);
        return this.session.deleteArticle(comId);
    }
    onGhostModeStart() { }
    onGhostModeEnd() { }
    async executeArticleActions() {
        const executionPromises = [];
        for (const [comId, actionConfig] of this.articleActions) {
            // article has been created through the ghostmode and actions have to be executed
            const rootId = this.session.getArticleGfxId(comId);
            if (rootId === undefined) {
                continue;
            }
            this.articleActions.delete(comId);
            executionPromises.push(Rule.executeActions(actionConfig.actionRules, rootId, actionConfig.snapTo).catch(() => Logger.error(`Could not execute actions from GhostInsertMode for ${comId}`)));
        }
        await Promise.all(executionPromises);
    }
    async onPrepareForUpdate(article) {
        if (!article.placement || !article.placement.position) {
            // article was not yet placed -> is not being restored or changed
            this.targets.set(article.comId, article);
            return;
        }
    }
    async onUpdateDone(article) {
        // article has been loaded into scene
        const isTarget = this.targets.has(article.comId);
        this.targets.delete(article.comId);
        // if a ghostMode is already running -> refresh
        if (this.ghostInsertModeInstance) {
            if (article.articleNumber !== undefined &&
                this.templateCatalogItemId ===
                    article.articleNumber.baseArticleNumber) {
                try {
                    Logger.info(`Refreshing ghost mode after article insertion`);
                    this.ghostInsertModeInstance.refreshGhosts(); //redraws the still available ghosts (might be one less)
                }
                catch (err) {
                    Logger.error(`Could not refresh ghosts after update`, err);
                }
                return;
            }
            this.ghostInsertModeInstance.stop();
        }
        // if the supplied article argument has not been set as a potential target -> ignore
        if (!isTarget || !article.gfxId) {
            return;
        }
        // check if ghost mode can be applied to object -> start ghost mode
        if (GhostInsertMode.isGhostModeNecessary(article.gfxId, findMultiArticleConfiguration(this.session, article.comId))) {
            AttachPointsLogger.info(`Starting ghost mode for ${article.comId}`);
            return this.triggerGhostMode(article);
        }
        AttachPointsLogger.info(`Not starting ghost mode for ${article.comId} because no points are available`);
    }
    async triggerGhostMode(article) {
        // save template article, so it can be deleted later
        this.templateArticle = article;
        // original base catalog item id
        const { articleNumber, gfxId } = article;
        this.templateCatalogItemId =
            articleNumber !== undefined
                ? articleNumber.baseArticleNumber
                : article.articleId;
        this.componentConfiguration = await findMultiArticleConfiguration(this.session, article.comId);
        this.ghostInsertModeInstance = new GhostInsertMode(gfxId, this.createArticleCopyFactory(this.templateCatalogItemId), this.cleanupGhostMode, this.componentConfiguration);
        this.ghostInsertModeInstance.start();
        this.onGhostModeStart();
    }
}
