"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.table = void 0;
const chalk_1 = __importDefault(require("chalk"));
const js_yaml_1 = require("js-yaml");
const natural_orderby_1 = require("natural-orderby");
const node_util_1 = require("node:util");
const slice_ansi_1 = __importDefault(require("slice-ansi"));
const string_width_1 = __importDefault(require("string-width"));
const F = __importStar(require("../../flags"));
const screen_1 = require("../../screen");
const util_1 = require("../../util/util");
const write_1 = __importDefault(require("../write"));
/**
 * @deprecated `ux` will be removed in the next major. See https://github.com/oclif/core/discussions/999
 */
class Table {
    data;
    columns;
    options;
    constructor(data, columns, options = {}) {
        this.data = data;
        // assign columns
        this.columns = Object.keys(columns).map((key) => {
            const col = columns[key];
            const extended = col.extended ?? false;
            // turn null and undefined into empty strings by default
            const get = col.get ?? ((row) => row[key] ?? '');
            const header = typeof col.header === 'string' ? col.header : (0, util_1.capitalize)(key.replaceAll('_', ' '));
            const minWidth = Math.max(col.minWidth ?? 0, (0, string_width_1.default)(header) + 1);
            return {
                extended,
                get,
                header,
                key,
                minWidth,
            };
        });
        // assign options
        const { columns: cols, csv, extended, filter, output, printLine, sort, title } = options;
        this.options = {
            columns: cols,
            extended,
            filter,
            'no-header': options['no-header'] ?? false,
            'no-truncate': options['no-truncate'] ?? false,
            output: csv ? 'csv' : output,
            printLine: printLine ?? ((s) => write_1.default.stdout(s + '\n')),
            rowStart: ' ',
            sort,
            title,
        };
    }
    display() {
        // build table rows from input array data
        let rows = this.data.map((d) => {
            const row = {};
            for (const col of this.columns) {
                let val = col.get(d);
                if (typeof val !== 'string')
                    val = (0, node_util_1.inspect)(val, { breakLength: Number.POSITIVE_INFINITY });
                row[col.key] = val;
            }
            return row;
        });
        // filter rows
        if (this.options.filter) {
            let [header, regex] = this.options.filter.split('=');
            const isNot = header[0] === '-';
            if (isNot)
                header = header.slice(1);
            const col = this.findColumnFromHeader(header);
            if (!col || !regex)
                throw new Error('Filter flag has an invalid value');
            rows = rows.filter((d) => {
                const re = new RegExp(regex);
                const val = d[col.key];
                const match = val.match(re);
                return isNot ? !match : match;
            });
        }
        // sort rows
        if (this.options.sort) {
            const sorters = this.options.sort.split(',');
            const sortHeaders = sorters.map((k) => (k[0] === '-' ? k.slice(1) : k));
            const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map((c) => (v) => v[c.key]);
            const sortKeysOrder = sorters.map((k) => (k[0] === '-' ? 'desc' : 'asc'));
            rows = (0, natural_orderby_1.orderBy)(rows, sortKeys, sortKeysOrder);
        }
        // and filter columns
        if (this.options.columns) {
            const filters = this.options.columns.split(',');
            this.columns = this.filterColumnsFromHeaders(filters);
        }
        else if (!this.options.extended) {
            // show extended columns/properties
            this.columns = this.columns.filter((c) => !c.extended);
        }
        this.data = rows;
        switch (this.options.output) {
            case 'csv': {
                this.outputCSV();
                break;
            }
            case 'json': {
                this.outputJSON();
                break;
            }
            case 'yaml': {
                this.outputYAML();
                break;
            }
            default: {
                this.outputTable();
            }
        }
    }
    filterColumnsFromHeaders(filters) {
        // unique
        filters = [...new Set(filters)];
        const cols = [];
        for (const f of filters) {
            const c = this.columns.find((c) => c.header.toLowerCase() === f.toLowerCase());
            if (c)
                cols.push(c);
        }
        return cols;
    }
    findColumnFromHeader(header) {
        return this.columns.find((c) => c.header.toLowerCase() === header.toLowerCase());
    }
    getCSVRow(d) {
        const values = this.columns.map((col) => d[col.key] || '');
        const lineToBeEscaped = values.find((e) => e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(','));
        return values.map((e) => (lineToBeEscaped ? `"${e.replaceAll('"', '""')}"` : e));
    }
    outputCSV() {
        const { columns, data, options } = this;
        if (!options['no-header']) {
            options.printLine(columns.map((c) => c.header).join(','));
        }
        for (const d of data) {
            const row = this.getCSVRow(d);
            options.printLine(row.join(','));
        }
    }
    outputJSON() {
        this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2));
    }
    outputTable() {
        const { data, options } = this;
        // column truncation
        //
        // find max width for each column
        const columns = this.columns.map((c) => {
            const maxWidth = Math.max((0, string_width_1.default)('.'.padEnd(c.minWidth - 1)), (0, string_width_1.default)(c.header), getWidestColumnWith(data, c.key)) + 1;
            return {
                ...c,
                maxWidth,
                width: maxWidth,
            };
        });
        // terminal width
        const maxWidth = screen_1.stdtermwidth - 2;
        // truncation logic
        const shouldShorten = () => {
            // don't shorten if full mode
            if (options['no-truncate'] || (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK))
                return;
            // don't shorten if there is enough screen width
            const dataMaxWidth = (0, util_1.sumBy)(columns, (c) => c.width);
            const overWidth = dataMaxWidth - maxWidth;
            if (overWidth <= 0)
                return;
            // not enough room, short all columns to minWidth
            for (const col of columns) {
                col.width = col.minWidth;
            }
            // if sum(minWidth's) is greater than term width
            // nothing can be done so
            // display all as minWidth
            const dataMinWidth = (0, util_1.sumBy)(columns, (c) => c.minWidth);
            if (dataMinWidth >= maxWidth)
                return;
            // some wiggle room left, add it back to "needy" columns
            let wiggleRoom = maxWidth - dataMinWidth;
            const needyCols = columns
                .map((c) => ({ key: c.key, needs: c.maxWidth - c.width }))
                .sort((a, b) => a.needs - b.needs);
            for (const { key, needs } of needyCols) {
                if (!needs)
                    continue;
                const col = columns.find((c) => key === c.key);
                if (!col)
                    continue;
                if (wiggleRoom > needs) {
                    col.width = col.width + needs;
                    wiggleRoom -= needs;
                }
                else if (wiggleRoom) {
                    col.width = col.width + wiggleRoom;
                    wiggleRoom = 0;
                }
            }
        };
        shouldShorten();
        // print table title
        if (options.title) {
            options.printLine(options.title);
            // print title divider
            options.printLine(''.padEnd(columns.reduce((sum, col) => sum + col.width, 1), '='));
            options.rowStart = '| ';
        }
        // print headers
        if (!options['no-header']) {
            let headers = options.rowStart;
            for (const col of columns) {
                const header = col.header;
                headers += header.padEnd(col.width);
            }
            options.printLine(chalk_1.default.bold(headers));
            // print header dividers
            let dividers = options.rowStart;
            for (const col of columns) {
                const divider = ''.padEnd(col.width - 1, '─') + ' ';
                dividers += divider.padEnd(col.width);
            }
            options.printLine(chalk_1.default.bold(dividers));
        }
        // print rows
        for (const row of data) {
            // find max number of lines
            // for all cells in a row
            // with multi-line strings
            let numOfLines = 1;
            for (const col of columns) {
                const d = row[col.key];
                const lines = d.split('\n').length;
                if (lines > numOfLines)
                    numOfLines = lines;
            }
            // eslint-disable-next-line unicorn/no-new-array
            const linesIndexess = [...new Array(numOfLines).keys()];
            // print row
            // including multi-lines
            for (const i of linesIndexess) {
                let l = options.rowStart;
                for (const col of columns) {
                    const width = col.width;
                    let d = row[col.key];
                    d = d.split('\n')[i] || '';
                    const visualWidth = (0, string_width_1.default)(d);
                    const colorWidth = d.length - visualWidth;
                    let cell = d.padEnd(width + colorWidth);
                    if (cell.length - colorWidth > width || visualWidth === width) {
                        // truncate the cell, preserving ANSI escape sequences, and keeping
                        // into account the width of fullwidth unicode characters
                        cell = (0, slice_ansi_1.default)(cell, 0, width - 2) + '… ';
                        // pad with spaces; this is necessary in case the original string
                        // contained fullwidth characters which cannot be split
                        cell += ' '.repeat(width - (0, string_width_1.default)(cell));
                    }
                    l += cell;
                }
                options.printLine(l);
            }
        }
    }
    outputYAML() {
        this.options.printLine((0, js_yaml_1.safeDump)(this.resolveColumnsToObjectArray()));
    }
    resolveColumnsToObjectArray() {
        const { columns, data } = this;
        return data.map((d) => Object.fromEntries(columns.map((col) => [col.key, d[col.key] ?? ''])));
    }
}
function table(data, columns, options = {}) {
    new Table(data, columns, options).display();
}
exports.table = table;
(function (table) {
    table.Flags = {
        columns: F.string({ description: 'only show provided columns (comma-separated)', exclusive: ['extended'] }),
        csv: F.boolean({ description: 'output is csv format [alias: --output=csv]', exclusive: ['no-truncate'] }),
        extended: F.boolean({ char: 'x', description: 'show extra columns', exclusive: ['columns'] }),
        filter: F.string({ description: 'filter property by partial string matching, ex: name=foo' }),
        'no-header': F.boolean({ description: 'hide table header from output', exclusive: ['csv'] }),
        'no-truncate': F.boolean({ description: 'do not truncate output to fit screen', exclusive: ['csv'] }),
        output: F.string({
            description: 'output in a more machine friendly format',
            exclusive: ['no-truncate', 'csv'],
            options: ['csv', 'json', 'yaml'],
        }),
        sort: F.string({ description: "property to sort by (prepend '-' for descending)" }),
    };
    function flags(opts) {
        if (opts) {
            const f = {};
            const o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(table.Flags);
            const e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [];
            for (const key of o) {
                if (!e.includes(key)) {
                    ;
                    f[key] = table.Flags[key];
                }
            }
            return f;
        }
        return table.Flags;
    }
    table.flags = flags;
})(table || (exports.table = table = {}));
const getWidestColumnWith = (data, columnKey) => data.reduce((previous, current) => {
    const d = current[columnKey];
    // convert multi-line cell to single longest line
    // for width calculations
    const manyLines = d.split('\n');
    return Math.max(previous, manyLines.length > 1 ? Math.max(...manyLines.map((r) => (0, string_width_1.default)(r))) : (0, string_width_1.default)(d));
}, 0);
