/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { StyledLine } from './styled-line.js';
import { getPathToRoot } from './dom.js';
import { toStyledCharacters } from './measure-text.js';
import { processLayout } from './layout.js';
import { squashTextNodesWithMap } from './squash-text-nodes.js';
const getPlainTextFromDomNode = (node) => {
    const isTextNode = node.nodeName === '#text';
    const isStaticRender = node.nodeName === 'ink-static-render';
    if (isTextNode || isStaticRender) {
        const text = isTextNode
            ? node.nodeValue
            : (node.cachedRender?.selectableText ?? '');
        const styledChars = toStyledCharacters(text);
        return {
            styledChars: styledChars.clone(),
            charOffsetMap: new Map([[node, { start: 0, end: styledChars.length }]]),
        };
    }
    const { state } = processLayout(node, {
        initialState: () => ({
            styledChars: undefined,
            charOffsetMap: new Map(),
        }),
        onNewline(count, state) {
            state.styledChars ||= new StyledLine();
            for (let i = 0; i < count; i++) {
                state.styledChars.pushChar('\n', 0);
            }
        },
        onSpace(count, state) {
            state.styledChars ||= new StyledLine();
            for (let i = 0; i < count; i++) {
                state.styledChars.pushChar(' ', 0);
            }
        },
        onText(fragment, state) {
            const styledChars = toStyledCharacters(fragment.text);
            const fragmentStartOffset = state.styledChars?.length ?? 0;
            state.styledChars = state.styledChars
                ? state.styledChars.combine(styledChars)
                : styledChars.clone();
            const map = new Map();
            const offsetRef = { current: 0 };
            squashTextNodesWithMap(fragment.node, map, offsetRef);
            state.charOffsetMap.set(fragment.node, {
                start: fragmentStartOffset,
                end: fragmentStartOffset + offsetRef.current,
            });
            for (const [n, span] of map) {
                state.charOffsetMap.set(n, {
                    start: fragmentStartOffset + span.start,
                    end: fragmentStartOffset + span.end,
                });
            }
        },
    });
    const resultStyledChars = state.styledChars ?? new StyledLine();
    const result = {
        styledChars: resultStyledChars,
        charOffsetMap: state.charOffsetMap,
    };
    result.charOffsetMap.set(node, { start: 0, end: resultStyledChars.length });
    return result;
};
export const comparePoints = (nodeA, offsetA, nodeB, offsetB) => {
    if (nodeA === nodeB) {
        return offsetA === offsetB ? 0 : offsetA < offsetB ? -1 : 1;
    }
    const pathA = getPathToRoot(nodeA);
    const pathB = getPathToRoot(nodeB);
    let lca;
    let i = 0;
    while (i < pathA.length && i < pathB.length && pathA[i] === pathB[i]) {
        lca = pathA[i];
        i++;
    }
    if (!lca) {
        return 0;
    }
    if (i === pathA.length) {
        const childB = pathB[i];
        const indexChildB = nodeA.childNodes.indexOf(childB);
        if (offsetA <= indexChildB)
            return -1;
        return 1;
    }
    if (i === pathB.length) {
        const childA = pathA[i];
        const indexChildA = nodeB.childNodes.indexOf(childA);
        if (indexChildA < offsetB)
            return -1;
        return 1;
    }
    const childA = pathA[i];
    const childB = pathB[i];
    const indexChildA = lca.childNodes.indexOf(childA);
    const indexChildB = lca.childNodes.indexOf(childB);
    return indexChildA < indexChildB ? -1 : 1;
};
const getRangeCharacterOffsets = (range, charOffsetMap) => {
    if (!range.startContainer || !range.endContainer) {
        return undefined;
    }
    const getOffset = (node, offset) => {
        const nodeRange = charOffsetMap.get(node);
        if (!nodeRange) {
            return undefined;
        }
        if (node.nodeName === '#text') {
            return nodeRange.start + offset;
        }
        if (node.nodeName === 'ink-static-render') {
            return nodeRange.start + offset;
        }
        const element = node;
        if (offset >= element.childNodes.length) {
            return nodeRange.end;
        }
        const child = element.childNodes[offset];
        const childRange = charOffsetMap.get(child);
        if (childRange) {
            return childRange.start;
        }
        for (let i = offset - 1; i >= 0; i--) {
            const sibling = element.childNodes[i];
            const siblingRange = charOffsetMap.get(sibling);
            if (siblingRange) {
                return siblingRange.end;
            }
        }
        return nodeRange.start;
    };
    const start = getOffset(range.startContainer, range.startOffset);
    const end = getOffset(range.endContainer, range.endOffset);
    if (start === undefined || end === undefined) {
        return undefined;
    }
    return {
        start,
        end,
    };
};
export class Range {
    startContainer = undefined;
    startOffset = 0;
    endContainer = undefined;
    endOffset = 0;
    collapsed = true;
    commonAncestorContainer = undefined;
    setStart(node, offset) {
        this.startContainer = node;
        this.startOffset = offset;
        this.updateCollapsed();
        this.updateCommonAncestor();
    }
    setEnd(node, offset) {
        this.endContainer = node;
        this.endOffset = offset;
        this.updateCollapsed();
        this.updateCommonAncestor();
    }
    collapse(toStart) {
        if (toStart) {
            this.endContainer = this.startContainer;
            this.endOffset = this.startOffset;
        }
        else {
            this.startContainer = this.endContainer;
            this.startOffset = this.endOffset;
        }
        this.collapsed = true;
        this.updateCommonAncestor();
    }
    selectNode(node) {
        if (!node.parentNode) {
            throw new Error('Node has no parent');
        }
        const index = node.parentNode.childNodes.indexOf(node);
        this.setStart(node.parentNode, index);
        this.setEnd(node.parentNode, index + 1);
    }
    selectNodeContents(node) {
        this.setStart(node, 0);
        if (node.nodeName === '#text') {
            this.setEnd(node, node.nodeValue.length);
        }
        else {
            this.setEnd(node, node.childNodes.length);
        }
    }
    toString() {
        if (this.collapsed || !this.startContainer || !this.endContainer) {
            return '';
        }
        if (!this.commonAncestorContainer) {
            return '';
        }
        const { styledChars: fullStyledChars, charOffsetMap } = getPlainTextFromDomNode(this.commonAncestorContainer);
        const offsets = getRangeCharacterOffsets(this, charOffsetMap);
        if (!offsets) {
            return '';
        }
        const sliced = fullStyledChars.slice(offsets.start, offsets.end);
        return sliced.getText();
    }
    updateCollapsed() {
        this.collapsed =
            this.startContainer === this.endContainer &&
                this.startOffset === this.endOffset;
    }
    updateCommonAncestor() {
        if (!this.startContainer || !this.endContainer) {
            this.commonAncestorContainer = undefined;
            return;
        }
        if (this.startContainer === this.endContainer) {
            this.commonAncestorContainer = this.startContainer;
            return;
        }
        const startPath = this.getPath(this.startContainer);
        const endPath = this.getPath(this.endContainer);
        let common;
        for (let i = 0; i < Math.min(startPath.length, endPath.length); i++) {
            if (startPath[i] === endPath[i]) {
                common = startPath[i];
            }
            else {
                break;
            }
        }
        this.commonAncestorContainer = common;
    }
    getPath(node) {
        return getPathToRoot(node);
    }
}
export class Selection {
    anchorNode = undefined;
    anchorOffset = 0;
    focusNode = undefined;
    focusOffset = 0;
    isCollapsed = true;
    rangeCount = 0;
    ranges = [];
    listeners = new Set();
    onChange(listener) {
        this.listeners.add(listener);
        return () => {
            this.listeners.delete(listener);
        };
    }
    toString() {
        return this.ranges.map(range => range.toString()).join('');
    }
    getRangeAt(index) {
        return this.ranges[index];
    }
    addRange(range) {
        if (!this.ranges.includes(range)) {
            this.ranges.push(range);
            this.rangeCount = this.ranges.length;
            if (this.rangeCount === 1) {
                this.anchorNode = range.startContainer;
                this.anchorOffset = range.startOffset;
                this.focusNode = range.endContainer;
                this.focusOffset = range.endOffset;
                this.isCollapsed = range.collapsed;
            }
            this.notifyChange();
        }
    }
    removeRange(range) {
        const index = this.ranges.indexOf(range);
        if (index !== -1) {
            this.ranges.splice(index, 1);
            this.rangeCount = this.ranges.length;
            if (this.rangeCount === 0) {
                this.clearState();
            }
            this.notifyChange();
        }
    }
    removeAllRanges() {
        this.ranges = [];
        this.rangeCount = 0;
        this.clearState();
        this.notifyChange();
    }
    collapse(node, offset) {
        this.removeAllRanges();
        const range = new Range();
        range.setStart(node, offset);
        range.setEnd(node, offset);
        this.addRange(range);
    }
    containsNode(node, allowPartialContainment = false) {
        if (!node.parentNode) {
            return false;
        }
        const parent = node.parentNode;
        const index = parent.childNodes.indexOf(node);
        const nodeStartContainer = parent;
        const nodeStartOffset = index;
        const nodeEndContainer = parent;
        const nodeEndOffset = index + 1;
        return this.ranges.some(range => {
            if (!range.startContainer || !range.endContainer) {
                return false;
            }
            const startToStart = comparePoints(range.startContainer, range.startOffset, nodeStartContainer, nodeStartOffset);
            const endToEnd = comparePoints(range.endContainer, range.endOffset, nodeEndContainer, nodeEndOffset);
            if (allowPartialContainment) {
                const startToEnd = comparePoints(range.startContainer, range.startOffset, nodeEndContainer, nodeEndOffset);
                const endToStart = comparePoints(range.endContainer, range.endOffset, nodeStartContainer, nodeStartOffset);
                return startToEnd < 0 && endToStart > 0;
            }
            return startToStart <= 0 && endToEnd >= 0;
        });
    }
    clearState() {
        this.anchorNode = undefined;
        this.anchorOffset = 0;
        this.focusNode = undefined;
        this.focusOffset = 0;
        this.isCollapsed = true;
    }
    notifyChange() {
        for (const listener of this.listeners) {
            listener();
        }
    }
}
export const applySelectionToStyledChars = (styledChars, selectionState, selectionStyle) => {
    const { range, currentOffset } = selectionState;
    const { start, end } = range;
    const newStyledChars = styledChars.clone();
    let charCodeUnitOffset = 0;
    for (let i = 0; i < newStyledChars.length; i++) {
        const val = newStyledChars.getValue(i);
        const globalOffset = currentOffset + charCodeUnitOffset;
        if (globalOffset >= start && globalOffset < end) {
            if (selectionStyle) {
                selectionStyle(newStyledChars, i);
            }
            else {
                newStyledChars.setInverted(i, true);
            }
        }
        charCodeUnitOffset += val.length;
    }
    selectionState.currentOffset += charCodeUnitOffset;
    return newStyledChars;
};
//# sourceMappingURL=selection.js.map