"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module ElementGeometry
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextBlockLayout = exports.LineLayout = exports.RunLayout = exports.TextStyleResolver = void 0;
exports.layoutTextBlock = layoutTextBlock;
exports.computeLayoutTextBlockResult = computeLayoutTextBlockResult;
exports.computeGraphemeOffsets = computeGraphemeOffsets;
const core_common_1 = require("@itwin/core-common");
const core_geometry_1 = require("@itwin/core-geometry");
const core_bentley_1 = require("@itwin/core-bentley");
const LineBreaker = require("linebreak");
const TextAnnotationElement_1 = require("./TextAnnotationElement");
/** @internal */
function createFindTextStyleImpl(iModel) {
    return function findTextStyleImpl(id) {
        const annotationTextStyle = iModel.elements.tryGetElement(id);
        if (annotationTextStyle && annotationTextStyle instanceof TextAnnotationElement_1.AnnotationTextStyle) {
            return annotationTextStyle.settings;
        }
        return core_common_1.TextStyleSettings.fromJSON();
    };
}
/**
 * Lays out the contents of a TextBlock into a series of lines containing runs.
 * Each paragraph is decomposed into a series of lines.
 * Each series of consecutive non-linebreak runs within a paragraph is concatenated into one line.
 * If the document specifies a width > 0, individual lines are split to try to avoid exceeding that width.
 * Individual TextRuns can be split onto multiple lines at word boundaries if necessary. Individual FractionRuns are never split.
 * @see [[computeLayoutTextBlockResult]]
 * @beta
 */
function layoutTextBlock(args) {
    const findFontId = args.findFontId ?? ((name, type) => args.iModel.fonts.findId({ name, type }) ?? 0);
    const computeTextRange = args.computeTextRange ?? ((x) => args.iModel.computeRangesForText(x));
    return new TextBlockLayout(args.textBlock, new LayoutContext(args.textStyleResolver, computeTextRange, findFontId));
}
/**
 * Gets the result of laying out the the contents of a TextBlock into a series of lines containing runs.
 * The visual layout accounts for the [[AnnotationTextStyle]]s, fonts, and [TextBlock.width]($common). It applies word-wrapping if needed.
 * The layout returned matches the visual layout of the geometry produced by [[appendTextAnnotationGeometry]].
 * @beta
 */
function computeLayoutTextBlockResult(args) {
    const layout = layoutTextBlock(args);
    return layout.toResult();
}
;
/**
 * Computes the range from the start of a [RunLayoutResult]($common) to the trailing edge of each grapheme.
 * It is the responsibility of the caller to determine the number and character indexes of the graphemes.
 * @returns If the [RunLayoutResult]($common)'s source is a [TextRun]($common), it returns an array containing the range of each grapheme.
 * Otherwise, it returns and empty array.
 * @beta
 */
function computeGraphemeOffsets(args) {
    const { source, runLayoutResult, graphemeCharIndexes, iModel } = args;
    const findFontId = args.findFontId ?? ((name, type) => iModel.fonts.findId({ name, type }) ?? 0);
    const computeTextRange = args.computeTextRange ?? ((x) => iModel.computeRangesForText(x));
    if (!(source instanceof core_common_1.TextRun) || runLayoutResult.characterCount === 0) {
        return [];
    }
    const style = core_common_1.TextStyleSettings.fromJSON(runLayoutResult.textStyle);
    const layoutContext = new LayoutContext(args.textStyleResolver, computeTextRange, findFontId);
    const graphemeRanges = [];
    graphemeCharIndexes.forEach((_, index) => {
        const nextGraphemeCharIndex = graphemeCharIndexes[index + 1] ?? runLayoutResult.characterCount;
        graphemeRanges.push(layoutContext.computeRangeForTextRun(style, source, runLayoutResult.characterOffset, nextGraphemeCharIndex).layout);
    });
    return graphemeRanges;
}
function scaleRange(range, scale) {
    range.low.scaleInPlace(scale);
    range.high.scaleInPlace(scale);
}
/**
 * Applies block level settings (lineSpacingFactor, paragraphSpacingFactor, widthFactor, frame, margins, justification, and leader) to a [TextStyleSettings]($common).
 * These must be set on the block, as they are meaningless on individual paragraphs/runs.
 * However, leaders are a special case and can override the block's leader settings.
 * Setting `isLeader` to `true` makes the [TextBlock]($common) settings not override the leader's settings.
 * @internal
 */
function applyBlockSettings(target, source, isLeader = false) {
    if (source === target) {
        return target;
    }
    const lineSpacingFactor = source.lineSpacingFactor ?? target.lineSpacingFactor;
    const paragraphSpacingFactor = source.paragraphSpacingFactor ?? target.paragraphSpacingFactor;
    const widthFactor = source.widthFactor ?? target.widthFactor;
    const justification = source.justification ?? target.justification;
    const frame = source.frame ?? target.frame;
    const margins = source.margins ?? target.margins;
    const leader = source.leader ?? target.leader;
    const leaderShouldChange = !isLeader && !target.leaderEquals(leader);
    if (lineSpacingFactor !== target.lineSpacingFactor ||
        paragraphSpacingFactor !== target.paragraphSpacingFactor ||
        widthFactor !== target.widthFactor ||
        justification !== target.justification ||
        !target.frameEquals(frame) ||
        !target.marginsEqual(margins) ||
        leaderShouldChange) {
        const cloneProps = {
            lineSpacingFactor,
            paragraphSpacingFactor,
            widthFactor,
            justification,
            frame,
            margins,
        };
        if (leaderShouldChange) {
            cloneProps.leader = leader;
        }
        target = target.clone(cloneProps);
    }
    return target;
}
/**
 * Resolves the effective style of TextBlockComponents and Leaders, taking into account overrides of the instance and its parent(s).
 * @beta
 */
class TextStyleResolver {
    /** The resolved style of the TextBlock. */
    blockSettings;
    constructor(args) {
        const findTextStyle = args.findTextStyle ?? createFindTextStyleImpl(args.iModel);
        this.blockSettings = findTextStyle(args.textStyleId);
        if (args.textBlock.styleOverrides)
            this.blockSettings = this.blockSettings.clone(args.textBlock.styleOverrides);
    }
    /**
     * Resolves the effective text style settings for a given TextBlockComponent, applying block-level overrides.
     */
    resolveSettings(overrides, isLeader = false) {
        let settings = this.blockSettings;
        if (overrides)
            settings = settings.clone(overrides);
        return applyBlockSettings(settings, this.blockSettings, isLeader);
    }
    resolveMarkerText(overrides, index) {
        const markerSettings = overrides.listMarker ?? this.blockSettings.listMarker;
        return (0, core_common_1.getMarkerText)(markerSettings, index);
    }
    /**
     * Computes the indentation based on its style and nesting depth.
     */
    resolveIndentation(styleOverrides, depth) {
        const overrides = this.resolveSettings(styleOverrides);
        const indentation = overrides.indentation;
        const tabInterval = overrides.tabInterval;
        return indentation + tabInterval * depth;
    }
}
exports.TextStyleResolver = TextStyleResolver;
class LayoutContext {
    textStyleResolver;
    _computeTextRange;
    _findFontId;
    _fontIds = new Map();
    constructor(textStyleResolver, _computeTextRange, _findFontId) {
        this.textStyleResolver = textStyleResolver;
        this._computeTextRange = _computeTextRange;
        this._findFontId = _findFontId;
    }
    findFontId(name, type) {
        let fontId = this._fontIds.get(name);
        if (undefined === fontId) {
            this._fontIds.set(name, fontId = this._findFontId(name, type));
        }
        return fontId;
    }
    computeRangeForText(chars, style, baselineShift) {
        if (chars.length === 0) {
            return {
                layout: new core_geometry_1.Range2d(0, 0, 0, style.textHeight),
                justification: new core_geometry_1.Range2d(),
            };
        }
        const fontId = this.findFontId(style.font.name, style.font.type);
        const { layout, justification } = this._computeTextRange({
            chars,
            fontId,
            baselineShift,
            bold: style.isBold,
            italic: style.isItalic,
            textHeight: style.textHeight,
            widthFactor: this.textStyleResolver.blockSettings.widthFactor,
        });
        if ("none" !== baselineShift) {
            const isSub = "subscript" === baselineShift;
            const scale = isSub ? style.subScriptScale : style.superScriptScale;
            const offsetFactor = isSub ? style.subScriptOffsetFactor : style.superScriptOffsetFactor;
            const offset = { x: 0, y: style.textHeight * offsetFactor };
            scaleRange(layout, scale);
            layout.cloneTranslated(offset, layout);
            scaleRange(justification, scale);
            justification.cloneTranslated(offset, justification);
        }
        return { layout, justification };
    }
    computeRangeForTextRun(style, run, charOffset, numChars) {
        let content;
        let baselineShift;
        if (run.type === "text") {
            content = run.content;
            baselineShift = run.baselineShift;
        }
        else {
            content = run.cachedContent;
            baselineShift = "none";
        }
        return this.computeRangeForText(content.substring(charOffset, charOffset + numChars), style, baselineShift);
    }
    computeRangeForFractionRun(style, source) {
        const numerator = this.computeRangeForText(source.numerator, style, "none").layout;
        scaleRange(numerator, style.stackedFractionScale);
        const denominator = this.computeRangeForText(source.denominator, style, "none").layout;
        scaleRange(denominator, style.stackedFractionScale);
        const numLen = numerator.xLength();
        const denomLen = denominator.xLength();
        switch (style.stackedFractionType) {
            case "horizontal": {
                if (numLen > denomLen) {
                    denominator.cloneTranslated({ x: (numLen - denomLen) / 2, y: 0 }, denominator);
                }
                else {
                    numerator.cloneTranslated({ x: (denomLen - numLen) / 2, y: 0 }, numerator);
                }
                numerator.cloneTranslated({ x: 0, y: 1.5 * denominator.yLength() }, numerator);
                break;
            }
            case "diagonal": {
                numerator.cloneTranslated({ x: 0, y: denominator.yLength() }, numerator);
                denominator.cloneTranslated({ x: numLen, y: 0 }, denominator);
                break;
            }
        }
        const layout = numerator.clone();
        layout.extendRange(denominator);
        return { layout, numerator, denominator };
    }
    computeRangeForTabRun(style, source, lengthFromLastTab) {
        const interval = source.styleOverrides.tabInterval ?? style.tabInterval;
        const tabEndX = interval - lengthFromLastTab % interval;
        const range = new core_geometry_1.Range2d(0, 0, 0, style.textHeight);
        range.extendXY(tabEndX, range.low.y);
        return range;
    }
}
function split(source) {
    if (source.length === 0) {
        return [];
    }
    let index = 0;
    const segments = [];
    const breaker = new LineBreaker(source);
    for (let brk = breaker.nextBreak(); brk; brk = breaker.nextBreak()) {
        segments.push({
            segment: source.slice(index, brk.position),
            index,
        });
        index = brk.position;
    }
    return segments;
}
function applyTabShift(run, parent, context) {
    if (run.source.type === "tab") {
        run.range.setFrom(context.computeRangeForTabRun(run.style, run.source, parent.lengthFromLastTab));
    }
}
/**
 * Represents the layout of a single run (text, fraction, or line break) within a line of text.
 * Stores information about the run's position, style, and font within the line.
 * Provides utilities for splitting text runs for word wrapping and converting to result objects.
 * @beta
 */
class RunLayout {
    source;
    charOffset;
    numChars;
    range;
    justificationRange;
    denominatorRange;
    numeratorRange;
    offsetFromLine;
    style;
    fontId;
    constructor(props) {
        this.source = props.source;
        this.charOffset = props.charOffset;
        this.numChars = props.numChars;
        this.range = props.range;
        this.justificationRange = props.justificationRange;
        this.denominatorRange = props.denominatorRange;
        this.numeratorRange = props.numeratorRange;
        this.offsetFromLine = props.offsetFromLine;
        this.style = props.style;
        this.fontId = props.fontId;
    }
    static create(source, context, cumulativeOverrides) {
        const style = context.textStyleResolver.resolveSettings(cumulativeOverrides);
        const fontId = context.findFontId(style.font.name, style.font.type);
        const charOffset = 0;
        const offsetFromLine = { x: 0, y: 0 };
        let numChars = 0;
        let range, justificationRange, numeratorRange, denominatorRange;
        switch (source.type) {
            case "field":
            case "text": {
                const content = source.type === "text" ? source.content : source.cachedContent;
                numChars = content.length;
                const ranges = context.computeRangeForTextRun(style, source, charOffset, numChars);
                range = ranges.layout;
                justificationRange = ranges.justification;
                break;
            }
            case "fraction": {
                numChars = 1;
                const ranges = context.computeRangeForFractionRun(style, source);
                range = ranges.layout;
                numeratorRange = ranges.numerator;
                denominatorRange = ranges.denominator;
                break;
            }
            default: { // "linebreak" or "tab"
                // "tab": Tabs rely on the context they are in, so we compute its range later.
                // lineBreak: We do this so that blank lines space correctly without special casing later.
                range = new core_geometry_1.Range2d(0, 0, 0, style.textHeight);
                break;
            }
        }
        return new RunLayout({ source, charOffset, numChars, range, justificationRange, denominatorRange, numeratorRange, offsetFromLine, style, fontId });
    }
    /** Compute a string representation, primarily for debugging purposes. */
    stringify() {
        return this.source.type === "text" ? this.source.content.substring(this.charOffset, this.charOffset + this.numChars) : this.source.stringify();
    }
    canWrap() {
        return this.source.type === "text";
    }
    cloneForWrap(args) {
        (0, core_bentley_1.assert)(this.canWrap());
        return new RunLayout({
            ...this,
            charOffset: args.charOffset,
            numChars: args.numChars,
            range: args.ranges.layout,
            justificationRange: args.ranges.justification,
            offsetFromLine: { ...this.offsetFromLine },
        });
    }
    split(context) {
        (0, core_bentley_1.assert)(this.charOffset === 0, "cannot re-split a run");
        if (!this.canWrap() || this.charOffset > 0) {
            return [this];
        }
        const myText = this.source.content.substring(this.charOffset, this.charOffset + this.numChars);
        const segments = split(myText);
        if (segments.length <= 1) {
            return [this];
        }
        return segments.map((segment) => {
            return this.cloneForWrap({
                ranges: context.computeRangeForText(segment.segment, this.style, this.source.baselineShift),
                charOffset: segment.index,
                numChars: segment.segment.length,
            });
        });
    }
    toResult() {
        const result = {
            fontId: this.fontId,
            characterOffset: this.charOffset,
            characterCount: this.numChars,
            range: this.range.toJSON(),
            offsetFromLine: this.offsetFromLine,
            textStyle: this.style.toJSON(),
        };
        if (this.justificationRange) {
            result.justificationRange = this.justificationRange.toJSON();
        }
        if (this.numeratorRange) {
            result.numeratorRange = this.numeratorRange.toJSON();
        }
        if (this.denominatorRange) {
            result.denominatorRange = this.denominatorRange.toJSON();
        }
        return result;
    }
}
exports.RunLayout = RunLayout;
/**
 * Represents the layout of a single line within a paragraph of a text block.
 * Contains a sequence of RunLayout objects, the computed range of the line, and its offset from the document origin.
 * Provides utilities for appending runs, computing ranges, and converting to result objects.
 * @beta
 */
class LineLayout {
    source;
    range = new core_geometry_1.Range2d(0, 0, 0, 0);
    runRange = new core_geometry_1.Range2d(0, 0, 0, 0); // Range of all runs excluding marker.
    justificationRange = new core_geometry_1.Range2d(0, 0, 0, 0);
    offsetFromDocument;
    depth;
    lengthFromLastTab = 0; // Used to track the length from the last tab for tab runs.
    _runs = [];
    _marker;
    constructor(source, style, context, depth = 0) {
        this.source = source;
        this.depth = depth;
        this.offsetFromDocument = { x: context?.textStyleResolver.resolveIndentation(style, depth) ?? 0, y: 0 };
    }
    /** Compute a string representation, primarily for debugging purposes. */
    stringify() {
        const runs = this._runs.map((run) => run.stringify());
        return `${runs.join("")}`;
    }
    /** Gets the array of RunLayout objects contained in this line. */
    get runs() { return this._runs; }
    /** Indicates whether this line contains any runs. */
    get isEmpty() { return this._runs.length === 0; }
    /** Gets the last RunLayout in this line. */
    get back() {
        (0, core_bentley_1.assert)(!this.isEmpty);
        return this._runs[this._runs.length - 1];
    }
    /**
     * Gets or sets the marker RunLayout for this line, used for lists.
     * A marker is the symbol or character that appears before each list item in a list, bullets, numbers, etc.
     * */
    get marker() { return this._marker; }
    set marker(value) { this._marker = value; }
    append(run) {
        this._runs.push(run);
        this.computeRanges();
    }
    /** Invoked every time a run is appended,. */
    computeRanges() {
        this.runRange.low.setZero();
        this.runRange.high.setZero();
        this.lengthFromLastTab = 0;
        let lineHeight = 0;
        let tallestNonFractionRun;
        for (const run of this._runs) {
            const runHeight = run.range.yLength();
            lineHeight = Math.max(lineHeight, runHeight);
            if (run.source.type !== "fraction" && (!tallestNonFractionRun || runHeight > tallestNonFractionRun.range.yLength())) {
                tallestNonFractionRun = run;
            }
        }
        // // The baseline for the line is the bottom of the tallest non-fraction run, centered in the line.
        let baseline = 0;
        if (tallestNonFractionRun) {
            baseline = (lineHeight + tallestNonFractionRun.range.yLength()) / 2;
        }
        for (const run of this._runs) {
            const runHeight = run.range.yLength();
            // Vertically align runs: normal text at baseline, fractions visually centered on text or line as appropriate.
            let yOffset = lineHeight - baseline;
            if (run.source.type === "fraction") {
                const denominatorHeight = run.denominatorRange?.yLength() ?? 0;
                if (tallestNonFractionRun && run.style.textHeight <= tallestNonFractionRun.style.textHeight) {
                    // Shift fraction to baseline, then down by half the denominator height so it appears centered relative to any non-fraction text of the same height.
                    yOffset = (lineHeight - baseline) - denominatorHeight / 2;
                }
                else {
                    // If the fraction text height is greater than the largest non-fraction text, just center it in the line.
                    yOffset = (lineHeight - runHeight) / 2;
                }
            }
            const runOffset = { x: this.runRange.high.x, y: yOffset };
            run.offsetFromLine = runOffset;
            const runLayoutRange = run.range.cloneTranslated(runOffset);
            this.runRange.extendRange(runLayoutRange);
            if ("linebreak" !== run.source.type) {
                const runJustificationRange = run.justificationRange?.cloneTranslated(runOffset);
                this.justificationRange.extendRange(runJustificationRange ?? runLayoutRange);
            }
            if ("tab" === run.source.type) {
                this.lengthFromLastTab = 0;
            }
            else {
                this.lengthFromLastTab += run.range.xLength();
            }
        }
        this.range.setFrom(this.runRange);
        if (this._marker) {
            const indentation = this.range.low.x;
            const x = indentation - (this._marker.style.tabInterval / 2) - this._marker.range.xLength();
            const runHeight = this._marker.range.yLength();
            const runOffset = {
                x,
                y: (lineHeight - runHeight) / 2 // Center the marker vertically in the line.
            };
            this._marker.offsetFromLine = runOffset;
            const markerRange = this._marker.range.cloneTranslated(this._marker.offsetFromLine);
            this.range.extendRange(markerRange);
        }
    }
    toResult() {
        return {
            runs: this.runs.map((x) => x.toResult()),
            marker: this.marker?.toResult(),
            range: this.range.toJSON(),
            justificationRange: this.justificationRange.toJSON(),
            offsetFromDocument: this.offsetFromDocument,
        };
    }
}
exports.LineLayout = LineLayout;
/**
 * Describes the layout of a text block as a collection of lines containing runs.
 * Computes the visual layout of the text block, including word wrapping, justification, and margins.
 * Provides access to the computed lines, ranges, and utilities for converting to result objects.
 * @beta
 */
class TextBlockLayout {
    source;
    /** @internal: This is primarily for debugging purposes. This is the range of text geometry */
    textRange = new core_geometry_1.Range2d();
    /** The range including margins of the [[TextBlock]]. */
    range = new core_geometry_1.Range2d();
    lines = [];
    _context;
    constructor(source, context) {
        this._context = context;
        this.source = source;
        if (source.width > 0) {
            this.textRange.low.x = 0;
            this.textRange.high.x = source.width;
        }
        this.populateLines(context);
        this.justifyLines(context);
        this.applyMargins(context.textStyleResolver.blockSettings.margins);
    }
    toResult() {
        return {
            lines: this.lines.map((x) => x.toResult()),
            range: this.range.toJSON(),
        };
    }
    /** Compute a string representation, primarily for debugging purposes. */
    stringify() {
        return this.lines.map((line) => line.stringify()).join("\n");
    }
    get _back() {
        (0, core_bentley_1.assert)(this.lines.length > 0);
        return this.lines[this.lines.length - 1];
    }
    populateLines(context) {
        const doc = this.source;
        if (!doc.children || doc.children.length === 0) {
            return;
        }
        let curLine = new LineLayout(doc.children[0], doc.children[0].styleOverrides, context);
        let childIndex = 0;
        for (const child of doc.children) {
            curLine = this.populateComponent(child, childIndex++, context, doc.width, curLine, doc, doc.styleOverrides);
        }
        if (curLine.runs.length > 0) {
            this.flushLine(context, curLine, doc.styleOverrides);
        }
    }
    populateComponent(component, componentIndex, context, docWidth, curLine, parent, cumulativeOverrides, depth = 0) {
        cumulativeOverrides = { ...cumulativeOverrides, ...component.styleOverrides };
        switch (component.type) {
            case "list": {
                // If we have any runs in the current line, flush it before starting the list.
                if (curLine.runs.length > 0) {
                    curLine = this.flushLine(context, curLine, cumulativeOverrides, component.children[0], true, depth + 1);
                }
                else {
                    // If not, we need to apply the indentation for the list to the first line.
                    curLine.offsetFromDocument.x = context.textStyleResolver.resolveIndentation(cumulativeOverrides, depth + 1);
                    curLine.depth = depth + 1;
                }
                // Iterate through each list item, setting the marker and populating its contents.
                component.children.forEach((child, index) => {
                    const markerContent = context.textStyleResolver.resolveMarkerText(cumulativeOverrides, index + 1);
                    const markerRun = core_common_1.TextRun.create({ content: markerContent });
                    curLine.marker = RunLayout.create(markerRun, context, cumulativeOverrides);
                    curLine = this.populateComponent(child, index, context, docWidth, curLine, component, cumulativeOverrides, depth + 1);
                });
                // Lastly flush the line.
                const nextSibling = parent?.children[componentIndex + 1];
                if (curLine && nextSibling) {
                    curLine = this.flushLine(context, curLine, cumulativeOverrides, nextSibling, true, depth);
                }
                break;
            }
            case "paragraph": {
                // Iterate through each paragraph child (either a list or a run), populating its contents.
                component.children.forEach((child, index) => {
                    curLine = this.populateComponent(child, index, context, docWidth, curLine, component, cumulativeOverrides, depth);
                });
                // Lastly flush the line.
                const nextSibling = parent?.children[componentIndex + 1];
                if (curLine && nextSibling) {
                    curLine = this.flushLine(context, curLine, cumulativeOverrides, nextSibling, true, depth);
                }
                break;
            }
            case "text": {
                const layout = RunLayout.create(component, context, cumulativeOverrides);
                // Text can be word-wrapped, so we need to split it into multiple runs if necessary.
                if (docWidth > 0) {
                    layout.split(context).forEach(r => { curLine = this.populateRun(curLine, r, context, cumulativeOverrides, docWidth); });
                }
                else {
                    curLine = this.populateRun(curLine, layout, context, cumulativeOverrides, docWidth);
                }
                break;
            }
            case "fraction":
            case "tab": {
                const layout = RunLayout.create(component, context, cumulativeOverrides);
                curLine = this.populateRun(curLine, layout, context, cumulativeOverrides, docWidth);
                break;
            }
            case "linebreak": {
                const layout = RunLayout.create(component, context, cumulativeOverrides);
                curLine.append(layout);
                curLine = this.flushLine(context, curLine, cumulativeOverrides, undefined, undefined, depth);
                break;
            }
            default: break;
        }
        return curLine;
    }
    ;
    populateRun(curLine, run, context, cumulativeOverrides, docWidth) {
        // If this is a tab, we need to apply the tab shift first, and then we can treat it like a text run.
        applyTabShift(run, curLine, context);
        // If our width is not set, then we don't have to compute word wrapping, so just append the run, and continue.
        if (docWidth <= 0) {
            curLine.append(run);
            return curLine;
        }
        // If not, we need to determine if we can append this run to the current line without exceeding the document width or if we need to word wrap.
        const runWidth = run.justificationRange?.xLength() ?? run.range.xLength();
        const lineWidth = curLine.runRange.xLength();
        const newWidth = runWidth + lineWidth + curLine.offsetFromDocument.x;
        // If true, then no word wrapping is required, so we can append to the current line.
        if (newWidth < docWidth || core_geometry_1.Geometry.isAlmostEqualNumber(newWidth, docWidth, core_geometry_1.Geometry.smallMetricDistance)) {
            curLine.append(run);
            return curLine;
        }
        // If not, do word wrapping
        if (curLine.runs.length === 0) {
            curLine.append(run);
            // Lastly, flush line
            curLine = this.flushLine(context, curLine, cumulativeOverrides, undefined, undefined, curLine.depth);
        }
        else {
            // First, flush line
            curLine = this.flushLine(context, curLine, cumulativeOverrides, undefined, undefined, curLine.depth);
            // Recompute tab shift if applicable
            applyTabShift(run, curLine, context);
            curLine.append(run);
        }
        return curLine;
    }
    ;
    flushLine(context, curLine, cumulativeOverrides, next, newParagraph = false, depth = 0) {
        next = next ?? curLine.source;
        // We want to guarantee that each layout line has at least one run.
        if (curLine.runs.length === 0) {
            if (this.lines.length === 0 || this._back.runs.length === 0) {
                return new LineLayout(next, cumulativeOverrides, context, depth);
            }
            if (curLine.source.type !== "linebreak") {
                const newLine = new LineLayout(next, cumulativeOverrides, context, depth);
                newLine.offsetFromDocument.y -= context.textStyleResolver.blockSettings.paragraphSpacingFactor * context.textStyleResolver.blockSettings.textHeight;
                return newLine;
            }
            const run = curLine.source.clone();
            curLine.append(RunLayout.create(run, context, cumulativeOverrides));
        }
        // Line origin is its baseline.
        const lineOffset = { ...curLine.offsetFromDocument }; // Start with the line's original offset, which includes indentation.
        lineOffset.y -= curLine.range.yLength(); // Shift down the baseline
        // Place it below any existing lines
        if (this.lines.length > 0) {
            lineOffset.y += this._back.offsetFromDocument.y;
            lineOffset.y -= context.textStyleResolver.blockSettings.lineSpacingFactor * context.textStyleResolver.blockSettings.textHeight;
        }
        curLine.offsetFromDocument = lineOffset;
        // Update document range from computed line range and position
        this.textRange.extendRange(curLine.range.cloneTranslated(lineOffset));
        this.lines.push(curLine);
        if (newParagraph) {
            const newLine = new LineLayout(next, cumulativeOverrides, context, depth);
            newLine.offsetFromDocument.y -= context.textStyleResolver.blockSettings.paragraphSpacingFactor * context.textStyleResolver.blockSettings.textHeight;
            return newLine;
        }
        return new LineLayout(next, cumulativeOverrides, context, depth);
    }
    justifyLines(context) {
        // We don't want to justify empty text, or a single line of text whose width is 0. By default text is already left justified.
        if (this.lines.length < 1 || (this.lines.length === 1 && this.source.width === 0) || "left" === context.textStyleResolver.blockSettings.justification) {
            return;
        }
        // This is the minimum width of the document's bounding box.
        const docWidth = this.source.width;
        let minOffset = Number.MAX_VALUE;
        for (const line of this.lines) {
            const lineWidth = line.justificationRange.xLength() + line.offsetFromDocument.x;
            let offset = docWidth - lineWidth;
            if ("center" === context.textStyleResolver.blockSettings.justification) {
                offset = offset / 2;
            }
            line.offsetFromDocument.x += offset;
            minOffset = Math.min(offset, minOffset);
        }
        if (minOffset < 0) {
            // Shift left to accommodate lines that exceeded the document's minimum width.
            this.textRange.low.x += minOffset;
            this.textRange.high.x += minOffset;
        }
    }
    applyMargins(margins) {
        this.range = this.textRange.clone();
        if (this.range.isNull)
            return;
        // Disregard negative margins.
        const right = margins.right >= 0 ? margins.right : 0;
        const left = margins.left >= 0 ? margins.left : 0;
        const top = margins.top >= 0 ? margins.top : 0;
        const bottom = margins.bottom >= 0 ? margins.bottom : 0;
        const xHigh = this.textRange.high.x + right;
        const yHigh = this.textRange.high.y + top;
        const xLow = this.textRange.low.x - left;
        const yLow = this.textRange.low.y - bottom;
        this.range.extendXY(xHigh, yHigh);
        this.range.extendXY(xLow, yLow);
    }
}
exports.TextBlockLayout = TextBlockLayout;
//# sourceMappingURL=TextBlockLayout.js.map