import { __read, __spread, __values } from "tslib";
import { uniq } from "lodash";
import * as Core from "@intelligentgraphics/ig.gfx.core";
import { isValue } from "@intelligentgraphics/ig.utilities";
import SnapCalculator from "./Interactor/SnapCalculator";
import { StrongChildMatch, StrongNeighborMatch, SnapMatch, } from "./Interactor/SnapMatches";
import { NeighborType } from "./Types";
import { AssociatesCache, } from "./AssociatesCache";
var CACHE_IDENTIFIER = "AssociatesCache";
var getOrCreateCache = function () {
    if (Core.State.API[CACHE_IDENTIFIER] !== undefined) {
        return Core.State.API[CACHE_IDENTIFIER];
    }
    var cache = (Core.State.API[CACHE_IDENTIFIER] = new AssociatesCache(Core.State.API));
    var invalidateNeighborhood = function (componentId) {
        componentId = Core.Hierarchy.fixIfSuperRoot(componentId);
        cache.invalidateRoot(componentId);
    };
    var updateComponentRegions = Core.State.API.RegionManager.updateComponentRegions.bind(Core.State.API.RegionManager);
    Core.State.API.RegionManager.updateComponentRegions = function (componentId) {
        invalidateNeighborhood(componentId);
        var newRegions = updateComponentRegions(componentId);
        return newRegions;
    };
    return cache;
};
/**
 * Gets all children.
 * @param componentIds
 * @param recursively
 * @returns {string[]} An array containing all the children.
 */
export var getChildren = function (componentIds, recursively) { return getAssociates(componentIds, recursively, children); };
/**
 * Get all children based on snap points of the component,
 * except when they are also grandchildren or something
 *
 * @param componentId
 */
export var children = function (componentId) {
    return getAttachPointsAtChildren(componentId).map(extractOtherId);
};
export var getAttachPointsAtChildren = function (componentId) {
    var tResult = calculateAttachPointsAtChildren(componentId);
    return tResult;
};
var calculateAttachPointsAtChildren = function (componentId) {
    var childOrMulti = calculatePotentialAttachPointsAtChildren(componentId).reduce(function (acc, match) {
        acc[match.otherId] = match;
        return acc;
    }, {});
    var removeChildren = function (componentId) {
        var e_1, _a;
        var children = getAttachPointsAtChildren(componentId);
        try {
            for (var children_1 = __values(children), children_1_1 = children_1.next(); !children_1_1.done; children_1_1 = children_1.next()) {
                var child = children_1_1.value;
                delete childOrMulti[child.otherId];
                removeChildren(child.otherId);
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (children_1_1 && !children_1_1.done && (_a = children_1.return)) _a.call(children_1);
            }
            finally { if (e_1) throw e_1.error; }
        }
    };
    Object.getOwnPropertyNames(childOrMulti).forEach(removeChildren);
    return Object.getOwnPropertyNames(childOrMulti).map(function (propertyKey) { return childOrMulti[propertyKey]; });
};
var calculatePotentialAttachPointsAtChildren = function (componentId) {
    var cache = getOrCreateCache();
    // caching
    var cached = cache.getChildren(componentId);
    var nearbyRoots = cached.unknowns;
    var convertedPointsOfParent = cache.getConvertedPoints(componentId, "Child");
    var lenParents = convertedPointsOfParent.length;
    var lenRoots = nearbyRoots.length;
    var result = [];
    var nonchildren = [];
    // try out all children in roots
    for (var idxRoot = 0; idxRoot < lenRoots; idxRoot++) {
        var rootId = nearbyRoots[idxRoot];
        var convertedPointsOfChildren = cache.getConvertedPoints(rootId, "Origin");
        var lenChild = convertedPointsOfChildren.length;
        var isValidMatchFound = false;
        // try out all parent points
        for (var idxParent = 0; idxParent < lenParents; idxParent++) {
            var pParent = convertedPointsOfParent[idxParent];
            // try out all child points
            for (var idxChild = 0; idxChild < lenChild; idxChild++) {
                var child = convertedPointsOfChildren[idxChild];
                if (child.transformedPoint.x <
                    pParent.transformedPoint.x - SnapMatch.maxDistance) {
                    continue;
                }
                if (child.transformedPoint.x >
                    pParent.transformedPoint.x + SnapMatch.maxDistance) {
                    continue;
                }
                if (child.transformedPoint.z <
                    pParent.transformedPoint.z - SnapMatch.maxDistance) {
                    continue;
                }
                if (child.transformedPoint.z >
                    pParent.transformedPoint.z + SnapMatch.maxDistance) {
                    continue;
                }
                if (child.transformedPoint.y <
                    pParent.transformedPoint.y - SnapMatch.maxDistance) {
                    continue;
                }
                if (child.transformedPoint.y >
                    pParent.transformedPoint.y + SnapMatch.maxDistance) {
                    continue;
                }
                var match = new StrongChildMatch(child, pParent, componentId, rootId);
                var isValid = match.isValid();
                var isPrecise = match.isPrecise();
                if (isValid && isPrecise) {
                    isValidMatchFound = true;
                    result.push({
                        ownPointKey: pParent.key,
                        otherId: rootId,
                        otherPointKey: child.key,
                    });
                    break;
                }
            }
            if (isValidMatchFound) {
                break;
            }
        }
        if (!isValidMatchFound) {
            nonchildren.push(rootId);
        }
    }
    cache.setChildren(componentId, result);
    cache.setNonChildren(componentId, nonchildren);
    result.push.apply(result, __spread(cached.knowns));
    //console.log("Child Got:", componentId, result);
    return result;
};
export var getParents = function (componentIds, recurse) {
    //console.log("getParents", componentIds);
    var parentsFunction = function (id) {
        return [parent(id)].filter(isValue);
    };
    return getAssociates(componentIds, recurse, parentsFunction);
};
/**
 * Gets the snapping parent of the component, specified by id. Calculated via the ChildMatch function of Point
 * @param componentId The component id.
 * @returns {string} The component id to which the id given as parameter is snapped to.
 */
export var parent = function (componentId) {
    var attachPointAtParent = getAttachPointAtParent(componentId);
    if (attachPointAtParent) {
        return attachPointAtParent.otherId;
    }
    return undefined;
};
/**
 * Gets the attach point to which the component is snapped to on the parent. Calculated via the ChildMatch function of Point
 * If the component is snapped to more than one parent in a hierarchy (e.g. t-element + c-element), the innermost one is given
 * @param componentId The component id.
 * @returns {string} The component id to which the id given as parameter is snapped to.
 */
export var getAttachPointAtParent = function (componentId) { return getAttachPointsAtParent(componentId)[0]; };
export var getAttachPointsAtParent = function (componentId) {
    var points = [];
    var point = calculateAttachPointAtParent(componentId);
    if (point !== undefined) {
        points = [point];
    }
    //withCache(
    //	"parent",
    //	componentId => {
    //	}
    //) (componentId, pointcache);
    //console.log("getAttachPointsAtParent(",componentId, "== has parent :>", points );
    return points;
};
var calculateAttachPointAtParent = function (componentId) {
    var e_2, _a, e_3, _b, e_4, _c;
    var cache = getOrCreateCache();
    var cached = cache.getParent(componentId);
    if (cached == null) {
        return null; // there is no entry
    }
    if (!Array.isArray(cached)) {
        return cached;
    }
    var nearbyRoots = cached;
    var convertedPointsOfChild = cache.getConvertedPoints(componentId, "Origin");
    // there can be different parents , especially for tablar in left part of T-Element inside corpus
    var parentCandidates = [];
    try {
        for (var nearbyRoots_1 = __values(nearbyRoots), nearbyRoots_1_1 = nearbyRoots_1.next(); !nearbyRoots_1_1.done; nearbyRoots_1_1 = nearbyRoots_1.next()) {
            var parentId = nearbyRoots_1_1.value;
            //foreign ids
            var convertedPointsOfParent = cache.getConvertedPoints(parentId, "Child");
            try {
                for (var convertedPointsOfParent_1 = (e_3 = void 0, __values(convertedPointsOfParent)), convertedPointsOfParent_1_1 = convertedPointsOfParent_1.next(); !convertedPointsOfParent_1_1.done; convertedPointsOfParent_1_1 = convertedPointsOfParent_1.next()) {
                    var parentPoint = convertedPointsOfParent_1_1.value;
                    try {
                        //points
                        for (var convertedPointsOfChild_1 = (e_4 = void 0, __values(convertedPointsOfChild)), convertedPointsOfChild_1_1 = convertedPointsOfChild_1.next(); !convertedPointsOfChild_1_1.done; convertedPointsOfChild_1_1 = convertedPointsOfChild_1.next()) {
                            var childPoint = convertedPointsOfChild_1_1.value;
                            //points
                            var match = new StrongChildMatch(childPoint, parentPoint, parentId, componentId);
                            var isValid = match.isValid();
                            var isPrecise = match.isPrecise();
                            if (isValid && isPrecise) {
                                parentCandidates.push({
                                    otherPointKey: parentPoint.key,
                                    ownPointKey: childPoint.key,
                                    otherId: parentId,
                                });
                            }
                        }
                    }
                    catch (e_4_1) { e_4 = { error: e_4_1 }; }
                    finally {
                        try {
                            if (convertedPointsOfChild_1_1 && !convertedPointsOfChild_1_1.done && (_c = convertedPointsOfChild_1.return)) _c.call(convertedPointsOfChild_1);
                        }
                        finally { if (e_4) throw e_4.error; }
                    }
                }
            }
            catch (e_3_1) { e_3 = { error: e_3_1 }; }
            finally {
                try {
                    if (convertedPointsOfParent_1_1 && !convertedPointsOfParent_1_1.done && (_b = convertedPointsOfParent_1.return)) _b.call(convertedPointsOfParent_1);
                }
                finally { if (e_3) throw e_3.error; }
            }
        }
    }
    catch (e_2_1) { e_2 = { error: e_2_1 }; }
    finally {
        try {
            if (nearbyRoots_1_1 && !nearbyRoots_1_1.done && (_a = nearbyRoots_1.return)) _a.call(nearbyRoots_1);
        }
        finally { if (e_2) throw e_2.error; }
    }
    if (parentCandidates.length > 0) {
        var parentCandidatesWithoutAncestors_1 = [];
        parentCandidates.forEach(function (association) {
            // for every candidate (e.g. corpus) find out if one of his children is also a parent candidate (e.g. t-element)
            var children = getChildren([association.otherId], true);
            if (parentCandidates.filter(function (x) { return children.indexOf(x.otherId) > -1; })
                .length < 1) {
                // if no candidate is itself my child -> add me
                parentCandidatesWithoutAncestors_1.push(association);
            }
        });
        if (parentCandidatesWithoutAncestors_1.length > 0) {
            //use the first if still >1 possibilities
            cache.setParent(componentId, parentCandidatesWithoutAncestors_1[0].otherId, parentCandidatesWithoutAncestors_1[0]);
            cache.setNonParents(componentId, nearbyRoots.filter(function (r) { return r !== parentCandidatesWithoutAncestors_1[0].otherId; }));
            return parentCandidatesWithoutAncestors_1[0];
        }
        else {
            return undefined;
        }
    }
    else {
        cache.setNonParents(componentId, nearbyRoots);
    }
    return undefined;
};
/**
 * Get the neighbor of the type.
 */
export var neighbor = function (componentId, type, key) {
    var typeAsString = NeighborType.getAsString(type);
    var matchingType = NeighborType.getOpposite(type);
    var matchingTypeAsString = NeighborType.getAsString(matchingType);
    var cache = getOrCreateCache();
    /*
    const componentPosition = Core.State.API.GetPosition(componentId);
    const componentYRotation =
        Core.State.API.GetYRotation(componentId) * Core.Deg2Rad;

    const rawPointsOfComponent = Core.State.API.GetPoints(
        componentId,
        key,
        null,
        typeAsString
    );
    const pointsOfComponent = rawPointsOfComponent.map(pt => {
        pt.ConvertForObject(componentPosition, componentYRotation);
        return pt;
    });
    */
    var pointsOfComponent = cache.getConvertedPoints(componentId, typeAsString);
    var lenPointsOfComponent = pointsOfComponent.length;
    var nearbyRoots = getRootsInAssociatesRange(componentId);
    var result = nearbyRoots.find(function (rootId) {
        /*
        const rootPosition = Core.State.API.GetPosition(rootId);
        const rootYRotation =
            Core.State.API.GetYRotation(rootId) * Core.Deg2Rad;

        const rawPointsOfRoot = Core.State.API.GetPoints(
            rootId,
            null,
            null,
            matchingTypeAsString
        );
        const pointsOfRoot = rawPointsOfRoot.map(pt => {
            pt.ConvertForObject(rootPosition, rootYRotation);
            return pt;
        });
        */
        var pointsOfRoot = cache.getConvertedPoints(rootId, matchingTypeAsString);
        var lenPointsOfRoot = pointsOfRoot.length;
        for (var idxComp = 0; idxComp < lenPointsOfComponent; idxComp++) {
            var firstPoint = pointsOfComponent[idxComp];
            for (var idxRoot = 0; idxRoot < lenPointsOfRoot; idxRoot++) {
                var secondPoint = pointsOfRoot[idxRoot];
                var match = new StrongNeighborMatch(firstPoint, secondPoint, componentId, rootId);
                var isMatching = match.isValid() && match.isPrecise();
                if (isMatching) {
                    // the current nearby rootId is a neighbor of componentId, so we can leave
                    return true;
                }
            }
        }
        return false;
    });
    return result;
};
/**
 * Gets all the neighbors of the selected ids.
 * @param componentIds the componentIds of which we want the neighbors
 * @param recursively If set, we add all neighbors of neighbors (and so on).
 * @returns {string[]} An array containing the neighbors.
 */
export var getNeighbors = function (componentIds, recursively) { return getAssociates(componentIds, recursively, neighbors); };
/**
 * Gets all Neighbors of the component, specified by id. Calculated via the NeighborMatch function of IGX.Point
 * @param componentId The id
 * @returns {Array} An array that contains the ids of the neighbors
 */
export var neighbors = function (componentId) {
    return getAttachPointsAtNeighbors(componentId).map(extractOtherId);
};
export var getAttachPointsAtNeighbors = function (componentId) {
    var tResult = calculateAttachPointsAtNeighbors(componentId);
    //console.log("N", componentId, " has Neighbors ", tResult);
    return tResult;
};
// export const getAttachPointsAtNeighbors: AssociationCalculator = withCache(
// 	"neighbors",
// 	componentId => calculateAttachPointsAtNeighbors(componentId)
// );
var calculateAttachPointsAtNeighbors = function (componentId) {
    var e_5, _a;
    // important to set snapToNeighbors to true, otherwise infinite loop occurs!
    // SnapPoints will call this method to exclude neihgbor points if snapToNeighbors=false
    var snapCalc = new SnapCalculator({ id: componentId }, true);
    var cache = getOrCreateCache();
    var cached = cache.getNeighbors(componentId);
    var nearbyRoots = cached.unknowns;
    var result = [];
    var nonNeighbors = [];
    try {
        for (var nearbyRoots_2 = __values(nearbyRoots), nearbyRoots_2_1 = nearbyRoots_2.next(); !nearbyRoots_2_1.done; nearbyRoots_2_1 = nearbyRoots_2.next()) {
            var rootId = nearbyRoots_2_1.value;
            var matches = snapCalc.getStrongNeighborMatches({ id: rootId });
            var match = matches.find(function (neighborMatch) { return neighborMatch.isPrecise(); });
            if (match) {
                result.push({
                    otherId: rootId,
                    otherPointKey: match.secondKey,
                    ownPointKey: match.firstKey,
                });
            }
            if (!match) {
                nonNeighbors.push(rootId);
            }
        }
    }
    catch (e_5_1) { e_5 = { error: e_5_1 }; }
    finally {
        try {
            if (nearbyRoots_2_1 && !nearbyRoots_2_1.done && (_a = nearbyRoots_2.return)) _a.call(nearbyRoots_2);
        }
        finally { if (e_5) throw e_5.error; }
    }
    cache.setNeighbors(componentId, result);
    cache.setNonNeighbors(componentId, nonNeighbors);
    result.push.apply(result, __spread(cached.knowns));
    return result;
};
var extractOtherId = function (association) {
    return association.otherId;
};
var getRootsInAssociatesRange = function (componentId) {
    var e_6, _a;
    var nearbyMaybeValidComponents = Core.State.API.RegionManager.getNearbyComponents([componentId], SnapMatch.maxDistance, "distance");
    var nearbyValidComponents = [];
    try {
        for (var nearbyMaybeValidComponents_1 = __values(nearbyMaybeValidComponents), nearbyMaybeValidComponents_1_1 = nearbyMaybeValidComponents_1.next(); !nearbyMaybeValidComponents_1_1.done; nearbyMaybeValidComponents_1_1 = nearbyMaybeValidComponents_1.next()) {
            var componentId_1 = nearbyMaybeValidComponents_1_1.value;
            if (isValue(Core.State.API.GetComponent(componentId_1))) {
                nearbyValidComponents.push(componentId_1);
            }
        }
    }
    catch (e_6_1) { e_6 = { error: e_6_1 }; }
    finally {
        try {
            if (nearbyMaybeValidComponents_1_1 && !nearbyMaybeValidComponents_1_1.done && (_a = nearbyMaybeValidComponents_1.return)) _a.call(nearbyMaybeValidComponents_1);
        }
        finally { if (e_6) throw e_6.error; }
    }
    return nearbyValidComponents;
};
var getAssociates = function (componentIds, recursively, associatesFunction) {
    var associates = [];
    if (recursively) {
        associates = getAssociatesRecursively(componentIds, associatesFunction);
    }
    else {
        associates = componentIds
            .map(function (id) { return associatesFunction(id); })
            .reduce(function (a, b) { return a.concat(b); }, []);
    }
    return cleanAssociates(componentIds, associates);
};
var cleanAssociates = function (componentIds, associates) {
    return uniq(associates).filter(function (e) { return componentIds.indexOf(e) === -1; });
};
/**
 * Adds the associates obtained by getAssociates to the storage array.
 * @param componentIds
 * @param getAssociates A function for obtaining associates
 */
var getAssociatesRecursively = function (componentIds, getAssociates) {
    var storage = [];
    var tNotYetTraversed = componentIds.slice(0);
    var tTraversed = componentIds.slice(0);
    var _loop_1 = function () {
        var tAllAssociates = [];
        tNotYetTraversed.forEach(function (id) {
            var tAssociates = getAssociates(id);
            if (tAssociates) {
                tAllAssociates = tAllAssociates.concat(tAssociates);
            }
        });
        tNotYetTraversed = tAllAssociates.filter(function (id) {
            return tTraversed.indexOf(id) === -1;
        });
        if (tNotYetTraversed) {
            tNotYetTraversed.forEach(function (id) {
                tTraversed.push(id);
                storage.push(id);
            });
        }
    };
    while (tNotYetTraversed && tNotYetTraversed.length > 0) {
        _loop_1();
    }
    return storage;
};
