/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/* Copyright (C) 2020-2025 Hans Petter Jansson
 *
 * This file is part of Chafa, a program that shows pictures on text terminals.
 *
 * Chafa is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Chafa is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Chafa.  If not, see <http://www.gnu.org/licenses/>. */

#include "config.h"

#include "chafa.h"
#include "internal/chafa-canvas-printer.h"

typedef struct
{
    ChafaCanvas *canvas;
    ChafaTermInfo *term_info;

    gunichar cur_char;
    gint n_reps;
    guint cur_inverted : 1;
    guint cur_bold : 1;
    guint32 cur_fg;
    guint32 cur_bg;

    /* For direct-color mode */
    ChafaColor cur_fg_direct;
    ChafaColor cur_bg_direct;
}
PrintCtx;

static gint
cmp_colors (ChafaColor a, ChafaColor b)
{
    return memcmp (&a, &b, sizeof (ChafaColor));
}

static ChafaColor
threshold_alpha (ChafaColor col, gint alpha_threshold)
{
    col.ch [3] = col.ch [3] < alpha_threshold ? 0 : 255;
    return col;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
flush_chars (PrintCtx *ctx, gchar *out)
{
    gchar buf [8];
    gint len;

    if (!ctx->cur_char)
        return out;

    len = g_unichar_to_utf8 (ctx->cur_char, buf);

    if ((ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REPEAT_CELLS)
        && chafa_term_info_have_seq (ctx->term_info, CHAFA_TERM_SEQ_REPEAT_CHAR)
        && ctx->n_reps > 1
        && ctx->n_reps * len > len + 4 /* ESC [#b */)
    {
        memcpy (out, buf, len);
        out += len;

        out = chafa_term_info_emit_repeat_char (ctx->term_info, out, ctx->n_reps - 1);

        ctx->n_reps = 0;
    }
    else
    {
        do
        {
            memcpy (out, buf, len);
            out += len;
            ctx->n_reps--;
        }
        while (ctx->n_reps != 0);
    }

    ctx->cur_char = 0;
    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
queue_char (PrintCtx *ctx, gchar *out, gunichar c)
{
    if (ctx->cur_char == c)
    {
        ctx->n_reps++;
    }
    else
    {
        if (ctx->cur_char)
            out = flush_chars (ctx, out);

        ctx->cur_char = c;
        ctx->n_reps = 1;
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
reset_fg (PrintCtx *ctx, gchar *out)
{
    out = chafa_term_info_emit_reset_color_fg (ctx->term_info, out);

    ctx->cur_fg = CHAFA_PALETTE_INDEX_TRANSPARENT;
    ctx->cur_fg_direct.ch [3] = 0;

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
reset_attributes (PrintCtx *ctx, gchar *out)
{
    out = chafa_term_info_emit_reset_attributes (ctx->term_info, out);

    ctx->cur_inverted = FALSE;
    ctx->cur_bold = FALSE;
    ctx->cur_fg = CHAFA_PALETTE_INDEX_TRANSPARENT;
    ctx->cur_bg = CHAFA_PALETTE_INDEX_TRANSPARENT;
    ctx->cur_fg_direct.ch [3] = 0;
    ctx->cur_bg_direct.ch [3] = 0;

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_attributes_truecolor (PrintCtx *ctx, gchar *out,
                           ChafaColor fg, ChafaColor bg, gboolean inverted)
{
    if (ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES)
    {
        if (!ctx->canvas->config.fg_only_enabled
            && ((ctx->cur_inverted && !inverted)
                || (ctx->cur_fg_direct.ch [3] != 0 && fg.ch [3] == 0)
                || (ctx->cur_bg_direct.ch [3] != 0 && bg.ch [3] == 0)))
        {
            out = flush_chars (ctx, out);
            out = reset_attributes (ctx, out);
        }

        if (!ctx->cur_inverted && inverted)
        {
            out = flush_chars (ctx, out);
            out = chafa_term_info_emit_invert_colors (ctx->term_info, out);
        }

        if (cmp_colors (fg, ctx->cur_fg_direct))
        {
            if (cmp_colors (bg, ctx->cur_bg_direct) && bg.ch [3] != 0)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fgbg_direct (ctx->term_info, out,
                                                                  fg.ch [0], fg.ch [1], fg.ch [2],
                                                                  bg.ch [0], bg.ch [1], bg.ch [2]);
            }
            else if (fg.ch [3] != 0)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fg_direct (ctx->term_info, out,
                                                                fg.ch [0], fg.ch [1], fg.ch [2]);
            }
        }
        else if (cmp_colors (bg, ctx->cur_bg_direct) && bg.ch [3] != 0)
        {
            out = flush_chars (ctx, out);
            out = chafa_term_info_emit_set_color_bg_direct (ctx->term_info, out,
                                                            bg.ch [0], bg.ch [1], bg.ch [2]);
        }
    }
    else
    {
        out = flush_chars (ctx, out);
        out = reset_attributes (ctx, out);
        if (inverted)
            out = chafa_term_info_emit_invert_colors (ctx->term_info, out);

        if (fg.ch [3] != 0)
        {
            if (bg.ch [3] != 0)
            {
                out = chafa_term_info_emit_set_color_fgbg_direct (ctx->term_info, out,
                                                                  fg.ch [0], fg.ch [1], fg.ch [2],
                                                                  bg.ch [0], bg.ch [1], bg.ch [2]);
            }
            else
            {
                out = chafa_term_info_emit_set_color_fg_direct (ctx->term_info, out,
                                                                fg.ch [0], fg.ch [1], fg.ch [2]);
            }
        }
        else if (bg.ch [3] != 0)
        {
            out = chafa_term_info_emit_set_color_bg_direct (ctx->term_info, out,
                                                            bg.ch [0], bg.ch [1], bg.ch [2]);
        }
    }

    ctx->cur_fg_direct = fg;
    ctx->cur_bg_direct = bg;
    ctx->cur_inverted = inverted;
    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_truecolor (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];
        ChafaColor fg, bg;

        /* Wide symbols have a zero code point in the rightmost cell */
        if (cell->c == 0)
            continue;

        chafa_unpack_color (cell->fg_color, &fg);
        fg = threshold_alpha (fg, ctx->canvas->config.alpha_threshold);
        chafa_unpack_color (cell->bg_color, &bg);
        bg = threshold_alpha (bg, ctx->canvas->config.alpha_threshold);

        if (fg.ch [3] == 0 && bg.ch [3] != 0)
            out = emit_attributes_truecolor (ctx, out, bg, fg, TRUE);
        else
            out = emit_attributes_truecolor (ctx, out, fg, bg, FALSE);

        if (fg.ch [3] == 0 && bg.ch [3] == 0)
        {
            out = queue_char (ctx, out, ' ');
            if (i < i_max - 1 && ctx->canvas->cells [i + 1].c == 0)
                out = queue_char (ctx, out, ' ');
        }
        else
        {
            out = queue_char (ctx, out, cell->c);
        }
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
handle_attrs_with_reuse (PrintCtx *ctx, gchar *out,
                         guint32 fg, guint32 bg,
                         gboolean inverted, gboolean bold)
{
    /* In FG-only mode, we can't use inverse color or bold because the
     * attribute reset would mess with the background color. */
    if (ctx->canvas->config.fg_only_enabled)
        return out;

    if ((ctx->cur_inverted && !inverted)
        || (ctx->cur_bold && !bold)
        || (ctx->cur_fg != CHAFA_PALETTE_INDEX_TRANSPARENT && fg == CHAFA_PALETTE_INDEX_TRANSPARENT)
        || (ctx->cur_bg != CHAFA_PALETTE_INDEX_TRANSPARENT && bg == CHAFA_PALETTE_INDEX_TRANSPARENT))
    {
        out = flush_chars (ctx, out);
        out = reset_attributes (ctx, out);
    }

    if (!ctx->cur_inverted && inverted)
    {
        out = flush_chars (ctx, out);
        out = chafa_term_info_emit_invert_colors (ctx->term_info, out);
    }

    if (!ctx->cur_bold && bold)
    {
        out = flush_chars (ctx, out);
        out = chafa_term_info_emit_enable_bold (ctx->term_info, out);
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_attributes_256 (PrintCtx *ctx, gchar *out,
                     guint32 fg, guint32 bg, gboolean inverted)
{
    if (ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES)
    {
        out = handle_attrs_with_reuse (ctx, out, fg, bg, inverted, FALSE);

        if (fg != ctx->cur_fg)
        {
            if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fgbg_256 (ctx->term_info, out, fg, bg);
            }
            else if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fg_256 (ctx->term_info, out, fg);
            }
        }
        else if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = flush_chars (ctx, out);
            out = chafa_term_info_emit_set_color_bg_256 (ctx->term_info, out, bg);
        }
    }
    else
    {
        out = flush_chars (ctx, out);
        out = reset_attributes (ctx, out);
        if (inverted)
            out = chafa_term_info_emit_invert_colors (ctx->term_info, out);

        if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = chafa_term_info_emit_set_color_fgbg_256 (ctx->term_info, out, fg, bg);
            }
            else
            {
                out = chafa_term_info_emit_set_color_fg_256 (ctx->term_info, out, fg);
            }
        }
        else if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = chafa_term_info_emit_set_color_bg_256 (ctx->term_info, out, bg);
        }
    }

    ctx->cur_fg = fg;
    ctx->cur_bg = bg;
    ctx->cur_inverted = inverted;
    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_256 (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];
        guint32 fg, bg;

        /* Wide symbols have a zero code point in the rightmost cell */
        if (cell->c == 0)
            continue;

        fg = cell->fg_color;
        bg = cell->bg_color;

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            out = emit_attributes_256 (ctx, out, bg, fg, TRUE);
        else
            out = emit_attributes_256 (ctx, out, fg, bg, FALSE);

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg == CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = queue_char (ctx, out, ' ');
            if (i < i_max - 1 && ctx->canvas->cells [i + 1].c == 0)
                out = queue_char (ctx, out, ' ');
        }
        else
        {
            out = queue_char (ctx, out, cell->c);
        }
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_attributes_16 (PrintCtx *ctx, gchar *out,
                    guint32 fg, guint32 bg, gboolean inverted)
{
    if (ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES)
    {
        out = handle_attrs_with_reuse (ctx, out, fg, bg, inverted, FALSE);

        if (fg != ctx->cur_fg)
        {
            if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fgbg_16 (ctx->term_info, out, fg, bg);
            }
            else if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fg_16 (ctx->term_info, out, fg);
            }
        }
        else if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = flush_chars (ctx, out);
            out = chafa_term_info_emit_set_color_bg_16 (ctx->term_info, out, bg);
        }
    }
    else
    {
        out = flush_chars (ctx, out);
        out = reset_attributes (ctx, out);
        if (inverted)
            out = chafa_term_info_emit_invert_colors (ctx->term_info, out);

        if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = chafa_term_info_emit_set_color_fgbg_16 (ctx->term_info, out, fg, bg);
            }
            else
            {
                out = chafa_term_info_emit_set_color_fg_16 (ctx->term_info, out, fg);
            }
        }
        else if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = chafa_term_info_emit_set_color_bg_16 (ctx->term_info, out, bg);
        }
    }

    ctx->cur_fg = fg;
    ctx->cur_bg = bg;
    ctx->cur_inverted = inverted;
    return out;
}

/* Uses aixterm control codes for bright colors */
G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_16 (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];
        guint32 fg, bg;

        /* Wide symbols have a zero code point in the rightmost cell */
        if (cell->c == 0)
            continue;

        fg = cell->fg_color;
        bg = cell->bg_color;

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            out = emit_attributes_16 (ctx, out, bg, fg, TRUE);
        else
            out = emit_attributes_16 (ctx, out, fg, bg, FALSE);

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg == CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = queue_char (ctx, out, ' ');
            if (i < i_max - 1 && ctx->canvas->cells [i + 1].c == 0)
                out = queue_char (ctx, out, ' ');
        }
        else
        {
            out = queue_char (ctx, out, cell->c);
        }
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_attributes_16_8 (PrintCtx *ctx, gchar *out,
                          guint32 fg, guint32 bg, gboolean inverted)
{
    if (ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES)
    {
        out = handle_attrs_with_reuse (ctx, out, fg, bg, inverted, fg > 7 && fg < 256 ? TRUE : FALSE);

        if (fg != ctx->cur_fg)
        {
            if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fgbg_8 (ctx->term_info, out, fg & 7, bg);
            }
            else if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_set_color_fg_8 (ctx->term_info, out, fg & 7);
            }
        }
        else if (bg != ctx->cur_bg && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = flush_chars (ctx, out);
            out = chafa_term_info_emit_set_color_bg_8 (ctx->term_info, out, bg);
        }
    }
    else
    {
        out = flush_chars (ctx, out);
        out = reset_attributes (ctx, out);
        if (inverted)
            out = chafa_term_info_emit_invert_colors (ctx->term_info, out);
        if (fg > 7)
            out = chafa_term_info_emit_enable_bold (ctx->term_info, out);

        if (fg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            {
                out = chafa_term_info_emit_set_color_fgbg_8 (ctx->term_info, out, fg & 7, bg);
            }
            else
            {
                out = chafa_term_info_emit_set_color_fg_8 (ctx->term_info, out, fg & 7);
            }
        }
        else if (bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = chafa_term_info_emit_set_color_bg_8 (ctx->term_info, out, bg);
        }
    }

    ctx->cur_fg = fg;
    ctx->cur_bg = bg;
    ctx->cur_inverted = inverted;
    ctx->cur_bold = fg > 7 && fg < 256 ? TRUE : FALSE;
    return out;
}

/* Uses bold for bright FG colors. */
G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_16_8 (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];
        guint32 fg, bg;

        /* Wide symbols have a zero code point in the rightmost cell */
        if (cell->c == 0)
            continue;

        fg = cell->fg_color;
        bg = cell->bg_color;

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg != CHAFA_PALETTE_INDEX_TRANSPARENT)
            out = emit_attributes_16_8 (ctx, out, bg, fg, TRUE);
        else
            out = emit_attributes_16_8 (ctx, out, fg, bg, FALSE);

        if (fg == CHAFA_PALETTE_INDEX_TRANSPARENT && bg == CHAFA_PALETTE_INDEX_TRANSPARENT)
        {
            out = queue_char (ctx, out, ' ');
            if (i < i_max - 1 && ctx->canvas->cells [i + 1].c == 0)
                out = queue_char (ctx, out, ' ');
        }
        else
        {
            out = queue_char (ctx, out, cell->c);
        }
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_fgbg_bgfg (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    gunichar blank_symbol = 0;

    if (chafa_symbol_map_has_symbol (&ctx->canvas->config.symbol_map, ' '))
    {
        blank_symbol = ' ';
    }
    else if (chafa_symbol_map_has_symbol (&ctx->canvas->config.symbol_map, 0x2588 /* Solid block */ ))
    {
        blank_symbol = 0x2588;
    }

    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];
        gboolean invert = FALSE;
        gunichar c = cell->c;

        /* Wide symbols have a zero code point in the rightmost cell */
        if (c == 0)
            continue;

        /* Replace with blank symbol only if this is a single-width cell */
        if (cell->fg_color == cell->bg_color && blank_symbol != 0
            && (i == i_max - 1 || (ctx->canvas->cells [i + 1].c != 0)))
        {
            c = blank_symbol;
            if (blank_symbol == 0x2588)
                invert = TRUE;
        }

        if (cell->bg_color == CHAFA_PALETTE_INDEX_FG)
        {
            invert ^= TRUE;
        }

        if (ctx->canvas->config.optimizations & CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES)
        {
            if (!ctx->cur_inverted && invert)
            {
                out = flush_chars (ctx, out);
                out = chafa_term_info_emit_invert_colors (ctx->term_info, out);
            }
            else if (ctx->cur_inverted && !invert)
            {
                out = flush_chars (ctx, out);
                out = reset_attributes (ctx, out);
            }

            ctx->cur_inverted = invert;
        }
        else
        {
            out = flush_chars (ctx, out);
            if (invert)
                out = chafa_term_info_emit_invert_colors (ctx->term_info, out);
            else
                out = reset_attributes (ctx, out);
        }

        out = queue_char (ctx, out, c);
    }

    return out;
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
emit_ansi_fgbg (PrintCtx *ctx, gchar *out, gsize i, gsize i_max)
{
    for ( ; i < i_max; i++)
    {
        ChafaCanvasCell *cell = &ctx->canvas->cells [i];

        /* Wide symbols have a zero code point in the rightmost cell */
        if (cell->c == 0)
            continue;

        out = queue_char (ctx, out, cell->c);
    }

    return out;
}

static void
prealloc_string (GString *gs, gint n_cells)
{
    gsize needed_len;

    /* Each cell produces at most three control sequences and six bytes
     * for the UTF-8 character. Each row may add one seq and one newline. */
    needed_len = ((gsize) n_cells + 1) * (CHAFA_TERM_SEQ_LENGTH_MAX * 3 + 6) + 1;

    if (gs->allocated_len - gs->len < needed_len)
    {
        gsize current_len = gs->len;
        g_string_set_size (gs, gs->len + needed_len * 2);
        gs->len = current_len;
    }
}

G_GNUC_WARN_UNUSED_RESULT static gchar *
build_ansi_row (PrintCtx *ctx, gint row, gchar *out)
{
    ChafaCanvas *canvas;
    gsize i, i_max;

    canvas = ctx->canvas;
    i = (gsize) row * (gsize) canvas->config.width;
    i_max = (gsize) (row + 1) * (gsize) canvas->config.width;

    if (row == 0
        && canvas->config.canvas_mode != CHAFA_CANVAS_MODE_FGBG)
    {
        if (canvas->config.fg_only_enabled)
            out = reset_fg (ctx, out);
        else
            out = reset_attributes (ctx, out);
    }

    switch (canvas->config.canvas_mode)
    {
        case CHAFA_CANVAS_MODE_TRUECOLOR:
            out = emit_ansi_truecolor (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_INDEXED_256:
        case CHAFA_CANVAS_MODE_INDEXED_240:
            out = emit_ansi_256 (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_INDEXED_16:
            out = emit_ansi_16 (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_INDEXED_16_8:
            out = emit_ansi_16_8 (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_INDEXED_8:
            out = emit_ansi_16 (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_FGBG_BGFG:
            out = emit_ansi_fgbg_bgfg (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_FGBG:
            out = emit_ansi_fgbg (ctx, out, i, i_max);
            break;
        case CHAFA_CANVAS_MODE_MAX:
            g_assert_not_reached ();
            break;
    }

    out = flush_chars (ctx, out);

    /* Avoid control codes in FGBG mode. Don't reset attributes when BG
     * is held, to preserve any BG color set previously. */
    if (canvas->config.canvas_mode != CHAFA_CANVAS_MODE_FGBG)
    {
        if (canvas->config.fg_only_enabled)
            out = reset_fg (ctx, out);
        else
            out = reset_attributes (ctx, out);
    }

    return out;
}

static GString *
build_ansi_gstring (ChafaCanvas *canvas, ChafaTermInfo *ti)
{
    GString *gs = g_string_new ("");
    PrintCtx ctx = { 0 };
    gint i;

    ctx.canvas = canvas;
    ctx.term_info = ti;

    for (i = 0; i < canvas->config.height; i++)
    {
        gchar *out;

        prealloc_string (gs, canvas->config.width);
        out = gs->str + gs->len;

        out = build_ansi_row (&ctx, i, out);

        /* Rows end in a newline, except for the last one. This allows for
         * filling the display area while avoiding a blank bottom row. */
        if (i < canvas->config.height - 1)
            *(out++) = '\n';

        *out = '\0';
        gs->len = out - gs->str;
    }

    return gs;
}

static void
build_ansi_gstring_array (ChafaCanvas *canvas, ChafaTermInfo *ti,
                          GString ***array_out, gint *array_len_out)
{
    PrintCtx ctx = { 0 };
    GString **array;
    gint i;

    ctx.canvas = canvas;
    ctx.term_info = ti;

    array = g_new (GString *, canvas->config.height + 1);

    for (i = 0; i < canvas->config.height; i++)
    {
        GString *gs = g_string_new ("");
        gchar *out;

        prealloc_string (gs, canvas->config.width);
        out = gs->str + gs->len;

        out = build_ansi_row (&ctx, i, out);
        *out = '\0';

        gs->len = out - gs->str;
        array [i] = gs;
    }

    array [canvas->config.height] = NULL;
    *array_out = array;

    if (array_len_out)
        *array_len_out = canvas->config.height;
}

GString *
chafa_canvas_print_symbols (ChafaCanvas *canvas, ChafaTermInfo *ti)
{
    g_assert (canvas != NULL);
    g_assert (ti != NULL);

    return build_ansi_gstring (canvas, ti);
}

void
chafa_canvas_print_symbol_rows (ChafaCanvas *canvas, ChafaTermInfo *ti,
                                GString ***array_out, gint *array_len_out)
{
    g_assert (canvas != NULL);
    g_assert (ti != NULL);
    g_assert (array_out != NULL);

    build_ansi_gstring_array (canvas, ti, array_out, array_len_out);
}
