/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Tiles
 */
import { assert, ByteStream, compareStrings, DbOpcode, Id64, partitionArray, SortedArray, } from "@itwin/core-bentley";
import { Range3d } from "@itwin/core-geometry";
import { TileFormat, } from "@itwin/core-common";
import { IModelApp } from "../../IModelApp";
import { ImdlReader, IModelTileTree, Tile, } from "../../tile/internal";
/** The root tile for the branch of an [[IModelTileTree]] containing graphics for elements that have been modified during the current
 * Not intended for direct consumption - exported for use by [[IModelTileTree]].
 * [[GraphicalEditingScope]].
 */
export class DynamicIModelTile extends Tile {
    constructor(params, tree) {
        super(params, tree);
    }
    static create(root, elements) {
        return new RootTile(root, elements);
    }
}
/** The child tiles of the root tile, representing inserted or modified elements and sorted by element Id. */
class ElementTiles extends SortedArray {
    constructor() {
        super((lhs, rhs) => compareStrings(lhs.contentId, rhs.contentId));
    }
    get array() {
        return this._array;
    }
}
/** The root tile. Each of its children represent a newly-inserted or modified element. */
class RootTile extends DynamicIModelTile {
    _hiddenElements;
    transformToTree;
    _elements;
    get _imodelRoot() { return this.parent; }
    get _elementChildren() {
        assert(undefined !== this.children);
        assert(this.children === this._elements.array);
        return this._elements.array;
    }
    constructor(parent, elements) {
        const params = {
            parent,
            isLeaf: false,
            contentId: "dynamic",
            range: Range3d.createNull(),
            maximumSize: parent.tileScreenSize,
        };
        super(params, parent.tree);
        this._hiddenElements = new Id64.Uint32Set();
        const inverseTransform = parent.tree.iModelTransform.inverse();
        assert(undefined !== inverseTransform);
        this.transformToTree = inverseTransform;
        this._elements = new ElementTiles();
        this.loadChildren(); // initially empty.
        assert(undefined !== this.children);
        this.setIsReady();
        this.handleGeometryChanges(elements);
    }
    get hiddenElements() {
        return this._hiddenElements.toId64Array();
    }
    get dynamicElements() {
        return this._elements.array.map((tile) => tile.contentId);
    }
    get appearanceProvider() {
        return this;
    }
    getFeatureAppearance(source, elemLo, elemHi, subcatLo, subcatHi, geomClass, modelLo, modelHi, type, animationNodeId) {
        if (this._hiddenElements.has(elemLo, elemHi))
            return undefined;
        return source.getAppearance(elemLo, elemHi, subcatLo, subcatHi, geomClass, modelLo, modelHi, type, animationNodeId);
    }
    handleGeometryChanges(changes) {
        assert(undefined !== this.children);
        for (const change of changes) {
            if (change.type !== DbOpcode.Insert)
                this._hiddenElements.addId(change.id);
            let tile = this._elements.findEquivalent((t) => compareStrings(t.contentId, change.id));
            if (change.type === DbOpcode.Delete) {
                if (tile) {
                    tile[Symbol.dispose]();
                    this._elements.remove(tile);
                }
            }
            else {
                const range = change.range.isNull ? change.range.clone() : this.transformToTree.multiplyRange(change.range);
                if (tile)
                    tile.update(range);
                else
                    this._elements.insert(tile = new ElementTile(this, change.id, range));
            }
        }
        // Recompute range.
        this.range.setNull();
        for (const element of this._elements)
            this.range.extendRange(element.range);
        this._imodelRoot.updateDynamicRange(this);
    }
    _loadChildren(resolve, _reject) {
        // This is invoked from constructor. We will add a child per element later - for now just mark the children as having been loaded.
        resolve(this._elements.array);
    }
    get channel() {
        throw new Error("Root dynamic tile has no content");
    }
    async requestContent() {
        assert(false, "Root dynamic tile has no content");
        return undefined;
    }
    async readContent(_data, _system, _isCanceled) {
        throw new Error("Root dynamic tile has no content");
    }
    selectTiles(selected, args) {
        for (const child of this._elementChildren)
            child.selectTiles(selected, args);
    }
    pruneChildren(olderThan) {
        // Never discard ElementTiles - do discard not-recently-used graphics.
        for (const child of this._elementChildren)
            child.pruneChildren(olderThan);
    }
}
/** Represents a single element that has been inserted or had its geometric properties modified during the current [[GraphicalEditingScope]].
 * It has no graphics of its own; it has any number of child tiles, each of which have graphics of a different level of detail.
 * Its contentId is the element's Id.
 */
class ElementTile extends Tile {
    constructor(parent, elementId, range) {
        super({
            parent,
            isLeaf: false,
            contentId: elementId,
            range,
            maximumSize: parent.maximumSize,
        }, parent.tree);
        this.loadChildren();
        this.setIsReady();
    }
    _loadChildren(resolve, _reject) {
        // Invoked from constructor. We'll add child tiles later as needed.
        resolve([]);
    }
    get channel() {
        throw new Error("ElementTile has no content");
    }
    async requestContent(_isCanceled) {
        assert(false, "ElementTile has no content");
        return undefined;
    }
    async readContent(_data, _system, _isCanceled) {
        throw new Error("ElementTile has no content");
    }
    pruneChildren(olderThan) {
        const children = this.children;
        assert(undefined !== children);
        const partitionIndex = partitionArray(children, (child) => !child.usageMarker.isExpired(olderThan));
        // Remove expired children.
        if (partitionIndex < children.length) {
            const expired = children.splice(partitionIndex);
            for (const child of expired)
                child[Symbol.dispose]();
        }
        // Restore ordering.
        children.sort((x, y) => y.toleranceLog10 - x.toleranceLog10);
    }
    selectTiles(selected, args) {
        assert(undefined !== this.children);
        if (this.isRegionCulled(args))
            return;
        args.markUsed(this);
        // ###TODO: Test content range culled.
        // Compute the ideal chord tolerance.
        assert(this.maximumSize > 0);
        const pixelSize = args.getPixelSizeInMetersAtClosestPoint(this);
        assert(pixelSize > 0);
        // Round down to the nearest power of ten.
        const toleranceLog10 = Math.floor(Math.log10(pixelSize));
        // Find (or create) a child tile of desired tolerance. Also find a child tile that can be substituted for the desired tile if that tile's content is not yet loaded.
        // NB: Children are sorted in descending order by log10(tolerance)
        const children = this.children;
        let closestMatch;
        let exactMatch;
        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            const tol = child.toleranceLog10;
            if (tol > toleranceLog10) {
                assert(undefined === exactMatch);
                if (child.hasGraphics)
                    closestMatch = child;
            }
            else if (tol === toleranceLog10) {
                exactMatch = child;
            }
            else if (tol < toleranceLog10) {
                if (!exactMatch)
                    children.splice(i++, 0, exactMatch = new GraphicsTile(this, toleranceLog10));
                if (child.hasGraphics && (!closestMatch || closestMatch.toleranceLog10 > toleranceLog10))
                    closestMatch = child;
            }
        }
        if (!exactMatch) {
            assert(children.length === 0 || children[children.length - 1].toleranceLog10 > toleranceLog10);
            children.push(exactMatch = new GraphicsTile(this, toleranceLog10));
        }
        if (!exactMatch.isReady) {
            args.insertMissing(exactMatch);
            if (closestMatch) {
                selected.push(closestMatch);
                args.markUsed(closestMatch);
            }
        }
        else if (exactMatch.hasGraphics) {
            selected.push(exactMatch);
            args.markUsed(exactMatch);
        }
    }
    update(range) {
        range.clone(this.range);
        const center = this.range.low.interpolate(0.5, this.range.high);
        const radius = 0.5 * this.range.low.distance(this.range.high);
        this.boundingSphere.init(center, radius);
        // Discard out-dated graphics.
        assert(undefined !== this.children);
        for (const child of this.children)
            child[Symbol.dispose]();
        this.children.length = 0;
    }
}
function* makeIdSequence() {
    let current = 0;
    while (true) {
        if (current >= Number.MAX_SAFE_INTEGER)
            current = 0;
        yield ++current;
    }
}
const requestIdSequence = makeIdSequence();
/** Supplies graphics of a specific LOD for a single element. */
class GraphicsTile extends Tile {
    toleranceLog10;
    tolerance;
    constructor(parent, toleranceLog10) {
        assert(Math.round(toleranceLog10) === toleranceLog10);
        super({
            parent,
            isLeaf: true,
            contentId: `${parent.contentId}_${toleranceLog10}`,
            range: parent.range,
            maximumSize: parent.maximumSize,
        }, parent.tree);
        this.toleranceLog10 = toleranceLog10;
        this.tolerance = 10 ** toleranceLog10;
    }
    computeLoadPriority() {
        // We want the element's graphics to be updated as soon as possible
        return 0;
    }
    _loadChildren(resolve, _reject) {
        resolve(undefined);
    }
    get channel() {
        return IModelApp.tileAdmin.channels.elementGraphicsRpc;
    }
    async requestContent(_isCanceled) {
        // ###TODO tree flags (enforce display priority)
        // ###TODO classifiers, animation
        assert(undefined !== this.parent);
        const requestId = requestIdSequence.next();
        assert(!requestId.done);
        assert(this.tree instanceof IModelTileTree);
        const idProvider = this.tree.contentIdProvider;
        const props = {
            id: requestId.value.toString(16),
            elementId: this.parent.contentId,
            toleranceLog10: this.toleranceLog10,
            formatVersion: idProvider.majorFormatVersion,
            location: this.tree.iModelTransform.toJSON(),
            contentFlags: idProvider.contentFlags,
            omitEdges: !this.tree.edgeOptions,
            edgeType: this.tree.edgeOptions && "non-indexed" !== this.tree.edgeOptions.type ? 2 : 1,
            smoothPolyfaceEdges: this.tree.edgeOptions && this.tree.edgeOptions.smooth,
            clipToProjectExtents: this.tree.is3d,
            sectionCut: this.tree.stringifiedSectionClip,
            useAbsolutePositions: true,
        };
        return IModelApp.tileAdmin.requestElementGraphics(this.tree.iModel, props);
    }
    async readContent(data, system, isCanceled) {
        if (undefined === isCanceled)
            isCanceled = () => !this.isLoading;
        assert(data instanceof Uint8Array);
        const stream = ByteStream.fromUint8Array(data);
        const position = stream.curPos;
        const format = stream.readUint32();
        stream.curPos = position;
        // ###TODO: IModelGraphics format wraps IModel format.
        assert(TileFormat.IModel === format);
        const tree = this.tree;
        assert(tree instanceof IModelTileTree);
        const { iModel, modelId, is3d, containsTransformNodes } = tree;
        const reader = ImdlReader.create({
            stream, iModel, modelId, is3d, system, isCanceled, containsTransformNodes,
            type: tree.batchType,
            loadEdges: false !== tree.edgeOptions,
            options: { tileId: this.contentId },
            timeline: tree.timeline,
        });
        let content = { isLeaf: true };
        if (reader) {
            try {
                content = await reader.read();
            }
            catch {
                //
            }
        }
        return content;
    }
}
//# sourceMappingURL=DynamicIModelTile.js.map