/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { expect } from "chai";
import { Guid, Id64 } from "@itwin/core-bentley";
import { LineString3d, Loop, Point3d } from "@itwin/core-geometry";
import { withEditTxn } from "../../EditTxn";
import { AreaPattern, Code, ColorDef, GeometryParams, GeometryStreamBuilder, GeometryStreamIterator, IModel, } from "@itwin/core-common";
import { _nativeDb, GenericSchema, GeometricElement3d, GeometryPart, PhysicalModel, PhysicalObject, PhysicalPartition, RenderMaterialElement, SnapshotDb, SpatialCategory, SubCategory, SubjectOwnsPartitionElements, } from "../../core-backend";
import { IModelTestUtils } from "../IModelTestUtils";
function makeGeomParams(symb) {
    const params = new GeometryParams(symb.categoryId ?? Id64.invalid, symb.subCategoryId);
    params.lineColor = symb.color;
    params.materialId = symb.materialId;
    if (symb.patternOrigin)
        params.pattern = AreaPattern.Params.fromJSON({ origin: [symb.patternOrigin, 0, 0] });
    return params;
}
class GeomWriter {
    builder;
    constructor(symbology) {
        this.builder = new GeometryStreamBuilder();
        if (symbology)
            this.append(symbology);
    }
    append(entry) {
        if (entry.partId)
            this.builder.appendGeometryPart3d(entry.partId, new Point3d(entry.origin, 0, 0));
        else if (entry.subCategoryId || entry.categoryId || entry.color || entry.materialId || entry.patternOrigin)
            this.builder.appendGeometryParamsChange(makeGeomParams(entry));
        else if (undefined !== entry.pos)
            this.builder.appendGeometry(Loop.createPolygon([new Point3d(entry.pos, 0, 0), new Point3d(entry.pos + 1, 0, 0), new Point3d(entry.pos + 1, 1, 0), new Point3d(entry.pos, 1, 0)]));
        else if (undefined !== entry.appendSubRanges)
            this.builder.appendGeometryRanges();
    }
}
function readGeomStream(iter) {
    const result = [];
    for (const entry of iter) {
        const symb = { categoryId: entry.geomParams.categoryId, subCategoryId: entry.geomParams.subCategoryId };
        if (undefined !== entry.geomParams.lineColor)
            symb.color = entry.geomParams.lineColor;
        if (undefined !== entry.geomParams.materialId)
            symb.materialId = entry.geomParams.materialId;
        if (entry.geomParams.pattern) {
            expect(entry.geomParams.pattern.origin).not.to.be.undefined;
            symb.patternOrigin = entry.geomParams.pattern.origin.x;
        }
        result.push(symb);
        if (entry.localRange) {
            expect(entry.localRange.low.y).to.equal(0);
            expect(entry.localRange.low.z).to.equal(0);
            expect(entry.localRange.high.y).to.equal(1);
            expect(entry.localRange.high.z).to.equal(0);
            expect(entry.localRange.high.x - entry.localRange.low.x).to.equal(1);
            result.push({ low: entry.localRange.low.x });
        }
        if (entry.primitive.type === "geometryQuery") {
            expect(entry.primitive.geometry.geometryCategory).to.equal("curveCollection");
            if (entry.primitive.geometry.geometryCategory === "curveCollection") {
                expect(entry.primitive.geometry.children.length).to.equal(1);
                expect(entry.primitive.geometry.children[0]).instanceOf(LineString3d);
                const pts = entry.primitive.geometry.children[0].points;
                expect(pts.length).to.equal(5);
                expect(pts[1].x).to.equal(pts[0].x + 1);
                result.push({ pos: pts[0].x });
            }
        }
        else {
            expect(entry.primitive.type).to.equal("partReference");
            if (entry.primitive.type === "partReference") {
                const partRef = { partId: entry.primitive.part.id };
                if (entry.primitive.part.toLocal)
                    partRef.origin = entry.primitive.part.toLocal.origin.x;
                result.push(partRef);
            }
        }
    }
    result.viewIndependent = iter.isViewIndependent;
    return result;
}
describe("DgnDb.inlineGeometryPartReferences", () => {
    let imodel;
    let modelId;
    let categoryId;
    let blueSubCategoryId;
    let redSubCategoryId;
    let materialId;
    beforeEach(() => {
        imodel = SnapshotDb.createEmpty(IModelTestUtils.prepareOutputFile("InlineGeomParts", `${Guid.createValue()}.bim`), {
            rootSubject: { name: "InlineGeomParts", description: "InlineGeomParts" },
        });
        GenericSchema.registerSchema();
        withEditTxn(imodel, (txn) => {
            const partitionId = txn.insertElement({
                classFullName: PhysicalPartition.classFullName,
                model: IModel.repositoryModelId,
                parent: new SubjectOwnsPartitionElements(IModel.rootSubjectId),
                code: PhysicalPartition.createCode(imodel, IModel.rootSubjectId, `PhysicalPartition_${Guid.createValue()}`),
            });
            expect(Id64.isValidId64(partitionId)).to.be.true;
            const model = imodel.models.createModel({
                classFullName: PhysicalModel.classFullName,
                modeledElement: { id: partitionId },
            });
            expect(model).instanceOf(PhysicalModel);
            modelId = txn.insertModel(model.toJSON());
            expect(Id64.isValidId64(modelId)).to.be.true;
            categoryId = SpatialCategory.insert(txn, IModel.dictionaryId, "ctgry", { color: ColorDef.blue.toJSON() });
            expect(Id64.isValidId64(categoryId)).to.be.true;
            blueSubCategoryId = IModel.getDefaultSubCategoryId(categoryId);
            redSubCategoryId = SubCategory.insert(txn, categoryId, "red", { color: ColorDef.red.toJSON() });
            expect(Id64.isValidId64(redSubCategoryId)).to.be.true;
            materialId = RenderMaterialElement.insert(txn, IModel.dictionaryId, "mat", { paletteName: "pal" });
            expect(Id64.isValidId64(materialId)).to.be.true;
        });
    });
    afterEach(() => {
        imodel.close();
    });
    function insertGeometryPart(geom) {
        const writer = new GeomWriter();
        for (const entry of geom)
            writer.append(entry);
        const props = {
            classFullName: GeometryPart.classFullName,
            model: IModel.dictionaryId,
            code: GeometryPart.createCode(imodel, IModel.dictionaryId, Guid.createValue()),
            geom: writer.builder.geometryStream,
        };
        const partId = withEditTxn(imodel, (txn) => txn.insertElement(props));
        expect(Id64.isValidId64(partId)).to.be.true;
        return partId;
    }
    function insertElement(geom, viewIndependent = false) {
        const writer = new GeomWriter({ categoryId });
        if (viewIndependent)
            writer.builder.isViewIndependent = true;
        for (const entry of geom)
            writer.append(entry);
        const props = {
            classFullName: PhysicalObject.classFullName,
            model: modelId,
            code: Code.createEmpty(),
            category: categoryId,
            geom: writer.builder.geometryStream,
            placement: {
                origin: [0, 0, 0],
                angles: {},
            },
        };
        const elemId = withEditTxn(imodel, (txn) => txn.insertElement(props));
        expect(Id64.isValidId64(elemId)).to.be.true;
        return elemId;
    }
    function readElementGeom(id) {
        let iter;
        const elem = imodel.elements.getElement({ id, wantGeometry: true });
        if (elem instanceof GeometryPart) {
            iter = GeometryStreamIterator.fromGeometryPart(elem);
        }
        else {
            expect(elem).instanceOf(GeometricElement3d);
            iter = GeometryStreamIterator.fromGeometricElement3d(elem);
        }
        return readGeomStream(iter);
    }
    function expectGeom(actual, expected) {
        expect(actual).to.deep.equal(expected);
    }
    function inlinePartRefs() {
        const result = imodel[_nativeDb].inlineGeometryPartReferences();
        expect(result.numCandidateParts).to.equal(result.numPartsDeleted);
        expect(result.numRefsInlined).to.equal(result.numCandidateParts);
        return result.numRefsInlined;
    }
    it("inlines and deletes a simple unique part reference", () => {
        // Create single reference to a part and perform sanity checks on our geometry validation code.
        const partId = insertGeometryPart([{ pos: 123 }]);
        expectGeom(readElementGeom(partId), [
            { categoryId: "0", subCategoryId: "0" },
            { pos: 123 },
        ]);
        const elemId = insertElement([{ partId }]);
        expectGeom(readElementGeom(elemId), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { partId },
        ]);
        // Inline and delete the part.
        expect(inlinePartRefs()).to.equal(1);
        expect(imodel.elements.tryGetElement(partId)).to.be.undefined;
        const geom = readElementGeom(elemId);
        expect(geom.viewIndependent).to.be.false;
        expectGeom(geom, [
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 123 },
            { pos: 123 },
        ]);
    });
    it("inlines and deletes unique parts, ignoring non-unique parts", () => {
        const part1 = insertGeometryPart([{ pos: 1 }]);
        const part2 = insertGeometryPart([{ pos: 2 }]);
        const part3 = insertGeometryPart([{ pos: 3 }]);
        const part4 = insertGeometryPart([{ pos: 4 }]);
        const elem1 = insertElement([{ partId: part1 }]);
        const elem2 = insertElement([{ partId: part2 }, { partId: part3 }]);
        const elem3 = insertElement([{ partId: part3 }]);
        const elem4 = insertElement([{ partId: part4 }]);
        const elem5 = insertElement([{ partId: part4 }]);
        expect(inlinePartRefs()).to.equal(2);
        const symb = { categoryId, subCategoryId: blueSubCategoryId };
        expectGeom(readElementGeom(elem1), [symb, { low: 1 }, { pos: 1 }]);
        expectGeom(readElementGeom(elem2), [symb, { low: 2 }, { pos: 2 }, symb, { partId: part3 }]);
        expectGeom(readElementGeom(elem3), [symb, { partId: part3 }]);
        expectGeom(readElementGeom(elem4), [symb, { partId: part4 }]);
        expectGeom(readElementGeom(elem5), [symb, { partId: part4 }]);
    });
    it("applies part transform", () => {
        const partId = insertGeometryPart([{ pos: -8 }]);
        const elemId = insertElement([{ partId, origin: 50 }]);
        expect(inlinePartRefs()).to.equal(1);
        expectGeom(readElementGeom(elemId), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 42 },
            { pos: 42 },
        ]);
    });
    it("applies element symbology to part and resets element symbology after embedding part", () => {
        const part1 = insertGeometryPart([
            { pos: 1 },
            { color: ColorDef.green },
            { pos: 1.5 },
        ]);
        const part2 = insertGeometryPart([
            { pos: 2 },
            { materialId },
            { pos: 2.5 },
        ]);
        // Sanity check.
        expectGeom(readElementGeom(part2), [
            { categoryId: "0", subCategoryId: "0" },
            { pos: 2 },
            { categoryId: "0", subCategoryId: "0", materialId },
            { pos: 2.5 },
        ]);
        const part3 = insertGeometryPart([
            { pos: 3 },
            { materialId: "0" },
            { pos: 3.5 },
        ]);
        const elemId = insertElement([
            { pos: -1 },
            { subCategoryId: redSubCategoryId },
            { partId: part1 },
            { pos: -2 },
            { color: ColorDef.black },
            { partId: part2 },
            { pos: -3 },
            { materialId, color: ColorDef.white },
            { partId: part3 },
            { pos: -4 },
        ]);
        expect(inlinePartRefs()).to.equal(3);
        expectGeom(readElementGeom(elemId), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { pos: -1 },
            { categoryId, subCategoryId: redSubCategoryId },
            { low: 1 },
            { pos: 1 },
            { categoryId, subCategoryId: redSubCategoryId, color: ColorDef.green },
            { low: 1.5 },
            { pos: 1.5 },
            { categoryId, subCategoryId: redSubCategoryId },
            { low: -2 },
            { pos: -2 },
            { categoryId, subCategoryId: redSubCategoryId, color: ColorDef.black },
            { low: 2 },
            { pos: 2 },
            { categoryId, subCategoryId: redSubCategoryId, materialId },
            { low: 2.5 },
            { pos: 2.5 },
            { categoryId, subCategoryId: redSubCategoryId, color: ColorDef.black },
            { low: -3 },
            { pos: -3 },
            { categoryId, subCategoryId: redSubCategoryId, color: ColorDef.white, materialId },
            { low: 3 },
            { pos: 3 },
            { categoryId, subCategoryId: redSubCategoryId, materialId: "0" },
            { low: 3.5 },
            { pos: 3.5 },
            { categoryId, subCategoryId: redSubCategoryId, color: ColorDef.white, materialId },
            { low: -4 },
            { pos: -4 },
        ]);
    });
    it("inserts subgraphic ranges for parts", () => {
        const part1 = insertGeometryPart([{ pos: 1 }]);
        const part2 = insertGeometryPart([{ pos: 2 }]);
        const elem = insertElement([
            { pos: -1 },
            { partId: part1 },
            { partId: part2, origin: 5 },
        ]);
        expect(inlinePartRefs()).to.equal(2);
        expectGeom(readElementGeom(elem), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { pos: -1 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 1 },
            { pos: 1 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 7 },
            { pos: 7 },
        ]);
    });
    it("preserves existing subgraphic ranges", () => {
        const partId = insertGeometryPart([{ pos: 0 }]);
        const elem = insertElement([
            { appendSubRanges: true },
            { pos: -1 },
            { partId },
            { pos: 1 },
        ]);
        expectGeom(readElementGeom(elem), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: -1 },
            { pos: -1 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { partId },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 1 },
            { pos: 1 },
        ]);
        expect(inlinePartRefs()).to.equal(1);
        expectGeom(readElementGeom(elem), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: -1 },
            { pos: -1 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 0 },
            { pos: 0 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 1 },
            { pos: 1 },
        ]);
    });
    it("inserts subgraphic ranges geometry following part", () => {
        const partId = insertGeometryPart([{ pos: 1 }]);
        const elem = insertElement([
            { pos: 0 },
            { partId },
            { pos: 2 },
        ]);
        expect(inlinePartRefs()).to.equal(1);
        expectGeom(readElementGeom(elem), [
            { categoryId, subCategoryId: blueSubCategoryId },
            { pos: 0 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 1 },
            { pos: 1 },
            { categoryId, subCategoryId: blueSubCategoryId },
            { low: 2 },
            { pos: 2 },
        ]);
    });
    it("applies transform to patterns", () => {
        const part1 = insertGeometryPart([{ pos: 1 }]);
        const part2 = insertGeometryPart([{ patternOrigin: 123 }, { pos: 2 }]);
        expectGeom(readElementGeom(part2), [
            { categoryId: "0", subCategoryId: "0", patternOrigin: 123 },
            { pos: 2 },
        ]);
        const elemId = insertElement([
            { patternOrigin: 456 },
            { pos: -1 },
            { partId: part1, origin: 8 },
            { pos: -2 },
            { partId: part2, origin: 12 },
            { pos: -3 },
        ]);
        expectGeom(readElementGeom(elemId), [
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { pos: -1 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { partId: part1, origin: 8 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { pos: -2 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { partId: part2, origin: 12 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { pos: -3 },
        ]);
        expect(inlinePartRefs()).to.equal(2);
        expectGeom(readElementGeom(elemId), [
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { pos: -1 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { low: 1 + 8 },
            { pos: 1 + 8 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { low: -2 },
            { pos: -2 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 123 + 12 },
            { low: 2 + 12 },
            { pos: 2 + 12 },
            { categoryId, subCategoryId: blueSubCategoryId, patternOrigin: 456 },
            { low: -3 },
            { pos: -3 },
        ]);
    });
    it("preserves element header flags", () => {
        const partId = insertGeometryPart([{ pos: 1 }]);
        const elemId = insertElement([{ partId }], true);
        expect(readElementGeom(elemId).viewIndependent).to.be.true;
        expect(inlinePartRefs()).to.equal(1);
        expect(readElementGeom(elemId).viewIndependent).to.be.true;
    });
});
//# sourceMappingURL=InlineGeometryPartReferences.test.js.map