import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
 * @license
 * Copyright 2025 Vybestack LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import React, { Fragment, useEffect, useId } from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../../colors.js';
import { getCachedStringWidth, toCodePoints } from '../../utils/textUtils.js';
import { useOverflowActions } from '../../contexts/OverflowContext.js';
let enableDebugLog = false;
/**
 * Minimum height for the MaxSizedBox component.
 * This ensures there is room for at least one line of content as well as the
 * message that content was truncated.
 */
export const MINIMUM_MAX_HEIGHT = 2;
export function setMaxSizedBoxDebugging(value) {
    enableDebugLog = value;
}
function debugReportError(message, element) {
    if (!enableDebugLog)
        return;
    if (!React.isValidElement(element)) {
        console.error(message, `Invalid element: '${String(element)}' typeof=${typeof element}`);
        return;
    }
    let sourceMessage = '<Unknown file>';
    try {
        const elementWithSource = element;
        const fileName = elementWithSource._source?.fileName;
        const lineNumber = elementWithSource._source?.lineNumber;
        sourceMessage = fileName ? `${fileName}:${lineNumber}` : '<Unknown file>';
    }
    catch (error) {
        console.error('Error while trying to get file name:', error);
    }
    console.error(message, `${String(element.type)}. Source: ${sourceMessage}`);
}
/**
 * A React component that constrains the size of its children and provides
 * content-aware truncation when the content exceeds the specified `maxHeight`.
 *
 * `MaxSizedBox` requires a specific structure for its children to correctly
 * measure and render the content:
 *
 * 1.  **Direct children must be `<Box>` elements.** Each `<Box>` represents a
 *     single row of content.
 * 2.  **Row `<Box>` elements must contain only `<Text>` elements.** These
 *     `<Text>` elements can be nested and there are no restrictions to Text
 *     element styling other than that non-wrapping text elements must be
 *     before wrapping text elements.
 *
 * **Constraints:**
 * - **Box Properties:** Custom properties on the child `<Box>` elements are
 *   ignored. In debug mode, runtime checks will report errors for any
 *   unsupported properties.
 * - **Text Wrapping:** Within a single row, `<Text>` elements with no wrapping
 *   (e.g., headers, labels) must appear before any `<Text>` elements that wrap.
 * - **Element Types:** Runtime checks will warn if unsupported element types
 *   are used as children.
 *
 * @example
 * <MaxSizedBox maxWidth={80} maxHeight={10}>
 *   <Box>
 *     <Text>This is the first line.</Text>
 *   </Box>
 *   <Box>
 *     <Text color="cyan" wrap="truncate">Non-wrapping Header: </Text>
 *     <Text>This is the rest of the line which will wrap if it's too long.</Text>
 *   </Box>
 *   <Box>
 *     <Text>
 *       Line 3 with <Text color="yellow">nested styled text</Text> inside of it.
 *     </Text>
 *   </Box>
 * </MaxSizedBox>
 */
export const MaxSizedBox = ({ children, maxWidth, maxHeight, overflowDirection = 'top', additionalHiddenLinesCount = 0, }) => {
    const id = useId();
    const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
    const laidOutStyledText = [];
    const targetMaxHeight = Math.max(Math.round(maxHeight ?? Number.MAX_SAFE_INTEGER), MINIMUM_MAX_HEIGHT);
    if (maxWidth === undefined) {
        throw new Error('maxWidth must be defined when maxHeight is set.');
    }
    function visitRows(element) {
        if (!React.isValidElement(element)) {
            return;
        }
        if (element.type === Fragment) {
            React.Children.forEach(element.props.children, visitRows);
            return;
        }
        if (element.type === Box) {
            layoutInkElementAsStyledText(element, maxWidth, laidOutStyledText);
            return;
        }
        debugReportError('MaxSizedBox children must be <Box> elements', element);
    }
    React.Children.forEach(children, visitRows);
    const contentWillOverflow = (targetMaxHeight !== undefined &&
        laidOutStyledText.length > targetMaxHeight) ||
        additionalHiddenLinesCount > 0;
    const visibleContentHeight = contentWillOverflow && targetMaxHeight !== undefined
        ? targetMaxHeight - 1
        : targetMaxHeight;
    const hiddenLinesCount = visibleContentHeight !== undefined
        ? Math.max(0, laidOutStyledText.length - visibleContentHeight)
        : 0;
    const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
    useEffect(() => {
        if (totalHiddenLines > 0) {
            addOverflowingId?.(id);
        }
        else {
            removeOverflowingId?.(id);
        }
        return () => {
            removeOverflowingId?.(id);
        };
    }, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
    const visibleStyledText = hiddenLinesCount > 0
        ? overflowDirection === 'top'
            ? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
            : laidOutStyledText.slice(0, visibleContentHeight)
        : laidOutStyledText;
    const visibleLines = visibleStyledText.map((line, index) => (_jsx(Box, { children: line.length > 0 ? (line.map((segment, segIndex) => (_jsx(Text, { ...segment.props, children: segment.text }, segIndex)))) : (_jsx(Text, { children: " " })) }, index)));
    return (_jsxs(Box, { flexDirection: "column", width: maxWidth, flexShrink: 0, children: [totalHiddenLines > 0 && overflowDirection === 'top' && (_jsxs(Text, { color: Colors.Gray, wrap: "truncate", children: ["... first ", totalHiddenLines, " line", totalHiddenLines === 1 ? '' : 's', ' ', "hidden ..."] })), visibleLines, totalHiddenLines > 0 && overflowDirection === 'bottom' && (_jsxs(Text, { color: Colors.Gray, wrap: "truncate", children: ["... last ", totalHiddenLines, " line", totalHiddenLines === 1 ? '' : 's', ' ', "hidden ..."] }))] }));
};
/**
 * Flattens the child elements of MaxSizedBox into an array of `Row` objects.
 *
 * This function expects a specific child structure to function correctly:
 * 1. The top-level child of `MaxSizedBox` should be a single `<Box>`. This
 *    outer box is primarily for structure and is not directly rendered.
 * 2. Inside the outer `<Box>`, there should be one or more children. Each of
 *    these children must be a `<Box>` that represents a row.
 * 3. Inside each "row" `<Box>`, the children must be `<Text>` components.
 *
 * The structure should look like this:
 * <MaxSizedBox>
 *   <Box> // Row 1
 *     <Text>...</Text>
 *     <Text>...</Text>
 *   </Box>
 *   <Box> // Row 2
 *     <Text>...</Text>
 *   </Box>
 * </MaxSizedBox>
 *
 * It is an error for a <Text> child without wrapping to appear after a
 * <Text> child with wrapping within the same row Box.
 *
 * @param element The React node to flatten.
 * @returns An array of `Row` objects.
 */
function visitBoxRow(element) {
    if (!React.isValidElement(element) ||
        element.type !== Box) {
        debugReportError(`All children of MaxSizedBox must be <Box> elements`, element);
        return {
            noWrapSegments: [{ text: '<ERROR>', props: {} }],
            segments: [],
        };
    }
    if (enableDebugLog) {
        const boxProps = element.props;
        // Ensure the Box has no props other than the default ones and key.
        let maxExpectedProps = 4;
        if (boxProps.children !== undefined) {
            // Allow the key prop, which is automatically added by React.
            maxExpectedProps += 1;
        }
        if (boxProps.flexDirection !== undefined &&
            boxProps.flexDirection !== 'row') {
            debugReportError('MaxSizedBox children must have flexDirection="row".', element);
        }
        if (Object.keys(boxProps).length > maxExpectedProps) {
            debugReportError(`Boxes inside MaxSizedBox must not have additional props. ${Object.keys(boxProps).join(', ')}`, element);
        }
    }
    const row = {
        noWrapSegments: [],
        segments: [],
    };
    let hasSeenWrapped = false;
    function visitRowChild(element, parentProps) {
        if (element === null) {
            return;
        }
        if (typeof element === 'string' || typeof element === 'number') {
            const text = String(element);
            // Ignore empty strings as they don't need to be rendered.
            if (!text) {
                return;
            }
            const segment = { text, props: parentProps ?? {} };
            // Check the 'wrap' property from the merged props to decide the segment type.
            if (parentProps === undefined || parentProps.wrap === 'wrap') {
                hasSeenWrapped = true;
                row.segments.push(segment);
            }
            else {
                if (!hasSeenWrapped) {
                    row.noWrapSegments.push(segment);
                }
                else {
                    // put in the wrapped segment as the row is already stuck in wrapped mode.
                    row.segments.push(segment);
                    debugReportError('Text elements without wrapping cannot appear after elements with wrapping in the same row.', element);
                }
            }
            return;
        }
        if (!React.isValidElement(element)) {
            debugReportError('Invalid element.', element);
            return;
        }
        if (element.type === Fragment) {
            React.Children.forEach(element.props.children, (child) => visitRowChild(child, parentProps));
            return;
        }
        if (element.type !== Text) {
            debugReportError('Children of a row Box must be <Text> elements.', element);
            return;
        }
        // Merge props from parent <Text> elements. Child props take precedence.
        const { children, ...currentProps } = element.props;
        const mergedProps = parentProps === undefined
            ? currentProps
            : { ...parentProps, ...currentProps };
        React.Children.forEach(children, (child) => visitRowChild(child, mergedProps));
    }
    React.Children.forEach(element.props.children, (child) => visitRowChild(child, undefined));
    return row;
}
function layoutInkElementAsStyledText(element, maxWidth, output) {
    const row = visitBoxRow(element);
    if (row.segments.length === 0 && row.noWrapSegments.length === 0) {
        // Return a single empty line if there are no segments to display
        output.push([]);
        return;
    }
    const lines = [];
    const nonWrappingContent = [];
    let noWrappingWidth = 0;
    // First, lay out the non-wrapping segments
    row.noWrapSegments.forEach((segment) => {
        nonWrappingContent.push(segment);
        noWrappingWidth += getCachedStringWidth(segment.text);
    });
    if (row.segments.length === 0) {
        // This is a bit of a special case when there are no segments that allow
        // wrapping. It would be ideal to unify.
        const lines = [];
        let currentLine = [];
        nonWrappingContent.forEach((segment) => {
            const textLines = segment.text.split('\n');
            textLines.forEach((text, index) => {
                if (index > 0) {
                    lines.push(currentLine);
                    currentLine = [];
                }
                if (text) {
                    currentLine.push({ text, props: segment.props });
                }
            });
        });
        if (currentLine.length > 0 ||
            (nonWrappingContent.length > 0 &&
                nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))) {
            lines.push(currentLine);
        }
        for (const line of lines) {
            output.push(line);
        }
        return;
    }
    const availableWidth = maxWidth - noWrappingWidth;
    if (availableWidth < 1) {
        // No room to render the wrapping segments. Truncate the non-wrapping
        // content and append an ellipsis so the line always fits within maxWidth.
        // Handle line breaks in non-wrapping content when truncating
        const lines = [];
        let currentLine = [];
        let currentLineWidth = 0;
        for (const segment of nonWrappingContent) {
            const textLines = segment.text.split('\n');
            textLines.forEach((text, index) => {
                if (index > 0) {
                    // New line encountered, finish current line and start new one
                    lines.push(currentLine);
                    currentLine = [];
                    currentLineWidth = 0;
                }
                if (text) {
                    const textWidth = getCachedStringWidth(text);
                    // When there's no room for wrapping content, be very conservative
                    // For lines after the first line break, show only ellipsis if the text would be truncated
                    if (index > 0 && textWidth > 0) {
                        // This is content after a line break - just show ellipsis to indicate truncation
                        currentLine.push({ text: '…', props: {} });
                        currentLineWidth = getCachedStringWidth('…');
                    }
                    else {
                        // This is the first line or a continuation, try to fit what we can
                        const maxContentWidth = Math.max(0, maxWidth - getCachedStringWidth('…'));
                        if (textWidth <= maxContentWidth && currentLineWidth === 0) {
                            // Text fits completely on this line
                            currentLine.push({ text, props: segment.props });
                            currentLineWidth += textWidth;
                        }
                        else {
                            // Text needs truncation
                            const codePoints = toCodePoints(text);
                            let truncatedWidth = currentLineWidth;
                            let sliceEndIndex = 0;
                            for (const char of codePoints) {
                                const charWidth = getCachedStringWidth(char);
                                if (truncatedWidth + charWidth > maxContentWidth) {
                                    break;
                                }
                                truncatedWidth += charWidth;
                                sliceEndIndex++;
                            }
                            const slice = codePoints.slice(0, sliceEndIndex).join('');
                            if (slice) {
                                currentLine.push({ text: slice, props: segment.props });
                            }
                            currentLine.push({ text: '…', props: {} });
                            currentLineWidth = truncatedWidth + getCachedStringWidth('…');
                        }
                    }
                }
            });
        }
        // Add the last line if it has content or if the last segment ended with \n
        if (currentLine.length > 0 ||
            (nonWrappingContent.length > 0 &&
                nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))) {
            lines.push(currentLine);
        }
        // If we don't have any lines yet, add an ellipsis line
        if (lines.length === 0) {
            lines.push([{ text: '…', props: {} }]);
        }
        for (const line of lines) {
            output.push(line);
        }
        return;
    }
    // Now, lay out the wrapping segments
    let wrappingPart = [];
    let wrappingPartWidth = 0;
    function addWrappingPartToLines() {
        if (lines.length === 0) {
            lines.push([...nonWrappingContent, ...wrappingPart]);
        }
        else {
            if (noWrappingWidth > 0) {
                lines.push([
                    ...[{ text: ' '.repeat(noWrappingWidth), props: {} }],
                    ...wrappingPart,
                ]);
            }
            else {
                lines.push(wrappingPart);
            }
        }
        wrappingPart = [];
        wrappingPartWidth = 0;
    }
    function addToWrappingPart(text, props) {
        if (wrappingPart.length > 0 &&
            wrappingPart[wrappingPart.length - 1].props === props) {
            wrappingPart[wrappingPart.length - 1].text += text;
        }
        else {
            wrappingPart.push({ text, props });
        }
    }
    row.segments.forEach((segment) => {
        const linesFromSegment = segment.text.split('\n');
        linesFromSegment.forEach((lineText, lineIndex) => {
            if (lineIndex > 0) {
                addWrappingPartToLines();
            }
            const words = lineText.split(/(\s+)/); // Split by whitespace
            words.forEach((word) => {
                if (!word)
                    return;
                const wordWidth = getCachedStringWidth(word);
                if (wrappingPartWidth + wordWidth > availableWidth &&
                    wrappingPartWidth > 0) {
                    addWrappingPartToLines();
                    if (/^\s+$/.test(word)) {
                        return;
                    }
                }
                if (wordWidth > availableWidth) {
                    // Word is too long, needs to be split across lines
                    const wordAsCodePoints = toCodePoints(word);
                    let remainingWordAsCodePoints = wordAsCodePoints;
                    while (remainingWordAsCodePoints.length > 0) {
                        let splitIndex = 0;
                        let currentSplitWidth = 0;
                        for (const char of remainingWordAsCodePoints) {
                            const charWidth = getCachedStringWidth(char);
                            if (wrappingPartWidth + currentSplitWidth + charWidth >
                                availableWidth) {
                                break;
                            }
                            currentSplitWidth += charWidth;
                            splitIndex++;
                        }
                        if (splitIndex > 0) {
                            const part = remainingWordAsCodePoints
                                .slice(0, splitIndex)
                                .join('');
                            addToWrappingPart(part, segment.props);
                            wrappingPartWidth += getCachedStringWidth(part);
                            remainingWordAsCodePoints =
                                remainingWordAsCodePoints.slice(splitIndex);
                        }
                        if (remainingWordAsCodePoints.length > 0) {
                            addWrappingPartToLines();
                        }
                    }
                }
                else {
                    addToWrappingPart(word, segment.props);
                    wrappingPartWidth += wordWidth;
                }
            });
        });
        // Split omits a trailing newline, so we need to handle it here
        if (segment.text.endsWith('\n')) {
            addWrappingPartToLines();
        }
    });
    if (wrappingPart.length > 0) {
        addWrappingPartToLines();
    }
    for (const line of lines) {
        output.push(line);
    }
}
//# sourceMappingURL=MaxSizedBox.js.map