"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const command_1 = require("@oclif/command");
const screen_1 = require("@oclif/screen");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const _ = tslib_1.__importStar(require("lodash"));
const util_1 = require("util");
const sw = require('string-width');
const { orderBy } = require('natural-orderby');
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;
            const get = col.get || ((row) => row[key]);
            const header = typeof col.header === 'string' ? col.header : _.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, extended, sort, printLine } = options;
        this.options = {
            columns: cols,
            csv,
            extended,
            filter,
            'no-header': options['no-header'] || false,
            'no-truncate': options['no-truncate'] || false,
            printLine: printLine || ((s) => process.stdout.write(s + '\n')),
            sort,
        };
    }
    display() {
        // build table rows from input array data
        let rows = this.data.map(d => {
            let row = {};
            for (let col of this.columns) {
                let val = col.get(d);
                if (typeof val !== 'string')
                    val = util_1.inspect(val, { breakLength: 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.substr(1);
            let col = this.findColumnFromHeader(header);
            if (!col || !regex)
                throw new Error('Filter flag has an invalid value');
            rows = rows.filter((d) => {
                let re = new RegExp(regex);
                let val = d[col.key];
                let match = val.match(re);
                return isNot ? !match : match;
            });
        }
        // sort rows
        if (this.options.sort) {
            let sorters = this.options.sort.split(',');
            let sortHeaders = sorters.map(k => k[0] === '-' ? k.substr(1) : k);
            let sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => {
                return ((v) => v[c.key]);
            });
            let sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc');
            rows = orderBy(rows, sortKeys, sortKeysOrder);
        }
        // and filter columns
        if (this.options.columns) {
            let 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;
        if (this.options.csv)
            this.outputCSV();
        else
            this.outputTable();
    }
    findColumnFromHeader(header) {
        return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase());
    }
    filterColumnsFromHeaders(filters) {
        // unique
        filters = [...(new Set(filters))];
        let cols = [];
        filters.forEach(f => {
            let c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase());
            if (c)
                cols.push(c);
        });
        return cols;
    }
    outputCSV() {
        // tslint:disable-next-line:no-this-assignment
        const { data, columns, options } = this;
        options.printLine(columns.map(c => c.header).join(','));
        data.forEach((d) => {
            let row = [];
            columns.forEach(col => row.push(d[col.key] || ''));
            options.printLine(row.join(','));
        });
    }
    outputTable() {
        // tslint:disable-next-line:no-this-assignment
        const { data, columns, options } = this;
        // column truncation
        //
        // find max width for each column
        for (let col of columns) {
            // convert multi-line cell to single longest line
            // for width calculations
            let widthData = data.map((row) => {
                let d = row[col.key];
                let manyLines = d.split('\n');
                if (manyLines.length > 1) {
                    return '*'.repeat(Math.max(...manyLines.map((r) => sw(r))));
                }
                return d;
            });
            const widths = ['.'.padEnd(col.minWidth - 1), col.header, ...widthData.map((row) => row)].map(r => sw(r));
            col.maxWidth = Math.max(...widths) + 1;
            col.width = col.maxWidth;
        }
        // terminal width
        const maxWidth = screen_1.stdtermwidth;
        // truncation logic
        const shouldShorten = () => {
            // don't shorten if full mode
            if (options['no-truncate'] || !process.stdout.isTTY)
                return;
            // don't shorten if there is enough screen width
            let dataMaxWidth = _.sumBy(columns, c => c.width);
            let overWidth = dataMaxWidth - maxWidth;
            if (overWidth <= 0)
                return;
            // not enough room, short all columns to minWidth
            for (let 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
            let dataMinWidth = _.sumBy(columns, c => c.minWidth);
            if (dataMinWidth >= maxWidth)
                return;
            // some wiggle room left, add it back to "needy" columns
            let wiggleRoom = maxWidth - dataMinWidth;
            let needyCols = _.sortBy(columns.map(c => ({ key: c.key, needs: c.maxWidth - c.width })), c => c.needs);
            for (let { key, needs } of needyCols) {
                if (!needs)
                    continue;
                let col = _.find(columns, c => (key === c.key));
                if (!col)
                    continue;
                if (wiggleRoom > needs) {
                    col.width = col.width + needs;
                    wiggleRoom = wiggleRoom - needs;
                }
                else if (wiggleRoom) {
                    col.width = col.width + wiggleRoom;
                    wiggleRoom = 0;
                }
            }
        };
        shouldShorten();
        // print headers
        if (!options['no-header']) {
            let headers = '';
            for (let col of columns) {
                let header = col.header;
                headers += header.padEnd(col.width);
            }
            options.printLine(chalk_1.default.bold(headers));
        }
        // print rows
        for (let row of data) {
            // find max number of lines
            // for all cells in a row
            // with multi-line strings
            let numOfLines = 1;
            for (let col of columns) {
                const d = row[col.key];
                let lines = d.split('\n').length;
                if (lines > numOfLines)
                    numOfLines = lines;
            }
            let linesIndexess = [...Array(numOfLines).keys()];
            // print row
            // including multi-lines
            linesIndexess.forEach((i) => {
                let l = '';
                for (let 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) {
                        cell = cell.slice(0, width - 2) + '… ';
                    }
                    l += cell;
                }
                options.printLine(l);
            });
        }
    }
}
function table(data, columns, options = {}) {
    new Table(data, columns, options).display();
}
exports.table = table;
(function (table) {
    table.Flags = {
        columns: command_1.flags.string({ exclusive: ['extended'], description: 'only show provided columns (comma-separated)' }),
        sort: command_1.flags.string({ description: 'property to sort by (prepend \'-\' for descending)' }),
        filter: command_1.flags.string({ description: 'filter property by partial string matching, ex: name=foo' }),
        csv: command_1.flags.boolean({ exclusive: ['no-truncate', 'no-header'], description: 'output is csv format' }),
        extended: command_1.flags.boolean({ exclusive: ['columns'], char: 'x', description: 'show extra columns' }),
        'no-truncate': command_1.flags.boolean({ exclusive: ['csv'], description: 'do not truncate output to fit screen' }),
        'no-header': command_1.flags.boolean({ exclusive: ['csv'], description: 'hide table header from output' }),
    };
    function flags(opts) {
        if (opts) {
            let f = {};
            let o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(table.Flags);
            let e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [];
            o.forEach((key) => {
                if (e.includes(key))
                    return;
                f[key] = table.Flags[key];
            });
            return f;
        }
        return table.Flags;
    }
    table.flags = flags;
})(table = exports.table || (exports.table = {}));
