import { AsyncHook, isNullOrUndefined, Http, } from "@intelligentgraphics/ig.utilities";
import { getAllMaterials } from "@intelligentgraphics/ig.gfx.threejs";
import * as Core from "@intelligentgraphics/ig.gfx.core";
import * as THREE from "@intelligentgraphics/three";
import * as MultiArticle from "@intelligentgraphics/ig.gfx.multiarticle";
import * as Planning from "@intelligentgraphics/ig.gfx.planning";
import { createHash } from "./Util";
import BaseLogger from "./Logger";
import { getMultiArticleConfiguration, getMultiArticleActions, getMultiArticleCategories, getMultiArticleAttachPoints, } from "./MultiArticleExecutor";
const Logger = BaseLogger.createSubLogger("ArticleInserter");
const evaluateMultiArticlePoints = (attachPoints, subRootName, properties) => {
    const evalConfiguration = {
        BBOX: MultiArticle.Rule.createBoundingBoxForRule(subRootName),
        ...properties,
    };
    const result = [];
    for (const point of attachPoints) {
        const tPoint = MultiArticle.Rule.evalAttachPoint(point, evalConfiguration);
        if (tPoint) {
            result.push(tPoint);
        }
    }
    return result;
};
const mergePointLists = (currentPoints, newMultiArticlePoints) => {
    if (currentPoints.length === 0) {
        return newMultiArticlePoints;
    }
    if (newMultiArticlePoints.length === 0) {
        return currentPoints;
    }
    const mergedPoints = new Map();
    for (const point of currentPoints) {
        mergedPoints.set(point.Key, point);
    }
    for (const point of newMultiArticlePoints) {
        mergedPoints.set(point.Key, point);
    }
    return Array.from(mergedPoints.values());
};
export class ArticleInserter {
    constructor(session, api, loadScheduler, options) {
        this.session = session;
        this.api = api;
        this.loadScheduler = loadScheduler;
        this.options = options;
        this.hooks = {
            /**
             * before a gfx representation is loaded
             */
            prepareForUpdate: new AsyncHook(),
            /**
             * after a gfx representation is loaded and we want to manipulate the gfx representation in the scene
             */
            manipulateAfterUpdate: new AsyncHook(),
            /**
             * after loading and manipulation of loaded articles
             */
            articleUpdateDone: new AsyncHook(),
        };
        this.articleStatus = new Map();
        this.updatePlanningMaterials = () => {
            const materials = getAllMaterials();
            for (const material of materials) {
                const mat = material;
                if (mat.name && mat.name.indexOf("D2PLANNINGMATERIAL") > -1) {
                    mat.transparent = true;
                    if ("map" in mat && mat["map"]) {
                        mat.map.wrapS = mat.map.wrapT = THREE.ClampToEdgeWrapping;
                    }
                }
            }
        };
    }
    getArticleLoadingStatus(comId) {
        return this.articleStatus.get(comId);
    }
    /**
     * Loads a gfx update for the article
     *
     * @private
     * @param {string} comId
     */
    async insertSingleArticle({ article, configurationOrArticleIdChange, force, animationPositionChange, }) {
        try {
            const comId = article.comId;
            const configurationHash = createHash(article.configuration);
            const isInitial = article.gfxId === undefined;
            await this.hooks.prepareForUpdate.emit(article, this.options.gfxType);
            const options = {
                type: this.options.gfxType,
            };
            if (article.gfxId !== undefined) {
                const data = this.api.GetComponentData(article.gfxId);
                if (data.plannerManager) {
                    if (!this.options.disableHashOptimizations) {
                        options.visualHash = data.plannerManager.visualHash;
                        options.geometricHash =
                            data.plannerManager.geometricHash;
                        options.pbrHash = data.plannerManager.pbrHash;
                        if (data.plannerManager.pbrHash !== undefined) {
                            options.animationPosition = data.animationProgress;
                        }
                    }
                    if (!configurationOrArticleIdChange && !force) {
                        if (data.animationProgress === data.plannerManager.animationPosition) {
                            return { status: "ok" };
                        }
                    }
                }
                // remove this once the placement is always up to date
                this.synchronizeArticleSessionPlacement(article.comId);
            }
            let articleStatus = this.articleStatus.get(comId);
            if (articleStatus !== undefined) {
                if (configurationHash !== articleStatus.configurationHash) {
                    // article has changed since the preview load -> throw away the current status and use next gfx representation from service
                    // clear hashes so we will always get a full igxc
                    this.articleStatus.delete(comId);
                    articleStatus = undefined;
                    options.visualHash = undefined;
                    options.geometricHash = undefined;
                    options.pbrHash = undefined;
                }
            }
            const gfxRepresentation = articleStatus !== undefined
                ? articleStatus.gfxRepresentation
                : await this.session.getGfxRepresentation(comId, options);
            if (isNullOrUndefined(gfxRepresentation)) {
                if (this.session.getArticleByComId(comId) === undefined) {
                    return {
                        status: "error",
                        error: {
                            type: "articleUnknown",
                        },
                    };
                }
                throw new Error(`Article "${comId}" does not have a gfx representation`);
            }
            if (gfxRepresentation === "UNCHANGED") {
                return { status: "ok" };
            }
            if (isNullOrUndefined(gfxRepresentation.scene)) {
                throw new Error(`Can not load gfxRepresentation for "${comId}". No scene has been provided`);
            }
            // preload the igxc so we can look for preview geometries
            if (typeof gfxRepresentation.scene === "string") {
                const path = gfxRepresentation.scene.split("/");
                const basePath = gfxRepresentation.scene.substring(0, gfxRepresentation.scene.indexOf(path[path.length - 1]));
                gfxRepresentation.scene = await Http.getJSON(gfxRepresentation.scene);
                gfxRepresentation.basePath = basePath;
            }
            if (article.sourcePlacement !== undefined &&
                article.sourcePlacement.type === "absolute") {
                gfxRepresentation.placement = article.sourcePlacement;
            }
            else {
                gfxRepresentation.placement = this.session.getArticlePlacement(comId);
            }
            let superRoot = this.session.getArticleGfxSuperRootId(comId);
            if (superRoot === undefined) {
                if (gfxRepresentation.placement === undefined &&
                    article.sourcePlacement !== undefined &&
                    article.sourcePlacement.type === "absolute") {
                    gfxRepresentation.placement = article.sourcePlacement;
                }
                superRoot = this.api.CreateSuperRoot();
            }
            if (gfxRepresentation.placement === undefined) {
                gfxRepresentation.placement = this.session.getArticlePlacement(comId);
            }
            const multiArticles = this.session.getMultiArticlesForComId(article.comId);
            const shouldLoadAsPreview = (this.options.loadMode ===
                Core.ELoadGeometryPreviews.PreviewOnly ||
                (articleStatus === undefined &&
                    this.options.loadMode ===
                        Core.ELoadGeometryPreviews.Staged)) &&
                this.canLoadAsPreview(gfxRepresentation.scene, article.gfxId);
            const gfxLoadMode = this.options.loadMode === undefined
                ? undefined
                : shouldLoadAsPreview
                    ? Core.ELoadGeometryPreviews.PreviewOnly
                    : Core.ELoadGeometryPreviews.Default;
            const catalogItemId = article.articleNumber !== undefined
                ? article.articleNumber.baseArticleNumber
                : undefined;
            const actionConfiguration = multiArticles
                ? {
                    ...article.configuration,
                    ...gfxRepresentation.actionConfig,
                    ...getMultiArticleConfiguration(multiArticles, article.articleId, catalogItemId),
                }
                : article.configuration || {};
            const gfxRepresentationSettings = gfxRepresentation.settings || {};
            const loadIGXCOptions = {
                computeNormals: gfxRepresentationSettings.computeNormals === true,
                loadMode: gfxLoadMode,
            };
            const mainComponentId = article.gfxId === undefined ||
                Core.Hierarchy.isRootGfxId(article.gfxId)
                ? superRoot
                : article.gfxId;
            // load a gfx representation for this article whenever there a no neighbor actions being executed
            const changes = await this.loadScheduler
                .scheduleGfxLoading(() => this.api.ProcessIGXC(mainComponentId, 
            // the scene will always be an igxc.scene, since it's preloaded in the plannermanager
            gfxRepresentation.scene, gfxRepresentation.basePath, loadIGXCOptions))
                .catch(err => {
                this.articleStatus.delete(comId);
                return Promise.reject(err);
            });
            const rootOrMainComponentId = Core.Hierarchy.fixIfSuperRoot(mainComponentId);
            this.api.SetCommercialId(rootOrMainComponentId, comId);
            if (isInitial) {
                this.session.setArticleGfxId(article.comId, Core.Hierarchy.getRootGfxId(superRoot));
            }
            this.api.RegionManager.updateComponentRegions(rootOrMainComponentId);
            this.updatePlanningMaterials();
            const appliedPlacement = this.tryApplyPlacement(rootOrMainComponentId, gfxRepresentation);
            if (mainComponentId ===
                Core.Hierarchy.getSuperRootGfxId(mainComponentId)) {
                this.applyMultiArticleUserData(rootOrMainComponentId, actionConfiguration, multiArticles, article.articleId, catalogItemId);
                this.setChildSelectionMode(rootOrMainComponentId);
                this.prepareNiches(rootOrMainComponentId, multiArticles, actionConfiguration);
            }
            this.prepareRoot(rootOrMainComponentId);
            if (!appliedPlacement) {
                this.executeDefaultPlacement(comId, rootOrMainComponentId);
            }
            const updateType = changes === Core.ProcessIGXCChanges.Structural
                ? "geometric"
                : "materialOnly";
            let result;
            if (shouldLoadAsPreview &&
                this.options.loadMode === Core.ELoadGeometryPreviews.Staged) {
                this.articleStatus.set(comId, {
                    status: "preview",
                    gfxRepresentation,
                    configurationHash,
                });
                result = {
                    status: "updateRequired",
                };
            }
            else if (articleStatus !== undefined) {
                this.articleStatus.delete(comId);
            }
            this.synchronizeArticleSessionPlacement(article.comId);
            const callbackParams = {
                initial: isInitial,
                status: gfxLoadMode !== Core.ELoadGeometryPreviews.PreviewOnly
                    ? "final"
                    : "preview",
                updateType,
            };
            const existingGraphicalItem = this.session.getArticleByComId(article.comId);
            this.applyComponentData(existingGraphicalItem.gfxId, {
                geometricHash: gfxRepresentation.geometricHash,
                visualHash: gfxRepresentation.visualHash,
                pbrHash: gfxRepresentation.pbrHash,
                animationPosition: options.animationPosition,
            });
            await this.loadScheduler
                .schedulePostUpdateManipulation(() => this.hooks.manipulateAfterUpdate.emit(existingGraphicalItem, callbackParams))
                .catch(err => Logger.error(`Encountered an error inside postUpdateManipulation hooks`, err));
            await this.hooks.articleUpdateDone
                .emit(existingGraphicalItem, callbackParams)
                .catch(err => Logger.error(`Encountered an error inside articleUpdateDone hooks`, err));
            return result || { status: "ok" };
        }
        catch (err) {
            return {
                status: "error",
                error: {
                    type: "unknown",
                    error: err,
                },
            };
        }
    }
    synchronizeArticleSessionPlacement(comId) {
        const placement = this.getSceneArticlePlacement(comId);
        if (placement !== undefined) {
            this.session.setArticlePlacement(comId, placement);
        }
    }
    canLoadAsPreview(igxc, gfxId) {
        return this.api.IsPreviewIGXCLoadPossibleAndNeccessary(igxc, gfxId);
    }
    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,
            },
        };
    }
    applyComponentData(gfxId, data) {
        this.api.SetComponentData(gfxId, "plannerManager", data);
    }
    applyMultiArticleUserData(rootId, properties, multiArticles, articleId, catalogItemId) {
        this.api.SetComponentData(rootId, "actions", multiArticles !== undefined
            ? getMultiArticleActions(multiArticles, articleId, catalogItemId)
            : []);
        // Add categories:
        if (multiArticles !== undefined) {
            const multiArticleCategories = getMultiArticleCategories(multiArticles, articleId, catalogItemId);
            if (multiArticleCategories) {
                const currentCategories = this.api
                    .GetCategories(rootId)
                    .slice(0);
                for (const tCategory in multiArticleCategories) {
                    if (multiArticleCategories.hasOwnProperty(tCategory)) {
                        if (MultiArticle.Rule.evalBooleanRule(multiArticleCategories[tCategory], properties)) {
                            currentCategories.push(tCategory);
                        }
                    }
                }
                this.api.SetCategories(rootId, currentCategories);
                const multiArticleConfig = getMultiArticleConfiguration(multiArticles, articleId, catalogItemId);
                this.api.SetComponentData(rootId, "multiArticleConfiguration", multiArticleConfig);
            }
            // Add attach points:
            const multiArticlePoints = getMultiArticleAttachPoints(multiArticles, articleId, catalogItemId);
            let points = this.api.GetIGXCPoints(rootId) || [];
            if (multiArticlePoints !== undefined) {
                const evaluatedAttachPoints = evaluateMultiArticlePoints(multiArticlePoints, rootId, properties);
                points = mergePointLists(points, evaluatedAttachPoints);
            }
            this.api.SetIGXCPoints(rootId, points);
        }
        else {
            // No multi articles -> add LeftRightChain category:
            this.api.SetCategories(rootId, [Core.Category.LeftRightChain]);
        }
    }
    setChildSelectionMode(rootId) {
        this.api.TraverseComponent(rootId, componentId => {
            this.api.SetSelectionMode(componentId, "Parent");
        });
    }
    prepareRoot(rootId) {
        if (Core.Category.hasCategory(rootId, Core.Category.NicheContainer)) {
            this.api.SetVisibility(rootId, false);
        }
    }
    prepareNiches(rootId, multiArticles, configuration) {
        const nicheIds = [];
        this.api.TraverseComponent(rootId, componentId => {
            const userData = this.api.GetComponentData(componentId);
            if (userData &&
                userData.layer &&
                MultiArticle.Niche.nicheRegex.test(userData.layer)) {
                nicheIds.push(componentId);
            }
        });
        if (multiArticles === undefined) {
            for (const componentId of nicheIds) {
                this.api.SetVisibility(componentId, false);
            }
            return;
        }
        const niches = nicheIds.map(nicheId => {
            const layer = this.api.GetLayer(nicheId);
            const [, nicheIdentifier] = MultiArticle.Niche.nicheRegex.exec(layer);
            const newNiche = new MultiArticle.Niche(nicheId, nicheIdentifier, rootId);
            return newNiche;
        });
        niches.forEach(niche => {
            const nicheActions = multiArticles.getActions(niche.multiArticlecId());
            this.api.SetComponentData(niche.getComponentId(), "actions", nicheActions);
        });
        const rootConverter = new Planning.WorldLocalConverter(rootId);
        const nichePoints = [];
        for (const niche of niches) {
            // convert from niche coordinates to world coordinates
            const nicheId = niche.getComponentId();
            const oldScaling = this.api.GetScaling(nicheId);
            this.api.SetScaling(nicheId, { x: 1, y: 1, z: 1 });
            const nicheConverter = new Planning.WorldLocalConverter(nicheId);
            const BBOX = MultiArticle.Rule.createBoundingBoxForRule(nicheId);
            // BUG: Some race conditions (?) may prevent niche to be loaded
            // in this case, just return nothing
            const tIsBboxInvalid = [
                BBOX.local.min.x,
                BBOX.local.min.y,
                BBOX.local.min.z,
            ].some(num => isNaN(num) || !isFinite(num));
            if (tIsBboxInvalid) {
                Logger.error(`There was a problem evaluating the BBOX: ${JSON.stringify(BBOX)} for the niche ${nicheId}`);
                this.api.SetScaling(nicheId, oldScaling);
                continue;
            }
            const nicheAttachPoints = niche.attachPoints(multiArticles, {
                BBOX: BBOX,
                ...configuration,
            });
            const nicheAttachPointsInLocalCoordinates = nicheAttachPoints.map(pt => {
                const inWorldCoordinates = nicheConverter.toWorld({
                    x: pt.Point.X,
                    y: pt.Point.Y,
                    z: pt.Point.Z,
                });
                const inRootCoordinates = rootConverter.toLocal(inWorldCoordinates);
                pt.Point = {
                    X: inRootCoordinates.x,
                    Y: inRootCoordinates.y,
                    Z: inRootCoordinates.z,
                };
                pt.ComponentId = niche.getComponentId();
                return pt;
            });
            this.api.SetScaling(nicheId, oldScaling);
            nichePoints.push(...nicheAttachPointsInLocalCoordinates);
        }
        const oldPoints = this.api.GetIGXCPoints(rootId);
        this.api.SetIGXCPoints(rootId, mergePointLists(oldPoints, nichePoints));
        for (const componentId of nicheIds) {
            this.api.SetVisibility(componentId, false);
        }
    }
    tryApplyPlacement(componentId, gfxRepresentation) {
        let placedArticle = false;
        let position = new Core.Vector3(0, 0, 0);
        let rotation = new Core.Vector3(0, 0, 0);
        if (gfxRepresentation.placement &&
            gfxRepresentation.placement.position) {
            position = gfxRepresentation.placement.position;
            placedArticle = true;
        }
        else {
            position = this.api.GetPosition(componentId);
        }
        if (gfxRepresentation.placement &&
            gfxRepresentation.placement.rotation) {
            rotation.x = Core.MathUtils.degToRad(gfxRepresentation.placement.rotation.x);
            rotation.y = Core.MathUtils.degToRad(gfxRepresentation.placement.rotation.y);
            rotation.z = Core.MathUtils.degToRad(gfxRepresentation.placement.rotation.z);
            placedArticle = true;
        }
        this.api.SetTransformation(componentId, Core.createTransformation(position, rotation));
        return placedArticle;
    }
    executeDefaultPlacement(comId, gfxId) {
        if (this.options.executeArticlePlacement !== undefined) {
            this.options.executeArticlePlacement(this.api, comId, gfxId);
            return;
        }
        const decider = new Planning.PlacementDecision(gfxId, this.api.GetAllSelectedComponents());
        const placementDecision = decider.getTransformation();
        this.api.SetTransformation(gfxId, placementDecision);
    }
}
