class CategoryTree {
    rootCategories;
    rootNodes;
    categoryMap;
    constructor(categories, nodes){
        this.categoryMap = new Map();
        const { rootCategories, rootNodes } = this.buildTree(categories, nodes);
        this.rootCategories = rootCategories;
        this.rootNodes = rootNodes;
    }
    static buildMap(tree) {
        const newCategoryMap = new Map();
        const addCategoriesToMap = (categories)=>{
            for (const tNode of categories){
                newCategoryMap.set(tNode.id, tNode);
                addCategoriesToMap(tNode.nestedCategories);
            }
        };
        addCategoriesToMap(tree);
        return newCategoryMap;
    }
    static fromPrebuilt(rootCategories, rootNodes) {
        const tree = Object.create(CategoryTree.prototype);
        tree.rootCategories = rootCategories;
        tree.rootNodes = rootNodes;
        tree.categoryMap = CategoryTree.buildMap(rootCategories);
        return tree;
    }
    buildTree(categories, nodes) {
        const categoryMap = new Map();
        for (const category of categories)categoryMap.set(category.id, {
            ...category,
            nestedCategories: [],
            nodes: []
        });
        const rootNodes = [];
        for (const node of nodes)if (node.category) {
            const category = categoryMap.get(node.category);
            if (category) category.nodes.push(node);
            else rootNodes.push(node);
        } else rootNodes.push(node);
        for (const category of categoryMap.values())category.nodes.sort((a, b)=>a.sortOrder - b.sortOrder);
        const rootCategories = [];
        for (const category of categoryMap.values()){
            const parentId = category.parentId;
            if (parentId) {
                const parent = categoryMap.get(parentId);
                if (parent) parent.nestedCategories.push(category);
                else rootCategories.push(category);
            } else rootCategories.push(category);
        }
        const sortChildren = (node)=>{
            node.nestedCategories.sort((a, b)=>a.sortOrder - b.sortOrder);
            for (const child of node.nestedCategories)sortChildren(child);
        };
        rootCategories.sort((a, b)=>a.sortOrder - b.sortOrder);
        for (const root of rootCategories)sortChildren(root);
        rootNodes.sort((a, b)=>a.sortOrder - b.sortOrder);
        this.categoryMap = categoryMap;
        return {
            rootCategories,
            rootNodes
        };
    }
    getRootCategories() {
        return this.rootCategories;
    }
    getRootNodes() {
        return this.rootNodes;
    }
    findCategory(categoryId) {
        return this.categoryMap.get(categoryId) ?? null;
    }
    getSubtree(categoryId) {
        const category = this.findCategory(categoryId);
        if (!category) return CategoryTree.fromPrebuilt([], []);
        return CategoryTree.fromPrebuilt([
            ...category.nestedCategories
        ], [
            ...category.nodes
        ]);
    }
    filter(options = {}) {
        const { categoryFilter = ()=>true, nodeFilter = ()=>true } = options;
        const filterTree = (tree)=>{
            const filtered = [];
            for (const item of tree){
                if (!categoryFilter(item)) continue;
                const nestedCategories = filterTree(item.nestedCategories);
                const nodes = item.nodes.filter(nodeFilter);
                if (nestedCategories.length > 0 || nodes.length > 0) filtered.push({
                    ...item,
                    nestedCategories,
                    nodes
                });
            }
            return filtered;
        };
        const filteredRoots = filterTree(this.rootCategories);
        const filteredRootNodes = this.rootNodes.filter(nodeFilter);
        return CategoryTree.fromPrebuilt(filteredRoots, filteredRootNodes);
    }
    filterBySearch(searchTerm) {
        if (!searchTerm) return this;
        const searchWords = searchTerm.toLowerCase().split(/\s+/).filter(Boolean);
        if (0 === searchWords.length) return this;
        const matchesAllWords = (node, ancestorCategoryNames)=>{
            const searchableTexts = [
                node.display.label.toLowerCase(),
                ...node.display.canvasLabel ? [
                    node.display.canvasLabel.toLowerCase()
                ] : [],
                node.nodeType.toLowerCase(),
                ...node.description ? [
                    node.description.toLowerCase()
                ] : [],
                ...node.tags?.filter((tag)=>'string' == typeof tag).map((tag)=>tag.toLowerCase()) ?? [],
                ...ancestorCategoryNames
            ];
            return searchWords.every((word)=>searchableTexts.some((text)=>text.includes(word)));
        };
        const filterTree = (tree, ancestorNames)=>{
            const filtered = [];
            for (const item of tree){
                const categoryNames = [
                    ...ancestorNames,
                    item.name.toLowerCase()
                ];
                const nestedCategories = filterTree(item.nestedCategories, categoryNames);
                const nodes = item.nodes.filter((node)=>matchesAllWords(node, categoryNames));
                if (nestedCategories.length > 0 || nodes.length > 0) filtered.push({
                    ...item,
                    nestedCategories,
                    nodes
                });
            }
            return filtered;
        };
        const filteredRoots = filterTree(this.rootCategories, []);
        const filteredRootNodes = this.rootNodes.filter((node)=>matchesAllWords(node, []));
        return CategoryTree.fromPrebuilt(filteredRoots, filteredRootNodes);
    }
    filterByConnections(validator) {
        return this.filter({
            categoryFilter: (category)=>validator.isValidCategoryForConnection(category.id),
            nodeFilter: (node)=>validator.canNodeConnect(node.nodeType)
        });
    }
    flattenSinglePath() {
        if (1 !== this.rootCategories.length || this.rootNodes.length > 0) return CategoryTree.fromPrebuilt([
            ...this.rootCategories
        ], [
            ...this.rootNodes
        ]);
        let current = this.rootCategories[0];
        while(current)if (1 === current.nestedCategories.length && 0 === current.nodes.length) current = current.nestedCategories[0];
        else break;
        return current ? CategoryTree.fromPrebuilt([
            ...current.nestedCategories
        ], [
            ...current.nodes
        ]) : CategoryTree.fromPrebuilt([], []);
    }
    flatten() {
        const collectNodes = (categories)=>{
            let nodes = [];
            for (const category of categories){
                nodes = nodes.concat(category.nodes);
                nodes = nodes.concat(collectNodes(category.nestedCategories));
            }
            return nodes;
        };
        const allNodes = collectNodes(this.rootCategories).concat(this.rootNodes);
        allNodes.sort((a, b)=>a.sortOrder - b.sortOrder);
        return CategoryTree.fromPrebuilt([], allNodes);
    }
    isEmpty() {
        return 0 === this.rootCategories.length && 0 === this.rootNodes.length;
    }
    getAllCategories() {
        return Array.from(this.categoryMap.values());
    }
    getAllNodes() {
        const nodes = [
            ...this.rootNodes
        ];
        for (const category of this.categoryMap.values())nodes.push(...category.nodes);
        return nodes;
    }
    getCategoryCount() {
        return this.categoryMap.size;
    }
    getNodeCount() {
        let count = this.rootNodes.length;
        for (const category of this.categoryMap.values())count += category.nodes.length;
        return count;
    }
}
export { CategoryTree };
