/*---------------------------------------------------------------------------------------------
* 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 { BisCodeSpec, Code, ElementGeometry, Placement2d, Placement3d, TextAnnotation, TextStyleSettings } from "@itwin/core-common";
import { AnnotationElement2d, DefinitionElement, Drawing, GraphicalElement3d } from "../Element";
import { assert } from "@itwin/core-bentley";
import { layoutTextBlock, TextStyleResolver } from "./TextBlockLayout";
import { appendTextAnnotationGeometry } from "./TextAnnotationGeometry";
import { ElementDrivesTextAnnotation, TextAnnotationUsesTextStyleByDefault } from "./ElementDrivesTextAnnotation";
import * as semver from "semver";
/** The version of the JSON stored in `TextAnnotation2d/3dProps.textAnnotationData` used by the code.
 * Uses the same semantics as [ECVersion]($ecschema-metadata).
 * @internal
*/
export const TEXT_ANNOTATION_JSON_VERSION = "1.0.0";
function validateAndMigrateVersionedJSON(json, currentVersion, migrate) {
    let parsed;
    try {
        parsed = JSON.parse(json);
    }
    catch {
        return undefined;
    }
    const version = parsed.version;
    if (typeof version !== "string" || !semver.valid(version))
        throw new Error("JSON version is missing or invalid.");
    if (typeof parsed.data !== "object" || parsed.data === null)
        throw new Error("JSON data is missing or invalid.");
    // Newer
    if (semver.gt(version, currentVersion))
        throw new Error(`JSON version ${parsed.version} is newer than supported version ${currentVersion}. Application update required to understand data.`);
    // Older
    if (semver.lt(version, currentVersion)) {
        parsed.data = migrate(parsed);
        parsed.version = currentVersion;
    }
    return parsed;
}
function migrateTextAnnotationData(oldData) {
    if (oldData.version === TEXT_ANNOTATION_JSON_VERSION)
        return oldData.data;
    // Place migration logic here.
    throw new Error(`Migration for textAnnotationData from version ${oldData.version} to ${TEXT_ANNOTATION_JSON_VERSION} failed.`);
}
/** Parses, validates, and potentially migrates the text annotation data from a JSON string. */
function parseTextAnnotationData(json) {
    if (!json)
        return undefined;
    return validateAndMigrateVersionedJSON(json, TEXT_ANNOTATION_JSON_VERSION, migrateTextAnnotationData);
}
function getElementGeometryBuilderParams(iModel, modelId, categoryId, _placementProps, annotationProps, textStyleId, _subCategory) {
    const textBlock = TextAnnotation.fromJSON(annotationProps).textBlock;
    const textStyleResolver = new TextStyleResolver({ textBlock, textStyleId: textStyleId ?? "", iModel });
    const layout = layoutTextBlock({ iModel, textBlock, textStyleResolver });
    const builder = new ElementGeometry.Builder();
    let scaleFactor = 1;
    const element = iModel.elements.getElement(modelId);
    if (element instanceof Drawing)
        scaleFactor = element.scaleFactor;
    appendTextAnnotationGeometry({ layout, textStyleResolver, scaleFactor, annotationProps: annotationProps ?? {}, builder, categoryId });
    return { entryArray: builder.entries };
}
/** An element that displays textual content within a 2d model.
 * The text is stored as a [TextAnnotation]($common) from which the element's [geometry]($docs/learning/common/GeometryStream.md) and [Placement]($common) are computed.
 * @see [[setAnnotation]] to change the textual content.
 * @public @preview
 */
export class TextAnnotation2d extends AnnotationElement2d /* implements ITextAnnotation */ {
    /** @internal */
    static get className() { return "TextAnnotation2d"; }
    /**
     * The default [[AnnotationTextStyle]] used by the TextAnnotation2d.
     * @beta
     */
    defaultTextStyle;
    /** The data associated with the text annotation. */
    _textAnnotationProps;
    /** Extract the textual content, if present.
     * @see [[setAnnotation]] to change it.
     */
    getAnnotation() {
        return this._textAnnotationProps ? TextAnnotation.fromJSON(this._textAnnotationProps) : undefined;
    }
    /** Change the textual content of the `TextAnnotation2d`.
     * @see [[getAnnotation]] to extract the current annotation.
     * @param annotation The new annotation
     */
    setAnnotation(annotation) {
        this._textAnnotationProps = annotation.toJSON();
    }
    constructor(props, iModel) {
        super(props, iModel);
        if (props.defaultTextStyle) {
            this.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(props.defaultTextStyle.id);
        }
        this._textAnnotationProps = parseTextAnnotationData(props.textAnnotationData)?.data;
    }
    /** Creates a new instance of `TextAnnotation2d` from its JSON representation. */
    static fromJSON(props, iModel) {
        return new TextAnnotation2d(props, iModel);
    }
    /**
     * Converts the current `TextAnnotation2d` instance to its JSON representation.
     * It also computes the `elementGeometryBuilderParams` property used to create the GeometryStream.
     * @inheritdoc
     */
    toJSON() {
        const props = super.toJSON();
        props.textAnnotationData = this._textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: this._textAnnotationProps }) : undefined;
        if (this._textAnnotationProps) {
            props.elementGeometryBuilderParams = getElementGeometryBuilderParams(this.iModel, this.model, this.category, this.placement, this._textAnnotationProps, this.defaultTextStyle ? this.defaultTextStyle.id : undefined);
        }
        return props;
    }
    /** Creates a new `TextAnnotation2d` instance with the specified properties.
     * @param iModelDb The iModel.
     * @param arg The arguments for creating the TextAnnotation2d.
     * @beta
     */
    static create(iModelDb, arg) {
        const elementProps = {
            classFullName: this.classFullName,
            textAnnotationData: arg.textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: arg.textAnnotationProps }) : undefined,
            defaultTextStyle: arg.defaultTextStyleId ? new TextAnnotationUsesTextStyleByDefault(arg.defaultTextStyleId).toJSON() : undefined,
            placement: arg.placement,
            model: arg.model,
            category: arg.category,
            code: arg.code ?? Code.createEmpty(),
        };
        return new this(elementProps, iModelDb);
    }
    /**
     * Updates the geometry of the TextAnnotation2d on insert and validates version.
     * @inheritdoc
     * @beta
     */
    static onInsert(arg) {
        super.onInsert(arg);
        this.validateVersionAndUpdateGeometry(arg);
    }
    /**
     * Updates the geometry of the TextAnnotation2d on update and validates version.
     * @inheritdoc
     * @beta
     */
    static onUpdate(arg) {
        super.onUpdate(arg);
        this.validateVersionAndUpdateGeometry(arg);
    }
    /**
     * Populates the `elementGeometryBuilderParams` property in the [TextAnnotation2dProps]($common).
     * Only does this if the `elementGeometryBuilderParams` is not already set and if there is actually a text annotation to produce geometry for.
     * Also, validates the version of the text annotation data and migrates it if necessary.
     * @beta
     */
    static validateVersionAndUpdateGeometry(arg) {
        const props = arg.props;
        const textAnnotationData = parseTextAnnotationData(props.textAnnotationData);
        if (!props.elementGeometryBuilderParams && textAnnotationData) {
            props.elementGeometryBuilderParams = getElementGeometryBuilderParams(arg.iModel, props.model, props.category, props.placement ?? Placement2d.fromJSON(), textAnnotationData.data, props.defaultTextStyle?.id);
        }
    }
    /**
     * TextAnnotation2d custom HandledProps include 'textAnnotationData'.
     * @inheritdoc
     * @internal
     */
    static _customHandledProps = [
        { propertyName: "textAnnotationData", source: "Class" },
    ];
    /**
     * TextAnnotation2d deserializes 'textAnnotationData'.
     * @inheritdoc
     * @beta
     */
    static deserialize(props) {
        const elProps = super.deserialize(props);
        const textAnnotationData = parseTextAnnotationData(props.row.textAnnotationData);
        if (textAnnotationData) {
            elProps.textAnnotationData = JSON.stringify(textAnnotationData);
        }
        return elProps;
    }
    /**
     * TextAnnotation2d serializes 'textAnnotationData'.
     * @inheritdoc
     * @beta
     */
    static serialize(props, iModel) {
        const inst = super.serialize(props, iModel);
        if (props.textAnnotationData !== undefined) {
            inst.textAnnotationData = props.textAnnotationData;
        }
        return inst;
    }
    /** @internal */
    getTextBlocks() {
        return getTextBlocks(this);
    }
    /** @internal */
    updateTextBlocks(textBlocks) {
        return updateTextBlocks(this, textBlocks);
    }
    /** @internal */
    static onInserted(arg) {
        super.onInserted(arg);
        ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
    }
    /** @internal */
    static onUpdated(arg) {
        super.onUpdated(arg);
        ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
    }
}
/** An element that displays textual content within a 3d model.
 * The text is stored as a [TextAnnotation]($common) from which the element's [geometry]($docs/learning/common/GeometryStream.md) and [Placement]($common) are computed.
 * @see [[setAnnotation]] to change the textual content.
 * @public @preview
 */
export class TextAnnotation3d extends GraphicalElement3d /* implements ITextAnnotation */ {
    /** @internal */
    static get className() { return "TextAnnotation3d"; }
    /**
     * The default [[AnnotationTextStyle]] used by the TextAnnotation3d.
     * @beta
     */
    defaultTextStyle;
    /** The data associated with the text annotation. */
    _textAnnotationProps;
    /** Extract the textual content, if present.
     * @see [[setAnnotation]] to change it.
     */
    getAnnotation() {
        return this._textAnnotationProps ? TextAnnotation.fromJSON(this._textAnnotationProps) : undefined;
    }
    /** Change the textual content of the `TextAnnotation3d`.
     * @see [[getAnnotation]] to extract the current annotation.
     * @param annotation The new annotation
     */
    setAnnotation(annotation) {
        this._textAnnotationProps = annotation.toJSON();
    }
    constructor(props, iModel) {
        super(props, iModel);
        if (props.defaultTextStyle) {
            this.defaultTextStyle = new TextAnnotationUsesTextStyleByDefault(props.defaultTextStyle.id);
        }
        this._textAnnotationProps = parseTextAnnotationData(props.textAnnotationData)?.data;
    }
    /** Creates a new instance of `TextAnnotation3d` from its JSON representation. */
    static fromJSON(props, iModel) {
        return new TextAnnotation3d(props, iModel);
    }
    /**
     * Converts the current `TextAnnotation3d` instance to its JSON representation.
     * It also computes the `elementGeometryBuilderParams` property used to create the GeometryStream.
     * @inheritdoc
     */
    toJSON() {
        const props = super.toJSON();
        props.textAnnotationData = this._textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: this._textAnnotationProps }) : undefined;
        if (this._textAnnotationProps) {
            props.elementGeometryBuilderParams = getElementGeometryBuilderParams(this.iModel, this.model, this.category, this.placement, this._textAnnotationProps, this.defaultTextStyle ? this.defaultTextStyle.id : undefined);
        }
        return props;
    }
    /** Creates a new `TextAnnotation3d` instance with the specified properties.
     * @param iModelDb The iModel.
     * @param arg The arguments for creating the TextAnnotation3d.
     * @beta
     */
    static create(iModelDb, arg) {
        const elementProps = {
            classFullName: this.classFullName,
            textAnnotationData: arg.textAnnotationProps ? JSON.stringify({ version: TEXT_ANNOTATION_JSON_VERSION, data: arg.textAnnotationProps }) : undefined,
            defaultTextStyle: arg.defaultTextStyleId ? new TextAnnotationUsesTextStyleByDefault(arg.defaultTextStyleId).toJSON() : undefined,
            placement: arg.placement,
            model: arg.model,
            category: arg.category,
            code: arg.code ?? Code.createEmpty(),
        };
        return new this(elementProps, iModelDb);
    }
    /**
     * Updates the geometry of the TextAnnotation3d on insert and validates version..
     * @inheritdoc
     * @beta
     */
    static onInsert(arg) {
        super.onInsert(arg);
        this.validateVersionAndUpdateGeometry(arg);
    }
    /**
     * Updates the geometry of the TextAnnotation3d on update and validates version..
     * @inheritdoc
     * @beta
     */
    static onUpdate(arg) {
        super.onUpdate(arg);
        this.validateVersionAndUpdateGeometry(arg);
    }
    /**
     * Populates the `elementGeometryBuilderParams` property in the [TextAnnotation3dProps]($common).
     * Only does this if the `elementGeometryBuilderParams` is not already set and if there is actually a text annotation to produce geometry for.
     * Also, validates the version of the text annotation data and migrates it if necessary.
     * @beta
     */
    static validateVersionAndUpdateGeometry(arg) {
        const props = arg.props;
        const textAnnotationData = parseTextAnnotationData(props.textAnnotationData);
        if (!props.elementGeometryBuilderParams && textAnnotationData) {
            props.elementGeometryBuilderParams = getElementGeometryBuilderParams(arg.iModel, props.model, props.category, props.placement ?? Placement3d.fromJSON(), textAnnotationData.data, props.defaultTextStyle?.id);
        }
    }
    /**
     * TextAnnotation3d custom HandledProps include 'textAnnotationData'.
     * @inheritdoc
     * @internal
     */
    static _customHandledProps = [
        { propertyName: "textAnnotationData", source: "Class" },
    ];
    /**
     * TextAnnotation3d deserializes 'textAnnotationData'.
     * @inheritdoc
     * @beta
     */
    static deserialize(props) {
        const elProps = super.deserialize(props);
        const textAnnotationData = parseTextAnnotationData(props.row.textAnnotationData);
        if (textAnnotationData) {
            elProps.textAnnotationData = JSON.stringify(textAnnotationData);
        }
        return elProps;
    }
    /**
     * TextAnnotation3d serializes 'textAnnotationData'.
     * @inheritdoc
     * @beta
     */
    static serialize(props, iModel) {
        const inst = super.serialize(props, iModel);
        if (props.textAnnotationData !== undefined) {
            inst.textAnnotationData = props.textAnnotationData;
        }
        return inst;
    }
    /** @internal */
    getTextBlocks() {
        return getTextBlocks(this);
    }
    /** @internal */
    updateTextBlocks(textBlocks) {
        return updateTextBlocks(this, textBlocks);
    }
    /** @internal */
    static onInserted(arg) {
        super.onInserted(arg);
        ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
    }
    /** @internal */
    static onUpdated(arg) {
        super.onUpdated(arg);
        ElementDrivesTextAnnotation.updateFieldDependencies(arg.id, arg.iModel);
    }
}
function getTextBlocks(elem) {
    const annotation = elem.getAnnotation();
    return annotation ? [{ textBlock: annotation.textBlock, id: undefined }] : [];
}
function updateTextBlocks(elem, textBlocks) {
    assert(textBlocks.length === 1);
    assert(textBlocks[0].id === undefined);
    const annotation = elem.getAnnotation();
    if (!annotation) {
        // We must obtain the TextBlockAndId from the element in the first place, so the only way we could end up here is if
        // somebody removed the text annotation after we called getTextBlocks. That's gotta be a mistake.
        throw new Error("Text annotation element has no text");
    }
    annotation.textBlock = textBlocks[0].textBlock;
    elem.setAnnotation(annotation);
    elem.update();
}
/** The version of the JSON stored in `AnnotationTextStyleProps.settings` used by the code.
 * Uses the same semantics as [ECVersion]($ecschema-metadata).
 * @internal
*/
export const TEXT_STYLE_SETTINGS_JSON_VERSION = "1.0.0";
function migrateTextStyleSettings(oldData) {
    if (oldData.version === TEXT_STYLE_SETTINGS_JSON_VERSION)
        return oldData.data;
    // Place migration logic here.
    throw new Error(`Migration for settings from version ${oldData.version} to ${TEXT_STYLE_SETTINGS_JSON_VERSION} failed.`);
}
/**
 * The definition element that holds text style information.
 * The style is stored as a [TextStyleSettings]($common).
 * @beta
 */
export class AnnotationTextStyle extends DefinitionElement {
    /** @internal */
    static get className() { return "AnnotationTextStyle"; }
    /**
     * Optional text describing the `AnnotationTextStyle`.
     */
    description;
    /**
     * The text style settings for the `AnnotationTextStyle`.
     * @see [[TextStyleSettings]] for more information.
     */
    settings;
    constructor(props, iModel) {
        super(props, iModel);
        this.description = props.description;
        const settingsProps = AnnotationTextStyle.parseTextStyleSettings(props.settings);
        this.settings = TextStyleSettings.fromJSON(settingsProps?.data);
    }
    /**
     * Creates a Code for an `AnnotationTextStyle` given a name that is meant to be unique within the scope of the specified DefinitionModel.
     *
     * @param iModel - The IModelDb.
     * @param definitionModelId - The ID of the DefinitionModel that contains the AnnotationTextStyle and provides the scope for its name.
     * @param name - The AnnotationTextStyle name.
     * @beta
     */
    static createCode(iModel, definitionModelId, name) {
        const codeSpec = iModel.codeSpecs.getByName(BisCodeSpec.annotationTextStyle);
        return new Code({ spec: codeSpec.id, scope: definitionModelId, value: name });
    }
    /**
     * Creates a new instance of `AnnotationTextStyle` with the specified properties.
     *
     * @param iModelDb - The iModelDb.
     * @param arg - The arguments for creating the AnnotationTextStyle.
     * @beta
     */
    static create(iModelDb, arg) {
        const props = {
            classFullName: this.classFullName,
            model: arg.definitionModelId,
            code: this.createCode(iModelDb, arg.definitionModelId, arg.name).toJSON(),
            description: arg.description,
            settings: arg.settings ? JSON.stringify({ version: TEXT_STYLE_SETTINGS_JSON_VERSION, data: arg.settings }) : undefined,
        };
        return new this(props, iModelDb);
    }
    /**
     * Converts the current `AnnotationTextStyle` instance to its JSON representation.
     * @inheritdoc
     */
    toJSON() {
        const props = super.toJSON();
        props.description = this.description;
        props.settings = JSON.stringify({ version: TEXT_STYLE_SETTINGS_JSON_VERSION, data: this.settings.toJSON() });
        return props;
    }
    /** Creates a new instance of `AnnotationTextStyle` from its JSON representation. */
    static fromJSON(props, iModel) {
        return new AnnotationTextStyle(props, iModel);
    }
    /**
     * Validates that the AnnotationTextStyle's settings are valid before insert.
     * @inheritdoc
     * @beta
     */
    static onInsert(arg) {
        super.onInsert(arg);
        this.validateSettings(arg.props);
    }
    /**
     * Validates that the AnnotationTextStyle's settings are valid before update.
     * @inheritdoc
     * @beta
     */
    static onUpdate(arg) {
        super.onUpdate(arg);
        this.validateSettings(arg.props);
    }
    static validateSettings(props) {
        const settingProps = AnnotationTextStyle.parseTextStyleSettings(props.settings);
        if (!settingProps)
            return;
        const settings = TextStyleSettings.fromJSON(settingProps.data);
        const errors = settings.getValidationErrors();
        if (errors.length > 0) {
            throw new Error(`Invalid AnnotationTextStyle settings: ${errors.join(", ")}`);
        }
    }
    /**
     * AnnotationTextStyle custom HandledProps include 'settings'.
     * @inheritdoc
     * @beta
     */
    static _customHandledProps = [
        { propertyName: "settings", source: "Class" },
    ];
    /**
     * AnnotationTextStyle deserializes 'settings'.
     * @inheritdoc
     * @beta
     */
    static deserialize(props) {
        const elProps = super.deserialize(props);
        const settings = this.parseTextStyleSettings(props.row.settings);
        if (settings) {
            elProps.settings = JSON.stringify(settings);
        }
        return elProps;
    }
    /**
     * AnnotationTextStyle serializes 'settings'.
     * @inheritdoc
     * @beta
     */
    static serialize(props, iModel) {
        const inst = super.serialize(props, iModel);
        if (props.settings !== undefined) {
            inst.settings = props.settings;
        }
        return inst;
    }
    /** Parses, validates, and potentially migrates the text style settings data from a JSON string. */
    static parseTextStyleSettings(json) {
        if (!json)
            return undefined;
        return validateAndMigrateVersionedJSON(json, TEXT_STYLE_SETTINGS_JSON_VERSION, migrateTextStyleSettings);
    }
}
//# sourceMappingURL=TextAnnotationElement.js.map