import { stringWidth } from './string-width/index.js';
import { stripAnsi } from './strip-ansi/index.js';
import { wrap } from './wrap-ansi/index.js';
const align = {
    right: (str, width) => `${' '.repeat(Math.max(0, width - stringWidth(str)))}${str}`,
    center: (str, width) => `${' '.repeat(Math.max(0, width - stringWidth(str)) >> 1)}${str}`,
};
const top = 0;
const right = 1;
const bottom = 2;
const left = 3;
export class UI {
    width;
    wrap;
    rows;
    constructor(opts) {
        this.width = opts.width;
        /* c8 ignore start */
        this.wrap = opts.wrap ?? true;
        /* c8 ignore stop */
        this.rows = [];
    }
    span(...args) {
        const cols = this.div(...args);
        cols.span = true;
    }
    resetOutput() {
        this.rows = [];
    }
    div(...args) {
        if (args.length === 0) {
            this.div('');
        }
        if (this.wrap &&
            this.shouldApplyLayoutDSL(...args) &&
            typeof args[0] === 'string') {
            return this.applyLayoutDSL(args[0]);
        }
        const cols = Object.assign(args.map(arg => {
            if (typeof arg === 'string') {
                return this.colFromString(arg);
            }
            return arg;
        }), { span: false });
        this.rows.push(cols);
        return cols;
    }
    shouldApplyLayoutDSL(...args) {
        return (args.length === 1 &&
            typeof args[0] === 'string' &&
            /[\t\n]/.test(args[0]));
    }
    applyLayoutDSL(str) {
        const rows = str.split('\n').map(row => row.split('\t'));
        let leftColumnWidth = 0;
        // simple heuristic for layout, make sure the
        // second column lines up along the left-hand.
        // don't allow the first column to take up more
        // than 50% of the screen.
        rows.forEach(columns => {
            if (columns.length > 1 &&
                stringWidth(String(columns[0])) > leftColumnWidth) {
                leftColumnWidth = Math.min(Math.floor(this.width * 0.5), stringWidth(String(columns[0])));
            }
        });
        // generate a table:
        //  replacing ' ' with padding calculations.
        //  using the algorithmically generated width.
        rows.forEach(columns => {
            this.div(...columns.map((r, i) => {
                return {
                    text: r.trim(),
                    padding: this.measurePadding(r),
                    width: i === 0 && columns.length > 1 ? leftColumnWidth : undefined,
                };
            }));
        });
        const row = this.rows[this.rows.length - 1];
        /* c8 ignore start */
        if (!row) {
            throw new Error('no rows found');
        }
        /* c8 ignore stop */
        return row;
    }
    colFromString(text) {
        return {
            text,
            padding: this.measurePadding(text),
        };
    }
    measurePadding(str) {
        // measure padding without ansi escape codes
        const noAnsi = stripAnsi(str);
        const [right = '', left = ''] = [
            noAnsi.match(/\s*$/)?.[0],
            noAnsi.match(/^\s*/)?.[0],
        ];
        return [0, right.length, 0, left.length];
    }
    toString() {
        const lines = [];
        this.rows.forEach(row => {
            this.rowToString(row, lines);
        });
        // don't display any lines with the
        // hidden flag set.
        return lines
            .filter(line => !line.hidden)
            .map(line => line.text)
            .join('\n');
    }
    rowToString(row, lines) {
        this.rasterize(row).forEach((rrow, r) => {
            let str = '';
            rrow.forEach((col, c) => {
                const column = row[c];
                const { width } = column; // the width with padding.
                const wrapWidth = this.negatePadding(column); // the width without padding.
                let ts = col; // temporary string used during alignment/padding.
                if (wrapWidth > stringWidth(col)) {
                    ts += ' '.repeat(wrapWidth - stringWidth(col));
                }
                // align the string within its column.
                if (column.align && column.align !== 'left' && this.wrap) {
                    const fn = align[column.align];
                    ts = fn(ts.trim(), wrapWidth);
                    if (stringWidth(ts) < wrapWidth) {
                        /* c8 ignore start */
                        const w = width || 0;
                        /* c8 ignore stop */
                        ts += ' '.repeat(w - stringWidth(ts) - 1);
                    }
                }
                // apply border and padding to string.
                const padding = column.padding || [0, 0, 0, 0];
                if (padding[left]) {
                    str += ' '.repeat(padding[left]);
                }
                str += addBorder(column, ts, '| ');
                str += ts;
                str += addBorder(column, ts, ' |');
                if (padding[right]) {
                    str += ' '.repeat(padding[right]);
                }
                // if prior row is span, try to render the
                // current row on the prior line.
                if (r === 0 && lines.length > 0) {
                    const lastLine = lines[lines.length - 1];
                    /* c8 ignore start */
                    if (!lastLine) {
                        throw new Error('last line not found');
                    }
                    /* c8 ignore stop */
                    str = this.renderInline(str, lastLine);
                }
            });
            // remove trailing whitespace.
            lines.push({
                text: str.replace(/ +$/, ''),
                span: row.span,
            });
        });
        return lines;
    }
    // if the full 'source' can render in
    // the target line, do so.
    renderInline(source, previousLine) {
        const match = source.match(/^ */);
        /* c8 ignore start */
        const leadingWhitespace = match ? match[0].length : 0;
        /* c8 ignore stop */
        const target = previousLine.text;
        const targetTextWidth = stringWidth(target.trimEnd());
        if (!previousLine.span) {
            return source;
        }
        // if we're not applying wrapping logic,
        // just always append to the span.
        if (!this.wrap) {
            previousLine.hidden = true;
            return target + source;
        }
        if (leadingWhitespace < targetTextWidth) {
            return source;
        }
        previousLine.hidden = true;
        return (target.trimEnd() +
            ' '.repeat(leadingWhitespace - targetTextWidth) +
            source.trimStart());
    }
    rasterize(row) {
        const rrows = [];
        const widths = this.columnWidths(row);
        let wrapped;
        // word wrap all columns, and create
        // a data-structure that is easy to rasterize.
        row.forEach((col, c) => {
            // leave room for left and right padding.
            col.width = widths[c];
            if (this.wrap) {
                wrapped = wrap(col.text, this.negatePadding(col), {
                    hard: true,
                }).split('\n');
            }
            else {
                wrapped = col.text.split('\n');
            }
            if (col.border) {
                wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
                wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
            }
            // add top and bottom padding.
            if (col.padding) {
                wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
                wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
            }
            wrapped.forEach((str, r) => {
                if (!rrows[r]) {
                    rrows.push([]);
                }
                /* c8 ignore start */
                const rrow = rrows[r] ?? [];
                /* c8 ignore stop */
                for (let i = 0; i < c; i++) {
                    if (rrow[i] === undefined) {
                        rrow.push('');
                    }
                }
                rrow.push(str);
            });
        });
        return rrows;
    }
    negatePadding(col) {
        /* c8 ignore start */
        let wrapWidth = col.width || 0;
        /* c8 ignore stop */
        if (col.padding) {
            wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
        }
        if (col.border) {
            wrapWidth -= 4;
        }
        return wrapWidth;
    }
    columnWidths(row) {
        if (!this.wrap) {
            return row.map(col => {
                return col.width || stringWidth(col.text);
            });
        }
        let unset = row.length;
        let remainingWidth = this.width;
        // column widths can be set in config.
        const widths = row.map(col => {
            if (col.width) {
                unset--;
                remainingWidth -= col.width;
                return col.width;
            }
            return undefined;
        });
        // any unset widths should be calculated.
        /* c8 ignore start */
        const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
        /* c8 ignore stop */
        return widths.map((w, i) => {
            if (w === undefined) {
                /* c8 ignore start */
                const col = row[i] ?? { text: '', padding: [] };
                /* c8 ignore stop */
                return Math.max(unsetWidth, _minWidth(col));
            }
            return w;
        });
    }
}
const addBorder = (col, ts, style) => {
    if (col.border) {
        if (/[.']-+[.']/.test(ts)) {
            return '';
        }
        if (ts.trim().length !== 0) {
            return style;
        }
        return '  ';
    }
    return '';
};
// calculates the minimum width of
// a column, based on padding preferences.
const _minWidth = (col) => {
    const padding = col.padding || [];
    const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
    if (col.border) {
        return minWidth + 4;
    }
    return minWidth;
};
const getWindowWidth = () => {
    /* c8 ignore start */
    if (typeof process === 'object' &&
        process.stdout &&
        process.stdout.columns) {
        return process.stdout.columns;
    }
    return 80;
};
/* c8 ignore stop */
export const cliui = (opts = {}) => new UI({
    /* c8 ignore start */
    width: opts?.width || getWindowWidth(),
    wrap: opts?.wrap,
    /* c8 ignore stop */
});
//# sourceMappingURL=index.js.map