"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.table = void 0;
const F = require("../../flags");
const screen_1 = require("../../screen");
const chalk = require("chalk");
const util_1 = require("../../util");
const js_yaml_1 = require("js-yaml");
const util_2 = require("util");
const stream_1 = require("../stream");
const sw = require('string-width');
const { orderBy } = require('natural-orderby');
const sliceAnsi = require('slice-ansi');
class Table {
    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.replace(/_/g, ' '));
            const minWidth = Math.max(col.minWidth ?? 0, sw(header) + 1);
            return {
                extended,
                get,
                header,
                key,
                minWidth,
            };
        });
        // assign options
        const { columns: cols, filter, csv, output, extended, sort, title, printLine } = options;
        this.options = {
            columns: cols,
            output: csv ? 'csv' : output,
            extended,
            filter,
            'no-header': options['no-header'] ?? false,
            'no-truncate': options['no-truncate'] ?? false,
            printLine: printLine ?? ((s) => stream_1.stdout.write(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, util_2.inspect)(val, { breakLength: Number.POSITIVE_INFINITY });
                row[col.key] = val;
            }
            return row;
        });
        // filter rows
        if (this.options.filter) {
            /* eslint-disable-next-line prefer-const */
            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 => {
                return ((v) => v[c.key]);
            });
            const sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc');
            rows = 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 extented 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();
        }
    }
    findColumnFromHeader(header) {
        return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase());
    }
    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;
    }
    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.replace('"', '""')}"` : e);
    }
    resolveColumnsToObjectArray() {
        const { data, columns } = this;
        return data.map((d) => {
            // eslint-disable-next-line unicorn/prefer-object-from-entries
            return columns.reduce((obj, col) => {
                return {
                    ...obj,
                    [col.key]: d[col.key] ?? '',
                };
            }, {});
        });
    }
    outputJSON() {
        this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2));
    }
    outputYAML() {
        this.options.printLine((0, js_yaml_1.safeDump)(this.resolveColumnsToObjectArray()));
    }
    outputCSV() {
        const { data, columns, 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(','));
        }
    }
    outputTable() {
        const { data, options } = this;
        // column truncation
        //
        // find max width for each column
        const columns = this.columns.map(c => {
            const maxWidth = Math.max(sw('.'.padEnd(c.minWidth - 1)), sw(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'] || (!stream_1.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.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.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 = sw(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 = sliceAnsi(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 - sw(cell));
                    }
                    l += cell;
                }
                options.printLine(l);
            }
        }
    }
}
function table(data, columns, options = {}) {
    new Table(data, columns, options).display();
}
exports.table = table;
(function (table) {
    table.Flags = {
        columns: F.string({ exclusive: ['extended'], description: 'only show provided columns (comma-separated)' }),
        sort: F.string({ description: 'property to sort by (prepend \'-\' for descending)' }),
        filter: F.string({ description: 'filter property by partial string matching, ex: name=foo' }),
        csv: F.boolean({ exclusive: ['no-truncate'], description: 'output is csv format [alias: --output=csv]' }),
        output: F.string({
            exclusive: ['no-truncate', 'csv'],
            description: 'output in a more machine friendly format',
            options: ['csv', 'json', 'yaml'],
        }),
        extended: F.boolean({ exclusive: ['columns'], char: 'x', description: 'show extra columns' }),
        'no-truncate': F.boolean({ exclusive: ['csv'], description: 'do not truncate output to fit screen' }),
        'no-header': F.boolean({ exclusive: ['csv'], description: 'hide table header from output' }),
    };
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    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 || (exports.table = {}));
const getWidestColumnWith = (data, columnKey) => {
    return 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) => sw(r))) : sw(d));
    }, 0);
};
