/*
    See license.txt in the root of this project.
*/

# include "luametatex.h"

print_state_info lmt_print_state = {
     .logfile               = NULL,
     .loggable_info         = NULL,
     .selector              = 0,
     .tally                 = 0,
     .terminal_offset       = 0,
     .logfile_offset        = 0,
     .new_string_line       = 0,
     .trick_buffer          = { 0 },
     .trick_count           = 0,
     .first_count           = 0,
     .saved_selector        = 0,
     .font_in_short_display = 0,
     .saved_logfile         = NULL,
     .saved_logfile_offset  = 0,
     .nesting_depth         = 0,
};

/*tex

    During the development of \LUAMETATEX\ reporting has been stepwise upgraded, for instance with more
    abstract print functions and a formatter. Much more detail is shown and additional tracing options
    have been added (like for marks, inserts, adjust, math, etc.). The format of the traditonal messages
    was mostly kept (sometimes under paramameter control using a higher tracing value) but after reading
    the nth ridiculous comment about logging in \LUATEX\ related to \CONTEXT\ I decided that it no
    longer made sense to offer compatibility because it will never satisfy everyone and we want to move
    on, so per spring 2022 we will see even further normalization and log compatibility options get (are)
    dropped. If there are inconsistencies left, assume they will be dealt with. It's all about being able
    to recognize what gets logged. If someone longs for the old reporting, there are plenty alternative
    engines available.

    [where: ...] : all kind of tracing
    {...}        : more traditional tex tracing
    <...>        : if tracing (maybe)

*/

/*tex

    Messages that are sent to a user's terminal and to the transcript-log file are produced by
    several |print| procedures. These procedures will direct their output to a variety of places,
    based on the setting of the global variable |selector|, which has the following possible values:

    \startitemize

    \startitem
        |term_and_log|, the normal setting, prints on the terminal and on the transcript file.
    \stopitem

    \startitem
        |log_only|, prints only on the transcript file.
    \stopitem

    \startitem
        |term_only|, prints only on the terminal.
    \stopitem

    \startitem
        |no_print|, doesn't print at all. This is used only in rare cases before the transcript
        file is open.
    \stopitem

    \startitem
        |pseudo|, puts output into a cyclic buffer that is used by the |show_context| routine; when
        we get to that routine we shall discuss the reasoning behind this curious mode.
    \stopitem

    \startitem
        |new_string|, appends the output to the current string in the string pool.
    \stopitem

    \startitem
        0 to 15, prints on one of the sixteen files for |\write| output.
    \stopitem

    \stopitemize

    The symbolic names |term_and_log|, etc., have been assigned numeric codes that satisfy the
    convenient relations |no_print + 1 = term_only|, |no_print + 2 = log_only|, |term_only + 2 =
    log_only + 1 = term_and_log|.

    Three additional global variables, |tally| and |term_offset| and |file_offset|, record the
    number of characters that have been printed since they were most recently cleared to zero. We
    use |tally| to record the length of (possibly very long) stretches of printing; |term_offset|
    and |file_offset|, on the other hand, keep track of how many characters have appeared so far on
    the current line that has been output to the terminal or to the transcript file, respectively.

    The state structure collects: |new_string_line| and |escape_controls|, the transcript handle of
    a \TEX\ session: |log_file|, the target of a message: |selector|, the digits in a number being
    output |dig[23]|, the number of characters recently printed |tally|, the number of characters
    on the current terminal line |term_offset|, the number of characters on the current file line
    |file_offset|, the circular buffer for pseudo printing |trick_buf|, the threshold for
    pseudo printing (explained later) |trick_count|, another variable for pseudo printing
    |first_count|, a blocker for minor adjustments to |show_token_list| namely |inhibit_par_tokens|.

    To end a line of text output, we call |print_ln|:

*/

void tex_print_ln(void)
{
    switch (lmt_print_state.selector) {
        case no_print_selector_code:
            break;
        case terminal_selector_code:
            fputc('\n', stdout);
            lmt_print_state.terminal_offset = 0;
            break;
        case logfile_selector_code:
            fputc('\n', lmt_print_state.logfile);
            lmt_print_state.logfile_offset = 0;
            break;
        case terminal_and_logfile_selector_code:
            fputc('\n', stdout);
            fputc('\n', lmt_print_state.logfile);
            lmt_print_state.terminal_offset = 0;
            lmt_print_state.logfile_offset = 0;
            break;
        case pseudo_selector_code:
            break;
        case new_string_selector_code:
            if (lmt_print_state.new_string_line > 0) {
                tex_print_char(lmt_print_state.new_string_line);
            }
            break;
        case luabuffer_selector_code:
            lmt_newline_to_buffer();
            break;
        default:
            break;
    }
    /*tex |tally| is not affected */
}

/*tex

    First a few helpers. Pseudo printing only happens when we have an error message and have
    to collect a few lines in a way that works well with excessively long token lists that
    the get serialized. The two helpers are non-critical but nevertheless optimized i.e.
    we don't go char-by-char when we have a string.

*/

static inline void tex_aux_print_pseudo_string(const unsigned char *s, size_t len)
{
    size_t buf_size = (size_t)lmt_error_state.line_limits.size;
    if (buf_size > 0 && lmt_print_state.tally < lmt_print_state.trick_count) {
        size_t avail = (size_t)(lmt_print_state.trick_count - lmt_print_state.tally);
        size_t to_copy = (len < avail) ? len : avail;
        if (to_copy > 0) {
            /* Check if the chunk wraps around the circular buffer boundary */
            size_t offset = (size_t)(lmt_print_state.tally % buf_size);
            if (offset + to_copy <= buf_size) {
                memcpy(&lmt_print_state.trick_buffer[offset], s, to_copy);
            } else {
                size_t first_part = buf_size - offset;
                memcpy(&lmt_print_state.trick_buffer[offset], s, first_part);
                memcpy(&lmt_print_state.trick_buffer[0], s + first_part, to_copy - first_part);
            }
        }
    }
    lmt_print_state.tally += (int) len;
}

static inline void tex_aux_print_pseudo_char(unsigned char c)
{
    int k = lmt_print_state.tally++;
    size_t buf_size = (size_t)lmt_error_state.line_limits.size;
    if (k < lmt_print_state.trick_count && buf_size > 0) {
        lmt_print_state.trick_buffer[k % buf_size] = c;
    }
}

/*tex

    The |print_char| procedure sends one byte to the desired destination. All printing comes through
    |print_ln| or |print_char|, except for the case of |print_str| (see below).

    The checking of the line length is an inheritance from previous engines and we dropped it here.
    It doesn't make much sense nowadays. The same is true for escaping.

    Incrementing the tally ... only needed in pseudo mode :

*/

void tex_print_char(int s)
{
 // s = s < 0 ? 0 : (s > 255 ? 255 : s);
    if lmt_unlikely(s < 0 || s > 255) {
        tex_formatted_warning("print", "weird character %i", s);
    } else {
        switch (lmt_print_state.selector) {
            case no_print_selector_code:
                break;
            case terminal_selector_code:
                if (s == new_line_char_par) {
                    fputc('\n', stdout);
                    lmt_print_state.terminal_offset = 0;
                } else {
                    fputc(s, stdout);
                    ++lmt_print_state.terminal_offset;
                }
                break;
            case logfile_selector_code:
                if (s == new_line_char_par) {
                    fputc('\n', lmt_print_state.logfile);
                    lmt_print_state.logfile_offset = 0;
                } else {
                    fputc(s, lmt_print_state.logfile);
                    ++lmt_print_state.logfile_offset;
                }
                break;
            case terminal_and_logfile_selector_code:
                if (s == new_line_char_par) {
                    fputc('\n', stdout);
                    fputc('\n', lmt_print_state.logfile);
                    lmt_print_state.terminal_offset = 0;
                    lmt_print_state.logfile_offset = 0;
                } else {
                    fputc(s, stdout);
                    fputc(s, lmt_print_state.logfile);
                    ++lmt_print_state.terminal_offset;
                    ++lmt_print_state.logfile_offset;
                }
                break;
            case pseudo_selector_code:
             // if (lmt_print_state.tally < lmt_print_state.trick_count) {
             //     lmt_print_state.trick_buffer[lmt_print_state.tally % lmt_error_state.line_limits.size] = (unsigned char) s;
             // }
             // ++lmt_print_state.tally;
                tex_aux_print_pseudo_char((unsigned char) s);
                break;
            case new_string_selector_code:
                tex_append_char((unsigned char) s);
                break;
            case luabuffer_selector_code:
                lmt_char_to_buffer((char) s);
                break;
            default:
                break;
        }
    }
}

/*tex

    An entire string is output by calling |print|. Note that if we are outputting the single
    standard \ASCII\ character |c|, we could call |print("c")|, since |"c" = 99| is the number of a
    single-character string, as explained above. But |print_char("c")| is quicker, so \TEX\ goes
    directly to the |print_char| routine when it knows that this is safe. (The present
    implementation assumes that it is always safe to print a visible \ASCII\ character.)

    The first 256 entries above the 17th unicode plane are used for a special trick: when \TEX\ has
    to print items in that range, it will instead print the character that results from substracting
    0x110000 from that value. This allows byte-oriented output to things like |\specials|. We dropped
    this feature because it was never used (we used it as part of experiments with \LUATEX). The old
    code branches can be found in the repository.

*/

/* no_print terminal | logfile | terminal_and_logfile | pseudo | new_string | luabuffer */

static void tex_aux_uprint(int s)
{
    /*tex We're not sure about this so it's disabled for now! */
    /*
    if ((print_state.selector > pseudo_selector_code)) {
        / *tex internal strings are not expanded * /
        print_char(s);
        return;
    }
    */
    if (s == new_line_char_par && lmt_print_state.selector < pseudo_selector_code) {
        tex_print_ln();
    } else {
        aux_uni2str_callback((unsigned) s, &tex_print_char);
    }
}

void tex_print_tex_str(int s)
{
    if (s >= lmt_string_pool_state.string_pool_data.ptr) {
        tex_normal_warning("print", "bad string pointer");
    } else if (s < cs_offset_value) {
        if (s < 0) {
            tex_normal_warning("print", "bad string offset");
        } else {
            tex_aux_uprint(s);
        }
    } else if (lmt_print_state.selector == new_string_selector_code) {
        tex_append_string(str_getstr(s), str_getlen(s));
    } else {
        lstring_string j = str_getstr(s);
        for (lstring_length i = 0; i < str_getlen(s); i++) {
            tex_print_char(j[i]);
        }
    }
}

/*tex

    The procedure |print_nl| is like |print|, but it makes sure that the string appears at the
    beginning of a new line.

*/

void tex_print_nlp(void)
{
    if (lmt_print_state.new_string_line > 0) {
        tex_print_char(lmt_print_state.new_string_line);
    } else {
        switch (lmt_print_state.selector) {
             case terminal_selector_code:
                 if (lmt_print_state.terminal_offset > 0) {
                     fputc('\n', stdout);
                     lmt_print_state.terminal_offset = 0;
                 }
                 break;
             case logfile_selector_code:
                 if (lmt_print_state.logfile_offset > 0) {
                     fputc('\n', lmt_print_state.logfile);
                     lmt_print_state.logfile_offset = 0;
                 }
                 break;
             case terminal_and_logfile_selector_code:
                 if (lmt_print_state.terminal_offset > 0) {
                     fputc('\n', stdout);
                     lmt_print_state.terminal_offset = 0;
                 }
                 if (lmt_print_state.logfile_offset > 0) {
                     fputc('\n', lmt_print_state.logfile);
                     lmt_print_state.logfile_offset = 0;
                 }
                 break;
             case luabuffer_selector_code:
                 lmt_newline_to_buffer();
                 break;
        }
    }
}

/*tex

    The |char *| versions of the same procedures. |print_str| is different because it uses
    buffering, which works well because most of the output actually comes through |print_str|.

*/

void tex_print_str_len(const char *s, int len)
{
    if (len <= 0 || ! s) {
        return;
    }
    int logfile = 0;
    int terminal = 0;
    switch (lmt_print_state.selector) {
        case no_print_selector_code:
            return;
        case terminal_selector_code:
            terminal = 1;
            break;
        case logfile_selector_code:
            logfile = 1;
            break;
        case terminal_and_logfile_selector_code:
            logfile = 1;
            terminal = 1;
            break;
        case pseudo_selector_code:
         // while (len-- && (lmt_print_state.tally < lmt_print_state.trick_count)) {
         //     lmt_print_state.trick_buffer[lmt_print_state.tally % lmt_error_state.line_limits.size] = (unsigned char) *s++;
         //     lmt_print_state.tally++;
         // }
            tex_aux_print_pseudo_string((const unsigned char *) s, (size_t) len);
            return;
        case new_string_selector_code:
            tex_append_string((const unsigned char *) s, (unsigned) len);
            return;
        case luabuffer_selector_code:
            lmt_string_to_buffer_len(s, len);
            return;
        default:
            return;
    }
    if (terminal || logfile) {
        if (logfile && ! lmt_fileio_state.log_opened) {
            logfile = 0;
        }
        int newline = (s[len-1] == '\n');
        if (logfile) {
            fwrite(s, 1, len, lmt_print_state.logfile);
            if (newline) {
                lmt_print_state.logfile_offset = 0;
            } else {
                lmt_print_state.logfile_offset += len;
            }
        }
        if (terminal) {
            fwrite(s, 1, len, stdout);
            if (newline) {
                lmt_print_state.terminal_offset = 0;
            } else {
                lmt_print_state.terminal_offset += len;
            }
        }
    }
}

void tex_print_str(const char *s)
{
    if (s) {
        tex_print_str_len(s, (int) strlen(s));
    }
}

/*tex

    Here is the very first thing that \TEX\ prints: a headline that identifies the version number
    and format package. The |term_offset| variable is temporarily incorrect, but the discrepancy is
    not serious since we assume that the banner and format identifier together will occupy at most
    |max_print_line| character positions. Well, we dropped that check in this variant.

    Maybe we should drop printing the format identifier.

*/

void tex_print_banner(void)
{
    fprintf(
        stdout,
        "%s %s\n",
        lmt_engine_state.luatex_banner,
        lmt_engine_state.dump_name
    );
}

void tex_print_log_banner(void)
{
    fprintf(
        lmt_print_state.logfile,
        "engine: %s, format id: %s, time stamp: %d-%d-%d %d:%d, startup file: %s, job name: %s",
        lmt_engine_state.luatex_banner,
        lmt_engine_state.dump_name,
        year_par, month_par > 12 ? 0 : month_par, day_par, time_par / 60, time_par % 60,
        lmt_engine_state.startup_filename ? lmt_engine_state.startup_filename : "-",
        lmt_engine_state.startup_jobname ? lmt_engine_state.startup_jobname : "-"
    );
}

void tex_print_version_banner(void)
{
    fputs(lmt_engine_state.luatex_banner, stdout);
}

/*tex

    The procedure |print_esc| prints a string that is preceded by the user's escape character
    (which is usually a backslash).

*/

static inline void tex_print_tex_str_esc(strnumber s)
{
    /*tex Set variable |c| to the current escape character: */
    int c = escape_char_par;
    if (c >= 0) {
        tex_print_tex_str(c);
    }
    if (s) {
        tex_print_tex_str(s);
    }
}

/*tex This prints escape character, then |s|. */

static inline void tex_print_str_esc(const char *s)
{
    /*tex Set variable |c| to the current escape character: */
    int c = escape_char_par;
    if (c >= 0) {
        tex_print_tex_str(c);
    }
    if (s && s[0]) {
        tex_print_str(s);
    }
}

/*tex

    The following procedure, which prints out the decimal representation of a given integer |n|,
    has been written carefully so that it works properly if |n = 0| or if |(-n)| would cause
    overflow. It does not apply |mod| or |div| to negative arguments, since such operations are not
    implemented consistently by all \PASCAL\ compilers.

*/

# if 0

    void tex_print_int(int n)
    {
        if (n < 0) {
            tex_print_char('-');
            n = -n;
        }
        if (n >= 0 && n <= 9) {
            tex_print_char('0' + n);
        } else if (n >= 0 && n <= 99) {
            tex_print_char('0' + n / 10);
            tex_print_char('0' + n % 10);
        } else {
            int k = 0;
            unsigned char digits[24];
            do {
                digits[k] = '0' + (unsigned char) (n % 10);
                n = n / 10;
                ++k;
            } while (n != 0);
            while (k-- > 0) {
                tex_print_char(digits[k]);
            }
        }
    }

# else

    static const char DIGIT_PAIRS[] =
        "00010203040506070809"
        "10111213141516171819"
        "20212223242526272829"
        "30313233343536373839"
        "40414243444546474849"
        "50515253545556575859"
        "60616263646566676869"
        "70717273747576777879"
        "80818283848586878889"
        "90919293949596979899";

    void tex_print_int(int n)
    {
        // Max size for 32-bit int: 1 byte sign + 10 digits = 11 bytes.
        // 16 bytes provides a safe, 16-byte aligned stack allocation.
        char buf[16];
        char *p = buf + sizeof(buf);
        // Cast to unsigned int safely handling INT_MIN (-2147483648)
        unsigned int val = (n < 0) ? (0u - (unsigned int)n) : (unsigned int)n;
        // Process 2 digits at a time
        while (val >= 100) {
            unsigned int q = val / 100;
            unsigned int rem = val - (q * 100);
            val = q;
            p -= 2;
            memcpy(p, &DIGIT_PAIRS[rem * 2], 2);
        }
        // Remaining 1 or 2 digits (< 100)
        if (val < 10) {
            *(--p) = (char)('0' + val);
        } else {
            p -= 2;
            memcpy(p, &DIGIT_PAIRS[val * 2], 2);
        }
        // Prepend sign
        if (n < 0) {
            *(--p) = '-';
        }
        // Pass exact pointer and calculated string length
        tex_print_str_len(p, (int) ((buf + sizeof(buf)) - p));
    }

# endif

/*tex

    Conversely, here is a procedure analogous to |print_int|. If the output of this procedure is
    subsequently read by \TEX\ and converted by the |round_decimals| routine above, it turns out
    that the original value will be reproduced exactly; the \quote {simplest} such decimal number
    is output, but there is always at least one digit following the decimal point.

    The invariant relation in the |repeat| loop is that a sequence of decimal digits yet to be
    printed will yield the original number if and only if they form a fraction~$f$ in the range $s
    - \delta \L10 \cdot 2^{16} f < s$. We can stop if and only if $f = 0$ satisfies this condition;
    the loop will terminate before $s$ can possibly become zero.

    The next one prints a scaled real, rounded to five digits.

    todo: time these two variants

*/

# if 0

    void tex_print_dimension(scaled s, int unit)
    {
        if (s == 0) {
            tex_print_str_len("0.0", 3); /* really .. just 0 is not ok for some applications */
        } else {
            /*tex The amount of allowable inaccuracy: */
            scaled delta = 10;
            char buffer[20] = { 0 } ;
            int i = 0;
            if (s < 0) {
                /*tex Print the sign, if negative. */
                tex_print_char('-');
                s = -s;
            }
            /*tex Print the integer part. */
            tex_print_int(s / unity);
            buffer[i++] = '.';
            s = 10 * (s % unity) + 5;
            do {
                if (delta > unity) {
                    /*tex Round the last digit, so: |s + 32768 - 50000| it is. */
                    s = s + 0x8000 - 50000;
                }
                buffer[i++] = (unsigned char) ('0' + (s / unity));
                s = 10 * (s % unity);
                delta *= 10;
            } while (s > delta);
         // buffer[i++] = '\0';
            tex_print_str(buffer);
        }
        if (unit != no_unit) {
            tex_print_unit(unit);
        }
    }

    void tex_print_sparse_dimension(scaled s, int unit)
    {
        if (s == 0) {
            tex_print_char('0');
        } else if (s == unity) {
            tex_print_char('1');
        } else {
            /*tex The amount of allowable inaccuracy: */
            scaled delta = 10;
            char buffer[20];
            int i = 0;
            if (s < 0) {
                /*tex Print the sign, if negative. */
                tex_print_char('-');
                /*tex So we trust it here while in printing int we mess around. */
                s = -s;
            }
            /*tex Print the integer part. */
            tex_print_int(s / unity);
            s = 10 * (s % unity) + 5;
            do {
                if (delta > unity) {
                    /*tex Round the last digit. */
                    s = s + 0100000 - 50000;
                }
                buffer[i++] = (unsigned char) ('0' + (s / unity));
                s = 10 * (s % unity);
                delta *= 10;
            } while (s > delta);
            if (i == 1 && buffer[i-1] == '0') {
                /* no need */
            } else {
                buffer[i++] = '\0';
                tex_print_char('.');
                tex_print_str(buffer);
            }
        }
        if (unit != no_unit) {
            tex_print_unit(unit);
        }
    }

# else

    # ifndef UNITY
        # define UNITY 65536
    # endif

    static void tex_aux_print_scaled(scaled s, int unit, int is_sparse)
    {
        // Fast path 1: 0.0 vs 0
        if (s == 0) {
            if (unit == no_unit) {
                if (is_sparse) { tex_print_char('0'); } else { tex_print_str_len("0.0", 3); }
            } else if (unit == pt_unit) {
                tex_print_str_len(is_sparse ? "0pt" : "0.0pt", is_sparse ? 3 : 5);
            } else {
                tex_print_str_len(is_sparse ? "0mu" : "0.0mu", is_sparse ? 3 : 5);
            }
            return;
        }
        // Fast path 2: sparse 1pt
        if (is_sparse && s == UNITY) {
            if (unit == no_unit) {
                if (is_sparse) { tex_print_char('1'); } else { tex_print_str_len("1.0", 3); }
            } else if (unit == pt_unit) {
                tex_print_str_len(is_sparse ? "1pt" : "1.0pt", is_sparse ? 3 : 5);
            } else {
                tex_print_str_len(is_sparse ? "1mu" : "1.0mu", is_sparse ? 3 : 5);
            }
            return;
        }
        // 1. Handle sign safely
        int is_negative = 0;
        unsigned int u_s;
        if (s < 0) {
            is_negative = 1;
         // u_s = -((unsigned int) s);
            u_s = 0u -((unsigned int) s); // for ms
        } else {
            u_s = (unsigned int) s;
        }
        // Stack buffer for entire dimension output .. plenty
        char buf[64];
        char *p = buf;
        if (is_negative) {
            *p++ = '-';
        }
        // 2. Format integer part into buffer
        unsigned int int_part = u_s / UNITY;
        unsigned int frac_part = u_s % UNITY;
        // Fast inline base-10 formatting for integer part
        if (int_part == 0) {
            *p++ = '0';
        } else {
            char int_buf[16];
            char *ip = &int_buf[sizeof(int_buf) - 1];
            *ip = '\0';
            while (int_part > 0) {
                *(--ip) = (char) ('0' + (int_part % 10));
                int_part /= 10;
            }
            size_t len = strlen(ip);
            memcpy(p, ip, len);
            p += len;
        }
        // 3. Format fractional part using TeX allowable inaccuracy loop
        char frac_buf[20];
        int frac_len = 0;
        scaled delta = 10;
        scaled work_s = (scaled) (10 * frac_part + 5);
        do {
            if (delta > UNITY) {
                // Round the last digit: s + 32768 - 50000 (0x8000 = 32768)
                work_s += 0x8000 - 50000;
            }
            frac_buf[frac_len++] = (char)('0' + (work_s / UNITY));
            work_s = 10 * (work_s % UNITY);
            delta *= 10;
        } while (work_s > delta);
        // 4. Apply sparse mode rules vs standard dimension rules
        if (is_sparse && frac_len == 1 && frac_buf[0] == '0') {
            // Skip writing decimal point for trailing zero in sparse mode (e.g., "5.0" -> "5")
        } else {
            *p++ = '.';
            memcpy(p, frac_buf, frac_len);
            p += frac_len;
        }
        if (unit != no_unit) {
            const char *u = (unit == pt_unit) ? "pt" : "mu";
            memcpy(p, u, 2);
            p += 2;
        }
        tex_print_str_len(buf, (int)(p - buf));
    }

    void tex_print_dimension(scaled s, int unit)
    {
        tex_aux_print_scaled(s, unit, 0);
    }

    void tex_print_sparse_dimension(scaled s, int unit)
    {
        tex_aux_print_scaled(s, unit, 1);
    }

# endif

/*tex
    Till we start using this more extensively this one will not be used often.
*/

static void tex_aux_print_posit(halfword s, int texlike)
{
    if (s == 0) {
        tex_print_char('0');
        return;
    }
    double n = tex_posit_to_double(s);
    if (n == 1.0) {
        tex_print_char('1');
        return;
    }
    if (n == -1.0) {
        tex_print_str_len("-1", 2);
        return;
    }
    if (fmod(n, 1.0) == 0.0 && n >= min_integer && n <= max_integer) {
        tex_print_int((int)n);
        return;
    }
    char buf[128];
    int len = snprintf(buf, sizeof(buf), texlike ? "%.5f" : "%.9f", n);
    if (len > 0 && len < (int) sizeof(buf)) {
        while (len > 0 && buf[len - 1] == '0') {
            len--;
        }
        if (len > 0 && buf[len - 1] == '.') {
            len--;
        }
        tex_print_str_len(buf, len);
    }
}

void tex_print_posit  (halfword s) { tex_aux_print_posit(s, 0); }
void tex_print_posit_5(halfword s) { tex_aux_print_posit(s, 1); }

/*tex

    Hexadecimal printing of nonnegative integers is accomplished by |print_hex|. We have a few
    variants. Because we have bitsets that can give upto |0xFFFFFFFF| we treat the given integer
    as an unsigned.
*/

# if 0

    void tex_print_hex(long long sn)
    {
        if (sn == 0) {
            tex_print_char('0');
        } else {
            unsigned long long n = 0;
            int k = 0;
            unsigned char digits[24];
            if (sn < 0) {
                tex_print_char('-');
                n = (unsigned long long) -sn;
            } else {
                n = (unsigned long long) sn;
            }
            do {
                unsigned char d = (unsigned char) (n % 16);
                if (d < 10) {
                    digits[k] = '0' + d;
                } else {
                    digits[k] = 'A' - 10 + d;
                }
                n = n / 16;
                ++k;
            } while (n != 0);
            while (k-- > 0) {
                tex_print_char(digits[k]);
            }
        }
    }

    static void tex_print_qhex(long long n)
    {
        tex_print_char('"');
        tex_print_hex(n);
    }

    static void tex_print_uhex(long long n)
    {
        tex_print_str_len("U+", 2);
        /* todo: loop */
        if (n < 16) {
            tex_print_char('0');
        }
        if (n < 256) {
            tex_print_char('0');
        }
        if (n < 4096) {
            tex_print_char('0');
        }
        tex_print_hex(n);
    }

    static void tex_print_xhex(long long n)
    {
        tex_print_char('"');
        /* todo: loop */
        if (n < 0xF) {
            tex_print_char('0');
        }
        if (n < 0xFF) {
            tex_print_char('0');
        }
        if (n < 0xFFF) {
            tex_print_char('0');
        }
        if (n < 0xFFFF) {
            tex_print_char('0');
        }
        if (n < 0xFFFFF) {
            tex_print_char('0');
        }
        if (n < 0xFFFFFF) {
            tex_print_char('0');
        }
        if (n < 0xFFFFFFF) {
            tex_print_char('0');
        }
        tex_print_hex(n);
    }

# else

    static inline void tex_aux_print_hex_engine(long long sn, int min_digits, const char *prefix, const char *suffix)
    {
        char buf[64];
        char *p = &buf[sizeof(buf) - 1];
        *p = '\0';
        // 1. Append suffix if present (e.g. closing quote '"')
        if (suffix) {
            size_t len = strlen(suffix);
            p -= len;
            memcpy(p, suffix, len);
        }
        // 2. Safe unsigned conversion (handles LLONG_MIN correctly)
        unsigned long long val;
        int is_negative = 0;
        if (sn < 0) {
            is_negative = 1;
         // val = -((unsigned long long) sn);
            val = 0ul -((unsigned long long) sn); // for ms
        } else {
            val = (unsigned long long) sn;
        }
        // 3. Extract hex digits backwards
        static const char hex_digits[] = "0123456789ABCDEF";
        int digits = 0;
        do {
            *(--p) = hex_digits[val & 0xF];
            val >>= 4;
            digits++;
        } while (val > 0);
        // 4. Apply minimum zero padding (if requested)
        while (digits < min_digits) {
            *(--p) = '0';
            digits++;
        }
        // 5. Prepend negative sign if needed
        if (is_negative) {
            *(--p) = '-';
        }
        // 6. Prepend prefix if present (e.g. opening quote '"' or "U+")
        if (prefix) {
            size_t len = strlen(prefix);
            p -= len;
            memcpy(p, prefix, len);
        }
        // Single multiplexer dispatch
        tex_print_str_len(p, (int) (&buf[sizeof(buf) - 1] - p));
    }

    void tex_print_hex(long long n)
    {
        tex_aux_print_hex_engine(n, 1, NULL, NULL);
    }

    static void tex_print_qhex(long long n)
    {
        tex_aux_print_hex_engine(n, 1, "\"", "\"");
    }

    static void tex_print_uhex(long long n)
    {
        tex_aux_print_hex_engine(n, 4, "U+", NULL);
    }

    static void tex_print_xhex(long long n)
    {
        tex_aux_print_hex_engine(n, 8, "\"", "\"");
    }

# endif

void tex_print_char_identifier(halfword c)
{
    if (c > 0x10FFFF) {
        return;
    } else if ((c >= 0x00E000 && c <= 0x00F8FF) ||
        (c >= 0x0F0000 && c <= 0x0FFFFF) ||
        (c >= 0x100000 && c <= 0x10FFFF) ||
        (c >= 0x00D800 && c <= 0x00DFFF))
    {
        // Formats as 6 hex digits with "0x" prefix (e.g., "0x00E000")
        tex_aux_print_hex_engine(c, 6, "0x", NULL);
    } else {
        // Formats as 6 hex digits with "U+" prefix (e.g., "U+000041")
        tex_aux_print_hex_engine(c, 6, "U+", NULL);
        tex_print_char(' ');
        tex_print_tex_str(c);
    }
}

/*tex

    Roman numerals are produced by the |print_roman_int| routine. Readers who like puzzles might
    enjoy trying to figure out how this tricky code works; therefore no explanation will be given.
    Notice that 1990 yields |mcmxc|, not |mxm|.

    When staring at this one once again, I wondered what Gemini would come up with so I asked and
    got this:

    \startquotation
    The beauty of \quotation {m2d5c2l5x2v5i} lies in how the string encodes both the divisors and
    the subtractive lookahead offsets:

    Letters: The Roman symbols m, d, c, l, x, v, i.

    Numbers: 2 and 5

    The factor to divide $v$ by to get to the next lower symbol!

    \startlines
       $1000 \div 2 = 500$ (d)
       $ 500 \div 5 = 100$ (c)
       $ 100 \div 2 =  50$ (l)
       $  50 \div 5 =  10$ (x)
       $  10 \div 2 =   5$ (v)
       $   5 \div 5 =   1$ (i)
    \stoplines

    When checking whether to write a subtractive pair (like iv for 4 or ix for 9), the algorithm
    inspects whether $n + u \ge v$, where $u$ is calculated by stepping forward in the mystery
    string to peek at the appropriate power-of-10 unit (c, x, or i).

    It's a masterclass in squeezing complex formatting logic into minimal instruction space!
    \stopquotation

    Indeed. After that I did a bit of research (actually we already have a variant in the regular
    \CONTEXT\ converter but that assumes \UNICODE) and chat a bit and ended up with the next more
    regular \ASCII\ variant (there has also been solutions using _ and ^ but for \TEX\ that sounds
    like a bad idea). Of course that means that we're not compatible but whem macro writers expect
    thousands of repeated \type {m}'s, well they can have it. I've seen some (in modern code)
    examples where that actually happens and it's an oversight, waste of tokens and string space,
    etc. but maybe a price a macro writer wants to pay for this \type {\romannumeral} obsession. I
    wonder if someone ever notices this variant.

*/

static void tex_print_knuth_roman(int n)
{
    char mystery[] = "m2d5c2l5x2v5i";
    char *j = (char *) mystery;
    int v = 1000;
    while (1) {
        while (n >= v) {
            tex_print_char(*j);
            n = n - v;
        }
        if (n <= 0) {
            return;
        } else {
            char *k = j + 2;
            int u = v / (*(k - 1) - '0');
            if (*(k - 1) == '2') {
                k = k + 2;
                u = u / (*(k - 1) - '0');
            }
            if (n + u >= v) {
                tex_print_char(*k);
                n = n + u;
            } else {
                j = j + 2;
                v = v / (*(j - 1) - '0');
            }
        }
    }
}

/*tex

    A parenthetical wrapper that delegates sub-4000 chunks to Knuth's algorithm: for 4000 and
    above we process in groups of 1000 using nested parentheses. We stay faithful to Knuth and
    use his clever solution. Anyone using romannumerals for more than 4000 pages should rethink
    the design anyway.

*/

void tex_print_roman_int(int n, int numerical)
{
    if (n == 0) {
        /*tex
            The romans had no zero and \TEX\ doesn't output something when we're zero or
            below. Some macro writers abuse that fact.
        */
    } else if (n < 4000 || ! numerical) {
        tex_print_knuth_roman(n);
    } else {
        unsigned int multiplier = 1;
        int depth = 0;
        while (n / multiplier >= 1000) {
            multiplier *= 1000;
            depth++;
        }
        while (multiplier > 0) {
            int chunk = (int)(n / multiplier);
            n %= multiplier;
            if (chunk > 0) {
                /*tex opening parentheses for high-order groups */
                for (int i = 0; i < depth; i++) {
                    tex_print_char('(');
                }
                /*tex chunk (1–999) using Knuth's routine */
                tex_print_knuth_roman(chunk);
                /*tex closing parentheses */
                for (int i = 0; i < depth; i++) {
                    tex_print_char(')');
                }
            }
            multiplier /= 1000;
            depth--;
        }
    }
}

/*tex

    Group codes were introduced in \ETEX\ but have been extended in the meantime in \LUATEX\ and
    later again in \LUAMETATEX. We might have (even) more granularity in the future.

    Todo: combine this with an array of struct(id,name,lua) ... a rainy day + stack of new cd's job.

*/

/*tex
    We have at most 4 pairs, in math choices, but let's be nice and check anyway.
*/

static inline void tex_print_group_count(int n)
{
    if (n > 0) {
        static const char group_pairs[] = "{}{}{}{}";
        while (n >= 4) {
            tex_print_str_len(group_pairs, 8);
            n -= 4;
        }
        if (n > 0) {
            tex_print_str_len(group_pairs, n * 2);
        }
    }
}

static inline void tex_print_group(int e)
{
    int line = tex_saved_line_at_level();
    tex_print_str(lmt_interface.group_code_values[cur_group].name);
    if (cur_group != bottom_level_group) {
        tex_print_str_len(" group", 6);
        if (line) {
            if (e) {
                tex_print_str_len(" entered at line ", 17);
            } else {
                tex_print_str_len(" at line ", 9);
            }
            tex_print_int(line);
        }
    }
}

/*tex

    The procedure |print_cs| prints the name of a control sequence, given a pointer to its address
    in |eqtb|. A space is printed after the name unless it is a single nonletter or an active
    character. This procedure might be invoked with invalid data, so it is \quote {extra robust}.
    The individual characters must be printed one at a time using |print|, since they may be
    unprintable.

*/

void tex_print_cs_checked(halfword p)
{
    switch (tex_cs_state(p)) {
        case cs_no_error:
            {
                strnumber t = cs_text(p);
                if (t < 0 || t >= lmt_string_pool_state.string_pool_data.ptr) {
                    tex_print_str(error_string_nonexistent(13));
                } else if (tex_is_active_cs(t)) {
                    tex_print_tex_str(tex_active_cs_value(t));
                } else {
                    tex_print_tex_str_esc(t);
                    if (! tex_single_letter(t) || (tex_get_cat_code(cat_code_table_par, aux_str2uni(str_getstr(t))) == letter_cmd)) {
                        tex_print_char(' ');
                    }
                }
            }
            break;
        case cs_null_error:
            tex_print_format("%ecsname%eendcsname");
            break;
        case cs_below_base_error:
            tex_print_str(error_string_impossible(11));
            break;
        case cs_undefined_error:
            tex_print_format("%eundefined");
            break;
        case cs_out_of_range_error:
            tex_print_str(error_string_impossible(12));
            break;
    }
}

/*tex

    Here is a similar procedure; it avoids the error checks, and it never prints a space after the
    control sequence. The other one doesn't even print the bogus cs.

*/

void tex_print_cs(halfword p)
{
    if (p == null_cs) {
        tex_print_format("%ecsname%eendcsname"); /* no space */
    } else {
        strnumber t = cs_text(p);
        if (tex_is_active_cs(t)) {
            tex_print_tex_str(tex_active_cs_value(t));
        } else {
            tex_print_tex_str_esc(t);
        }
    }
}

void tex_print_cs_name(halfword p)
{
    if (p != null_cs) {
        strnumber t = cs_text(p);
        if (tex_is_active_cs(t)) {
            tex_print_tex_str(tex_active_cs_value(t));
        } else {
            tex_print_tex_str(t);
        }
    }
}

/*tex

    Then there is a subroutine that prints glue stretch and shrink, possibly followed by the name
    of finite units:

*/

void tex_print_glue(scaled d, int order, int unit)
{
    if (order == normal_glue_order) { /* 0 */
        tex_print_dimension(d, unit);
    } else {
        tex_print_dimension(d, no_unit);
        switch (order) {
            case fi_glue_order    : tex_print_str_len("fi",    2); break;
            case fil_glue_order   : tex_print_str_len("fil",   3); break;
            case fill_glue_order  : tex_print_str_len("fill",  4); break;
            case filll_glue_order : tex_print_str_len("filll", 5); break;
            default               : tex_print_str_len("foul",  4); break;
        }
    }
}

void tex_print_glue_set(halfword p)
{
    double g = (double) (box_glue_set(p));
    if ((g != 0.0) && (box_glue_sign(p) != normal_glue_sign)) {
        tex_print_str_len(", glue set ", 11); // This was |glue set|.
        if (box_glue_sign(p) == shrinking_glue_sign) {
            tex_print_str_len("- ", 2);
        }
        if (g > 20000.0 || g < -20000.0) {
            if (g > 0.0) {
                tex_print_char('>');
            } else {
                tex_print_str_len("< -", 3);
            }
            tex_print_glue(20000 * unity, box_glue_order(p), no_unit);
        } else {
            tex_print_glue((scaled) glueround(unity *g), box_glue_order(p), no_unit);
        }
    }
}

/*tex The next subroutine prints a whole glue specification. */

void tex_print_unit(int unit)
{
    if (unit != no_unit) {
        tex_print_str_len(unit == pt_unit ? "pt" : "mu", 2);
    }
}

void tex_print_spec(int p, int unit)
{
    if (p < 0) {
        tex_print_char('*');
    } else if (p == 0) {
        tex_print_dimension(0, unit);
    } else {
        tex_print_dimension(glue_amount(p), unit);
        /* todo */
        if (glue_stretch(p)) {
            tex_print_str_len(" plus ", 6);
            tex_print_glue(glue_stretch(p), glue_stretch_order(p), unit);
        }
        if (glue_shrink(p)) {
            tex_print_str_len(" minus ", 7);
            tex_print_glue(glue_shrink(p), glue_shrink_order(p), unit);
        }
    }
}

static void tex_print_gluespec(
        scaled total,
        scaled stretch,
        scaled fistretch,
        scaled filstretch,
        scaled fillstretch,
        scaled filllstretch,
        scaled shrink,
        int    unit
)
{
    tex_print_dimension(total, unit);
    if (stretch) {
        tex_print_str_len(" plus ", 6);
        tex_print_dimension(stretch, unit);
    } else if (fistretch) {
        tex_print_str_len(" plus ", 6);
        tex_print_dimension(fistretch, no_unit);
        tex_print_str_len(" fi", 3);
    } else if (filstretch) {
        tex_print_str_len(" plus ", 6);
        tex_print_dimension(filstretch, no_unit);
        tex_print_str_len(" fil", 4);
    } else if (fillstretch) {
        tex_print_str_len(" plus ", 6);
        tex_print_dimension(fillstretch, no_unit);
        tex_print_str_len(" fill", 5);
    } else if (filllstretch) {
        tex_print_str_len(" plus ", 6);
        tex_print_dimension(filllstretch, no_unit);
        tex_print_str_len(" filll", 6);
    }
    if (shrink) {
        tex_print_str_len(" minus ", 7);
        tex_print_dimension(shrink, unit);
    }
}

void tex_print_fontspec(int p)
{
    tex_print_int(font_spec_identifier(p));
    if (font_spec_scale(p) != unused_scale_value) {
        tex_print_format(" scale %i", font_spec_scale(p));
    }
    if (font_spec_x_scale(p) != unused_scale_value) {
        tex_print_format(" xscale %i", font_spec_x_scale(p));
    }
    if (font_spec_y_scale(p) != unused_scale_value) {
        tex_print_format(" yscale %i", font_spec_y_scale(p));
    }
    if (font_spec_slant(p)) {
        tex_print_format(" slant %i", font_spec_slant(p));
    }
    if (font_spec_weight(p)) {
        tex_print_format(" weight %i", font_spec_weight(p));
    }
}

/*tex Math characters: */

void tex_print_mathspec(int p)
{
    if (p) {
        mathcodeval m = tex_get_math_spec(p);
        tex_show_mathcode_value(m, node_subtype(p));
    } else {
        tex_print_str_len("[invalid mathspec]", 18);
    }
}

/*tex

    We can reinforce our knowledge of the data structures just introduced by considering two
    procedures that display a list in symbolic form. The first of these, called |short_display|, is
    used in \quotation {overfull box} messages to give the top-level description of a list. The
    other one, called |show_node_list|, prints a detailed description of exactly what is in the
    data structure.

    The philosophy of |short_display| is to ignore the fine points about exactly what is inside
    boxes, except that ligatures and discretionary breaks are expanded. As a result,
    |short_display| is a recursive procedure, but the recursion is never more than one level deep.

    A global variable |font_in_short_display| keeps track of the font code that is assumed to be
    present when |short_display| begins; deviations from this font will be printed.

    Boxes, rules, inserts, whatsits, marks, and things in general that are sort of \quote
    {complicated} are indicated only by printing |[]|.

    We print a bit more than original \TEX. A value of 0 or 1 or any large value will behave the
    same as before. The reason for this extension is that a |name| not always makes sense.

    \starttyping
    0   \foo xyz
    1   \foo (bar)
    2   <bar> xyz
    3   <bar @ ..> xyz
    4   <id>
    5   <id: bar>
    6   <id: bar @ ..> xyz
    \stoptyping

    This is no longer the case: we now always print a full specification. The |\tracingfonts|
    register will be dropped.

*/

void tex_print_font_identifier(halfword f)
{
    /*tex |< >| is less likely to clash with text parenthesis */
    if (f < 0) {
        f = cur_font_par; /* bonus */
    }
    if (tex_is_valid_font(f)) {
        tex_print_format("<%i: %s @ %p>", f, font_name(f), font_size(f));
    } else {
        tex_print_str_len("<*>", 3);
    }
}

void tex_print_font_specifier(halfword e)
{
    if (e && tex_is_valid_font(font_spec_identifier(e))) {
        tex_print_format("<%i: %i %i %i %i %i>", font_spec_identifier(e), font_spec_scale(e), font_spec_x_scale(e), font_spec_y_scale(e), font_spec_slant(e), font_spec_weight(e));
    } else {
        tex_print_str_len("<*>", 3);
    }
}

void tex_print_font(halfword f)
{
    if (f < 0) {
        f = cur_font_par; /* bonus */
    }
    if (! f) {
        tex_print_str_len("nullfont", 8);
    } else if (tex_is_valid_font(f)) {
        tex_print_str(font_name(f));
     /* if (font_size(f) != font_design_size(f)) { */
            /*tex
                Nowadays this check for design size is rather meaningless so we could as well
                always enter this branch. We can even make this while blob a callback.
            */
            tex_print_format(" at %p", font_size(f));
     /* } */
    } else {
        tex_print_str_len("nofont", 6);
    }
}

/*tex This prints highlights of list |p|. */

void tex_short_display(halfword p)
{
 // tex_print_levels();
    if (p) {
        tex_print_short_node_contents(p);
    } else {
        tex_print_str_len("[empty list]", 12);
    }
}

/*tex This prints token list data in braces. */

void tex_print_token_list(const char *s, halfword p)
{
    tex_print_levels();
    tex_print_str_len("..", 2);
    if (s) {
        tex_print_str(s);
        tex_print_char(' ');
    }
    tex_print_char('{');
    if ((p >= 0) && (p <= (int) lmt_token_memory_state.tokens_data.top)) {
        tex_show_token_list(p, 0, 0);
    } else {
        tex_print_str(error_string_clobbered(21));
    }
    tex_print_char('}');
}

/*tex This prints dimensions of a rule node. */

static inline void tex_print_rule_dimension(scaled d)
{
    if (d == null_flag) {
        tex_print_char('*');
    } else {
        tex_print_dimension(d, pt_unit);
    }
}

/*tex

    Since boxes can be inside of boxes, |show_node_list| is inherently recursive, up to a given
    maximum number of levels. The history of nesting is indicated by the current string, which
    will be printed at the beginning of each line; the length of this string, namely |cur_length|,
    is the depth of nesting.

    A global variable called |depth_threshold| is used to record the maximum depth of nesting for
    which |show_node_list| will show information. If we have |depth_threshold = 0|, for example,
    only the top level information will be given and no sub-lists will be traversed. Another global
    variable, called |breadth_max|, tells the maximum number of items to show at each level;
    |breadth_max| had better be positive, or you won't see anything.

    The maximum nesting depth in box displays is kept in |depth_threshold| and the maximum number
    of items shown at the same list level in |breadth_max|.

    The recursive machinery is started by calling |show_box|. Assign the values |depth_threshold :=
    show_box_depth| and |breadth_max := show_box_breadth|

*/

void tex_show_box(halfword p)
{
    /*tex the show starts at |p| */
    tex_show_node_list(p, show_box_depth_par, show_box_breadth_par);
    tex_print_ln();
}

/*tex

    \TEX\ is occasionally supposed to print diagnostic information that goes only into the
    transcript file, unless |tracing_online| is positive. Here are two routines that adjust the
    destination of print commands:

*/

void tex_begin_diagnostic(void)
{
    lmt_print_state.saved_selector = lmt_print_state.selector;
    if ((tracing_online_par <= 0) && (lmt_print_state.selector == terminal_and_logfile_selector_code)) {
        lmt_print_state.selector = logfile_selector_code;
        if (lmt_error_state.history == spotless) {
            lmt_error_state.history = warning_issued;
        }
    }
 // tex_print_levels();
}

/*tex Restore proper conditions after tracing. */

void tex_end_diagnostic(void)
{
    tex_print_nlp();
    lmt_print_state.selector = lmt_print_state.saved_selector;
}

static void tex_print_padding(void)
{
    switch (lmt_print_state.selector) {
        case terminal_selector_code:
            if (! odd_int(lmt_print_state.terminal_offset)) {
                tex_print_char(' ');
            }
            break;
        case logfile_selector_code:
        case terminal_and_logfile_selector_code:
            if (! odd_int(lmt_print_state.logfile_offset)) {
                tex_print_char(' ');
            }
            break;
        case luabuffer_selector_code:
            break;
    }
}

static void tex_print_nesting(int n)
{
    static const char periods[32] = "................................";
    while (n > 0) {
        int chunk = (n > 32) ? 32 : n;
        tex_print_str_len(periods, chunk);
        n -= chunk;
    }
}

void tex_print_levels(void)
{
    int l0 = tracing_levels_par;
    tex_print_nlp();
    if (l0 > 0) {
        int l1 = (l0 & tracing_levels_group   ) == tracing_levels_group;
        int l2 = (l0 & tracing_levels_input   ) == tracing_levels_input;
        int l4 = (l0 & tracing_levels_catcodes) == tracing_levels_catcodes;
        if (l1 && l2) {
            if (l4) {
                tex_print_format("%i:%i:%i ", cur_level, lmt_input_state.input_stack_data.ptr, cat_code_table_par);
                return;
            } else {
                /*tex This is the default in context: */
                tex_print_format("%i:%i: ", cur_level, lmt_input_state.input_stack_data.ptr);
                return;
            }
        }
        if (l1) {
            tex_print_format("%i:", cur_level);
        }
        if (l2) {
            tex_print_format("%i:", lmt_input_state.input_stack_data.ptr);
        }
        if (l4) {
            tex_print_format("%i:", cat_code_table_par);
        }
        if (l1 || l2 || l4) {
            tex_print_char(' ');
        }
        tex_print_padding();
    }
}

/*tex

    We have these formatting keys:

    \starttyping
    %c   int            char
    %d   int            detail (node)
    %e                  backslash (tex escape)
    %h  *char           error help
    %i   int            integer
    %l                  levels
    %n   int            extended subtype
    %m   int            cs checked (macro)
    %p   int            dimension with pt unit
    %q  *char          'string'
    %s  *char           string
    %u   int            utf character
    %x   int            quoted hex

    %B   int            badness
    %C   int int        symbolic representation of cmd chr
    %D   int int        dimension plus unit
    %E  *char           \cs
    %F   int            font identifier
    %G   int            group
    %L   int            (if) linenumber
    %M   int            mode
    %N   int            node name
    %O   int int int    glue component
    %P   7 int          glue spec
    %Q   int            glue node
    %R   int            rule dimensions
    %S   int            tex cs string
    %T   int            tex string
    %U   int            unicode
    %X   int            unicode

    %#   int            group count : n*{}

    %2   int            direction

    %%                  percent
    \stoptyping

    Using a variant that collects stripes between formatters is faster: so that is what we do now,
    and  we gained over 30\percent which is nice on extreme tracing.

*/

inline const char *tex_print_format_args(const char *format, va_list args)
{
    const char *start = format;
    while (1) {
        int chr = *format++;
        switch (chr) {
            case '\0':
                if (format - 1 > start) {
                    tex_print_str_len(start, (int) (format - 1 - start));
                }
                return NULL;
            case '%':
                /*tex We need to flush preceding literal characters. */
                if (format - 1 > start) {
                    tex_print_str_len(start, (int) (format - 1 - start));
                }
                chr = *format++;
                switch (chr) {
                    case '\0':
                        return NULL;
                    case 'c':
                        {
                            int chr = va_arg(args, int);
                            tex_aux_uprint(chr);
                            break;
                        }
                    case 'd': /* detail */
                        {
                            tex_print_str(tex_aux_subtype_str(va_arg(args, int)));
                            break;
                        }
                    case 'e':
                        {
                            tex_print_str_esc("");
                            break;
                        }
                    case 'h':
                        {
                            /*tex Optional help info gets returned and comes last. */
                            return va_arg(args, const char *);
                        }
                    case 'i':
                        {
                            tex_print_int(va_arg(args, int));
                            break;
                        }
                    case 'l':
                        {
                            tex_print_levels();
                            break;
                        }
                    case 'n':
                        {
                            quarterword subtype = (quarterword) va_arg(args, int);
                            tex_print_extended_subtype(null, subtype);
                            break;
                        }
                    case 'm':
                        {
                            tex_print_cs_checked(va_arg(args, int));
                            break;
                        }
                    case 'p':
                        {
                            tex_print_dimension((scaled) va_arg(args, int), pt_unit);
                            break;
                        }
                    case 'q':
                        {
                            const char *str = va_arg(args, const char *);
                            tex_print_char('\'');
                            if (str) {
                                tex_print_str_len(str, (int) strlen(str));
                            }
                            tex_print_char('\'');
                            break;
                        }
                    case 's':
                        {
                            const char *str = va_arg(args, const char *);
                            if (str) {
                                tex_print_str_len(str, (int) strlen(str));
                            }
                            break;
                        }
                    case 'u':
                        tex_aux_uprint(va_arg(args, int));
                        break;
                    case 'x':
                        {
                            int value = va_arg(args, int);
                            tex_print_qhex(value);
                            break;
                        }
                    case 'B': /* badness */
                        {
                            scaled badness = va_arg(args, int);
                            if (badness == awful_bad) {
                                tex_print_char('*');
                            } else {
                                tex_print_int(badness);
                            }
                            break;
                        }
                    case 'C':
                        {
                            int cmd = va_arg(args, int);
                            int val = va_arg(args, int);
                            tex_print_cmd_chr((singleword) cmd, val);
                            break;
                        }
                    case 'D': /* dimension */
                        {
                            scaled amount = va_arg(args, int);
                            int    unit   = va_arg(args, int);
                            tex_print_dimension(amount, unit);
                            break;
                        }
                    case 'E':
                        {
                            tex_print_str_esc(va_arg(args, const char *));
                            break;
                        }
                    case 'F':
                        {
                            halfword id = va_arg(args, int);
                            tex_print_font_identifier(id);
                            break;
                        }
                    case 'G':
                        {
                            tex_print_group(va_arg(args, int));
                            break;
                        }
                    case 'L':
                        {
                            halfword line = va_arg(args, int);
                            if (line) {
                                tex_print_str_len(" entered on line ", 17);
                                tex_print_int(line);
                            }
                            break;
                        }
                    case 'M':
                        {
                            halfword mode = va_arg(args, int);
                            tex_print_str(tex_string_mode(mode));
                            break;
                        }
                    case 'N':
                        {
                            halfword node = va_arg(args, int);
                            if (node) {
                                tex_print_str(lmt_interface.node_data[node_type(node)].name);
                            }
                            break;
                        }
                    case 'O':
                        {
                            scaled   s = va_arg(args, int);
                            halfword o = va_arg(args, int);
                            int      u = va_arg(args, int);
                            tex_print_glue(s, o, u);
                            break;
                        }
                    case 'P':
                        {
                            scaled total        = va_arg(args, int);
                            scaled stretch      = va_arg(args, int);
                            scaled fistretch    = va_arg(args, int);
                            scaled filstretch   = va_arg(args, int);
                            scaled fillstretch  = va_arg(args, int);
                            scaled filllstretch = va_arg(args, int);
                            scaled shrink       = va_arg(args, int);
                            tex_print_gluespec(total, stretch, fistretch, filstretch,
                                fillstretch, filllstretch, shrink, pt_unit);
                            break;
                        }
                    case 'Q':
                        {
                            scaled glue = va_arg(args, int);
                            int    unit = va_arg(args, int);
                            tex_print_spec(glue, unit);
                            break;
                        }
                    case 'R':
                        {
                            halfword rule = va_arg(args, int);
                            tex_print_rule_dimension(rule);
                            break;
                        }
                    case 'S':
                        {
                            halfword cs = va_arg(args, int);
                            tex_print_cs(cs);
                            break;
                        }
                    case 'T':
                        {
                            strnumber s = va_arg(args, int);
                            tex_print_tex_str(s);
                            break;
                        }
                    case 'U':
                        {
                            halfword chr = va_arg(args, int);
                            tex_print_uhex(chr);
                            break;
                        }
                    case 'X':
                        {
                            halfword chr = va_arg(args, int);
                            tex_print_xhex(chr);
                            break;
                        }
                    case '2':
                        {
                            halfword direction = va_arg(args, int);
                            switch (direction) {
                                case direction_l2r : tex_print_str_len("l2r",   3); break;
                                case direction_r2l : tex_print_str_len("r2l",   3); break;
                                default            : tex_print_str_len("unset", 5); break;
                            }
                            break;
                        }
                    case '#':
                        {
                            halfword groups = va_arg(args, int);
                            tex_print_group_count(groups);
                            break;
                        }
                    case '%':
                        {
                            tex_print_char('%');
                            break;
                        }
                    case '.':
                        {
                            int n = va_arg(args, int);
                            tex_print_nesting(n);
                            break;
                        }
                    default:
                        /* ignore bad one */
                        break;
                }
                /*tex Reset start after processing argument specifier. */
                start = format;
                break;
            case '\n':
            case '\r':
                /*tex Flush the literal string up to newline before dealing with the newline. */
                if (format - 1 > start) {
                    tex_print_str_len(start, (int) (format - 1 - start));
                }
                tex_print_nlp();
                start = format;
                break;
            default:
                /*tex We just advance. */
                break;
        }
    }
}

void tex_print_format(const char *format, ...)
{
    va_list args;
    va_start(args, format);
    tex_print_format_args(format, args);
    va_end(args);
}

void tex_print_message(const char *s)
{
    tex_print_format("\n(%s)\n", s);
}
