import { __assign, __awaiter, __generator, __read, __values } from "tslib";
import { Http, Obj, isNullOrUndefined, } from "@intelligentgraphics/ig.utilities";
import * as IGXC from "../IGXC";
import BaseLogger from "../Logger";
import * as Hierarchy from "../Hierarchy";
import { State, ELoadGeometryPreviews } from "../State";
import * as Preview from "../Preview";
import { preProcessIGXC } from "./Validation";
import { buildObjectTree } from "./ObjectTree";
import * as LoaderCaches from "./Cache";
import * as LoaderUtils from "./Utils";
import { ResourceAtlas } from "./ResourceAtlas";
export { ResourceAtlas, LoaderCaches, LoaderUtils, };
var Logger = BaseLogger.createSubLogger("ObjectLoader");
var DEFAULT_TRANSFORM = {
    Position: {
        X: 0,
        Y: 0,
        Z: 0,
    },
    Rotation: {
        X: 0,
        Y: 0,
        Z: 0,
    },
    Scale: {
        X: 1,
        Y: 1,
        Z: 1,
    },
};
/*
type ExistingNode = Partial<
    Pick<
        ObjectTreeEntry,
        | "Material"
        | "MaterialCategory"
        | "Geometry"
        | "Transform"
        | "GeometryParameters"
    >
> & { Path: string; Children: ExistingNode[] };
*/
export var ProcessIGXCChanges;
(function (ProcessIGXCChanges) {
    ProcessIGXCChanges["Structural"] = "Structural";
    /**
     * Non structual changes (only material)
     */
    ProcessIGXCChanges["Visual"] = "Visual";
})(ProcessIGXCChanges || (ProcessIGXCChanges = {}));
export var processIGXC = function (api, objectLoader, mainRootId, rawIGXC, igxcBasePath, _a) {
    var _b = _a === void 0 ? {} : _a, _c = _b.loadMode, loadMode = _c === void 0 ? State.loadGeometryPreviews : _c, _d = _b.computeNormals, computeNormals = _d === void 0 ? false : _d;
    return __awaiter(void 0, void 0, void 0, function () {
        var igxc_1, mainRoot, updates_1, atlas_1, processNode_1, loadIt, loadResult, _e, missingResult, err_1;
        return __generator(this, function (_f) {
            switch (_f.label) {
                case 0:
                    _f.trys.push([0, 12, , 13]);
                    Logger.debug("processing igxc for " + mainRootId);
                    igxc_1 = preProcessIGXC(rawIGXC, api, mainRootId);
                    if (igxcBasePath) {
                        igxc_1.BasePath = getBasePath(igxcBasePath, igxc_1);
                    }
                    mainRoot = api.GetComponent(mainRootId);
                    if (isNullOrUndefined(mainRoot)) {
                        throw new Error("Can not load igxc because there is no object available for component id \"" + mainRootId + "\"");
                    }
                    updates_1 = IGXC.determineSceneUpdates(igxc_1);
                    atlas_1 = new ResourceAtlas();
                    processNode_1 = function (node, previousNode, parent, context, branchContext, options) { return __awaiter(void 0, void 0, void 0, function () {
                        var componentId, aoMap, component, newTransform, geometryNormalMap, geometryNormalMapUrl, aoMapUrl, params, previousChildren, newChildren, matches, childPromises, index, match, _a, childNode, previousChildNode, err_2;
                        return __generator(this, function (_b) {
                            switch (_b.label) {
                                case 0:
                                    if (node === undefined) {
                                        if (previousNode === undefined) {
                                            return [2 /*return*/];
                                        }
                                        api.RemoveComponent(createNodeName(mainRootId, previousNode.Path));
                                        return [2 /*return*/];
                                    }
                                    componentId = createNodeName(mainRootId, node.Path);
                                    _b.label = 1;
                                case 1:
                                    _b.trys.push([1, 7, , 8]);
                                    if (previousNode &&
                                        !objectLoader.capabilities.geometryUpdates &&
                                        (previousNode.Geometry !== node.Geometry ||
                                            !areObjectsEqual(previousNode.GeometryParameters, node.GeometryParameters))) {
                                        Logger.debug("deleting previous node because of geometry update", componentId);
                                        api.RemoveComponent(componentId);
                                        return [2 /*return*/, processNode_1(node, undefined, parent, context, branchContext, options)];
                                    }
                                    aoMap = (igxc_1.ObjectAmbientOcclusionMaps &&
                                        igxc_1.ObjectAmbientOcclusionMaps[node.Path]) ||
                                        branchContext.aoMap;
                                    branchContext.aoMap = aoMap;
                                    component = api.GetComponent(componentId);
                                    if (isNullOrUndefined(previousNode) &&
                                        !isNullOrUndefined(component)) {
                                        Logger.debug("deleting component because it should not exist at this point", componentId);
                                        api.RemoveComponent(componentId);
                                        previousNode = undefined;
                                        component = null;
                                    }
                                    if (isNullOrUndefined(component)) {
                                        Logger.debug("creating node", componentId);
                                        component = objectLoader.createComponent(componentId, parent, options, context);
                                    }
                                    objectLoader.setComponentData(component, node, options, context);
                                    newTransform = __assign({}, DEFAULT_TRANSFORM);
                                    if (node.Transform) {
                                        if (node.Transform.Position) {
                                            newTransform.Position = node.Transform.Position;
                                        }
                                        if (node.Transform.Rotation) {
                                            newTransform.Rotation = node.Transform.Rotation;
                                        }
                                        if (node.Transform.Scale) {
                                            newTransform.Scale = node.Transform.Scale;
                                            if (!node.Transform.Scale.X ||
                                                !node.Transform.Scale.Y ||
                                                !node.Transform.Scale.Z) {
                                                console.log("Scaling " +
                                                    node.Path +
                                                    " with factor 0 in one direction might not be intended: ", node.Transform.Scale);
                                            }
                                        }
                                    }
                                    if (isNullOrUndefined(previousNode) ||
                                        !areObjectsEqual(newTransform, previousNode.Transform)) {
                                        Logger.debug("applying node transform", componentId);
                                        objectLoader.setComponentTransform(component, newTransform, options, context);
                                    }
                                    if (!(isNullOrUndefined(previousNode) ||
                                        previousNode.Material !== node.Material ||
                                        previousNode.MaterialCategory !== node.MaterialCategory)) return [3 /*break*/, 3];
                                    Logger.debug("applying node materials ", componentId);
                                    geometryNormalMap = node.Geometry !== undefined
                                        ? igxc_1.GeometryNormalMaps[node.Geometry]
                                        : undefined;
                                    geometryNormalMapUrl = geometryNormalMap !== undefined
                                        ? atlas_1.getResolvedName(geometryNormalMap) ||
                                            geometryNormalMap
                                        : undefined;
                                    aoMapUrl = aoMap !== undefined
                                        ? atlas_1.getResolvedName(aoMap) || aoMap
                                        : undefined;
                                    params = {
                                        material: node.Material,
                                        aoMapFileName: aoMapUrl,
                                        aoTransform: node.AOTransform,
                                        geometryNormalMap: geometryNormalMapUrl,
                                        preview: options.loadMode === "preview" ||
                                            options.loadMode === "previewOnly",
                                        geoUvMapping: node.Mapping,
                                    };
                                    return [4 /*yield*/, objectLoader
                                            .setComponentMaterials(component, params, options, context)
                                            .catch(function (err) {
                                            return Promise.reject(new Error("Could not apply materials to component " + componentId + ": " + err.message));
                                        })];
                                case 2:
                                    _b.sent();
                                    _b.label = 3;
                                case 3:
                                    if (!(isNullOrUndefined(previousNode) ||
                                        previousNode.Geometry !== node.Geometry ||
                                        !areObjectsEqual(previousNode.GeometryParameters, node.GeometryParameters))) return [3 /*break*/, 5];
                                    Logger.debug("applying node geometry", componentId);
                                    return [4 /*yield*/, objectLoader.setComponentGeometry(component, node, options, context)];
                                case 4:
                                    _b.sent();
                                    _b.label = 5;
                                case 5:
                                    previousChildren = isNullOrUndefined(previousNode)
                                        ? []
                                        : previousNode.Children;
                                    newChildren = isNullOrUndefined(node.Children)
                                        ? []
                                        : node.Children;
                                    matches = createNodeMatches(newChildren, previousChildren);
                                    childPromises = [];
                                    for (index = 0; index < matches.length; index++) {
                                        match = matches[index];
                                        if (match === undefined) {
                                            continue;
                                        }
                                        _a = __read(match, 2), childNode = _a[0], previousChildNode = _a[1];
                                        if (childNode !== undefined &&
                                            previousChildNode !== undefined &&
                                            childNode.Path !== previousChildNode.Path) {
                                            throw new Error("Encountered path mismatch. Expected \"" + childNode.Path + "\" as path for both, got \"" + previousChildNode.Path + "\"");
                                        }
                                        childPromises.push(processNode_1(childNode, previousChildNode, component, context, __assign(__assign({}, branchContext), { aoMap: aoMap }), options));
                                    }
                                    return [4 /*yield*/, Promise.all(childPromises)];
                                case 6:
                                    _b.sent();
                                    return [2 /*return*/, component];
                                case 7:
                                    err_2 = _b.sent();
                                    Logger.error(err_2);
                                    return [3 /*break*/, 8];
                                case 8: return [2 /*return*/];
                            }
                        });
                    }); };
                    loadIt = function (loadMode) { return __awaiter(void 0, void 0, void 0, function () {
                        var patchedIGXC, context, loaderOptions, rootId, rootParent, nextObjectTree, previousObjectTree, rootComponent;
                        return __generator(this, function (_a) {
                            switch (_a.label) {
                                case 0:
                                    patchedIGXC = loadMode === "preview" || loadMode === "previewOnly"
                                        ? Preview.createIGXC(igxc_1)
                                        : igxc_1;
                                    context = objectLoader.createContext &&
                                        objectLoader.createContext(patchedIGXC);
                                    loaderOptions = {
                                        computeNormals: computeNormals,
                                        loadMode: loadMode,
                                    };
                                    if (!objectLoader.preloadResources) return [3 /*break*/, 2];
                                    return [4 /*yield*/, objectLoader.preloadResources(patchedIGXC, loaderOptions, context, atlas_1)];
                                case 1:
                                    _a.sent();
                                    _a.label = 2;
                                case 2:
                                    rootId = Hierarchy.fixIfSuperRoot(mainRootId);
                                    if (!(updates_1 !== undefined && updates_1.length > 0)) return [3 /*break*/, 5];
                                    return [4 /*yield*/, applyUpdateIGXC(api, objectLoader, mainRootId, igxc_1, updates_1, loaderOptions, context, atlas_1)];
                                case 3:
                                    _a.sent();
                                    atlas_1.reset();
                                    return [4 /*yield*/, api.Hooks.afterGfxLoad.emit(rootId, loadMode === "preview" ? "previewLoaded" : "finalLoaded")];
                                case 4:
                                    _a.sent();
                                    return [2 /*return*/, ProcessIGXCChanges.Visual];
                                case 5:
                                    rootParent = api.GetComponent(mainRootId);
                                    nextObjectTree = buildObjectTree(patchedIGXC.Objects);
                                    previousObjectTree = !objectLoader.capabilities.nodeUpdates ||
                                        objectLoader.getComponentState === undefined
                                        ? undefined
                                        : buildSceneObjectTree(api, objectLoader, mainRootId, Hierarchy.fixIfSuperRoot(mainRootId));
                                    return [4 /*yield*/, processNode_1(nextObjectTree, previousObjectTree, rootParent, context, {}, loaderOptions)];
                                case 6:
                                    rootComponent = _a.sent();
                                    if (objectLoader.postProcessRootComponent) {
                                        objectLoader.postProcessRootComponent(rootComponent, loaderOptions, context);
                                    }
                                    atlas_1.reset();
                                    return [4 /*yield*/, api.Hooks.afterGfxLoad.emit(rootId, loadMode === "preview" ? "previewLoaded" : "finalLoaded")];
                                case 7:
                                    _a.sent();
                                    return [2 /*return*/, ProcessIGXCChanges.Structural];
                            }
                        });
                    }); };
                    loadResult = void 0;
                    _e = loadMode;
                    switch (_e) {
                        case ELoadGeometryPreviews.Default: return [3 /*break*/, 1];
                        case ELoadGeometryPreviews.PreviewOnly: return [3 /*break*/, 3];
                        case ELoadGeometryPreviews.Staged: return [3 /*break*/, 5];
                    }
                    return [3 /*break*/, 11];
                case 1: return [4 /*yield*/, loadIt("full")];
                case 2:
                    loadResult = _f.sent();
                    return [3 /*break*/, 11];
                case 3: return [4 /*yield*/, loadIt("previewOnly")];
                case 4:
                    loadResult = _f.sent();
                    return [3 /*break*/, 11];
                case 5:
                    if (!api.IsPreviewIGXCLoadPossibleAndNeccessary(igxc_1)) return [3 /*break*/, 8];
                    return [4 /*yield*/, loadIt("preview")];
                case 6:
                    loadResult = _f.sent();
                    api.RedrawScene(true);
                    return [4 /*yield*/, loadIt("missing")];
                case 7:
                    missingResult = _f.sent();
                    if (missingResult === ProcessIGXCChanges.Structural) {
                        loadResult = ProcessIGXCChanges.Structural;
                    }
                    return [3 /*break*/, 10];
                case 8: return [4 /*yield*/, loadIt("full")];
                case 9:
                    loadResult = _f.sent();
                    _f.label = 10;
                case 10: return [3 /*break*/, 11];
                case 11: return [2 /*return*/, loadResult];
                case 12:
                    err_1 = _f.sent();
                    throw err_1;
                case 13: return [2 /*return*/];
            }
        });
    });
};
var applyUpdateIGXC = function (api, objectLoader, mainRootId, igxc, updates, options, context, atlas) { return __awaiter(void 0, void 0, void 0, function () {
    var superRootId, sceneTree, updateList, newNodesByPath, _a, _b, object, processNode;
    var e_1, _c;
    return __generator(this, function (_d) {
        switch (_d.label) {
            case 0:
                superRootId = Hierarchy.getSuperRootGfxId(mainRootId);
                sceneTree = buildSceneObjectTree(api, objectLoader, superRootId, Hierarchy.fixIfSuperRoot(mainRootId));
                if (sceneTree === undefined) {
                    throw new Error("Can not apply an igxc update, because no root exists inside super root \"" + mainRootId + "\"");
                }
                updateList = new Set(updates);
                newNodesByPath = new Map();
                if (igxc.Objects) {
                    try {
                        for (_a = __values(igxc.Objects), _b = _a.next(); !_b.done; _b = _a.next()) {
                            object = _b.value;
                            newNodesByPath.set(object.Path, object);
                        }
                    }
                    catch (e_1_1) { e_1 = { error: e_1_1 }; }
                    finally {
                        try {
                            if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
                        }
                        finally { if (e_1) throw e_1.error; }
                    }
                }
                processNode = function (node, branchContext) { return __awaiter(void 0, void 0, void 0, function () {
                    var relativePath, nodeUpdate, componentId, aoMap, materialParams, materialCategory, material, newAoTransform, newAoMap;
                    return __generator(this, function (_a) {
                        switch (_a.label) {
                            case 0:
                                relativePath = node.Path;
                                nodeUpdate = newNodesByPath.get(relativePath);
                                componentId = Hierarchy.getAbsoluteGfxId(node.Path, superRootId);
                                aoMap = (igxc.ObjectAmbientOcclusionMaps !== undefined
                                    ? igxc.ObjectAmbientOcclusionMaps[relativePath]
                                    : undefined) || branchContext.aoMap;
                                if (updateList.has("Material")) {
                                    materialCategory = node.MaterialCategory;
                                    material = node.Material;
                                    if (nodeUpdate !== undefined) {
                                        // if there's an update for this node inside the igxc
                                        if (nodeUpdate.Material !== undefined) {
                                            // check if there's a material name provided in the update and use it
                                            material = nodeUpdate.Material;
                                            materialCategory = undefined;
                                        }
                                        else {
                                            // if the node does not contain a material name
                                            // we need to use the material of it's category
                                            // if it doesn't have a category inside the update, we need to reuse the old one
                                            materialCategory =
                                                nodeUpdate.MaterialCategory || materialCategory;
                                            if (materialCategory !== undefined) {
                                                // object has a category (either new or old) -> get the new material name from the categories
                                                material = igxc.Categories[materialCategory];
                                            }
                                        }
                                    }
                                    else if (materialCategory !== undefined) {
                                        // there was no update for this node inside the igxc, so we'll resolve the current material using it's old material category
                                        material = igxc.Categories[materialCategory];
                                    }
                                    if (material !== undefined && material !== node.Material) {
                                        // component does have a material and it's different from the old one -> we need to update it
                                        materialParams = {
                                            material: material,
                                        };
                                    }
                                }
                                if (updateList.has("AOTransform")) {
                                    newAoTransform = node.AOTransform;
                                    newAoMap = node.AOMap;
                                    if (nodeUpdate !== undefined) {
                                        // if there's an update for this node inside the igxc
                                        // determine new aoTransform and aoMap or reuse old data if there's no new data
                                        newAoTransform = nodeUpdate.AOTransform || newAoTransform;
                                        newAoMap =
                                            aoMap !== undefined
                                                ? atlas.getResolvedName(aoMap) || aoMap
                                                : newAoMap;
                                    }
                                    // if both aoTransform and aoMap are available and they're different from previous ones -> we need to update it
                                    if (newAoMap !== undefined &&
                                        newAoTransform !== undefined &&
                                        newAoMap !== node.AOMap &&
                                        !Obj.shallowEqual(newAoTransform, node.AOTransform)) {
                                        materialParams = __assign(__assign({}, materialParams), { aoTransform: newAoTransform, aoMapFileName: newAoMap });
                                    }
                                }
                                if (!(materialParams !== undefined)) return [3 /*break*/, 2];
                                // if we found some thing to update -> ask objectloader to apply updates
                                return [4 /*yield*/, objectLoader.setComponentMaterials(api.GetComponent(componentId), materialParams, options, context)];
                            case 1:
                                // if we found some thing to update -> ask objectloader to apply updates
                                _a.sent();
                                _a.label = 2;
                            case 2:
                                if (!(node.Children !== undefined && node.Children.length > 0)) return [3 /*break*/, 4];
                                return [4 /*yield*/, Promise.all(node.Children.map(function (child) {
                                        return processNode(child, __assign(__assign({}, branchContext), { aoMap: aoMap }));
                                    }))];
                            case 3:
                                _a.sent();
                                _a.label = 4;
                            case 4: return [2 /*return*/];
                        }
                    });
                }); };
                return [4 /*yield*/, processNode(sceneTree, {})];
            case 1:
                _d.sent();
                return [2 /*return*/, ProcessIGXCChanges.Visual];
        }
    });
}); };
var buildSceneObjectTree = function (api, objectLoader, superRootId, id) {
    var component = api.GetComponent(id);
    if (isNullOrUndefined(component)) {
        return undefined;
    }
    var node = __assign(__assign({}, IGXC.createObject(id.replace(superRootId + "__", ""))), { Children: [] });
    var componentData = objectLoader.getComponentState(component);
    node.Transform = componentData.Transform;
    node.Material = componentData.Material;
    node.MaterialCategory = componentData.MaterialCategory;
    node.Geometry = componentData.Geometry;
    node.AOTransform = componentData.AOTransform;
    node.AOMap = componentData.AOMap;
    var children = api.GetChildren(id);
    node.Children = children.map(function (childId) {
        return buildSceneObjectTree(api, objectLoader, superRootId, childId);
    });
    return node;
};
var createNodeMatches = function (nextNodes, prevNodes) {
    var e_2, _a, e_3, _b;
    if (prevNodes.length === 0) {
        return nextNodes.map(function (node) { return [node, undefined]; });
    }
    var matches = [];
    try {
        for (var nextNodes_1 = __values(nextNodes), nextNodes_1_1 = nextNodes_1.next(); !nextNodes_1_1.done; nextNodes_1_1 = nextNodes_1.next()) {
            var node = nextNodes_1_1.value;
            var match = node.Path.match(/(\d+)$/);
            if (match === null) {
                continue;
            }
            var _c = __read(match, 2), id = _c[1];
            var numPath = parseInt(id) - 1;
            if (matches[numPath] === undefined) {
                matches[numPath] = [node, undefined];
            }
            else {
                matches[numPath][0] = node;
            }
        }
    }
    catch (e_2_1) { e_2 = { error: e_2_1 }; }
    finally {
        try {
            if (nextNodes_1_1 && !nextNodes_1_1.done && (_a = nextNodes_1.return)) _a.call(nextNodes_1);
        }
        finally { if (e_2) throw e_2.error; }
    }
    try {
        for (var prevNodes_1 = __values(prevNodes), prevNodes_1_1 = prevNodes_1.next(); !prevNodes_1_1.done; prevNodes_1_1 = prevNodes_1.next()) {
            var node = prevNodes_1_1.value;
            var match = node.Path.match(/(\d+)$/);
            if (match === null) {
                continue;
            }
            var _d = __read(match, 2), id = _d[1];
            var numPath = parseInt(id) - 1;
            if (matches[numPath] === undefined) {
                matches[numPath] = [undefined, node];
            }
            else {
                matches[numPath][1] = node;
            }
        }
    }
    catch (e_3_1) { e_3 = { error: e_3_1 }; }
    finally {
        try {
            if (prevNodes_1_1 && !prevNodes_1_1.done && (_b = prevNodes_1.return)) _b.call(prevNodes_1);
        }
        finally { if (e_3) throw e_3.error; }
    }
    return matches;
};
var areObjectsEqual = function (a, b) {
    if (a === b) {
        return true;
    }
    if (!a || !b) {
        return false;
    }
    var aProperties = Object.getOwnPropertyNames(a);
    var bProperties = Object.getOwnPropertyNames(b);
    if (aProperties.length !== bProperties.length) {
        return false;
    }
    var checkProperties = function (properties) {
        var e_4, _a;
        try {
            for (var properties_1 = __values(properties), properties_1_1 = properties_1.next(); !properties_1_1.done; properties_1_1 = properties_1.next()) {
                var prop = properties_1_1.value;
                var aValue = a[prop];
                var bValue = b[prop];
                if (aValue === bValue) {
                    continue;
                }
                if (typeof aValue === "object" && typeof bValue === "object") {
                    if (!areObjectsEqual(aValue, bValue)) {
                        return false;
                    }
                    continue;
                }
                return false;
            }
        }
        catch (e_4_1) { e_4 = { error: e_4_1 }; }
        finally {
            try {
                if (properties_1_1 && !properties_1_1.done && (_a = properties_1.return)) _a.call(properties_1);
            }
            finally { if (e_4) throw e_4.error; }
        }
        return true;
    };
    if (!checkProperties(aProperties)) {
        return false;
    }
    return checkProperties(bProperties);
};
var createNodeName = function (superRootId, path) {
    return superRootId + "__" + path;
};
var getBasePath = function (igxcPath, data) {
    var tBasePath;
    if (data.BasePath && Http.isAbsoluteUrl(data.BasePath)) {
        tBasePath = data.BasePath;
    }
    else if (data.BasePath) {
        tBasePath =
            igxcPath !== undefined
                ? Http.joinPaths(igxcPath, data.BasePath)
                : data.BasePath;
    }
    else {
        tBasePath = igxcPath;
    }
    if (tBasePath) {
        if (tBasePath[tBasePath.length - 1] !== "/") {
            tBasePath = tBasePath + "/";
        }
    }
    return tBasePath;
};
