/*---------------------------------------------------------------------------------------------
* 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 { Id64, JsonUtils } from "@itwin/core-bentley";
import { ClipVector, Point2d, Point3d, Range3d, Transform } from "@itwin/core-geometry";
import { ColorDef, Gradient, ImageSource, RenderMaterial, RenderTexture, TextureMapping, } from "@itwin/core-common";
import { AuxChannelTable } from "../common/internal/render/AuxChannelTable";
import { createSurfaceMaterial } from "../common/internal/render/SurfaceParams";
import { convertFeatureTable, edgeParamsFromImdl, toMaterialParams, toVertexTable } from "../common/imdl/ParseImdlDocument";
import { VertexIndices } from "../common/internal/render/VertexIndices";
import { GraphicBranch } from "../render/GraphicBranch";
import { isGraphicDescription } from "../common/internal/render/GraphicDescriptionBuilderImpl";
import { _textures } from "../common/internal/Symbols";
async function loadNamedTexture(name, namedTex, options) {
    // Reasons a texture could be embedded in the tile content instead of requested separately from the backend:
    // - external textures are disabled
    // - the texture name is not a valid Id64 string
    // - the texture is below a certain backend-hardcoded size threshold
    // The bufferViewJson being defined signifies any of the above conditions. In that case, the image content
    // has been embedded in the tile contents. Otherwise, we will attempt to request the image content separately
    // from the backend.
    try {
        let textureType = RenderTexture.Type.Normal;
        const isGlyph = JsonUtils.asBool(namedTex.isGlyph);
        const isTileSection = !isGlyph && JsonUtils.asBool(namedTex.isTileSection);
        if (isGlyph)
            textureType = RenderTexture.Type.Glyph;
        else if (isTileSection)
            textureType = RenderTexture.Type.TileSection;
        // We produce unique tile sections for very large (> 8 megapixel) textures, and unique glyph atlases for raster text.
        // Neither should be cached.
        const cacheable = !isGlyph && !isTileSection;
        const ownership = cacheable ? { iModel: options.iModel, key: name } : undefined;
        const bufferViewId = JsonUtils.asString(namedTex.bufferView);
        const bufferViewJson = 0 !== bufferViewId.length ? options.document.json.bufferViews[bufferViewId] : undefined;
        if (undefined !== bufferViewJson) { // presence of bufferViewJson signifies we should read the texture from the tile content
            const byteOffset = JsonUtils.asInt(bufferViewJson.byteOffset);
            const byteLength = JsonUtils.asInt(bufferViewJson.byteLength);
            if (0 === byteLength)
                return undefined;
            const texBytes = options.document.binaryData.subarray(byteOffset, byteOffset + byteLength);
            const format = namedTex.format;
            const source = new ImageSource(texBytes, format);
            return await options.system.createTextureFromSource({ source, ownership, type: textureType, transparency: namedTex.transparency });
        }
        // bufferViewJson was undefined, so attempt to request the texture directly from the backend
        // eslint-disable-next-line deprecation/deprecation
        const params = new RenderTexture.Params(cacheable ? name : undefined, textureType);
        return options.system.createTextureFromElement(name, options.iModel, params, namedTex.format);
    }
    catch (_) {
        return undefined;
    }
}
async function loadNamedTextures(options) {
    const result = new Map();
    const namedTextures = options.document.json.namedTextures;
    if (!namedTextures)
        return result;
    const promises = new Array();
    for (const [name, namedTexture] of Object.entries(namedTextures)) {
        const texture = options.system.findTexture(name, options.iModel);
        if (texture) {
            result.set(name, texture);
            continue;
        }
        else if (namedTexture) {
            promises.push(loadNamedTexture(name, namedTexture, options).then((tx) => {
                if (tx)
                    result.set(name, tx);
            }));
        }
    }
    if (promises.length > 0)
        await Promise.all(promises);
    return result;
}
function constantLodParamPropsFromJson(propsJson) {
    if (undefined === propsJson)
        return undefined;
    const constantLodPops = {
        repetitions: JsonUtils.asDouble(propsJson.repetitions, 1.0),
        offset: { x: propsJson.offset ? JsonUtils.asDouble(propsJson.offset[0]) : 0.0, y: propsJson.offset ? JsonUtils.asDouble(propsJson.offset[1]) : 0.0 },
        minDistClamp: JsonUtils.asDouble(propsJson.minDistClamp, 1.0),
        maxDistClamp: JsonUtils.asDouble(propsJson.maxDistClamp, 4096.0 * 1024.0 * 1024.0),
    };
    return constantLodPops;
}
function textureMappingFromJson(json, options) {
    if (!json)
        return undefined;
    const texture = options.textures.get(JsonUtils.asString(json.name));
    if (!texture)
        return undefined;
    const paramsJson = json.params;
    const tf = paramsJson.transform;
    const paramProps = {
        textureMat2x3: new TextureMapping.Trans2x3(tf[0][0], tf[0][1], tf[0][2], tf[1][0], tf[1][1], tf[1][2]),
        textureWeight: JsonUtils.asDouble(paramsJson.weight, 1.0),
        mapMode: JsonUtils.asInt(paramsJson.mode),
        worldMapping: JsonUtils.asBool(paramsJson.worldMapping),
        useConstantLod: JsonUtils.asBool(paramsJson.useConstantLod),
        constantLodProps: constantLodParamPropsFromJson(paramsJson.constantLodParams),
    };
    const textureMapping = new TextureMapping(texture, new TextureMapping.Params(paramProps));
    const normalMapJson = json.normalMapParams;
    if (normalMapJson) {
        let normalMap;
        const normalTexName = JsonUtils.asString(normalMapJson.textureName);
        if (normalTexName.length === 0 || undefined !== (normalMap = options.textures.get(normalTexName))) {
            textureMapping.normalMapParams = {
                normalMap,
                greenUp: JsonUtils.asBool(normalMapJson.greenUp),
                scale: JsonUtils.asDouble(normalMapJson.scale, 1),
                useConstantLod: JsonUtils.asBool(normalMapJson.useConstantLod),
            };
        }
    }
    return textureMapping;
}
function getMaterial(mat, options) {
    if (typeof mat !== "string") {
        const args = toMaterialParams(mat);
        return options.system.createRenderMaterial(args);
    }
    if (!options.iModel) {
        return undefined;
    }
    const material = options.system.findMaterial(mat, options.iModel);
    if (material || !options.document?.json.renderMaterials)
        return material;
    const json = options.document.json.renderMaterials[mat];
    if (!json)
        return undefined;
    function colorDefFromJson(col) {
        return col ? ColorDef.from(col[0] * 255 + 0.5, col[1] * 255 + 0.5, col[2] * 255 + 0.5) : undefined;
    }
    // eslint-disable-next-line deprecation/deprecation
    const params = new RenderMaterial.Params(mat);
    params.diffuseColor = colorDefFromJson(json.diffuseColor);
    if (json.diffuse !== undefined)
        params.diffuse = JsonUtils.asDouble(json.diffuse);
    params.specularColor = colorDefFromJson(json.specularColor);
    if (json.specular !== undefined)
        params.specular = JsonUtils.asDouble(json.specular);
    params.reflectColor = colorDefFromJson(json.reflectColor);
    if (json.reflect !== undefined)
        params.reflect = JsonUtils.asDouble(json.reflect);
    if (json.specularExponent !== undefined)
        params.specularExponent = json.specularExponent;
    if (undefined !== json.transparency)
        params.alpha = 1.0 - json.transparency;
    params.refract = JsonUtils.asDouble(json.refract);
    params.shadows = JsonUtils.asBool(json.shadows);
    params.ambient = JsonUtils.asDouble(json.ambient);
    if (undefined !== json.textureMapping)
        params.textureMapping = textureMappingFromJson(json.textureMapping.texture, options);
    // eslint-disable-next-line deprecation/deprecation
    return options.system.createMaterial(params, options.iModel);
}
function getModifiers(primitive) {
    const mod = primitive.modifier;
    switch (mod?.type) {
        case "instances":
            return {
                instances: {
                    ...mod,
                    transformCenter: Point3d.fromJSON(mod.transformCenter),
                    range: mod.range ? Range3d.fromJSON(mod.range) : undefined,
                },
            };
        case "viewIndependentOrigin":
            return {
                viOrigin: Point3d.fromJSON(mod.origin),
            };
        default:
            return {};
    }
}
function createPrimitiveGeometry(primitive, options, viOrigin) {
    switch (primitive.type) {
        case "point":
            return options.system.createPointStringGeometry({
                ...primitive.params,
                vertices: toVertexTable(primitive.params.vertices),
                indices: new VertexIndices(primitive.params.indices),
            }, viOrigin);
        case "polyline":
            return options.system.createPolylineGeometry({
                ...primitive.params,
                vertices: toVertexTable(primitive.params.vertices),
                polyline: {
                    ...primitive.params.polyline,
                    indices: new VertexIndices(primitive.params.polyline.indices),
                    prevIndices: new VertexIndices(primitive.params.polyline.prevIndices),
                },
            }, viOrigin);
        case "mesh": {
            const surf = primitive.params.surface;
            let material;
            if (surf.material) {
                if (!surf.material.isAtlas)
                    material = createSurfaceMaterial(getMaterial(surf.material.material, options));
                else
                    material = surf.material;
            }
            let textureMapping;
            if (surf.textureMapping) {
                let texture;
                if (typeof surf.textureMapping.texture === "string") {
                    texture = options.textures.get(surf.textureMapping.texture);
                }
                else {
                    const gradient = Gradient.Symb.fromJSON(surf.textureMapping.texture);
                    texture = options.system.getGradientTexture(gradient, options.iModel);
                }
                if (texture)
                    textureMapping = { texture, alwaysDisplayed: surf.textureMapping.alwaysDisplayed };
            }
            return options.system.createMeshGeometry({
                ...primitive.params,
                edges: primitive.params.edges ? edgeParamsFromImdl(primitive.params.edges) : undefined,
                vertices: toVertexTable(primitive.params.vertices),
                auxChannels: primitive.params.auxChannels ? AuxChannelTable.fromJSON(primitive.params.auxChannels) : undefined,
                surface: {
                    ...primitive.params.surface,
                    material,
                    textureMapping,
                    indices: new VertexIndices(primitive.params.surface.indices),
                },
            }, viOrigin);
        }
    }
}
function createPrimitiveGraphic(primitive, options) {
    const mods = getModifiers(primitive);
    const geometry = createPrimitiveGeometry(primitive, options, mods.viOrigin);
    return geometry ? options.system.createRenderGraphic(geometry, mods.instances) : undefined;
}
function createPatternGeometries(primitives, options) {
    const geometries = [];
    for (const primitive of primitives) {
        const geometry = createPrimitiveGeometry(primitive, options, undefined);
        if (geometry)
            geometries.push(geometry);
    }
    return geometries;
}
function createPatternGraphic(params, options) {
    const geometries = options.patterns.get(params.symbolName);
    if (!geometries || geometries.length === 0)
        return undefined;
    const clip = ClipVector.fromJSON(params.clip);
    const clipVolume = clip?.isValid ? options.system.createClipVolume(clip) : undefined;
    if (!clipVolume)
        return undefined;
    const viewIndependentOrigin = params.viewIndependentOrigin ? Point3d.fromJSON(params.viewIndependentOrigin) : undefined;
    const pattern = options.system.createAreaPattern({
        xyOffsets: params.xyOffsets,
        featureId: params.featureId,
        orgTransform: Transform.fromJSON(params.orgTransform),
        origin: Point2d.fromJSON(params.origin),
        scale: params.scale,
        spacing: Point2d.fromJSON(params.spacing),
        patternToModel: Transform.fromJSON(params.modelTransform),
        range: Range3d.fromJSON(params.range),
        symbolTranslation: Point3d.fromJSON(params.symbolTranslation),
        viewIndependentOrigin,
    });
    if (!pattern)
        return undefined;
    const branch = new GraphicBranch(true);
    for (const geometry of geometries) {
        const graphic = options.system.createRenderGraphic(geometry, pattern);
        if (graphic)
            branch.add(graphic);
    }
    return branch.isEmpty ? undefined : options.system.createGraphicBranch(branch, Transform.createIdentity(), { clipVolume });
}
function createNodeGraphics(node, options) {
    if (undefined === node.groupId)
        return createPrimitivesNodeGraphics(node, options);
    const graphics = [];
    for (const child of node.nodes) {
        graphics.push(...createPrimitivesNodeGraphics(child, options));
    }
    if (graphics.length === 0)
        return graphics;
    const branch = new GraphicBranch(true);
    branch.groupNodeId = node.groupId;
    branch.entries.push(...graphics);
    return [options.system.createBranch(branch, Transform.createIdentity())];
}
function createPrimitivesNodeGraphics(node, options) {
    let graphics = [];
    for (const primitive of node.primitives) {
        const graphic = primitive.type === "pattern" ? createPatternGraphic(primitive.params, options) : createPrimitiveGraphic(primitive, options);
        if (graphic)
            graphics.push(graphic);
    }
    if (!graphics.length)
        return graphics;
    if (undefined !== node.layerId) {
        const layerGraphic = 1 === graphics.length ? graphics[0] : options.system.createGraphicList(graphics);
        graphics = [options.system.createGraphicLayer(layerGraphic, node.layerId)];
    }
    else if (undefined !== node.animationNodeId) {
        const branch = new GraphicBranch(true);
        branch.animationId = node.animationId;
        branch.animationNodeId = node.animationNodeId;
        branch.entries.push(...graphics);
        graphics = [options.system.createBranch(branch, Transform.createIdentity())];
    }
    return graphics;
}
/** @internal */
export async function decodeImdlGraphics(options) {
    const textures = await loadNamedTextures(options);
    if (options.isCanceled && options.isCanceled())
        return undefined;
    const patterns = new Map();
    const graphicsOptions = { ...options, textures, patterns };
    for (const [name, primitives] of options.document.patterns)
        patterns.set(name, createPatternGeometries(primitives, graphicsOptions));
    const system = options.system;
    const graphics = [];
    for (const node of options.document.nodes) {
        graphics.push(...createNodeGraphics(node, graphicsOptions));
    }
    switch (graphics.length) {
        case 0: return undefined;
        case 1: return graphics[0];
        default: return system.createGraphicList(graphics);
    }
}
function remapGraphicDescription(descr, context) {
    if (descr.remapContext === context) {
        // Already remapped.
        return;
    }
    else if (descr.remapContext) {
        throw new Error("You cannot create a graphic from a GraphicDescription using two different contexts");
    }
    descr.remapContext = context;
    const batch = descr.batch;
    if (!batch) {
        return;
    }
    if (Id64.isTransient(batch.modelId)) {
        const modelLocalId = context.remapTransientLocalId(Id64.getLocalId(batch.modelId));
        batch.modelId = Id64.fromLocalAndBriefcaseIds(modelLocalId, 0xffffff);
    }
    const data = batch.featureTable.data;
    const remapId = (offset) => {
        const hi = data[offset + 1];
        if (hi >= 0xffffff00) {
            const hi8 = hi & 0xff;
            const lo = data[offset];
            // Avoid bitwise operators because they truncate at 32 bits (we need 40) and are stupid about the sign bit.
            const sourceLocalId = lo + (hi8 * 0xffffffff);
            const localId = context.remapTransientLocalId(sourceLocalId);
            data[offset] = localId & 0xffffffff;
            data[offset + 1] = 0xffffff00 + hi8;
        }
    };
    const subCatsOffset = 3 * batch.featureTable.numFeatures;
    for (let i = 0; i < subCatsOffset; i += 3) {
        remapId(i);
    }
    const subCatsEnd = undefined !== batch.featureTable.numSubCategories ? subCatsOffset + 2 * batch.featureTable.numSubCategories : data.length;
    for (let i = subCatsOffset; i < subCatsEnd; i++) {
        remapId(i);
    }
    if (undefined !== batch.featureTable.numSubCategories) {
        for (let i = subCatsEnd; i < data.length; i++) {
            remapId(i + 1);
        }
    }
}
/** @internal */
export async function createGraphicFromDescription(descr, context, system) {
    if (!isGraphicDescription(descr)) {
        throw new Error("Invalid GraphicDescription");
    }
    remapGraphicDescription(descr, context);
    const graphics = [];
    const graphicsOptions = {
        system,
        textures: context[_textures],
        patterns: new Map(),
    };
    for (const primitive of descr.primitives) {
        const gf = createPrimitiveGraphic(primitive, graphicsOptions);
        if (gf) {
            graphics.push(gf);
        }
    }
    if (graphics.length === 0) {
        return undefined;
    }
    let graphic = graphics.length === 1 ? graphics[0] : system.createGraphicList(graphics);
    if (descr.batch) {
        const featureTable = convertFeatureTable(descr.batch.featureTable, descr.batch.modelId);
        graphic = system.createBatch(graphic, featureTable, Range3d.fromJSON(descr.batch.range), descr.batch);
    }
    if (descr.translation) {
        const branch = new GraphicBranch(true);
        branch.add(graphic);
        graphic = system.createBranch(branch, Transform.createTranslation(Point3d.fromJSON(descr.translation)));
    }
    return graphic;
}
//# sourceMappingURL=ImdlGraphicsCreator.js.map