/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelComponents
 */
import { BeEvent } from "@itwin/core-bentley";
import { QueryBinder, QueryRowFormat } from "@itwin/core-common";
import { PerModelCategoryVisibility } from "@itwin/core-frontend";
import { KeySet, NodeKey } from "@itwin/presentation-common";
import { Presentation } from "@itwin/presentation-frontend";
import { UiFramework } from "../../UiFramework";
import { CachingElementIdsContainer } from "./Utils";
/**
 * Visibility tree node types.
 * @beta
 * @deprecated in 3.6. Was moved to `@itwin/tree-widget-react` package.
 */
export var ModelsTreeNodeType;
(function (ModelsTreeNodeType) {
    ModelsTreeNodeType[ModelsTreeNodeType["Unknown"] = 0] = "Unknown";
    ModelsTreeNodeType[ModelsTreeNodeType["Subject"] = 1] = "Subject";
    ModelsTreeNodeType[ModelsTreeNodeType["Model"] = 2] = "Model";
    ModelsTreeNodeType[ModelsTreeNodeType["Category"] = 3] = "Category";
    ModelsTreeNodeType[ModelsTreeNodeType["Element"] = 4] = "Element";
    ModelsTreeNodeType[ModelsTreeNodeType["Grouping"] = 5] = "Grouping";
})(ModelsTreeNodeType || (ModelsTreeNodeType = {}));
/**
 * Visibility handler used by [[ModelsTree]] to control visibility of the tree items.
 * @alpha
 * @deprecated in 3.6. Was moved to `@itwin/tree-widget-react` package.
 */
export class ModelsVisibilityHandler {
    constructor(props) {
        var _a;
        this._listeners = new Array();
        this.onVisibilityChange = new BeEvent();
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this.onViewChanged = (_vp) => {
            this.onVisibilityChangeInternal();
        };
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this.onElementAlwaysDrawnChanged = () => {
            this.onVisibilityChangeInternal();
        };
        // eslint-disable-next-line @typescript-eslint/naming-convention
        this.onElementNeverDrawnChanged = () => {
            this.onVisibilityChangeInternal();
        };
        this._props = props;
        this._subjectModelIdsCache = (_a = props.subjectModelIdsCache) !== null && _a !== void 0 ? _a : new SubjectModelIdsCache(this._props.viewport.iModel);
        this._elementIdsCache = new ElementIdsCache(this._props.viewport.iModel, this._props.rulesetId);
        this._listeners.push(this._props.viewport.onViewedCategoriesPerModelChanged.addListener(this.onViewChanged));
        this._listeners.push(this._props.viewport.onViewedCategoriesChanged.addListener(this.onViewChanged));
        this._listeners.push(this._props.viewport.onViewedModelsChanged.addListener(this.onViewChanged));
        this._listeners.push(this._props.viewport.onAlwaysDrawnChanged.addListener(this.onElementAlwaysDrawnChanged));
        this._listeners.push(this._props.viewport.onNeverDrawnChanged.addListener(this.onElementNeverDrawnChanged));
        if (this._props.hierarchyAutoUpdateEnabled) {
            this._listeners.push(Presentation.presentation.onIModelHierarchyChanged.addListener(/* istanbul ignore next */ () => this._elementIdsCache.clear()));
        }
    }
    dispose() {
        this._listeners.forEach((remove) => remove());
        clearTimeout(this._pendingVisibilityChange);
    }
    /** Sets data provider that is used to get filtered tree hierarchy. */
    setFilteredDataProvider(provider) { this._filteredDataProvider = provider; }
    static getNodeType(item, dataProvider) {
        if (NodeKey.isClassGroupingNodeKey(dataProvider.getNodeKey(item)))
            return ModelsTreeNodeType.Grouping;
        if (!item.extendedData)
            return ModelsTreeNodeType.Unknown;
        if (this.isSubjectNode(item))
            return ModelsTreeNodeType.Subject;
        if (this.isModelNode(item))
            return ModelsTreeNodeType.Model;
        if (this.isCategoryNode(item))
            return ModelsTreeNodeType.Category;
        return ModelsTreeNodeType.Element;
    }
    static isSubjectNode(node) {
        return node.extendedData && node.extendedData.isSubject;
    }
    static isModelNode(node) {
        return node.extendedData && node.extendedData.isModel;
    }
    static isCategoryNode(node) {
        return node.extendedData && node.extendedData.isCategory;
    }
    /** Returns visibility status of the tree node. */
    getVisibilityStatus(node, nodeKey) {
        if (NodeKey.isClassGroupingNodeKey(nodeKey))
            return this.getElementGroupingNodeDisplayStatus(node.id, nodeKey);
        if (!NodeKey.isInstancesNodeKey(nodeKey))
            return { state: "hidden", isDisabled: true };
        if (ModelsVisibilityHandler.isSubjectNode(node)) {
            // note: subject nodes may be merged to represent multiple subject instances
            return this.getSubjectNodeVisibility(nodeKey.instanceKeys.map((key) => key.id), node);
        }
        if (ModelsVisibilityHandler.isModelNode(node))
            return this.getModelDisplayStatus(nodeKey.instanceKeys[0].id);
        if (ModelsVisibilityHandler.isCategoryNode(node))
            return this.getCategoryDisplayStatus(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node));
        return this.getElementDisplayStatus(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node));
    }
    /** Changes visibility of the items represented by the tree node. */
    async changeVisibility(node, nodeKey, on) {
        if (NodeKey.isClassGroupingNodeKey(nodeKey)) {
            await this.changeElementGroupingNodeState(nodeKey, on);
            return;
        }
        if (!NodeKey.isInstancesNodeKey(nodeKey))
            return;
        if (ModelsVisibilityHandler.isSubjectNode(node)) {
            await this.changeSubjectNodeState(nodeKey.instanceKeys.map((key) => key.id), node, on);
        }
        else if (ModelsVisibilityHandler.isModelNode(node)) {
            await this.changeModelState(nodeKey.instanceKeys[0].id, on);
        }
        else if (ModelsVisibilityHandler.isCategoryNode(node)) {
            this.changeCategoryState(nodeKey.instanceKeys[0].id, this.getCategoryParentModelId(node), on);
        }
        else {
            await this.changeElementState(nodeKey.instanceKeys[0].id, this.getElementModelId(node), this.getElementCategoryId(node), on);
        }
    }
    async getSubjectNodeVisibility(ids, node) {
        if (!this._props.viewport.view.isSpatialView())
            return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "subject.nonSpatialView") };
        if (this._filteredDataProvider)
            return this.getFilteredSubjectDisplayStatus(this._filteredDataProvider, ids, node);
        return this.getSubjectDisplayStatus(ids);
    }
    async getSubjectDisplayStatus(ids) {
        const modelIds = await this.getSubjectModelIds(ids);
        const isDisplayed = modelIds.some((modelId) => this.getModelDisplayStatus(modelId).state === "visible");
        if (isDisplayed)
            return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") };
        return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") };
    }
    async getFilteredSubjectDisplayStatus(provider, ids, node) {
        if (provider.nodeMatchesFilter(node))
            return this.getSubjectDisplayStatus(ids);
        const children = await provider.getNodes(node);
        const childrenDisplayStatuses = await Promise.all(children.map((childNode) => this.getVisibilityStatus(childNode, provider.getNodeKey(childNode))));
        if (childrenDisplayStatuses.some((status) => status.state === "visible"))
            return { state: "visible", tooltip: createTooltip("visible", "subject.atLeastOneModelVisible") };
        return { state: "hidden", tooltip: createTooltip("hidden", "subject.allModelsHidden") };
    }
    getModelDisplayStatus(id) {
        if (!this._props.viewport.view.isSpatialView())
            return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "model.nonSpatialView") };
        const isDisplayed = this._props.viewport.view.viewsModel(id);
        return { state: isDisplayed ? "visible" : "hidden", tooltip: createTooltip(isDisplayed ? "visible" : "hidden", undefined) };
    }
    getCategoryDisplayStatus(id, parentModelId) {
        if (parentModelId) {
            if (this.getModelDisplayStatus(parentModelId).state === "hidden")
                return { state: "hidden", isDisabled: true, tooltip: createTooltip("disabled", "category.modelNotDisplayed") };
            const override = this._props.viewport.perModelCategoryVisibility.getOverride(parentModelId, id);
            switch (override) {
                case PerModelCategoryVisibility.Override.Show:
                    return { state: "visible", tooltip: createTooltip("visible", "category.displayedThroughPerModelOverride") };
                case PerModelCategoryVisibility.Override.Hide:
                    return { state: "hidden", tooltip: createTooltip("hidden", "category.hiddenThroughPerModelOverride") };
            }
        }
        const isDisplayed = this._props.viewport.view.viewsCategory(id);
        return {
            state: isDisplayed ? "visible" : "hidden",
            tooltip: isDisplayed
                ? createTooltip("visible", "category.displayedThroughCategorySelector")
                : createTooltip("hidden", "category.hiddenThroughCategorySelector"),
        };
    }
    async getElementGroupingNodeDisplayStatus(_id, key) {
        const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key);
        if (!modelId || !this._props.viewport.view.viewsModel(modelId))
            return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") };
        if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size > 0) {
            let atLeastOneElementForceDisplayed = false;
            for await (const elementId of elementIds.getElementIds()) {
                if (this._props.viewport.alwaysDrawn.has(elementId)) {
                    atLeastOneElementForceDisplayed = true;
                    break;
                }
            }
            if (atLeastOneElementForceDisplayed)
                return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") };
        }
        if (this._props.viewport.alwaysDrawn !== undefined && this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive)
            return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") };
        // istanbul ignore else
        if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.size > 0) {
            let allElementsForceHidden = true;
            for await (const elementId of elementIds.getElementIds()) {
                if (!this._props.viewport.neverDrawn.has(elementId)) {
                    allElementsForceHidden = false;
                    break;
                }
            }
            if (allElementsForceHidden)
                return { state: "hidden", tooltip: createTooltip("visible", "element.hiddenThroughNeverDrawnList") };
        }
        if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible")
            return { state: "visible", tooltip: createTooltip("visible", undefined) };
        return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") };
    }
    getElementDisplayStatus(elementId, modelId, categoryId) {
        if (!modelId || !this._props.viewport.view.viewsModel(modelId))
            return { isDisabled: true, state: "hidden", tooltip: createTooltip("disabled", "element.modelNotDisplayed") };
        if (this._props.viewport.neverDrawn !== undefined && this._props.viewport.neverDrawn.has(elementId))
            return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughNeverDrawnList") };
        if (this._props.viewport.alwaysDrawn !== undefined) {
            if (this._props.viewport.alwaysDrawn.has(elementId))
                return { state: "visible", tooltip: createTooltip("visible", "element.displayedThroughAlwaysDrawnList") };
            if (this._props.viewport.alwaysDrawn.size !== 0 && this._props.viewport.isAlwaysDrawnExclusive)
                return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenDueToOtherElementsExclusivelyAlwaysDrawn") };
        }
        if (categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible")
            return { state: "visible", tooltip: createTooltip("visible", undefined) };
        return { state: "hidden", tooltip: createTooltip("hidden", "element.hiddenThroughCategory") };
    }
    async changeSubjectNodeState(ids, node, on) {
        if (!this._props.viewport.view.isSpatialView())
            return;
        if (this._filteredDataProvider)
            return this.changeFilteredSubjectState(this._filteredDataProvider, ids, node, on);
        return this.changeSubjectState(ids, on);
    }
    async changeFilteredSubjectState(provider, ids, node, on) {
        if (provider.nodeMatchesFilter(node))
            return this.changeSubjectState(ids, on);
        const children = await provider.getNodes(node);
        return Promise.all(children.map(async (childNode) => this.changeVisibility(childNode, provider.getNodeKey(childNode), on)));
    }
    async changeSubjectState(ids, on) {
        const modelIds = await this.getSubjectModelIds(ids);
        return this.changeModelsVisibility(modelIds, on);
    }
    async changeModelState(id, on) {
        if (!this._props.viewport.view.isSpatialView())
            return;
        return this.changeModelsVisibility([id], on);
    }
    async changeModelsVisibility(ids, visible) {
        if (visible)
            return this._props.viewport.addViewedModels(ids);
        else
            this._props.viewport.changeModelDisplay(ids, false);
    }
    changeCategoryState(categoryId, parentModelId, on) {
        if (parentModelId) {
            const isDisplayedInSelector = this._props.viewport.view.viewsCategory(categoryId);
            const ovr = (on === isDisplayedInSelector) ? PerModelCategoryVisibility.Override.None
                : on ? PerModelCategoryVisibility.Override.Show : PerModelCategoryVisibility.Override.Hide;
            this._props.viewport.perModelCategoryVisibility.setOverride(parentModelId, categoryId, ovr);
            if (ovr === PerModelCategoryVisibility.Override.None && on) {
                // we took off the override which means the category is displayed in selector, but
                // doesn't mean all its subcategories are displayed - this call ensures that
                this._props.viewport.changeCategoryDisplay([categoryId], true, true);
            }
            return;
        }
        this._props.viewport.changeCategoryDisplay([categoryId], on, on ? true : false);
    }
    async changeElementGroupingNodeState(key, on) {
        const { modelId, categoryId, elementIds } = await this.getGroupedElementIds(key);
        await this.changeElementsState(modelId, categoryId, elementIds.getElementIds(), on);
    }
    async changeElementState(id, modelId, categoryId, on) {
        const childIdsContainer = this.getAssemblyElementIds(id);
        async function* elementIds() {
            yield id;
            for await (const childId of childIdsContainer.getElementIds())
                yield childId;
        }
        await this.changeElementsState(modelId, categoryId, elementIds(), on);
    }
    async changeElementsState(modelId, categoryId, elementIds, on) {
        const isDisplayedByDefault = modelId && this.getModelDisplayStatus(modelId).state === "visible"
            && categoryId && this.getCategoryDisplayStatus(categoryId, modelId).state === "visible";
        const isHiddenDueToExclusiveAlwaysDrawnElements = this._props.viewport.isAlwaysDrawnExclusive && this._props.viewport.alwaysDrawn && 0 !== this._props.viewport.alwaysDrawn.size;
        const currNeverDrawn = new Set(this._props.viewport.neverDrawn ? this._props.viewport.neverDrawn : []);
        const currAlwaysDrawn = new Set(this._props.viewport.alwaysDrawn ?
            this._props.viewport.alwaysDrawn : /* istanbul ignore next */ []);
        for await (const elementId of elementIds) {
            if (on) {
                currNeverDrawn.delete(elementId);
                if (!isDisplayedByDefault || isHiddenDueToExclusiveAlwaysDrawnElements)
                    currAlwaysDrawn.add(elementId);
            }
            else {
                currAlwaysDrawn.delete(elementId);
                if (isDisplayedByDefault && !isHiddenDueToExclusiveAlwaysDrawnElements)
                    currNeverDrawn.add(elementId);
            }
        }
        this._props.viewport.setNeverDrawn(currNeverDrawn);
        this._props.viewport.setAlwaysDrawn(currAlwaysDrawn, this._props.viewport.isAlwaysDrawnExclusive);
    }
    onVisibilityChangeInternal() {
        if (this._pendingVisibilityChange)
            return;
        this._pendingVisibilityChange = setTimeout(() => {
            this.onVisibilityChange.raiseEvent();
            this._pendingVisibilityChange = undefined;
        }, 0);
    }
    getCategoryParentModelId(categoryNode) {
        return categoryNode.extendedData ? categoryNode.extendedData.modelId : /* istanbul ignore next */ undefined;
    }
    getElementModelId(elementNode) {
        return elementNode.extendedData ? elementNode.extendedData.modelId : /* istanbul ignore next */ undefined;
    }
    getElementCategoryId(elementNode) {
        return elementNode.extendedData ? elementNode.extendedData.categoryId : /* istanbul ignore next */ undefined;
    }
    async getSubjectModelIds(subjectIds) {
        return (await Promise.all(subjectIds.map(async (id) => this._subjectModelIdsCache.getSubjectModelIds(id))))
            .reduce((allModelIds, curr) => [...allModelIds, ...curr], []);
    }
    // istanbul ignore next
    getAssemblyElementIds(assemblyId) {
        return this._elementIdsCache.getAssemblyElementIds(assemblyId);
    }
    // istanbul ignore next
    async getGroupedElementIds(groupingNodeKey) {
        return this._elementIdsCache.getGroupedElementIds(groupingNodeKey);
    }
}
/** @internal */
export class SubjectModelIdsCache {
    constructor(imodel) {
        this._imodel = imodel;
    }
    async initSubjectModels() {
        var _a;
        const querySubjects = () => {
            const subjectsQuery = `
        SELECT ECInstanceId id, Parent.Id parentId, json_extract(JsonProperties, '$.Subject.Model.TargetPartition') targetPartitionId
        FROM bis.Subject
      `;
            return this._imodel.query(subjectsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames });
        };
        const queryModels = () => {
            const modelsQuery = `
        SELECT p.ECInstanceId id, p.Parent.Id parentId
        FROM bis.InformationPartitionElement p
        INNER JOIN bis.GeometricModel3d m ON m.ModeledElement.Id = p.ECInstanceId
        WHERE NOT m.IsPrivate
      `;
            return this._imodel.query(modelsQuery, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames });
        };
        function pushToMap(map, key, value) {
            let list = map.get(key);
            if (!list) {
                list = [];
                map.set(key, list);
            }
            list.push(value);
        }
        this._subjectsHierarchy = new Map();
        const targetPartitionSubjects = new Map();
        for await (const subject of querySubjects()) {
            // istanbul ignore else
            if (subject.parentId)
                pushToMap(this._subjectsHierarchy, subject.parentId, subject.id);
            // istanbul ignore if
            if (subject.targetPartitionId)
                pushToMap(targetPartitionSubjects, subject.targetPartitionId, subject.id);
        }
        this._subjectModels = new Map();
        for await (const model of queryModels()) {
            // istanbul ignore next
            const subjectIds = (_a = targetPartitionSubjects.get(model.id)) !== null && _a !== void 0 ? _a : [];
            // istanbul ignore else
            if (!subjectIds.includes(model.parentId))
                subjectIds.push(model.parentId);
            subjectIds.forEach((subjectId) => {
                pushToMap(this._subjectModels, subjectId, model.id);
            });
        }
    }
    async initCache() {
        if (!this._init) {
            this._init = this.initSubjectModels().then(() => { });
        }
        return this._init;
    }
    appendSubjectModelsRecursively(modelIds, subjectId) {
        const subjectModelIds = this._subjectModels.get(subjectId);
        if (subjectModelIds)
            modelIds.push(...subjectModelIds);
        const childSubjectIds = this._subjectsHierarchy.get(subjectId);
        if (childSubjectIds)
            childSubjectIds.forEach((cs) => this.appendSubjectModelsRecursively(modelIds, cs));
    }
    async getSubjectModelIds(subjectId) {
        await this.initCache();
        const modelIds = new Array();
        this.appendSubjectModelsRecursively(modelIds, subjectId);
        return modelIds;
    }
}
// istanbul ignore next
class ElementIdsCache {
    constructor(_imodel, _rulesetId) {
        this._imodel = _imodel;
        this._rulesetId = _rulesetId;
        this._assemblyElementIdsCache = new Map();
        this._groupedElementIdsCache = new Map();
    }
    clear() {
        this._assemblyElementIdsCache.clear();
        this._groupedElementIdsCache.clear();
    }
    getAssemblyElementIds(assemblyId) {
        const ids = this._assemblyElementIdsCache.get(assemblyId);
        if (ids)
            return ids;
        const container = createAssemblyElementIdsContainer(this._imodel, this._rulesetId, assemblyId);
        this._assemblyElementIdsCache.set(assemblyId, container);
        return container;
    }
    async getGroupedElementIds(groupingNodeKey) {
        const keyString = JSON.stringify(groupingNodeKey);
        const ids = this._groupedElementIdsCache.get(keyString);
        if (ids)
            return ids;
        const info = await createGroupedElementsInfo(this._imodel, this._rulesetId, groupingNodeKey);
        this._groupedElementIdsCache.set(keyString, info);
        return info;
    }
}
// istanbul ignore next
async function* createInstanceIdsGenerator(imodel, rulesetId, displayType, inputKeys) {
    const res = await Presentation.presentation.getContentInstanceKeys({
        imodel,
        rulesetOrId: rulesetId,
        displayType,
        keys: new KeySet(inputKeys),
    });
    for await (const key of res.items()) {
        yield key.id;
    }
}
// istanbul ignore next
function createAssemblyElementIdsContainer(imodel, rulesetId, assemblyId) {
    return new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [{ className: "BisCore:Element", id: assemblyId }]));
}
// istanbul ignore next
async function createGroupedElementsInfo(imodel, rulesetId, groupingNodeKey) {
    const groupedElementIdsContainer = new CachingElementIdsContainer(createInstanceIdsGenerator(imodel, rulesetId, "AssemblyElementsRequest", [groupingNodeKey]));
    const elementId = await groupedElementIdsContainer.getElementIds().next();
    if (elementId.done)
        throw new Error("Invalid grouping node key");
    let modelId, categoryId;
    const query = `SELECT Model.Id AS modelId, Category.Id AS categoryId FROM bis.GeometricElement3d WHERE ECInstanceId = ? LIMIT 1`;
    for await (const modelAndCategoryIds of imodel.query(query, QueryBinder.from([elementId.value]), { rowFormat: QueryRowFormat.UseJsPropertyNames })) {
        modelId = modelAndCategoryIds.modelId;
        categoryId = modelAndCategoryIds.categoryId;
        break;
    }
    return { modelId, categoryId, elementIds: groupedElementIdsContainer };
}
const createTooltip = (status, tooltipStringId) => {
    const statusStringId = `UiFramework:modelTree.status.${status}`;
    const statusString = UiFramework.localization.getLocalizedString(statusStringId);
    if (!tooltipStringId)
        return statusString;
    tooltipStringId = `UiFramework:modelTree.tooltips.${tooltipStringId}`;
    const tooltipString = UiFramework.localization.getLocalizedString(tooltipStringId);
    return `${statusString}: ${tooltipString}`;
};
//# sourceMappingURL=ModelsVisibilityHandler.js.map