import { flatMap } from "lodash";
import * as Core from "@intelligentgraphics/ig.gfx.core";
import { AsyncHook, isNullOrUndefined, delay, Limiter, warnOnce, valueOrDefault, } from "@intelligentgraphics/ig.utilities";
import { Associates, NeighborType } from "@intelligentgraphics/ig.gfx.planning";
import * as MultiArticle from "@intelligentgraphics/ig.gfx.multiarticle";
import { Logger } from "./Logger";
import { updateArticles } from "./Updater";
import { LoadScheduler } from "./Scheduler";
import { createAPIUtil } from "./APIUtil";
import { SceneSnapshot } from "./Snapshot";
import { ArticleInserter, } from "./ArticleInserter";
const LifecycleLogger = Logger.createSubLogger("Lifecycle");
const NOTIFICATION_ID = "PlannerManager";
const addItemToQueue = (queue, article, update) => {
    const updateObject = queue.get(article.comId) || {
        article,
        animationPositionChange: false,
        configurationOrArticleIdChange: false,
        force: false,
    };
    const newUpdate = {
        ...updateObject,
        article,
        animationPositionChange: updateObject.animationPositionChange ||
            update.animationPositionChange === true,
        configurationOrArticleIdChange: updateObject.configurationOrArticleIdChange ||
            update.configurationOrArticleIdChange === true,
        force: updateObject.force || update.force === true,
    };
    queue.set(article.comId, newUpdate);
};
/**
 * Manages the lifecycle of a commercial article inside the gfx context
 *
 * @export
 * @class PlannerManager
 */
export class PlannerManager {
    constructor(session, options = {}) {
        this.session = session;
        this.loadScheduler = new LoadScheduler();
        this.setUpdateQueue = new Set();
        this.articleUpdateQueue = new Map();
        const defaultsInjectedOptions = {
            parallelUpdates: options.parallelUpdates === undefined
                ? true
                : options.parallelUpdates,
            loadMode: options.loadMode,
            gfxType: options.gfxType === undefined ? "3D" : options.gfxType,
            executeArticlePlacement: options.executeArticlePlacement,
            scheduleSceneUpdate: options.scheduleSceneUpdate !== undefined
                ? options.scheduleSceneUpdate
                : cb => delay().then(cb),
            generatePreUpdateSnapshot: options.generatePreUpdateSnapshot !== undefined
                ? options.generatePreUpdateSnapshot
                : articles => this.createSceneSnapshot(articles),
            disableHashOptimizations: valueOrDefault(options.disableHashOptimizations, false),
        };
        this.options = defaultsInjectedOptions;
        this.articleInserter = new ArticleInserter(session, Core.State.API, this.loadScheduler, {
            executeArticlePlacement: options.executeArticlePlacement,
            gfxType: defaultsInjectedOptions.gfxType,
            loadMode: defaultsInjectedOptions.loadMode,
            disableHashOptimizations: defaultsInjectedOptions.disableHashOptimizations,
        });
        this.hooks = {
            prepareForRemove: new AsyncHook(),
            articleRemoved: new AsyncHook(),
            sceneUpdateStart: new AsyncHook(),
            sceneUpdateDone: new AsyncHook(),
            ...this.articleInserter.hooks,
        };
        this.articleLoadLimiter = new Limiter(this.options.parallelUpdates ? Infinity : 1);
        this.apiUtil = createAPIUtil(session, this.api);
        if (options.onArticleUpdateStart !== undefined) {
            this.hooks.prepareForUpdate.tap("PlannerManagerOptionCallbacks", ({ comId }) => {
                warnOnce(`The "onArticleUpdateStart" option is deprecated. Please use the "prepareForUpdate" hook instead`);
                options.onArticleUpdateStart(comId);
            });
        }
        if (options.onArticleUpdateEnd !== undefined) {
            this.hooks.articleUpdateDone.tap("PlannerManagerOptionCallbacks", ({ comId }, { initial }) => {
                warnOnce(`The "onArticleUpdateEnd" option is deprecated. Please use the "articleUpdateDone" hook instead`);
                options.onArticleUpdateEnd(comId, initial);
            });
        }
        if (options.onSceneUpdateStart !== undefined) {
            this.hooks.sceneUpdateStart.tap("PlannerManagerOptionCallbacks", () => {
                warnOnce(`The "onSceneUpdateStart" option is deprecated. Please use the "sceneUpdateStart" hook instead`);
                options.onSceneUpdateStart();
            });
        }
        if (options.onSceneUpdateEnd !== undefined) {
            this.hooks.sceneUpdateDone.tap("PlannerManagerOptionCallbacks", () => {
                warnOnce(`The "onSceneUpdateEnd" option is deprecated. Please use the "sceneUpdateDone" hook instead`);
                options.onSceneUpdateEnd();
            });
        }
    }
    get api() {
        return Core.State.API;
    }
    createSceneSnapshot(articles) {
        return new SceneSnapshot({
            apiUtil: this.apiUtil,
            articles,
        });
    }
    hasPendingUpdates() {
        return this.articleUpdateQueue.size > 0 || this.setUpdateQueue.size > 0;
    }
    /**
     * Determines the loading status of the whole scene. Returns loadingFull if all loading articles are transitioning from preview to actual geometries
     *
     * @returns {LoadingStatus}
     */
    getLoadingStatus() {
        if (this.loadInfo === undefined) {
            return "idle";
        }
        if (this.options.loadMode !== Core.ELoadGeometryPreviews.Staged) {
            return "loading";
        }
        let inPreviewState = true;
        for (const comId of this.loadInfo.articles) {
            const status = this.articleInserter.getArticleLoadingStatus(comId);
            if (status === undefined) {
                inPreviewState = false;
            }
        }
        return inPreviewState ? "loadingFull" : "loading";
    }
    /**
     * Determines the status of the whole scene. Returns "preview" if at least one article is in a preview state
     *
     * @returns {SceneStatus}
     */
    getSceneStatus() {
        for (const article of this.session.getArticlesOfType("graphical")) {
            if (article.gfxId === undefined) {
                continue;
            }
            const component = this.api.GetComponent(article.gfxId);
            if (isNullOrUndefined(component)) {
                continue;
            }
            const data = this.api.GetComponentData(article.gfxId);
            if (data.geometryStatus === "preview") {
                return "preview";
            }
        }
        return "normal";
    }
    /**
     * Subscribes the PlannerManager to the supplied session and insert/updates or removes articles from the gfx scene on session notifications
     *
     * @returns
     */
    subscribe() {
        if (this.subscription !== undefined) {
            return;
        }
        this.subscription = this.session.subscribe({
            ItemRemoved: item => {
                if (item.type !== "set") {
                    this.processItems([item]);
                }
            },
            ItemCreated: item => this.processItems([item]),
            ItemConfigured: item => this.processItems([item]),
        });
        this.session.hooks.beforeSave.tap("PlannerManager", () => {
            this.session
                .getGraphicalArticles()
                .forEach(article => this.synchronizeArticleSessionPlacement(article.comId));
        });
        this.api.Hooks.afterAnimationProgressChange.tap("PlannerManager", (componentId, progress) => {
            if (progress !== 0 && progress !== 1) {
                return;
            }
            const comId = this.session.getComIdByGfxId(componentId);
            if (comId === undefined) {
                return;
            }
            const article = this.session.getArticleByComId(comId);
            const { animationProgress, plannerManager, } = this.api.GetComponentData(article.gfxId);
            const loadedPosition = plannerManager === undefined
                ? 0
                : plannerManager.animationPosition || 0;
            const activePosition = animationProgress || 0;
            if (loadedPosition === activePosition) {
                return;
            }
            this.addQueueItem(article, {
                animationPositionChange: true,
            });
            this.startProcessingQueueIfNecessary();
        });
    }
    /**
     * Removes the subscription to the session
     *
     */
    unsubscribe() {
        if (this.subscription !== undefined) {
            this.subscription.destroy();
        }
        this.session.hooks.beforeSave.untap("PlannerManager");
        this.api.Hooks.afterAnimationProgressChange.untap("PlannerManager");
    }
    /**
     * Deletes the gfx representation of an article instance or of a comId
     *
     * Should not be used manually when the PlannerManager has been subscribed to the session.
     *
     * @param {(Article | string)} item
     * @returns
     */
    async deleteArticle(item) {
        const instance = typeof item === "object"
            ? item
            : this.session.getArticleByComId(item);
        if (instance === undefined ||
            instance.type !== "graphical" ||
            instance.gfxId === undefined) {
            return;
        }
        const superRootId = Core.Hierarchy.isSuperRootGfxId(instance.gfxId)
            ? instance.gfxId
            : Core.Hierarchy.getSuperRootGfxId(instance.gfxId);
        if (isNullOrUndefined(superRootId) ||
            isNullOrUndefined(this.api.GetComponent(superRootId))) {
            return;
        }
        const neighbors = Associates.getNeighbors([instance.gfxId], false);
        await this.hooks.prepareForRemove.emit(instance);
        this.api.RemoveComponent(superRootId);
        await this.hooks.articleRemoved.emit(instance);
        this.session.sendNotification(NOTIFICATION_ID, "ItemGfxRemoved", instance);
        neighbors.forEach(neighbor => {
            if (!this.api.GetComponent(neighbor) ||
                this.session.getArticleByGfxId(neighbor) === undefined) {
                return;
            }
            this.executeNeighborActions(neighbor);
        });
    }
    /**
     * Loads or updates all available session articles inside the scene
     *
     * Should not be used manually when the PlannerManager has been subscribed to the session.
     *
     * @returns
     */
    loadAllArticles() {
        return this.processItems(this.session.getAllArticles());
    }
    /**
     * Loads or updates a spefic session article inside the scene.
     *
     * Should not be used manually when the PlannerManager has been subscribed to the session.
     *
     * @param {string} comId
     * @returns {Promise<void>}
     */
    loadArticle(comId) {
        return this.processItems([this.session.getArticleByComId(comId)]);
    }
    /**
     * Loads or updates spefic session articles inside the scene.
     *
     * Should not be used manually when the PlannerManager has been subscribed to the session.
     *
     * @param {string[]} comIds
     * @returns {Promise<void>}
     */
    loadArticles(comIds) {
        return this.processItems(comIds.map(comId => this.session.getArticleByComId(comId)));
    }
    processItems(items) {
        for (const item of items) {
            if (item === undefined) {
                continue;
            }
            if (item.type === "graphical") {
                this.addQueueItem(item, { force: true });
            }
            if (item.type === "set") {
                this.setUpdateQueue.add(item.comId);
                continue;
            }
        }
        return this.startProcessingQueueIfNecessary();
    }
    startProcessingQueueIfNecessary() {
        if (this.updatePromise !== undefined) {
            return this.updatePromise;
        }
        this.updatePromise = this.executeAllPendingAndCascadingUpdates().then(() => {
            this.updatePromise = undefined;
        });
        return this.updatePromise;
    }
    addQueueItem(article, update) {
        addItemToQueue(this.articleUpdateQueue, article, update);
    }
    /**
     * Executes all pending article updates and keeps updating as long as the queue is not empty after an update
     *
     * @private
     */
    async executeAllPendingAndCascadingUpdates() {
        await this.options
            .scheduleSceneUpdate(() => this.updateSceneFromCurrentQueue())
            .catch(err => Logger.error(err));
        if (this.articleUpdateQueue.size > 0) {
            await this.executeAllPendingAndCascadingUpdates();
        }
    }
    /**
     * Updates all articles and sets inside of the current queues
     *
     * @private
     * @returns {Promise<void>}
     */
    async updateSceneFromCurrentQueue() {
        if (this.articleUpdateQueue.size === 0 &&
            this.setUpdateQueue.size === 0) {
            return;
        }
        Logger.info("Updating scene");
        const articleQueue = this.articleUpdateQueue;
        this.articleUpdateQueue = new Map();
        const setQueue = this.setUpdateQueue;
        this.setUpdateQueue = new Set();
        this.loadInfo = {
            articles: Array.from(articleQueue.keys()),
            sets: Array.from(setQueue),
        };
        const allArticles = new Set();
        for (const comId of setQueue) {
            const children = this.session.getArticleChildren(comId);
            for (const childComId of children) {
                allArticles.add(childComId);
            }
        }
        try {
            for (const [comId, queuedArticle] of articleQueue) {
                const article = this.session.getArticleByComId(comId);
                if (article === undefined) {
                    await this.deleteArticle(queuedArticle.article);
                    articleQueue.delete(queuedArticle.article.comId);
                    continue;
                }
                allArticles.add(comId);
            }
            await this.hooks.sceneUpdateStart.emit(Array.from(allArticles));
            for (const comId of setQueue) {
                try {
                    const remainingArticles = await this.updateSet(comId);
                    const set = this.session.getArticleByComId(comId);
                    set.children.forEach(child => articleQueue.delete(child));
                    for (const article of remainingArticles) {
                        addItemToQueue(articleQueue, article, {
                            configurationOrArticleIdChange: true,
                        });
                    }
                }
                catch (err) {
                    Logger.error(err);
                }
            }
            const articles = Array.from(articleQueue.keys());
            const { newPlacedArticles, newUnplacedArticles, existingArticles, } = this.filterArticles(articles);
            // load all articles which already have a placement
            if (newPlacedArticles.length > 0) {
                await Promise.all(newPlacedArticles.map(comId => this.queueArticleInsert(articleQueue.get(comId)).catch(Logger.error)));
            }
            // deep update all existing articles and their children aswell as neighbors
            if (existingArticles.length > 0) {
                await updateArticles({
                    snapshot: this.options.generatePreUpdateSnapshot(existingArticles),
                    articles: existingArticles,
                    loadGeometry: (comId) => this.queueArticleInsert(articleQueue.get(comId)),
                    executeParentActions: (actionHolder, comId) => this.executeParentActions(actionHolder, this.session.getArticleGfxId(comId)),
                    executeNeighborActions: comId => this.executeNeighborActions(this.session.getArticleGfxId(comId)),
                    deleteArticleAndChildren: comId => this.session.deleteArticle(comId),
                    updateArticlePosition: this.apiUtil
                        .placeArticleAtAttachPoint,
                });
            }
            // sequentially load all unplaced articles (important because placement might screw up otherwise)
            for (const comId of newUnplacedArticles) {
                await this.queueArticleInsert(articleQueue.get(comId)).catch(Logger.error);
            }
            // all articles are loaded -> we can execute neighbor actions
            await Promise.all(articles.map(comId => {
                const article = this.session.getArticleByComId(comId);
                if (article !== undefined && article.gfxId !== undefined) {
                    return this.executeNeighborActions(article.gfxId);
                }
            }));
        }
        catch (err) {
            Logger.error(`Encountered an error while updating the scene`, err);
        }
        await this.hooks.sceneUpdateDone
            .emit(Array.from(allArticles))
            .catch(Logger.error);
        Logger.info("Finished updating scene");
        this.loadInfo = undefined;
    }
    async updateSet(setComId) {
        const setArticles = this.session
            .getArticleChildren(setComId)
            .map(comId => this.session.getArticleByComId(comId));
        // for (const article of setArticles) {
        // 	if (article.sourcePlacement !== undefined) {
        // 		this.session.setArticlePlacement(
        // 			article.comId,
        // 			article.sourcePlacement,
        // 		);
        // 		if (article.gfxId !== undefined) {
        // 			this.synchronizeArticleScenePlacement(article.comId);
        // 		}
        // 	}
        // }
        return setArticles;
    }
    //#region Old Set stuff
    // private async updateSet(setComId: string) {
    // 	try {
    // 		const setArticles = this.session
    // 			.getArticleChildren(setComId)
    // 			.map(comId => this.session.getArticleByComId(comId)!);
    // 		if (setArticles.length === 0) {
    // 			throw new Error(
    // 				`Could not find any articles for the set: ${setComId}`,
    // 			);
    // 		}
    // 		this.setUpdateQueue.delete(setComId);
    // 		if (setArticles.length === 0) {
    // 			return [];
    // 		}
    // 		let positionRootArticle: Article | undefined;
    // 		const secondaryArticles: Article[] = [];
    // 		const articlePlacements: Record<
    // 			string,
    // 			SourcePlacement | undefined
    // 		> = {};
    // 		for (const article of setArticles) {
    // 			secondaryArticles.push(article);
    // 			articlePlacements[article.comId] = article.sourcePlacement;
    // 		}
    // 		/* ICOM Article Placement
    // 			a) Scan all the already placed articles for the smallest position and use that article as the placement anchor for all the others
    // 			b) Look for an article which doesn't have a placement inside the icom context
    // 			c) Look for the smallest icom placement
    // 		*/
    // 		// random condition block here, so smallestPosition and smallestIndex dont leak out into the main method scope
    // 		if (true) {
    // 			let smallestPosition: Core.vec3 | undefined;
    // 			let smallestIndex: number | undefined;
    // 			for (let index = 0; index < setArticles.length; index++) {
    // 				const article = setArticles[index];
    // 				const placement = this.getSceneArticlePlacement(
    // 					article.comId,
    // 				);
    // 				if (
    // 					placement !== undefined &&
    // 					(smallestPosition === undefined ||
    // 						placement.position.x < smallestPosition.x)
    // 				) {
    // 					smallestPosition = placement.position;
    // 					smallestIndex = index;
    // 				}
    // 			}
    // 			if (smallestIndex !== undefined) {
    // 				positionRootArticle = secondaryArticles.splice(
    // 					smallestIndex,
    // 					1,
    // 				)[0];
    // 				const rootArticlePlacement =
    // 					articlePlacements[positionRootArticle.comId];
    // 				if (rootArticlePlacement !== undefined) {
    // 					for (const { comId } of secondaryArticles) {
    // 						const articlePlacement = articlePlacements[comId]!;
    // 						if (articlePlacement.type === "absolute") {
    // 							continue;
    // 						}
    // 						articlePlacement.position = Core.Vector.Sub3(
    // 							articlePlacement.position,
    // 							rootArticlePlacement.position,
    // 						);
    // 					}
    // 				}
    // 			}
    // 		}
    // 		if (positionRootArticle === undefined) {
    // 			const articleWithoutPlacement = setArticles.find(
    // 				article => articlePlacements[article.comId] === undefined,
    // 			);
    // 			if (articleWithoutPlacement === undefined) {
    // 				positionRootArticle = articleWithoutPlacement;
    // 			}
    // 		}
    // 		if (positionRootArticle === undefined) {
    // 			// no article has a position of undefined -> look for the smallest position and use it as the root
    // 			let smallestPosition: Core.vec3 | undefined;
    // 			let smallestIndex: number;
    // 			for (let index = 0; index < secondaryArticles.length; index++) {
    // 				const article = secondaryArticles[index];
    // 				const articlePosition = articlePlacements[article.comId]!;
    // 				if (
    // 					smallestPosition === undefined ||
    // 					smallestPosition.x > articlePosition.position!.x
    // 				) {
    // 					smallestPosition = articlePosition.position;
    // 					smallestIndex = index;
    // 				}
    // 			}
    // 			positionRootArticle = secondaryArticles[smallestIndex!];
    // 			secondaryArticles.splice(smallestIndex!, 1);
    // 			for (const { comId } of secondaryArticles) {
    // 				const articlePlacement = articlePlacements[comId]!;
    // 				if (articlePlacement.type === "absolute") {
    // 					continue;
    // 				}
    // 				articlePlacement.position = Core.Vector.Sub3(
    // 					articlePlacement.position,
    // 					smallestPosition!,
    // 				);
    // 			}
    // 		}
    // 		if (positionRootArticle === undefined) {
    // 			// this should never happen
    // 			throw new Error(
    // 				`Could not find the position root article for set: ${setComId}`,
    // 			);
    // 		}
    // 		// load the position root article first, so we can place other articles relative to it
    // 		await this.insertSingleArticle(positionRootArticle.comId);
    // 		const positionRootGfxId = this.session.getArticleGfxId(
    // 			positionRootArticle.comId,
    // 		)!;
    // 		const rootPlacement = articlePlacements[positionRootArticle.comId];
    // 		const mainPosition = this.api.GetPosition(positionRootGfxId);
    // 		if (
    // 			rootPlacement !== undefined &&
    // 			rootPlacement.rotation !== undefined
    // 		) {
    // 			this.api.SetYRotation(
    // 				positionRootGfxId,
    // 				rootPlacement.rotation.y,
    // 			);
    // 		}
    // 		this.synchronizeArticlePlacement(positionRootArticle.comId);
    // 		// calculate absolute positions for all remaining articles and save them to be used later
    // 		for (const article of secondaryArticles) {
    // 			const articlePlacement = articlePlacements[article.comId];
    // 			const placement = this.session.getArticlePlacement(
    // 				article.comId,
    // 			);
    // 			const dependentPlacement = RuleUtil.hasRule(
    // 				article,
    // 				"manipulable",
    // 			);
    // 			// when an article has not yet been placed, or if it's not manipulable by itself
    // 			// -> always update placement
    // 			if (
    // 				(placement === undefined || dependentPlacement) &&
    // 				articlePlacement !== undefined
    // 			) {
    // 				if (articlePlacement.type === "absolute") {
    // 					this.session.setArticlePlacement(
    // 						article.comId,
    // 						articlePlacement,
    // 					);
    // 					continue;
    // 				}
    // 				const newPlacement: Required<IPlacement> = {
    // 					position: Core.Vector3.add(
    // 						mainPosition,
    // 						articlePlacement.position,
    // 					),
    // 					rotation: {
    // 						x: articlePlacement.rotation.x,
    // 						y: articlePlacement.rotation.y,
    // 						z: articlePlacement.rotation.z,
    // 					},
    // 				};
    // 				this.session.setArticlePlacement(
    // 					article.comId,
    // 					newPlacement,
    // 				);
    // 			}
    // 		}
    // 		// return all the remaining not yet loaded articles and let them be handled by the normal article loading
    // 		return secondaryArticles;
    // 	} catch (err) {
    // 		throw err;
    // 	}
    // }
    //#endregion
    queueArticleInsert(update) {
        return this.session.scheduleArticleOperation(update.article.comId, () => this.articleLoadLimiter.queue(() => this.insertSingleArticle(update)));
    }
    /**
     * Load graphics for the passed commercial article directly.
     *
     * @private
     * @param {string} comId
     */
    async insertSingleArticle(update) {
        const comId = update.article.comId;
        LifecycleLogger.info(`Inserting article "${comId}"`);
        const queueItem = this.articleUpdateQueue.get(comId);
        // if this article has been added to the queue again, we can safely remove it from the queue and apply both updates at once
        this.articleUpdateQueue.delete(comId);
        const item = this.session.getArticleByComId(comId);
        if (item === undefined) {
            if (queueItem !== undefined) {
                await this.deleteArticle(queueItem.article);
            }
            LifecycleLogger.info(`Skipping update for "${comId}" because it does not exist`);
            return;
        }
        const result = await this.articleInserter.insertSingleArticle({
            ...update,
            article: item,
        });
        if (result.status === "error") {
            switch (result.error.type) {
                case "articleUnknown":
                    await this.deleteArticle(comId);
                    return;
                case "noGfxRepresentation":
                    throw new Error(`Article "${comId}" does not have a gfx representation`);
                default:
                    throw result.error.error;
            }
        }
        if (result.status === "updateRequired") {
            this.addQueueItem(this.session.getArticleByComId(comId), {
                force: true,
            });
        }
        this.session.sendNotification(NOTIFICATION_ID, "ItemGfxInserted", this.session.getArticleByComId(comId));
        LifecycleLogger.info(`Finished inserting article "${comId}"`);
    }
    filterArticles(changedArticles) {
        const uniqueArticles = new Set(changedArticles);
        const existingArticles = [];
        const newUnplacedArticles = [];
        const newPlacedArticles = [];
        for (const comId of uniqueArticles) {
            const article = this.session.getArticleByComId(comId);
            if (article === undefined) {
                continue;
            }
            if (this.apiUtil.getRootGfxId(comId) !== undefined) {
                existingArticles.push(comId);
                continue;
            }
            const { placement, sourcePlacement, } = this.session.getArticleByComId(comId);
            if (placement !== undefined ||
                (sourcePlacement !== undefined &&
                    sourcePlacement.type === "absolute")) {
                newPlacedArticles.push(comId);
                continue;
            }
            newUnplacedArticles.push(comId);
        }
        return {
            existingArticles,
            newUnplacedArticles,
            newPlacedArticles,
        };
    }
    synchronizeArticleSessionPlacement(comId) {
        const placement = this.getSceneArticlePlacement(comId);
        if (placement !== undefined) {
            this.session.setArticlePlacement(comId, placement);
        }
    }
    getSceneArticlePlacement(comId) {
        const gfxId = this.session.getArticleGfxId(comId);
        if (gfxId === undefined) {
            return undefined;
        }
        const component = this.api.GetComponent(gfxId);
        if (isNullOrUndefined(component)) {
            return undefined;
        }
        return {
            position: this.api.GetPosition(gfxId),
            rotation: {
                x: 0,
                y: this.api.GetYRotation(gfxId),
                z: 0,
            },
        };
    }
    async executeParentActions(actionHolderId, gfxId) {
        const parentAction = this.api
            .GetActions(actionHolderId)
            .find(action => action.Mode === "ParentDependency");
        if (!parentAction) {
            return false;
        }
        const executionResult = await MultiArticle.Rule.executeActions(parentAction.Rules, gfxId, actionHolderId);
        const didChange = typeof executionResult === "boolean" ? executionResult : true;
        return didChange;
    }
    async executeNeighborActions(subRootName) {
        const neighbors = [
            Associates.neighbor(subRootName, NeighborType.Left),
            Associates.neighbor(subRootName, NeighborType.Right),
        ].filter(Boolean);
        const rootsThatShouldExecuteActions = [subRootName, ...neighbors];
        const extractRules = (action) => action.Rules;
        let didChange = false;
        for (const id of rootsThatShouldExecuteActions) {
            const neighborActions = this.api
                .GetActions(id)
                .filter((action) => action.Mode === "NeighborDependency");
            const actionRules = flatMap(neighborActions, extractRules);
            if (actionRules.length) {
                const result = await MultiArticle.Rule.executeActions(actionRules, id, id);
                didChange =
                    didChange || (typeof result === "boolean" ? result : true);
            }
        }
        return didChange;
    }
}
