/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Elements
 */
import { assert, DbResult, Logger, LogLevel } from "@itwin/core-bentley";
import { IModel } from "@itwin/core-common";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { DefinitionContainer, DefinitionElement, DefinitionPartition, Element, Subject } from "./Element";
import { IModelDb } from "./IModelDb";
import { DefinitionModel, Model } from "./Model";
const loggerCategory = `${BackendLoggerCategory.IModelDb}.ElementTreeWalker`;
function sortChildrenBeforeParents(iModel, ids) {
    const children = [];
    const parents = [];
    for (const eid of ids) {
        const parentId = iModel.elements.queryParent(eid);
        if (parentId !== undefined && ids.includes(parentId))
            children.push(eid);
        else
            parents.push(eid);
    }
    if (children.length === 0)
        return [parents];
    return [...sortChildrenBeforeParents(iModel, children), parents];
}
function isModelEmpty(iModel, modelId) {
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    return iModel.withPreparedStatement(`select count(*) from ${Element.classFullName} where Model.Id = ?`, (stmt) => {
        stmt.bindId(1, modelId);
        stmt.step();
        return stmt.getValue(0).getInteger() === 0;
    });
}
function isDefinitionModel(model) {
    return (model.id !== IModel.repositoryModelId) && (model instanceof DefinitionModel);
}
var ElementPruningClassification;
(function (ElementPruningClassification) {
    ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Normal"] = 0] = "PRUNING_CLASS_Normal";
    ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Subject"] = 1] = "PRUNING_CLASS_Subject";
    ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Definition"] = 2] = "PRUNING_CLASS_Definition";
    ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_DefinitionPartition"] = 3] = "PRUNING_CLASS_DefinitionPartition";
})(ElementPruningClassification || (ElementPruningClassification = {}));
function classifyElementForPruning(iModel, elementId) {
    const el = iModel.elements.getElement(elementId);
    // DefinitionContainer is submodeled by a DefinitionModel and so it must be classified as PRUNING_CLASS_DefinitionPartition for tree-walking purposes.
    // Since DefinitionContainer is-a DefinitionElement the (el instanceof DefinitionElement) case below would classify it as PRUNING_CLASS_Definition.
    // That is why we special-case it here.
    if (el instanceof DefinitionContainer)
        return ElementPruningClassification.PRUNING_CLASS_DefinitionPartition;
    return (el instanceof Subject) ? ElementPruningClassification.PRUNING_CLASS_Subject :
        (el instanceof DefinitionElement) ? ElementPruningClassification.PRUNING_CLASS_Definition :
            (el instanceof DefinitionPartition) ? ElementPruningClassification.PRUNING_CLASS_DefinitionPartition :
                ElementPruningClassification.PRUNING_CLASS_Normal;
}
/** Records the path that a tree search took to reach an element or model. This object is immutable.
 * @beta
 */
export class ElementTreeWalkerScope {
    topElement = "";
    /** path of parent elements and enclosing models */
    path = [];
    /** cached info about the immediately enclosing model (i.e., the last model in path) */
    enclosingModelInfo;
    constructor(arg1, arg2) {
        if (typeof arg1 === "string") {
            // normal constructor
            assert(arg2 instanceof Model);
            this.topElement = arg1;
            this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
        }
        else if (arg1 instanceof ElementTreeWalkerScope) {
            // copy-like constructor
            this.topElement = arg1.topElement;
            this.path = [...arg1.path];
            if (typeof arg2 === "string") {
                // with new parent
                this.path.push(arg2);
                this.enclosingModelInfo = arg1.enclosingModelInfo;
            }
            else {
                // with new enclosing model
                this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
            }
        }
        else {
            throw new Error("invalid constructor signature");
        }
    }
    get enclosingModel() { return this.enclosingModelInfo.model; }
    get inDefinitionModel() { return this.enclosingModelInfo.isDefinitionModel; } // NB: this will return false for the RepositoryModel!
    get inRepositoryModel() { return this.enclosingModelInfo.model.id === IModelDb.repositoryModelId; }
    static createTopScope(iModel, topElementId) {
        const topElement = iModel.elements.getElement(topElementId);
        const topElementModel = iModel.models.getModel(topElement.model);
        return new ElementTreeWalkerScope(topElementId, topElementModel);
    }
    fmtItem(v) {
        if (typeof v === "string")
            return `element ${v}`;
        return `model ${v.model.id} ${v.isDefinitionModel ? "(DEFN)" : ""}`;
    }
    toString() {
        return `[ ${this.path.map((v) => this.fmtItem(v)).join(" / ")} ]`;
    }
}
function fmtElement(iModel, elementId) {
    const el = iModel.elements.getElement(elementId);
    return `${el.id} ${el.classFullName} ${el.getDisplayLabel()}`;
}
function fmtModel(model) {
    return `${model.id} ${model.classFullName} ${model.name}`;
}
let isTraceEnabledChecked = -1;
function isTraceEnabled() {
    if (isTraceEnabledChecked === -1)
        isTraceEnabledChecked = Logger.isEnabled(loggerCategory, LogLevel.Trace) ? 1 : 0;
    return isTraceEnabledChecked === 1;
}
function logElement(op, iModel, elementId, scope, logChildren) {
    if (!isTraceEnabled())
        return;
    Logger.logTrace(loggerCategory, `${op} ${fmtElement(iModel, elementId)} ${scope ? scope.toString() : ""}`);
    if (logChildren)
        iModel.elements.queryChildren(elementId).forEach((c) => logElement(" - ", iModel, c, undefined, true));
}
function logModel(op, iModel, modelId, scope, logElements) {
    if (!isTraceEnabled())
        return;
    const model = iModel.models.getModel(modelId);
    Logger.logTrace(loggerCategory, `${op} ${fmtModel(model)} ${scope ? scope.toString() : ""}`);
    if (logElements) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        iModel.withPreparedStatement(`select ecinstanceid from ${Element.classFullName} where Model.Id = ?`, (stmt) => {
            stmt.bindId(1, modelId);
            while (stmt.step() === DbResult.BE_SQLITE_ROW) {
                logElement(" - ", iModel, stmt.getValue(0).getId());
            }
        });
    }
}
/** Does a depth-first search on the tree defined by an element and its sub-models and children.
 * Sub-models are visited before their modeled elements, and children are visited before their parents.
 *
 * The following callbacks allow the subclass to exclude elements and sub-trees from the search:
 *  * [[ElementTreeBottomUp.shouldExploreModel]], [[ElementTreeBottomUp.shouldExploreChildren]]
 *  * [[ElementTreeBottomUp.shouldVisitElement]], [[ElementTreeBottomUp.shouldVisitModel]]
 *
 * The [[ElementTreeBottomUp.visitElement]] and [[ElementTreeBottomUp.visitModel]] callbacks allow
 * the subclass to process the elements and models that are encountered in the search.
 * @beta
 */
export class ElementTreeBottomUp {
    _iModel;
    constructor(_iModel) {
        this._iModel = _iModel;
    }
    /** Return true if the search should recurse into this model  */
    shouldExploreModel(_model, _scope) { return true; }
    /** Return true if the search should recurse into the children (if any) of this element  */
    shouldExploreChildren(_parentId, _scope) { return true; }
    /** Return true if the search should visit this element  */
    shouldVisitElement(_elementId, _scope) { return true; }
    /** Return true if the search should visit this model  */
    shouldVisitModel(_model, _scope) { return true; }
    /** The main tree-walking function */
    processElementTree(element, scope) {
        const subModel = this._iModel.models.tryGetModel(element);
        if (subModel !== undefined) {
            if (this.shouldExploreModel(subModel, scope))
                this._processSubModel(subModel, scope);
            if (this.shouldVisitModel(subModel, scope))
                this.visitModel(subModel, scope);
        }
        if (this.shouldExploreChildren(element, scope))
            this._processChildren(element, scope);
        if (this.shouldVisitElement(element, scope))
            this.visitElement(element, scope);
    }
    /** process the children of the specified parent element */
    _processChildren(parentElement, parentScope) {
        const children = this._iModel.elements.queryChildren(parentElement);
        if (children.length === 0)
            return;
        const childrenScope = new ElementTreeWalkerScope(parentScope, parentElement);
        for (const childElement of children)
            this.processElementTree(childElement, childrenScope);
    }
    /** process the elements in the specified model */
    _processSubModel(model, parenScope) {
        const scope = new ElementTreeWalkerScope(parenScope, model);
        // Visit only the top-level parents. processElementTree will visit their children (bottom-up).
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        model.iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => {
            stmt.bindId(1, model.id);
            while (stmt.step() === DbResult.BE_SQLITE_ROW) {
                const elementId = stmt.getValue(0).getId();
                this.processElementTree(elementId, scope);
            }
        });
    }
}
/** Helper class that manages the deletion of definitions and subjects */
class SpecialElements {
    definitionModels = [];
    definitions = [];
    subjects = [];
    recordSpecialElement(iModel, elementId) {
        // Defer Definitions and Subjects
        const cls = classifyElementForPruning(iModel, elementId);
        if (cls === ElementPruningClassification.PRUNING_CLASS_Subject) {
            this.subjects.push(elementId);
            return true;
        }
        else if (cls === ElementPruningClassification.PRUNING_CLASS_Definition) {
            this.definitions.push(elementId);
            return true;
        }
        else if (cls === ElementPruningClassification.PRUNING_CLASS_DefinitionPartition) {
            this.definitionModels.push(elementId);
            return true;
        }
        return false; // not a special element
    }
    /** Delete special elements - This calls Elements.deleteDefinitionElements to process the collected definition elements as a group and then
     * calls Models.deleteModel on collected definition models. It then deletes all collected Subjects by calling Element.deleteElement on them.
     * @note Caller must ensure that the special elements were recorded in a depth-first search.
     */
    deleteSpecialElements(iModel) {
        // It's dangerous to pass a mixture of SubCategories and Categories to deleteDefinitionElements.
        // That function will delete the Categories first, which automatically deletes all their child
        // SubCategories (in native code). If a SubCategory in the list is one of those children, then
        // deleteDefinitionElements will try and fail with an exception to delete that SubCategory in a subsequent step.
        // To work around this, we delete the SubCategories first, then everything else.
        // A similar problem occurs when you pass other kinds of elements to deleteDefinitionElements, where some are
        // children and others are parents. deleteDefinitionElements does not preserve the order that you specify,
        // and it does not process children before parents.
        for (const definitions of sortChildrenBeforeParents(iModel, this.definitions)) {
            if (isTraceEnabled())
                definitions.forEach((e) => logElement("try delete", iModel, e));
            iModel.elements.deleteDefinitionElements(definitions); // will not delete definitions that are still in use.
        }
        for (const m of this.definitionModels) {
            if (!isModelEmpty(iModel, m)) {
                logModel("Model not empty - cannot delete - may contain Definitions that are still in use", iModel, m, undefined, true);
            }
            else {
                logModel("delete", iModel, m);
                iModel.models.deleteModel(m);
                iModel.elements.deleteElement(m);
            }
        }
        for (const e of this.subjects) {
            if (iModel.elements.queryChildren(e).length !== 0) {
                logElement("Subject still has children - cannot delete - may have child DefinitionModels", iModel, e, undefined, true);
            }
            else {
                logElement("delete", iModel, e);
                iModel.elements.deleteElement(e);
            }
        }
    }
}
/** Deletes an entire element tree, including sub-models and child elements.
 * Items are deleted in bottom-up order. Definitions and Subjects are deleted after normal elements.
 * Call deleteNormalElements on each tree. Then call deleteSpecialElements.
 * @see deleteElementTree for a simple way to use this class.
 * @beta
 */
export class ElementTreeDeleter extends ElementTreeBottomUp {
    _special = new SpecialElements();
    shouldExploreModel(_model) { return true; }
    shouldVisitElement(_elementId) { return true; }
    visitModel(model, _scope) {
        if (isDefinitionModel(model))
            return; // we recorded definition models in visitElement when we encountered the DefinitionPartition elements.
        // visitElement has already deleted the elements in the model. So, now it's safe to delete the model itself.
        logModel("delete", this._iModel, model.id, _scope);
        model.delete();
    }
    visitElement(elementId, _scope) {
        if (!this._special.recordSpecialElement(this._iModel, elementId)) {
            logElement("delete", this._iModel, elementId, _scope);
            this._iModel.elements.deleteElement(elementId);
        }
    }
    /**
     * Delete the "normal" elements and record the special elements for deferred processing.
     * @param topElement The parent of the sub-tree to be deleted. Top element itself is also deleted.
     * @param scope How the parent was found
     * @see deleteSpecialElements
     */
    deleteNormalElements(topElement, scope) {
        const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement);
        this.processElementTree(topElement, topScope); //
    }
    /** Delete all special elements that were found and deferred by deleteNormalElements. Call this
     * function once after all element trees are processed by deleteNormalElements.
     */
    deleteSpecialElements() {
        this._special.deleteSpecialElements(this._iModel);
    }
}
/** Does a breadth-first search on the tree defined by an element and its sub-models and children.
 * Parents are visited first, then children, then sub-models.
 * The subclass can "prune" sub-trees from the search. When a sub-tree is "pruned" the search does *not* recurse into it.
 * If a sub-tree is not pruned, then the search does recurse into it.
 * @beta
 */
class ElementTreeTopDown {
    _iModel;
    constructor(_iModel) {
        this._iModel = _iModel;
    }
    /** Should the search *not* recurse into this sub-tree? */
    shouldPrune(_elementId, _scope) { return false; }
    processElementTree(element, scope) {
        if (this.shouldPrune(element, scope)) {
            this.prune(element, scope);
            return;
        }
        this._processChildren(element, scope);
        const subModel = this._iModel.models.tryGetModel(element);
        if (subModel !== undefined) {
            this._processSubModel(subModel, scope);
        }
    }
    _processChildren(element, scope) {
        let parentScope;
        for (const childElement of this._iModel.elements.queryChildren(element)) {
            if (parentScope === undefined)
                parentScope = new ElementTreeWalkerScope(scope, element);
            this.processElementTree(childElement, parentScope);
        }
    }
    _processSubModel(subModel, scope) {
        const subModelScope = new ElementTreeWalkerScope(scope, subModel);
        // Visit only the top-level parents. processElementTree will recurse into their children.
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        this._iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => {
            stmt.bindId(1, subModel.id);
            while (stmt.step() === DbResult.BE_SQLITE_ROW) {
                const elementId = stmt.getValue(0).getId();
                this.processElementTree(elementId, subModelScope);
            }
        });
    }
}
/** Performs a breadth-first search to visit elements in top-down order.
 * When the supplied filter function chooses an element, ElementTreeDeleter is used to delete it and its sub-tree.
 * @beta
 */
export class ElementSubTreeDeleter extends ElementTreeTopDown {
    _treeDeleter;
    _shouldPruneCb;
    /** Construct an ElementSubTreeDeleter.
     * @param iModel The iModel
     * @param topElement Where to start the search.
     * @param shouldPruneCb Callback that selects sub-trees that should be deleted.
     * @see deleteElementSubTrees for a simple way to use this class.
     */
    constructor(iModel, shouldPruneCb) {
        super(iModel);
        this._treeDeleter = new ElementTreeDeleter(this._iModel);
        this._shouldPruneCb = shouldPruneCb;
    }
    shouldPrune(elementId, scope) { return this._shouldPruneCb(elementId, scope); }
    prune(elementId, scope) {
        this._treeDeleter.deleteNormalElements(elementId, scope);
    }
    /** Traverses the tree of elements beginning with the top element, and deletes all selected sub-trees.
     * Normal elements are deleted. Any special elements that are encountered are deferred.
     * Call deleteSpecialElementSubTrees after all top elements have been processed. */
    deleteNormalElementSubTrees(topElement, scope) {
        const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement);
        this.processElementTree(topElement, topScope); // deletes normal elements and their sub-trees, defers special elements
    }
    /** Delete all special elements and their sub-trees that were found in the course of processing.
     * The sub-trees were already expanded by ElementTreeDeleter.deleteNormalElements.
     */
    deleteSpecialElementSubTrees() {
        this._treeDeleter.deleteSpecialElements();
    }
}
/** @internal */
export function deleteElementTree(arg0, arg1) {
    let maxPasses;
    let iModel;
    let topElement;
    if (arg0 instanceof IModelDb) {
        assert(typeof arg1 === "string");
        iModel = arg0;
        topElement = arg1;
    }
    else {
        iModel = arg0.iModel;
        topElement = arg0.topElement;
        maxPasses = arg0.maxPasses;
    }
    maxPasses = maxPasses ?? 5;
    let pass = 0;
    do {
        const del = new ElementTreeDeleter(iModel);
        del.deleteNormalElements(topElement);
        del.deleteSpecialElements();
    } while ((iModel.elements.tryGetElement(topElement) !== undefined) && (++pass < maxPasses));
}
/** Deletes all element sub-trees that are selected by the supplied filter. Uses ElementSubTreeDeleter.
 * If the filter selects the top element itself, then the entire tree (including the top element) is deleted.
 * That has the same effect as calling [[deleteElementTree]] on the top element.
 * @note The caller may have to call this function multiple times if there are multiple layers of dependencies among definition elements.
 * @param iModel The iModel
 * @param topElement Where to start the search.
 * @param filter Callback that selects sub-trees that should be deleted.
 * @beta
 */
export function deleteElementSubTrees(iModel, topElement, filter) {
    const del = new ElementSubTreeDeleter(iModel, filter);
    del.deleteNormalElementSubTrees(topElement);
    del.deleteSpecialElementSubTrees();
}
//# sourceMappingURL=ElementTreeWalker.js.map