/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, compareArrays, debugAssert, fail } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EmptyKey, forEachNodeSubsequence, } from "../core/index.js";
import { currentObserver, buildNodeComparator } from "../feature-libraries/index.js";
import { TreeAlpha } from "../shared-tree/index.js";
import { enumFromStrings, eraseSchemaDetails, getInnerNode, SchemaFactory, SchemaFactoryAlpha, TreeArrayNode, TreeBeta, } from "../simple-tree/index.js";
import { brand, mapIterable, validateIndex, validateIndexRange } from "../util/index.js";
import { charactersFromString } from "./textDomain.js";
const sf = new SchemaFactoryAlpha("com.fluidframework.text.formatted");
class TextNode extends sf.object("Text", {
    content: SchemaFactory.required([() => StringArray], { key: EmptyKey }),
}) {
    constructor() {
        super(...arguments);
        this.defaultFormat = new FormattedTextAsTree.CharacterFormat(defaultFormat);
    }
    insertAt(index, additionalCharacters) {
        this.content.insertAt(index, TreeArrayNode.spread(textAtomsFromString(additionalCharacters, this.defaultFormat)));
    }
    removeRange(index, end) {
        this.content.removeRange(index, end);
    }
    characters() {
        return mapIterable(this.content, (atom) => atom.content.content);
    }
    charactersCopy() {
        const result = this.content.charactersCopy();
        debugAssert(() => compareArrays(result, this.charactersCopy_reference()) ||
            "invalid charactersCopy optimizations");
        return result;
    }
    characterCount() {
        return this.content.length;
    }
    fullString() {
        const result = this.content.fullString();
        debugAssert(() => result === this.fullString_reference() || "invalid fullString optimizations");
        return result;
    }
    /**
     * A non-optimized reference implementation of fullString.
     */
    fullString_reference() {
        return [...this.characters()].join("");
    }
    /**
     * Unoptimized trivially correct implementation of charactersCopy.
     */
    charactersCopy_reference() {
        return [...this.characters()];
    }
    static fromString(value, format) {
        // Constructing an ArrayNode from an iterator is supported, so creating an array from the iterable of characters seems like it's not necessary here,
        // but to reduce the risk of incorrect data interpretation, we actually ban this in the special case where the iterable is a string directly, which is the case here.
        // Thus the array construction here is necessary to avoid a runtime error.
        return new TextNode({
            content: [
                ...textAtomsFromString(value, format ?? new FormattedTextAsTree.CharacterFormat(defaultFormat)),
            ],
        });
    }
    charactersWithFormatting() {
        return this.content;
    }
    insertWithFormattingAt(index, additionalCharacters) {
        this.content.insertAt(index, TreeArrayNode.spread(additionalCharacters));
    }
    formatRange(start, end, format) {
        const formatStart = start ?? 0;
        validateIndex(formatStart, this.content, "FormattedTextAsTree.formatRange", true);
        const formatEnd = Math.min(this.content.length, end ?? this.content.length);
        validateIndexRange(formatStart, formatEnd, this.content, "FormattedTextAsTree.formatRange");
        const branch = TreeAlpha.branch(this);
        const applyFormatting = () => {
            for (let i = formatStart; i < formatEnd; i++) {
                const atom = this.content[i];
                if (atom === undefined) {
                    throw new UsageError("Index out of bounds while formatting text range.");
                }
                for (const [key, value] of Object.entries(format)) {
                    // Object.entries should only return string keyed enumerable own properties.
                    // The TypeScript typing does not account for this, and thus this assertion is necessary for this code to compile.
                    assert(typeof key === "string", 0xcc8 /* Object.entries returned a non-string key. */);
                    const f = FormattedTextAsTree.CharacterFormat.fields.get(key);
                    if (f === undefined) {
                        throw new UsageError(`Unknown format key: ${key}`);
                    }
                    // Ensures that if the input is a node, it is cloned before being inserted into the tree.
                    atom.format[key] = TreeBeta.clone(TreeBeta.create(f, value));
                }
            }
        };
        if (branch === undefined) {
            // If this node does not have a corresponding branch, then it is unhydrated.
            // I.e., it is not part of a collaborative session yet.
            // Therefore, we don't need to run the edits as a transaction.
            applyFormatting();
        }
        else {
            // Wrap all formatting operations in a single transaction for atomicity.
            branch.runTransaction(() => {
                applyFormatting();
            });
        }
    }
    getUniformRun(startIndex, endIndex) {
        return this.content.getUniformRun(startIndex, endIndex);
    }
    getString(startIndex, endIndex) {
        return this.content.getString(startIndex, endIndex);
    }
}
const defaultFormat = {
    bold: false,
    italic: false,
    underline: false,
    size: 12,
    font: "Arial",
};
const formatKey = brand("format");
function textAtomsFromString(value, format) {
    const result = mapIterable(charactersFromString(value), (char) => new FormattedTextAsTree.StringAtom({
        content: { content: char },
        format: TreeBeta.clone(format),
    }));
    return result;
}
class StringArray extends sf.array("StringArray", [() => FormattedTextAsTree.StringAtom]) {
    withBorrowedSequenceCursor(f) {
        const innerNode = getInnerNode(this);
        // Since the cursor will be used to read content from the tree and won't track observations,
        // treat it as if it observed the whole subtree.
        currentObserver?.observeNodeDeep(innerNode);
        const cursor = innerNode.borrowCursor();
        cursor.enterField(EmptyKey);
        const result = f(cursor);
        cursor.exitField();
        return result;
    }
    getCharactersSubarray(startIndex, endIndex) {
        return this.withBorrowedSequenceCursor((cursor) => {
            const result = [];
            forEachNodeSubsequence(cursor, startIndex, endIndex, () => {
                debugAssert(() => cursor.type === FormattedTextAsTree.StringAtom.identifier ||
                    "invalid fullString type optimizations");
                cursor.enterField(EmptyKey);
                cursor.enterNode(0);
                let content;
                switch (cursor.type) {
                    case FormattedTextAsTree.StringTextAtom.identifier: {
                        cursor.enterField(EmptyKey);
                        cursor.enterNode(0);
                        content = cursor.value;
                        debugAssert(() => typeof content === "string" || "invalid fullString type optimizations");
                        cursor.exitNode();
                        cursor.exitField();
                        break;
                    }
                    case FormattedTextAsTree.StringLineAtom.identifier: {
                        content = "\n";
                        break;
                    }
                    default: {
                        fail(0xcde /* Unsupported node type in text array */, () => `${cursor.type}`);
                    }
                }
                cursor.exitNode();
                cursor.exitField();
                result.push(content);
            });
            return result;
        });
    }
    charactersCopy() {
        return this.getCharactersSubarray(0, this.length);
    }
    fullString() {
        return this.charactersCopy().join("");
    }
    getString(startIndex, endIndex = this.length) {
        validateIndexRange(startIndex, endIndex, this, "FormattedTextAsTree.getString");
        return this.getCharactersSubarray(startIndex, endIndex).join("");
    }
    getUniformRun(startIndex, endIndex = this.length) {
        validateIndexRange(startIndex, endIndex, this, "FormattedTextAsTree.getUniformRun");
        if (endIndex === startIndex) {
            throw new UsageError("endIndex must be greater than startIndex for getUniformRun.");
        }
        const arrayLength = this.length;
        return this.withBorrowedSequenceCursor((cursor) => {
            cursor.enterNode(startIndex);
            // Capture the content type of the first atom
            cursor.enterField(EmptyKey);
            cursor.enterNode(0);
            const contentType = cursor.type;
            cursor.exitNode();
            cursor.exitField();
            // Build a comparator from the format subtree of the first atom
            // This compares by field key
            cursor.enterField(formatKey);
            cursor.enterNode(0);
            const formatComparator = buildNodeComparator(cursor);
            cursor.exitNode();
            cursor.exitField();
            let runLength = 1;
            const limit = Math.min(endIndex, arrayLength) - startIndex;
            while (runLength < limit && cursor.nextNode()) {
                // Compare atom type
                cursor.enterField(EmptyKey);
                cursor.enterNode(0);
                const typeMatches = cursor.type === contentType;
                cursor.exitNode();
                cursor.exitField();
                if (!typeMatches) {
                    break;
                }
                // Compare format subtree using the compiled comparator
                cursor.enterField(formatKey);
                cursor.enterNode(0);
                const formatMatches = formatComparator(cursor);
                cursor.exitNode();
                cursor.exitField();
                if (formatMatches !== true) {
                    break;
                }
                runLength++;
            }
            cursor.exitNode();
            return runLength;
        });
    }
}
/**
 * A collection of text related types, schema and utilities for working with text beyond the basic {@link SchemaStatics.string}.
 * @privateRemarks
 * This has hard-coded assumptions about what kind of embedded content and what kind of formatting is supported.
 * We will want to generalize this with a more generic schema factory function like with table.
 * Then either that and/or the output from it can be package exported.
 * This version is just an initial prototype.
 * @internal
 */
export var FormattedTextAsTree;
(function (FormattedTextAsTree) {
    /**
     * Formatting options for characters.
     * @internal
     */
    class CharacterFormat extends sf.objectAlpha("CharacterFormat", {
        bold: SchemaFactory.boolean,
        italic: SchemaFactory.boolean,
        underline: SchemaFactory.boolean,
        size: SchemaFactory.number,
        font: SchemaFactory.string,
    }) {
    }
    FormattedTextAsTree.CharacterFormat = CharacterFormat;
    /**
     * Unit in the string representing a single character.
     * @internal
     */
    class StringTextAtom extends sf.object("StringTextAtom", {
        /**
         * The underlying text content of this atom.
         * @remarks
         * This is typically a single unicode codepoint, and thus may contain multiple utf-16 surrogate pair code units.
         * Using longer strings is still valid. For example, so users might store whole grapheme clusters here, or even longer sections of text.
         * Anything combined into a single atom will be treated atomically, and can not be partially selected or formatted.
         * Using larger atoms and splitting them as needed is NOT a recommended approach, since this will result in poor merge behavior for concurrent edits.
         * Instead atoms should always be the smallest unit of text which will be independently selected, moved or formatted.
         * @privateRemarks
         * This content logically represents the whole atom's content, so using {@link EmptyKey} makes sense to help indicate that.
         */
        content: SchemaFactory.required([SchemaFactory.string], { key: EmptyKey }),
    }) {
    }
    FormattedTextAsTree.StringTextAtom = StringTextAtom;
    /**
     * Tag with which a line in text can be formatted from HTML.
     * @internal
     */
    FormattedTextAsTree.LineTag = enumFromStrings(sf.scopedFactory("lineTag"), [
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "li",
        "ol",
        "checked",
        "unchecked",
        "blockquote",
        "codeBlock",
    ]);
    /**
     * Unit in the string representing a new line character with line formatting.
     * @remarks
     * This aligns with how Quill represents line formatting.
     * Quill formats line attributes (headers, list, blockquote, etc... ) on the newline character
     * and only lines using this atom can have line-specific formatting.
     * The optional indent level mirrors Quill's indent attribute,
     * which is applies to the line before the line break.
     * Any tagged line can be indented independently.
     * @internal
     */
    class StringLineAtom extends sf.object("StringLineAtom", {
        tag: FormattedTextAsTree.LineTag.schema,
        indent: SchemaFactory.number,
    }) {
        constructor() {
            super(...arguments);
            this.content = "\n";
        }
    }
    FormattedTextAsTree.StringLineAtom = StringLineAtom;
    /**
     * Types of "atoms" that make up the text.
     * @internal
     */
    FormattedTextAsTree.StringAtomContent = [StringTextAtom, StringLineAtom];
    /**
     * A unit of the text, with formatting.
     * @internal
     */
    class StringAtom extends sf.object("StringAtom", {
        content: SchemaFactory.required(FormattedTextAsTree.StringAtomContent, { key: EmptyKey }),
        format: CharacterFormat,
    }) {
    }
    FormattedTextAsTree.StringAtom = StringAtom;
    /**
     * Schema for a text node.
     * @remarks
     * See {@link FormattedTextAsTree.Members} for the API.
     * See {@link FormattedTextAsTree.Statics} for static APIs on this Schema, including construction.
     * @internal
     */
    FormattedTextAsTree.Tree = eraseSchemaDetails()(TextNode);
})(FormattedTextAsTree || (FormattedTextAsTree = {}));
//# sourceMappingURL=textDomainFormatted.js.map