 /* print.c - functions to print table
 *
 * Copyright (C) 2010-2014 Karel Zak <kzak@redhat.com>
 * Copyright (C) 2016 Igor Gnatenko <i.gnatenko.brain@gmail.com>
 *
 * This file may be redistributed under the terms of the
 * GNU Lesser General Public License.
 */

/**
 * SECTION: table_print
 * @title: Table print
 * @short_description: output functions
 *
 * Table output API.
 */

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <termios.h>
#include <ctype.h>

#include "mbsalign.h"
#include "carefulputc.h"
#include "smartcolsP.h"

/* Fallback for symbols
 *
 * Note that by default library define all the symbols, but in case user does
 * not define all symbols or if we extended the symbols struct then we need
 * fallback to be more robust and backwardly compatible.
 */
#define titlepadding_symbol(tb)	((tb)->symbols->title_padding ? (tb)->symbols->title_padding : " ")
#define branch_symbol(tb)	((tb)->symbols->tree_branch ? (tb)->symbols->tree_branch : "|-")
#define vertical_symbol(tb)	((tb)->symbols->tree_vert ? (tb)->symbols->tree_vert : "| ")
#define right_symbol(tb)	((tb)->symbols->tree_right ? (tb)->symbols->tree_right : "`-")

#define grp_vertical_symbol(tb)	((tb)->symbols->group_vert ? (tb)->symbols->group_vert : "|")
#define grp_horizontal_symbol(tb) ((tb)->symbols->group_horz ? (tb)->symbols->group_horz : "-")
#define grp_m_first_symbol(tb)	((tb)->symbols->group_first_member ? (tb)->symbols->group_first_member : ",->")
#define grp_m_last_symbol(tb)	((tb)->symbols->group_last_member ? (tb)->symbols->group_last_member : "\\->")
#define grp_m_middle_symbol(tb)	((tb)->symbols->group_middle_member ? (tb)->symbols->group_middle_member : "|->")
#define grp_c_middle_symbol(tb)	((tb)->symbols->group_middle_child ? (tb)->symbols->group_middle_child : "|-")
#define grp_c_last_symbol(tb)	((tb)->symbols->group_last_child ? (tb)->symbols->group_last_child : "`-")

#define cellpadding_symbol(tb)  ((tb)->padding_debug ? "." : \
				 ((tb)->symbols->cell_padding ? (tb)->symbols->cell_padding: " "))

#define want_repeat_header(tb)	(!(tb)->header_repeat || (tb)->header_next <= (tb)->termlines_used)

static int is_next_columns_empty(
			struct libscols_table *tb,
			struct libscols_column *cl,
			struct libscols_line *ln)
{
	struct libscols_iter itr;

	if (!tb || !cl)
		return 0;
	if (is_last_column(cl))
		return 1;
	if (!ln)
		return 0;

	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	scols_table_set_columns_iter(tb, &itr, cl);

	/* skip current column */
	scols_table_next_column(tb, &itr, &cl);

	while (scols_table_next_column(tb, &itr, &cl) == 0) {
		struct libscols_cell *ce;
		const char *data = NULL;

                if (scols_column_is_hidden(cl))
                        continue;
		if (scols_column_is_tree(cl))
			return 0;

		ce = scols_line_get_cell(ln, cl->seqnum);
		if (ce)
			data = scols_cell_get_data(ce);
		if (data && *data)
			return 0;
	}
	return 1;
}

/* returns pointer to the end of used data */
static int tree_ascii_art_to_buffer(struct libscols_table *tb,
				    struct libscols_line *ln,
				    struct ul_buffer *buf)
{
	const char *art;
	int rc;

	assert(ln);
	assert(buf);

	if (!ln->parent)
		return 0;

	rc = tree_ascii_art_to_buffer(tb, ln->parent, buf);
	if (rc)
		return rc;

	if (is_last_child(ln))
		art = "  ";
	else
		art = vertical_symbol(tb);

	return ul_buffer_append_string(buf, art);
}

static int grpset_is_empty(	struct libscols_table *tb,
				size_t idx,
				size_t *rest)
{
	size_t i;

	for (i = idx; i < tb->grpset_size; i++) {
		if (tb->grpset[i] == NULL) {
			if (rest)
				(*rest)++;
		} else
			return 0;
	}
	return 1;
}

static int groups_ascii_art_to_buffer(	struct libscols_table *tb,
				struct libscols_line *ln,
				struct ul_buffer *buf,
				int empty)
{
	int filled = 0;
	size_t i, rest = 0;
	const char *filler = cellpadding_symbol(tb);

	if (!has_groups(tb))
		return 0;

	DBG(LINE, ul_debugobj(ln, "printing groups chart"));

	if (tb->is_dummy_print)
		return 0;		/* allocate grpset[] only */

	for (i = 0; i < tb->grpset_size; i += SCOLS_GRPSET_CHUNKSIZ) {
		struct libscols_group *gr = tb->grpset[i];

		if (!gr) {
			ul_buffer_append_ntimes(buf, SCOLS_GRPSET_CHUNKSIZ, cellpadding_symbol(tb));
			continue;
		}

		/*
		 * Empty cells (multi-line entries, etc.), print vertical symbols only
		 * to show that the group continues.
		 */
		if (empty) {
			switch (gr->state) {
			case SCOLS_GSTATE_FIRST_MEMBER:
			case SCOLS_GSTATE_MIDDLE_MEMBER:
			case SCOLS_GSTATE_CONT_MEMBERS:
				ul_buffer_append_string(buf, grp_vertical_symbol(tb));
				ul_buffer_append_ntimes(buf, 2, filler);
				break;

			case SCOLS_GSTATE_LAST_MEMBER:
			case SCOLS_GSTATE_MIDDLE_CHILD:
			case SCOLS_GSTATE_CONT_CHILDREN:
				ul_buffer_append_string(buf, filler);
				ul_buffer_append_string(buf, grp_vertical_symbol(tb));
				ul_buffer_append_string(buf, filler);
				break;
			case SCOLS_GSTATE_LAST_CHILD:
				ul_buffer_append_ntimes(buf, 3, filler);
				break;
			}
			continue;
		}

		/*
		 * Regular cell
		 */
		switch (gr->state) {
		case SCOLS_GSTATE_FIRST_MEMBER:
			ul_buffer_append_string(buf, grp_m_first_symbol(tb));
			break;
		case SCOLS_GSTATE_MIDDLE_MEMBER:
			ul_buffer_append_string(buf, grp_m_middle_symbol(tb));
			break;
		case SCOLS_GSTATE_LAST_MEMBER:
			ul_buffer_append_string(buf, grp_m_last_symbol(tb));
			break;
		case SCOLS_GSTATE_CONT_MEMBERS:
			ul_buffer_append_string(buf, grp_vertical_symbol(tb));
			ul_buffer_append_ntimes(buf, 2, filler);
			break;
		case SCOLS_GSTATE_MIDDLE_CHILD:
			ul_buffer_append_string(buf, filler);
			ul_buffer_append_string(buf, grp_c_middle_symbol(tb));
			if (grpset_is_empty(tb, i + SCOLS_GRPSET_CHUNKSIZ, &rest)) {
				ul_buffer_append_ntimes(buf, rest+1, grp_horizontal_symbol(tb));
				filled = 1;
			}
			filler = grp_horizontal_symbol(tb);
			break;
		case SCOLS_GSTATE_LAST_CHILD:
			ul_buffer_append_string(buf, cellpadding_symbol(tb));
			ul_buffer_append_string(buf, grp_c_last_symbol(tb));
			if (grpset_is_empty(tb, i + SCOLS_GRPSET_CHUNKSIZ, &rest)) {
				ul_buffer_append_ntimes(buf, rest+1, grp_horizontal_symbol(tb));
				filled = 1;
			}
			filler = grp_horizontal_symbol(tb);
			break;
		case SCOLS_GSTATE_CONT_CHILDREN:
			ul_buffer_append_string(buf, filler);
			ul_buffer_append_string(buf, grp_vertical_symbol(tb));
			ul_buffer_append_string(buf, filler);
			break;
		}

		if (filled)
			break;
	}

	if (!filled)
		ul_buffer_append_string(buf, filler);
	return 0;
}

static int has_pending_data(struct libscols_table *tb)
{
	struct libscols_column *cl;
	struct libscols_iter itr;

	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	while (scols_table_next_column(tb, &itr, &cl) == 0) {
		if (scols_column_is_hidden(cl))
			continue;
		if (cl->pending_data)
			return 1;
	}
	return 0;
}

static void fputs_color_reset(struct libscols_table *tb)
{
	if (tb->cur_color) {
		fputs(UL_COLOR_RESET, tb->out);
		tb->cur_color = NULL;
	}
}

static void fputs_color(struct libscols_table *tb, const char *color)
{
	if (tb->cur_color)
		fputs_color_reset(tb);

	tb->cur_color = color;
	if (color)
		fputs(color, tb->out);
}

static const char *get_cell_color(struct libscols_table *tb,
				struct libscols_column *cl,
				struct libscols_line *ln,
				struct libscols_cell *ce)
{
	const char *color = NULL;

	if (!tb || !tb->colors_wanted || tb->format != SCOLS_FMT_HUMAN)
		return NULL;
	if (ce)
		color = ce->color;
	if (!color && (!ln || !ln->color) && cl)
		color = cl->color;
	return color;
}

/* switch from line color to cell/column color */
static void fputs_color_cell_open(struct libscols_table *tb,
				struct libscols_column *cl,
				struct libscols_line *ln,
				struct libscols_cell *ce)
{
	const char *color = get_cell_color(tb, cl, ln, ce);

	if (color)
		fputs_color(tb, color);
}

/* switch from cell/column color to line color or reset */
static void fputs_color_cell_close(struct libscols_table *tb,
				struct libscols_column *cl,
				struct libscols_line *ln,
				struct libscols_cell *ce)
{
	const char *color = get_cell_color(tb, cl, ln, ce);

	if (color)
		fputs_color(tb, ln ? ln->color : NULL);
}

/* switch to line color */
static void fputs_color_line_open(struct libscols_table *tb,
				struct libscols_line *ln)
{
	if (!tb || !tb->colors_wanted || tb->format != SCOLS_FMT_HUMAN)
		return;
	fputs_color(tb, ln ? ln->color : NULL);
}

/* switch off all colors */
static void fputs_color_line_close(struct libscols_table *tb)
{
	if (!tb || !tb->colors_wanted || tb->format != SCOLS_FMT_HUMAN)
		return;
	fputs_color_reset(tb);
}

/* print padding or ASCII-art instead of data of @cl */
static void print_empty_cell(struct libscols_table *tb,
			  struct libscols_column *cl,
			  struct libscols_line *ln,	/* optional */
			  struct libscols_cell *ce,
			  size_t bufsz)
{
	size_t len_pad = 0;		/* in screen cells as opposed to bytes */

	DBG(COL, ul_debugobj(cl, " printing empty cell"));

	fputs_color_cell_open(tb, cl, ln, ce);

	/* generate tree/group ASCII-art rather than padding
	 */
	if (ln && scols_column_is_tree(cl)) {
		struct ul_buffer art = UL_INIT_BUFFER;
		char *data;

		if (ul_buffer_alloc_data(&art, bufsz) != 0)
			goto done;

		if (cl->is_groups)
			groups_ascii_art_to_buffer(tb, ln, &art, 1);

		tree_ascii_art_to_buffer(tb, ln, &art);

		if (!list_empty(&ln->ln_branch) && has_pending_data(tb))
			ul_buffer_append_string(&art, vertical_symbol(tb));

		if (scols_table_is_noencoding(tb))
			data = ul_buffer_get_data(&art, NULL, &len_pad);
		else
			data = ul_buffer_get_safe_data(&art, NULL, &len_pad, NULL);

		if (data && len_pad)
			fputs(data, tb->out);
		ul_buffer_free_data(&art);
	}

done:
	/* minout -- don't fill */
	if (scols_table_is_minout(tb) && is_next_columns_empty(tb, cl, ln)) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return;
	}

	/* default -- fill except last column */
	if (!scols_table_is_maxout(tb) && is_last_column(cl)) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return;
	}

	/* fill rest of cell with space */
	for(; len_pad < cl->width; ++len_pad)
		fputs(cellpadding_symbol(tb), tb->out);

	fputs_color_cell_close(tb, cl, ln, ce);

	if (!is_last_column(cl))
		fputs(colsep(tb), tb->out);
}



/* Fill the start of a line with padding (or with tree ascii-art).
 *
 * This is necessary after a long non-truncated column, as this requires the
 * next column to be printed on the next line. For example (see 'DDD'):
 *
 * aaa bbb ccc ddd eee
 * AAA BBB CCCCCCC
 *             DDD EEE
 * ^^^^^^^^^^^^
 *  new line padding
 */
static void print_newline_padding(struct libscols_table *tb,
				  struct libscols_column *cl,
				  struct libscols_line *ln,	/* optional */
				  struct libscols_cell *ce,
				  size_t bufsz)
{
	size_t i;

	assert(tb);
	assert(cl);

	DBG(LINE, ul_debugobj(ln, "printing newline padding"));

	fputs(linesep(tb), tb->out);		/* line break */
	tb->termlines_used++;

	fputs_color_line_open(tb, ln);

	/* fill cells after line break */
	for (i = 0; i <= (size_t) cl->seqnum; i++)
		print_empty_cell(tb, scols_table_get_column(tb, i), ln, ce, bufsz);

	fputs_color_line_close(tb);
}

/*
 * Pending data
 *
 * The first line in the multi-line cells (columns with SCOLS_FL_WRAP flag) is
 * printed as usually and output is truncated to match column width.
 *
 * The rest of the long text is printed on next extra line(s). The extra lines
 * don't exist in the table (not represented by libscols_line). The data for
 * the extra lines are stored in libscols_column->pending_data_buf and the
 * function print_line() adds extra lines until the buffer is not empty in all
 * columns.
 */

/* set data that will be printed by extra lines */
static int set_pending_data(struct libscols_column *cl, const char *data, size_t sz)
{
	char *p = NULL;

	if (data && *data) {
		DBG(COL, ul_debugobj(cl, "setting pending data"));
		assert(sz);
		p = strdup(data);
		if (!p)
			return -ENOMEM;
	}

	free(cl->pending_data_buf);
	cl->pending_data_buf = p;
	cl->pending_data_sz = sz;
	cl->pending_data = cl->pending_data_buf;
	return 0;
}

/* the next extra line has been printed, move pending data cursor */
static int step_pending_data(struct libscols_column *cl, size_t bytes)
{
	DBG(COL, ul_debugobj(cl, "step pending data %zu -= %zu", cl->pending_data_sz, bytes));

	if (bytes >= cl->pending_data_sz)
		return set_pending_data(cl, NULL, 0);

	cl->pending_data += bytes;
	cl->pending_data_sz -= bytes;
	return 0;
}

/* print next pending data for the column @cl */
static int print_pending_data(
		struct libscols_table *tb,
		struct libscols_column *cl,
		struct libscols_line *ln,	/* optional */
		struct libscols_cell *ce)
{
	size_t width = cl->width, bytes;
	size_t len = width, i;
	char *data;
	char *nextchunk = NULL;

	if (!cl->pending_data)
		return 0;
	if (!width)
		return -EINVAL;

	DBG(COL, ul_debugobj(cl, "printing pending data"));

	data = strdup(cl->pending_data);
	if (!data)
		goto err;

	if (scols_column_is_customwrap(cl)
	    && (nextchunk = cl->wrap_nextchunk(cl, data, cl->wrapfunc_data))) {
		bytes = nextchunk - data;

		len = scols_table_is_noencoding(tb) ?
				mbs_nwidth(data, bytes) :
				mbs_safe_nwidth(data, bytes, NULL);
	} else
		bytes = mbs_truncate(data, &len);

	if (bytes == (size_t) -1)
		goto err;

	if (bytes)
		step_pending_data(cl, bytes);

	fputs_color_cell_open(tb, cl, ln, ce);

	fputs(data, tb->out);
	free(data);

	/* minout -- don't fill */
	if (scols_table_is_minout(tb) && is_next_columns_empty(tb, cl, ln)) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return 0;
	}

	/* default -- fill except last column */
	if (!scols_table_is_maxout(tb) && is_last_column(cl)) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return 0;
	}

	/* fill rest of cell with space */
	for(i = len; i < width; i++)
		fputs(cellpadding_symbol(tb), tb->out);

	fputs_color_cell_close(tb, cl, ln, ce);

	if (!is_last_column(cl))
		fputs(colsep(tb), tb->out);

	return 0;
err:
	free(data);
	return -errno;
}

static void print_json_data(struct libscols_table *tb,
			    struct libscols_column *cl,
			    const char *name,
			    char *data)
{
	switch (cl->json_type) {
	case SCOLS_JSON_STRING:
		/* name: "aaa" */
		ul_jsonwrt_value_s(&tb->json, name, data);
		break;
	case SCOLS_JSON_NUMBER:
		/* name: 123 */
		ul_jsonwrt_value_raw(&tb->json, name, data);
		break;
	case SCOLS_JSON_BOOLEAN:
	case SCOLS_JSON_BOOLEAN_OPTIONAL:
		/* name: true|false|null */
		if (cl->json_type == SCOLS_JSON_BOOLEAN_OPTIONAL && (!*data || !strcmp(data, "-"))) {
			ul_jsonwrt_value_null(&tb->json, name);
		} else {
			ul_jsonwrt_value_boolean(&tb->json, name,
					!*data ? 0 :
					*data == '0' ? 0 :
					*data == 'N' || *data == 'n' ? 0 : 1);
		}
		break;
	case SCOLS_JSON_ARRAY_STRING:
	case SCOLS_JSON_ARRAY_NUMBER:
		/* name: [ "aaa", "bbb", "ccc" ] */
		ul_jsonwrt_array_open(&tb->json, name);

		if (!scols_column_is_customwrap(cl))
			ul_jsonwrt_value_s(&tb->json, NULL, data);
		else do {
				char *next = cl->wrap_nextchunk(cl, data, cl->wrapfunc_data);

				if (cl->json_type == SCOLS_JSON_ARRAY_STRING)
					ul_jsonwrt_value_s(&tb->json, NULL, data);
				else
					ul_jsonwrt_value_raw(&tb->json, NULL, data);
				data = next;
		} while (data);

		ul_jsonwrt_array_close(&tb->json);
		break;
	}
}

static int print_data(struct libscols_table *tb,
		      struct libscols_column *cl,
		      struct libscols_line *ln,	/* optional */
		      struct libscols_cell *ce,	/* optional */
		      struct ul_buffer *buf)
{
	size_t len = 0, i, width, bytes;
	char *data, *nextchunk;
	const char *name = NULL;
	int is_last;

	assert(tb);
	assert(cl);

	data = ul_buffer_get_data(buf, NULL, NULL);
	if (!data)
		data = "";

	if (tb->format != SCOLS_FMT_HUMAN) {
		name = scols_table_is_shellvar(tb) ?
				scols_column_get_name_as_shellvar(cl) :
				scols_column_get_name(cl);
	}

	is_last = is_last_column(cl);

	if (is_last && scols_table_is_json(tb) &&
	    scols_table_is_tree(tb) && has_children(ln))
		/* "children": [] is the real last value */
		is_last = 0;

	switch (tb->format) {
	case SCOLS_FMT_RAW:
		fputs_nonblank(data, tb->out);
		if (!is_last)
			fputs(colsep(tb), tb->out);
		return 0;

	case SCOLS_FMT_EXPORT:
		fputs(name ?: "", tb->out);
		fputc('=', tb->out);
		fputs_quoted(data, tb->out);
		if (!is_last)
			fputs(colsep(tb), tb->out);
		return 0;

	case SCOLS_FMT_JSON:
		print_json_data(tb, cl, name, data);
		return 0;

	case SCOLS_FMT_HUMAN:
		break;		/* continue below */
	}

	/* Encode. Note that 'len' and 'width' are number of cells, not bytes.
	 */
	if (scols_table_is_noencoding(tb))
		data = ul_buffer_get_data(buf, &bytes, &len);
	else
		data = ul_buffer_get_safe_data(buf, &bytes, &len, scols_column_get_safechars(cl));

	if (!data)
		data = "";
	width = cl->width;

	/* custom multi-line cell based */
	if (*data && scols_column_is_customwrap(cl)
	    && (nextchunk = cl->wrap_nextchunk(cl, data, cl->wrapfunc_data))) {
		set_pending_data(cl, nextchunk, bytes - (nextchunk - data));
		bytes = nextchunk - data;

		len = scols_table_is_noencoding(tb) ?
				mbs_nwidth(data, bytes) :
				mbs_safe_nwidth(data, bytes, NULL);
	}

	if (is_last
	    && len < width
	    && !scols_table_is_maxout(tb)
	    && !scols_column_is_right(cl))
		width = len;

	/* truncate data */
	if (len > width && scols_column_is_trunc(cl)) {
		len = width;
		bytes = mbs_truncate(data, &len);	/* updates 'len' */
	}

	/* standard multi-line cell */
	if (len > width && scols_column_is_wrap(cl)
	    && !scols_column_is_customwrap(cl)) {
		set_pending_data(cl, data, bytes);

		len = width;
		bytes = mbs_truncate(data, &len);
		if (bytes  != (size_t) -1 && bytes > 0)
			step_pending_data(cl, bytes);
	}

	if (bytes == (size_t) -1) {
		bytes = len = 0;
		data = NULL;
	}

	fputs_color_cell_open(tb, cl, ln, ce);

	if (data && *data) {
		if (scols_column_is_right(cl)) {
			for (i = len; i < width; i++)
				fputs(cellpadding_symbol(tb), tb->out);
			len = width;
		}
		fputs(data, tb->out);

	}

	/* minout -- don't fill */
	if (scols_table_is_minout(tb) && is_next_columns_empty(tb, cl, ln)) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return 0;
	}

	/* default -- fill except last column */
	if (!scols_table_is_maxout(tb) && is_last) {
		fputs_color_cell_close(tb, cl, ln, ce);
		return 0;
	}

	/* fill rest of cell with space */
	for(i = len; i < width; i++)
		fputs(cellpadding_symbol(tb), tb->out);

	fputs_color_cell_close(tb, cl, ln, ce);

	if (len > width && !scols_column_is_trunc(cl)) {
		DBG(COL, ul_debugobj(cl, "*** data len=%zu > column width=%zu", len, width));
		print_newline_padding(tb, cl, ln, ce, ul_buffer_get_bufsiz(buf));	/* next column starts on next line */

	} else if (!is_last)
		fputs(colsep(tb), tb->out);		/* columns separator */

	return 0;
}

int __cell_to_buffer(struct libscols_table *tb,
			  struct libscols_line *ln,
			  struct libscols_column *cl,
			  struct ul_buffer *buf)
{
	const char *data;
	struct libscols_cell *ce;
	int rc = 0;

	assert(tb);
	assert(ln);
	assert(cl);
	assert(buf);
	assert(cl->seqnum <= tb->ncols);

	ul_buffer_reset_data(buf);

	ce = scols_line_get_cell(ln, cl->seqnum);
	data = ce ? scols_cell_get_data(ce) : NULL;

	if (!scols_column_is_tree(cl))
		return data ? ul_buffer_append_string(buf, data) : 0;

	/*
	 * Group stuff
	 */
	if (!scols_table_is_json(tb) && cl->is_groups)
		rc = groups_ascii_art_to_buffer(tb, ln, buf, 0);

	/*
	 * Tree stuff
	 */
	if (!rc && ln->parent && !scols_table_is_json(tb)) {
		rc = tree_ascii_art_to_buffer(tb, ln->parent, buf);

		if (!rc && is_last_child(ln))
			rc = ul_buffer_append_string(buf, right_symbol(tb));
		else if (!rc)
			rc = ul_buffer_append_string(buf, branch_symbol(tb));
	}

	if (!rc && (ln->parent || cl->is_groups) && !scols_table_is_json(tb))
		ul_buffer_save_pointer(buf, SCOLS_BUFPTR_TREEEND);

	if (!rc && data)
		rc = ul_buffer_append_string(buf, data);
	return rc;
}

/*
 * Prints data. Data can be printed in more formats (raw, NAME=xxx pairs), and
 * control and non-printable characters can be encoded in the \x?? encoding.
 */
static int print_line(struct libscols_table *tb,
		      struct libscols_line *ln,
		      struct ul_buffer *buf)
{
	int rc = 0, pending = 0;
	struct libscols_column *cl;
	struct libscols_iter itr;

	assert(ln);

	DBG(LINE, ul_debugobj(ln, "     printing line"));

	fputs_color_line_open(tb, ln);

	/* regular line */
	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) {
		if (scols_column_is_hidden(cl))
			continue;
		rc = __cell_to_buffer(tb, ln, cl, buf);
		if (rc == 0)
			rc = print_data(tb, cl, ln,
					scols_line_get_cell(ln, cl->seqnum),
					buf);
		if (rc == 0 && cl->pending_data)
			pending = 1;
	}
	fputs_color_line_close(tb);

	/* extra lines of the multi-line cells */
	while (rc == 0 && pending) {
		DBG(LINE, ul_debugobj(ln, "printing pending data"));
		pending = 0;
		fputs(linesep(tb), tb->out);
		fputs_color_line_open(tb, ln);
		tb->termlines_used++;
		scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
		while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) {
			if (scols_column_is_hidden(cl))
				continue;
			if (cl->pending_data) {
				rc = print_pending_data(tb, cl, ln, scols_line_get_cell(ln, cl->seqnum));
				if (rc == 0 && cl->pending_data)
					pending = 1;
			} else
				print_empty_cell(tb, cl, ln, NULL, ul_buffer_get_bufsiz(buf));
		}
		fputs_color_line_close(tb);
	}

	return 0;
}

int __scols_print_title(struct libscols_table *tb)
{
	int rc;
	mbs_align_t align;
	size_t width, len = 0, bufsz, titlesz;
	char *title = NULL, *buf = NULL;

	assert(tb);

	if (!tb->title.data)
		return 0;

	DBG(TAB, ul_debugobj(tb, "printing title"));

	/* encode data */
	if (tb->no_encode) {
		len = bufsz = strlen(tb->title.data) + 1;
		buf = strdup(tb->title.data);
		if (!buf) {
			rc = -ENOMEM;
			goto done;
		}
	} else {
		bufsz = mbs_safe_encode_size(strlen(tb->title.data)) + 1;
		if (bufsz == 1) {
			DBG(TAB, ul_debugobj(tb, "title is empty string -- ignore"));
			return 0;
		}
		buf = malloc(bufsz);
		if (!buf) {
			rc = -ENOMEM;
			goto done;
		}

		if (!mbs_safe_encode_to_buffer(tb->title.data, &len, buf, NULL) ||
		    !len || len == (size_t) -1) {
			rc = -EINVAL;
			goto done;
		}
	}

	/* truncate and align */
	width = tb->is_term ? tb->termwidth : 80;
	titlesz = width + bufsz;

	title = malloc(titlesz);
	if (!title) {
		rc = -EINVAL;
		goto done;
	}

	switch (scols_cell_get_alignment(&tb->title)) {
	case SCOLS_CELL_FL_RIGHT:
		align = MBS_ALIGN_RIGHT;
		break;
	case SCOLS_CELL_FL_CENTER:
		align = MBS_ALIGN_CENTER;
		break;
	case SCOLS_CELL_FL_LEFT:
	default:
		align = MBS_ALIGN_LEFT;
		/*
		 * Don't print extra blank chars after the title if on left
		 * (that's same as we use for the last column in the table).
		 */
		if (len < width
		    && !scols_table_is_maxout(tb)
		    && isblank(*titlepadding_symbol(tb)))
			width = len;
		break;

	}

	/* copy from buf to title and align to width with title_padding */
	rc = mbsalign_with_padding(buf, title, titlesz,
			&width, align,
			0, (int) *titlepadding_symbol(tb));

	if (rc == -1) {
		rc = -EINVAL;
		goto done;
	}


	if (tb->colors_wanted)
		fputs_color(tb, tb->title.color);

	fputs(title, tb->out);

	if (tb->colors_wanted)
		fputs_color_reset(tb);

	fputc('\n', tb->out);
	rc = 0;
done:
	free(buf);
	free(title);
	DBG(TAB, ul_debugobj(tb, "printing title done [rc=%d]", rc));
	return rc;
}

int __scols_print_header(struct libscols_table *tb, struct ul_buffer *buf)
{
	int rc = 0;
	struct libscols_column *cl;
	struct libscols_iter itr;

	assert(tb);

	if ((tb->header_printed == 1 && tb->header_repeat == 0) ||
	    scols_table_is_noheadings(tb) ||
	    scols_table_is_export(tb) ||
	    scols_table_is_json(tb) ||
	    list_empty(&tb->tb_lines))
		return 0;

	DBG(TAB, ul_debugobj(tb, "printing header"));

	/* set the width according to the size of the data */
	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	while (rc == 0 && scols_table_next_column(tb, &itr, &cl) == 0) {
		if (scols_column_is_hidden(cl))
			continue;

		ul_buffer_reset_data(buf);

		if (cl->is_groups
		    && scols_table_is_tree(tb) && scols_column_is_tree(cl)) {
			size_t i;
			for (i = 0; i < tb->grpset_size + 1; i++) {
				rc = ul_buffer_append_data(buf, " ", 1);
				if (rc)
					break;
			}
		}
		if (!rc)
			rc = ul_buffer_append_string(buf,
					scols_table_is_shellvar(tb) ?
						scols_column_get_name_as_shellvar(cl) :
						scols_column_get_name(cl));
		if (!rc)
			rc = print_data(tb, cl, NULL, &cl->header, buf);
	}

	if (rc == 0) {
		fputs(linesep(tb), tb->out);
		tb->termlines_used++;
	}

	tb->header_printed = 1;
	tb->header_next = tb->termlines_used + tb->termheight;
	if (tb->header_repeat)
		DBG(TAB, ul_debugobj(tb, "\tnext header: %zu [current=%zu, rc=%d]",
					tb->header_next, tb->termlines_used, rc));
	return rc;
}


int __scols_print_range(struct libscols_table *tb,
			struct ul_buffer *buf,
			struct libscols_iter *itr,
			struct libscols_line *end)
{
	int rc = 0;
	struct libscols_line *ln;

	assert(tb);
	DBG(TAB, ul_debugobj(tb, "printing range"));

	while (rc == 0 && scols_table_next_line(tb, itr, &ln) == 0) {

		int last = scols_iter_is_last(itr);

		if (scols_table_is_json(tb))
			ul_jsonwrt_object_open(&tb->json, NULL);

		rc = print_line(tb, ln, buf);

		if (scols_table_is_json(tb))
			ul_jsonwrt_object_close(&tb->json);
		else if (last == 0 && tb->no_linesep == 0) {
			fputs(linesep(tb), tb->out);
			tb->termlines_used++;
		}

		if (end && ln == end)
			break;

		if (!last && want_repeat_header(tb))
			__scols_print_header(tb, buf);
	}

	return rc;

}

int __scols_print_table(struct libscols_table *tb, struct ul_buffer *buf)
{
	struct libscols_iter itr;

	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	return __scols_print_range(tb, buf, &itr, NULL);
}

/* scols_walk_tree() callback to print tree line */
static int print_tree_line(struct libscols_table *tb,
			   struct libscols_line *ln,
			   struct libscols_column *cl __attribute__((__unused__)),
			   void *data)
{
	struct ul_buffer *buf = (struct ul_buffer *) data;
	int rc;

	DBG(LINE, ul_debugobj(ln, "   printing tree line"));

	if (scols_table_is_json(tb))
		ul_jsonwrt_object_open(&tb->json, NULL);

	rc = print_line(tb, ln, buf);
	if (rc)
		return rc;

	if (has_children(ln)) {
		if (scols_table_is_json(tb))
			ul_jsonwrt_array_open(&tb->json, "children");
		else {
			/* between parent and child is separator */
			fputs(linesep(tb), tb->out);
			tb->termlines_used++;
		}
	} else {
		int last;

		/* terminate all open last children for JSON */
		if (scols_table_is_json(tb)) {
			do {
				last = (is_child(ln) && is_last_child(ln)) ||
				       (is_tree_root(ln) && is_last_tree_root(tb, ln));

				ul_jsonwrt_object_close(&tb->json);
				if (last && is_child(ln))
					ul_jsonwrt_array_close(&tb->json);
				ln = ln->parent;
			} while(ln && last);

		} else if (tb->no_linesep == 0) {
			int last_in_tree = scols_walk_is_last(tb, ln);

			if (last_in_tree == 0) {
				/* standard output */
				fputs(linesep(tb), tb->out);
				tb->termlines_used++;
			}
		}
	}

	return 0;
}

int __scols_print_tree(struct libscols_table *tb, struct ul_buffer *buf)
{
	assert(tb);
	DBG(TAB, ul_debugobj(tb, "----printing-tree-----"));

	return scols_walk_tree(tb, NULL, print_tree_line, (void *) buf);
}

static size_t strlen_line(struct libscols_line *ln)
{
	size_t i, sz = 0;

	assert(ln);

	for (i = 0; i < ln->ncells; i++) {
		struct libscols_cell *ce = scols_line_get_cell(ln, i);
		const char *data = ce ? scols_cell_get_data(ce) : NULL;

		sz += data ? strlen(data) : 0;
	}

	return sz;
}

void __scols_cleanup_printing(struct libscols_table *tb, struct ul_buffer *buf)
{
	if (!tb)
		return;

	ul_buffer_free_data(buf);

	if (tb->priv_symbols) {
		scols_table_set_symbols(tb, NULL);
		tb->priv_symbols = 0;
	}
}

int __scols_initialize_printing(struct libscols_table *tb, struct ul_buffer *buf)
{
	size_t bufsz, extra_bufsz = 0;
	struct libscols_line *ln;
	struct libscols_iter itr;
	int rc;

	DBG(TAB, ul_debugobj(tb, "initialize printing"));

	if (!tb->symbols) {
		rc = scols_table_set_default_symbols(tb);
		if (rc)
			goto err;
		tb->priv_symbols = 1;
	} else
		tb->priv_symbols = 0;

	if (tb->format == SCOLS_FMT_HUMAN)
		tb->is_term = tb->termforce == SCOLS_TERMFORCE_NEVER  ? 0 :
			      tb->termforce == SCOLS_TERMFORCE_ALWAYS ? 1 :
			      isatty(STDOUT_FILENO);

	if (tb->is_term) {
		size_t width = (size_t) scols_table_get_termwidth(tb);

		if (tb->termreduce > 0 && tb->termreduce < width) {
			width -= tb->termreduce;
			scols_table_set_termwidth(tb, width);
		}
		bufsz = width;
	} else
		bufsz = BUFSIZ;

	if (!tb->is_term || tb->format != SCOLS_FMT_HUMAN || scols_table_is_tree(tb))
		tb->header_repeat = 0;

	/*
	 * Estimate extra space necessary for tree, JSON or another output
	 * decoration.
	 */
	if (scols_table_is_tree(tb))
		extra_bufsz += tb->nlines * strlen(vertical_symbol(tb));

	switch (tb->format) {
	case SCOLS_FMT_RAW:
		extra_bufsz += tb->ncols;			/* separator between columns */
		break;
	case SCOLS_FMT_JSON:
		ul_jsonwrt_init(&tb->json, tb->out, 0);
		extra_bufsz += tb->nlines * 3;		/* indentation */
		/* fallthrough */
	case SCOLS_FMT_EXPORT:
	{
		struct libscols_column *cl;

		scols_reset_iter(&itr, SCOLS_ITER_FORWARD);

		while (scols_table_next_column(tb, &itr, &cl) == 0) {
			if (scols_column_is_hidden(cl))
				continue;

			if (scols_column_get_name(cl))
				extra_bufsz += strlen(scols_column_get_name(cl));	/* data */
			extra_bufsz += 2;						/* separators */
		}
		break;
	}
	case SCOLS_FMT_HUMAN:
		break;
	}

	/*
	 * Enlarge buffer if necessary, the buffer should be large enough to
	 * store line data and tree ascii art (or another decoration).
	 */
	scols_reset_iter(&itr, SCOLS_ITER_FORWARD);
	while (scols_table_next_line(tb, &itr, &ln) == 0) {
		size_t sz;

		sz = strlen_line(ln) + extra_bufsz;
		if (sz > bufsz)
			bufsz = sz;
	}

	/* pre-allocate space for data */
	rc = ul_buffer_alloc_data(buf, bufsz + 1);	/* data + space for \0 */
	if (rc)
		goto err;

	/*
	 * Make sure groups members are in the same orders as the tree
	 */
	if (has_groups(tb) && scols_table_is_tree(tb))
		scols_groups_fix_members_order(tb);

	if (tb->format == SCOLS_FMT_HUMAN) {
		rc = __scols_calculate(tb, buf);
		if (rc != 0)
			goto err;
	}

	return 0;
err:
	__scols_cleanup_printing(tb, buf);
	return rc;
}

