/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Rendering
 */
import { assert, compareNumbers, compareStrings, Id64, IndexedValue, IndexMap, } from "@itwin/core-bentley";
import { GeometryClass } from "./GeometryParams";
/** Describes a discrete entity within a batched [RenderGraphic]($frontend) that can be
 * grouped with other such entities in a [[FeatureTable]].
 * Features roughly correlate to elements: a [Tile]($frontend)'s graphics combines geometry from every
 * [GeometricElement]($backend) that intersects the tile's volume, so each element produces at least one feature.
 * However, an element's geometry stream can contain geometry belonging to multiple different combinations of [SubCategory]($backend) and
 * [[GeometryClass]], so an individual element may produce more than one feature.
 * @see [[FeatureOverrides]] for customizing the appearance of individual features.
 * @public
 */
export class Feature {
    constructor(elementId = Id64.invalid, subCategoryId = Id64.invalid, geometryClass = GeometryClass.Primary) {
        this.elementId = elementId;
        this.subCategoryId = subCategoryId;
        this.geometryClass = geometryClass;
    }
    get isDefined() { return !Id64.isInvalid(this.elementId) || !Id64.isInvalid(this.subCategoryId) || this.geometryClass !== GeometryClass.Primary; }
    get isUndefined() { return !this.isDefined; }
    /** Returns true if this feature is equivalent to the supplied feature. */
    equals(other) { return 0 === this.compare(other); }
    /** Performs ordinal comparison of this feature with another.
     * @param rhs The feature to compare with.
     * @returns zero if the features are equivalent, a negative value if this feature compares as "less than" `rhs`, or a positive value if this feature compares "greater than" `rhs`.
     */
    compare(rhs) {
        if (this === rhs)
            return 0;
        let cmp = compareNumbers(this.geometryClass, rhs.geometryClass);
        if (0 === cmp) {
            cmp = compareStrings(this.elementId, rhs.elementId);
            if (0 === cmp) {
                cmp = compareStrings(this.subCategoryId, rhs.subCategoryId);
            }
        }
        return cmp;
    }
}
/** @public */
export var ModelFeature;
(function (ModelFeature) {
    /** Create a ModelFeature of [[GeometryClass.Primary]] with all invalid Ids.
     * This is primarily useful for creating a `result` argument for [[RenderFeatureTable.findFeature]] and [[RenderFeatureTable.getFeature]].
     */
    function create() {
        return {
            modelId: Id64.invalid,
            elementId: Id64.invalid,
            subCategoryId: Id64.invalid,
            geometryClass: GeometryClass.Primary,
        };
    }
    ModelFeature.create = create;
    /** Returns `true` if any of `feature`'s properties differ from the defaults (invalid Ids and [[GeometryClass.Primary]]). */
    function isDefined(feature) {
        return !Id64.isInvalid(feature.modelId) || !Id64.isInvalid(feature.elementId) || !Id64.isInvalid(feature.subCategoryId) || feature.geometryClass !== GeometryClass.Primary;
    }
    ModelFeature.isDefined = isDefined;
    /** @alpha */
    function unpack(packed, result, unpackedModelId) {
        result.modelId = unpackedModelId ?? Id64.fromUint32PairObject(packed.modelId);
        result.elementId = Id64.fromUint32PairObject(packed.elementId);
        result.subCategoryId = Id64.fromUint32PairObject(packed.subCategoryId);
        result.geometryClass = packed.geometryClass;
        return result;
    }
    ModelFeature.unpack = unpack;
})(ModelFeature || (ModelFeature = {}));
/** @public */
export var PackedFeature;
(function (PackedFeature) {
    /** Create a PackedFeature of [[GeometryClass.Primary]] with all invalid Ids.
     * This is primarily useful for creating a `result` argument for [[RenderFeatureTable.getPackedFeature]].
     */
    function create() {
        const pair = { upper: 0, lower: 0 };
        return {
            modelId: { ...pair },
            elementId: { ...pair },
            subCategoryId: { ...pair },
            geometryClass: GeometryClass.Primary,
            animationNodeId: 0,
        };
    }
    PackedFeature.create = create;
    /** Create a PackedFeatureWithIndex of [[GeometryClass.Primary]] with all invalid Ids and an index of zero.
     * This is primarily useful for creating a reusable `output` argument for [[RenderFeatureTable.iterable]].
     */
    function createWithIndex() {
        const result = create();
        result.index = 0;
        return result;
    }
    PackedFeature.createWithIndex = createWithIndex;
})(PackedFeature || (PackedFeature = {}));
/** Describes the type of a 'batch' of graphics representing multiple [[Feature]]s.
 * The most commonly-encountered batches are Tiles, which can be of either Primary or
 * Classifier type.
 * @public
 * @extensions
 */
export var BatchType;
(function (BatchType) {
    /** This batch contains graphics derived from a model's visible geometry. */
    BatchType[BatchType["Primary"] = 0] = "Primary";
    /**
     * This batch contains color volumes which are used to classify a model's visible geometry.
     * The graphics themselves are not rendered to the screen; instead they are rendered to the stencil buffer
     * to resymbolize the primary geometry.
     */
    BatchType[BatchType["VolumeClassifier"] = 1] = "VolumeClassifier";
    /**
     * This batch contains planar graphics which are used to classify a model's visible geometry.
     * The graphics themselves are not rendered to the screen; instead they are rendered to a texture buffer
     * to resymbolize the primary geometry.
     */
    BatchType[BatchType["PlanarClassifier"] = 2] = "PlanarClassifier";
})(BatchType || (BatchType = {}));
/** Defines a look-up table for [[Feature]]s within a batched [RenderGraphic]($frontend). Consecutive 32-bit
 * indices are assigned to each unique Feature. Primitives within the RenderGraphic can
 * use per-vertex indices to specify the distribution of Features within the primitive. The appearance of individual
 * features can be customized using [[FeatureOverrides]]. Typically a [Tile]($frontend) will contain a feature table
 * identifying the elements whose geometry appears within that tile.
 * @see [[FeatureOverrides]] for customizing the appearance of individual features.
 * @public
 */
export class FeatureTable extends IndexMap {
    /** Construct an empty FeatureTable. */
    constructor(maxFeatures, modelId = Id64.invalid, type = BatchType.Primary) {
        super((lhs, rhs) => lhs.compare(rhs), maxFeatures);
        this.modelId = modelId;
        this.type = type;
    }
    /** Returns the maximum number of [[Feature]]s this FeatureTable can contain. */
    get maxFeatures() { return this._maximumSize; }
    /** @internal */
    get anyDefined() { return this.length > 1 || (1 === this.length && this._array[0].value.isDefined); }
    /** Returns true if this FeatureTable contains exactly one [[Feature]]. */
    get isUniform() { return 1 === this.length; }
    /** If this FeatureTable contains exactly one [[Feature]], returns that Feature; otherwise returns undefined. */
    get uniform() { return 1 === this.length ? this._array[0].value : undefined; }
    /** Returns true if this FeatureTable is associated with [[BatchType.VolumeClassifier]] geometry. */
    get isVolumeClassifier() { return BatchType.VolumeClassifier === this.type; }
    /** Returns true if this FeatureTable is associated with [[BatchType.PlanarClassifier]] geometry. */
    get isPlanarClassifier() { return BatchType.PlanarClassifier === this.type; }
    /** Returns the Feature corresponding to the specified index, or undefined if the index is not present. */
    findFeature(index) {
        for (const entry of this._array)
            if (entry.index === index)
                return entry.value;
        return undefined;
    }
    /** @internal */
    insertWithIndex(feature, index) {
        const bound = this.lowerBound(feature);
        assert(!bound.equal);
        assert(!this.isFull);
        const entry = new IndexedValue(feature, index);
        this._array.splice(bound.index, 0, entry);
    }
    /** @internal */
    getArray() { return this._array; }
    /** Convert this feature table to a representation that can be supplied to [RenderSystem.createBatch]($frontend). */
    pack() {
        return PackedFeatureTable.pack(this);
    }
}
const scratchPackedFeature = PackedFeature.create();
function populateAnimationNodeIds(table, computeNodeId, maxNodeId) {
    assert(maxNodeId > 0);
    let nodeIds;
    const outputFeature = PackedFeature.createWithIndex();
    for (const feature of table.iterable(outputFeature)) {
        const nodeId = computeNodeId(feature);
        assert(nodeId <= maxNodeId);
        if (0 !== nodeId) {
            if (!nodeIds) {
                const size = table.numFeatures;
                nodeIds = maxNodeId < 0x100 ? new Uint8Array(size) : (maxNodeId < 0x10000 ? new Uint16Array(size) : new Uint32Array(size));
            }
            nodeIds[feature.index] = nodeId;
        }
    }
    return nodeIds;
}
/**
 * An immutable, packed representation of a [[FeatureTable]]. The features are packed into a single array of 32-bit integer values,
 * wherein each feature occupies 3 32-bit integers.
 * @internal
 */
export class PackedFeatureTable {
    get byteLength() { return this.data.byteLength; }
    /** Construct a PackedFeatureTable from the packed binary data.
     * This is used internally when deserializing Tiles in iMdl format.
     * @internal
     */
    constructor(data, modelId, numFeatures, type, animationNodeIds) {
        this.data = data;
        this.batchModelId = modelId;
        this.batchModelIdPair = Id64.getUint32Pair(modelId);
        this.numFeatures = numFeatures;
        this.type = type;
        this.animationNodeIds = animationNodeIds;
        switch (this.numFeatures) {
            case 0:
                this.anyDefined = false;
                break;
            case 1:
                this.anyDefined = ModelFeature.isDefined(this.getFeature(0, ModelFeature.create()));
                break;
            default:
                this.anyDefined = true;
                break;
        }
        assert(this.data.length >= this._subCategoriesOffset);
        assert(undefined === this.animationNodeIds || this.animationNodeIds.length === this.numFeatures);
    }
    /** Create a packed feature table from a [[FeatureTable]]. */
    static pack(featureTable) {
        // We must determine how many subcategories we have ahead of time to compute the size of the Uint32Array, as
        // the array cannot be resized after it is created.
        // We are not too worried about this as FeatureTables created on the front-end will contain few if any features; those obtained from the
        // back-end arrive within tiles already in the packed format.
        const subcategories = new Map();
        for (const iv of featureTable.getArray()) {
            const found = subcategories.get(iv.value.subCategoryId.toString());
            if (undefined === found)
                subcategories.set(iv.value.subCategoryId, subcategories.size);
        }
        // We need 3 32-bit integers per feature, plus 2 32-bit integers per subcategory.
        const subCategoriesOffset = 3 * featureTable.length;
        const nUint32s = subCategoriesOffset + 2 * subcategories.size;
        const uint32s = new Uint32Array(nUint32s);
        for (const iv of featureTable.getArray()) {
            const feature = iv.value;
            const index = iv.index * 3;
            let subCategoryIndex = subcategories.get(feature.subCategoryId);
            assert(undefined !== subCategoryIndex); // we inserted it above...
            subCategoryIndex |= (feature.geometryClass << 24);
            uint32s[index + 0] = Id64.getLowerUint32(feature.elementId);
            uint32s[index + 1] = Id64.getUpperUint32(feature.elementId);
            uint32s[index + 2] = subCategoryIndex;
        }
        subcategories.forEach((index, id, _map) => {
            const index32 = subCategoriesOffset + 2 * index;
            uint32s[index32 + 0] = Id64.getLowerUint32(id);
            uint32s[index32 + 1] = Id64.getUpperUint32(id);
        });
        return new PackedFeatureTable(uint32s, featureTable.modelId, featureTable.length, featureTable.type);
    }
    /** Retrieve the Feature associated with the specified index. */
    getFeature(featureIndex, result) {
        const packed = this.getPackedFeature(featureIndex, scratchPackedFeature);
        return ModelFeature.unpack(packed, result, this.batchModelId);
    }
    /** Returns the Feature associated with the specified index, or undefined if the index is out of range. */
    findFeature(featureIndex, result) {
        return featureIndex < this.numFeatures ? this.getFeature(featureIndex, result) : undefined;
    }
    /** @internal */
    getElementIdPair(featureIndex, out) {
        out = out ?? { lower: 0, upper: 0 };
        assert(featureIndex < this.numFeatures);
        const offset = 3 * featureIndex;
        out.lower = this.data[offset];
        out.upper = this.data[offset + 1];
        return out;
    }
    /** @internal */
    getSubCategoryIdPair(featureIndex) {
        const index = 3 * featureIndex;
        let subCatIndex = this.data[index + 2];
        subCatIndex = (subCatIndex & 0x00ffffff) >>> 0;
        subCatIndex = subCatIndex * 2 + this._subCategoriesOffset;
        return { lower: this.data[subCatIndex], upper: this.data[subCatIndex + 1] };
    }
    /** @internal */
    getAnimationNodeId(featureIndex) {
        return undefined !== this.animationNodeIds && featureIndex < this.numFeatures ? this.animationNodeIds[featureIndex] : 0;
    }
    /** @internal */
    getPackedFeature(featureIndex, result) {
        assert(featureIndex < this.numFeatures);
        const index32 = 3 * featureIndex;
        result.elementId.lower = this.data[index32];
        result.elementId.upper = this.data[index32 + 1];
        const subCatIndexAndClass = this.data[index32 + 2];
        result.geometryClass = (subCatIndexAndClass >>> 24) & 0xff;
        let subCatIndex = (subCatIndexAndClass & 0x00ffffff) >>> 0;
        subCatIndex = subCatIndex * 2 + this._subCategoriesOffset;
        result.subCategoryId.lower = this.data[subCatIndex];
        result.subCategoryId.upper = this.data[subCatIndex + 1];
        result.animationNodeId = this.getAnimationNodeId(featureIndex);
        result.modelId.lower = this.batchModelIdPair.lower;
        result.modelId.upper = this.batchModelIdPair.upper;
        return result;
    }
    getModelIdPair(_featureIndex, out) {
        out.lower = this.batchModelIdPair.lower;
        out.upper = this.batchModelIdPair.upper;
        return out;
    }
    /** Returns the element ID of the Feature associated with the specified index, or undefined if the index is out of range. */
    findElementId(featureIndex) {
        if (featureIndex >= this.numFeatures)
            return undefined;
        else
            return this.readId(3 * featureIndex);
    }
    /** Return true if this table contains exactly 1 feature. */
    get isUniform() { return 1 === this.numFeatures; }
    /** If this table contains exactly 1 feature, return it. */
    getUniform(result) {
        return this.isUniform ? this.getFeature(0, result) : undefined;
    }
    get isVolumeClassifier() { return BatchType.VolumeClassifier === this.type; }
    get isPlanarClassifier() { return BatchType.VolumeClassifier === this.type; }
    get isClassifier() { return this.isVolumeClassifier || this.isPlanarClassifier; }
    /** Unpack the features into a [[FeatureTable]]. */
    unpack() {
        const table = new FeatureTable(this.numFeatures, this.batchModelId);
        const feature = ModelFeature.create();
        for (let i = 0; i < this.numFeatures; i++) {
            this.getFeature(i, feature);
            table.insertWithIndex(new Feature(feature.elementId, feature.subCategoryId, feature.geometryClass), i);
        }
        return table;
    }
    populateAnimationNodeIds(computeNodeId, maxNodeId) {
        assert(undefined === this.animationNodeIds);
        this.animationNodeIds = populateAnimationNodeIds(this, computeNodeId, maxNodeId);
    }
    *iterator(output) {
        for (let i = 0; i < this.numFeatures; i++) {
            this.getPackedFeature(i, output);
            output.index = i;
            yield output;
        }
    }
    iterable(output) {
        return {
            [Symbol.iterator]: () => this.iterator(output),
        };
    }
    get _subCategoriesOffset() { return this.numFeatures * 3; }
    readId(offset32) {
        return Id64.fromUint32Pair(this.data[offset32], this.data[offset32 + 1]);
    }
}
const scratchPackedFeatureModelEntry = { lastFeatureIndex: -1, idLower: -1, idUpper: -1 };
/** A table of model Ids associated with a [[MultiModelPackedFeatureTable]].
 * The feature indices in the packed feature table are grouped together by model, such that the first N features belong to model 1, the next M features to model 2, and so on.
 * The model table itself consists of one entry per model, where each entry looks like:
 *  indexOfLastFeatureInModel: u32
 *  modelId: u64
 * The modelId associated with a feature can therefore be derived by finding the entry in the model table with the highest indexOfLastFeatureInModel no greater than the feature index.
 * This lookup can be optimized using binary search.
 * Moreover, while iterating the feature table in sequence, the model table can be iterated in parallel so that no per-feature lookup of model Id is required.
 * @internal
 */
export class PackedFeatureModelTable {
    constructor(data) {
        this._data = data;
        assert(this._data.length % 3 === 0);
    }
    /** The number of models in the table. */
    get length() {
        return this._data.length / 3;
    }
    get byteLength() {
        return this._data.byteLength;
    }
    getLastFeatureIndex(modelIndex) {
        return this._data[modelIndex * 3];
    }
    getEntry(modelIndex, result) {
        if (modelIndex >= this.length) {
            result.idLower = result.idUpper = 0;
            result.lastFeatureIndex = Number.MAX_SAFE_INTEGER;
            return result;
        }
        const index = modelIndex * 3;
        result.lastFeatureIndex = this._data[index + 0];
        result.idLower = this._data[index + 1];
        result.idUpper = this._data[index + 2];
        return result;
    }
    /** Get the Id of the model associated with the specified feature, or an invalid Id if the feature is not associated with any model. */
    getModelIdPair(featureIndex, result) {
        if (!result)
            result = { lower: 0, upper: 0 };
        else
            result.lower = result.upper = 0;
        let first = 0;
        const last = this.length;
        let count = last;
        while (count > 0) {
            const step = Math.floor(count / 2);
            const mid = first + step;
            const lastFeatureIndex = this.getLastFeatureIndex(mid);
            if (featureIndex > lastFeatureIndex) {
                first = mid + 1;
                count -= step + 1;
            }
            else {
                count = step;
            }
        }
        if (first < last) {
            result.lower = this._data[first * 3 + 1];
            result.upper = this._data[first * 3 + 2];
        }
        return result;
    }
}
/** A PackedFeatureTable with a PackedFeatureModelTable appended to it, capable of storing features belonging to more than one model.
 * @internal
 */
export class MultiModelPackedFeatureTable {
    constructor(features, models) {
        this._features = features;
        this._models = models;
    }
    static create(data, batchModelId, numFeatures, type, numSubCategories) {
        const modelTableOffset = 3 * numFeatures + 2 * numSubCategories;
        const featureData = data.subarray(0, modelTableOffset);
        const features = new PackedFeatureTable(featureData, batchModelId, numFeatures, type);
        const modelData = data.subarray(modelTableOffset);
        const models = new PackedFeatureModelTable(modelData);
        return new MultiModelPackedFeatureTable(features, models);
    }
    get batchModelId() { return this._features.batchModelId; }
    get batchModelIdPair() { return this._features.batchModelIdPair; }
    get numFeatures() { return this._features.numFeatures; }
    get type() { return this._features.type; }
    get animationNodeIds() { return this._features.animationNodeIds; }
    set animationNodeIds(ids) { this._features.animationNodeIds = ids; }
    get byteLength() {
        return this._features.byteLength + this._models.byteLength;
    }
    getPackedFeature(featureIndex, result) {
        this._features.getPackedFeature(featureIndex, result);
        this._models.getModelIdPair(featureIndex, result.modelId);
        return result;
    }
    getFeature(featureIndex, result) {
        const packed = this.getPackedFeature(featureIndex, scratchPackedFeature);
        return ModelFeature.unpack(packed, result);
    }
    findFeature(featureIndex, result) {
        return featureIndex < this.numFeatures ? this.getFeature(featureIndex, result) : undefined;
    }
    getElementIdPair(featureIndex, out) {
        return this._features.getElementIdPair(featureIndex, out);
    }
    getModelIdPair(featureIndex, out) {
        this._models.getModelIdPair(featureIndex, out);
        return out;
    }
    findElementId(featureIndex) {
        return this._features.findElementId(featureIndex);
    }
    *iterator(output) {
        // Rather than perform a binary search on the model table to find each feature's model Id, traverse the model table in parallel with the feature table.
        let modelIndex = 0;
        const modelEntry = this._models.getEntry(modelIndex, scratchPackedFeatureModelEntry);
        for (let featureIndex = 0; featureIndex < this.numFeatures; featureIndex++) {
            if (featureIndex > modelEntry.lastFeatureIndex)
                this._models.getEntry(++modelIndex, modelEntry);
            this._features.getPackedFeature(featureIndex, output);
            output.modelId.lower = modelEntry.idLower;
            output.modelId.upper = modelEntry.idUpper;
            output.index = featureIndex;
            yield output;
        }
    }
    iterable(output) {
        return {
            [Symbol.iterator]: () => this.iterator(output),
        };
    }
    getAnimationNodeId(featureIndex) {
        return this._features.getAnimationNodeId(featureIndex);
    }
    populateAnimationNodeIds(computeNodeId, maxNodeId) {
        this._features.animationNodeIds = populateAnimationNodeIds(this, computeNodeId, maxNodeId);
    }
}
//# sourceMappingURL=FeatureTable.js.map