/* SPDX-License-Identifier: BSD-2-Clause */
/* Copyright 1996-2020 The NASM Authors - All Rights Reserved */

/*
 * listing.c    listing file generator for the Netwide Assembler
 */

#include "compiler.h"

#include "nctype.h"

#include "nasm.h"
#include "nasmlib.h"
#include "error.h"
#include "strlist.h"
#include "listing.h"

#define LIST_MAX_LEN 1024       /* something sensible */
#define LIST_INDENT  40
#define LIST_HEXBIT  18

static const char xdigit[] = "0123456789ABCDEF";

#define HEX(a,b) (*(a)=xdigit[((b)>>4)&15],(a)[1]=xdigit[(b)&15])

uint64_t list_options, active_list_options;
bool user_nolist;

static char listline[LIST_MAX_LEN];
static bool listlinep;

static struct strlist *list_errors;

static char listdata[2 * LIST_INDENT];  /* we need less than that actually */
static int32_t listoffset;

static int32_t listlineno;

static int suppress;            /* for INCBIN & TIMES special cases */

static int listlevel, listlevel_e;

static FILE *listfp;

static inline char err_fill_char(errflags severity)
{
    severity &= ERR_MASK;

    if (severity < ERR_NOTE)
        return ' ';
    else if (severity < ERR_WARNING)
        return '-';
    else if (severity < ERR_CRITICAL)
        return '*';
    else
        return 'X';
}

static void list_emit(void)
{
    int i;
    const struct strlist_entry *e;

    if (listlinep || *listdata) {
        fprintf(listfp, "%6"PRId32" ", listlineno);

        if (listdata[0])
            fprintf(listfp, "%08"PRIX32" %-*s", listoffset, LIST_HEXBIT + 1,
                    listdata);
        else
            fprintf(listfp, "%*s", LIST_HEXBIT + 10, "");

        if (listlevel_e)
            fprintf(listfp, "%s<%d>", (listlevel < 10 ? " " : ""),
                    listlevel_e);
        else if (listlinep)
            fprintf(listfp, "    ");

        if (listlinep)
            fprintf(listfp, " %s", listline);

        putc('\n', listfp);
        listlinep = false;
        listdata[0] = '\0';
    }

    if (list_errors) {
        strlist_for_each(e, list_errors) {
            char fillchar;

            fprintf(listfp, "%6"PRId32"          ", listlineno);
            fillchar = err_fill_char(e->pvt.u);
            for (i = 0; i < LIST_HEXBIT; i++)
                putc(fillchar, listfp);

            if (listlevel_e)
                fprintf(listfp, " %s<%d>", (listlevel < 10 ? " " : ""),
                        listlevel_e);
            else
                fprintf(listfp, "     ");

            fprintf(listfp, "  %s\n", e->str);
        }

        strlist_free(&list_errors);
    }
}

static void list_cleanup(void)
{
    if (!listfp)
        return;

    list_emit();
    fclose(listfp);
    listfp = NULL;
    active_list_options = 0;
}

static void list_init(const char *fname)
{
    enum file_flags flags = NF_TEXT;

    if (listfp)
        list_cleanup();

    if (!fname || fname[0] == '\0') {
	listfp = NULL;
	return;
    }

    if (list_option('w'))
        flags |= NF_IOLBF;

    listfp = nasm_open_write(fname, flags);
    if (!listfp) {
        nasm_nonfatal("unable to open listing file `%s'", fname);
        return;
    }

    active_list_options = list_options | 1;

    *listline = '\0';
    listlineno = 0;
    list_errors = NULL;
    listlevel = 0;
    suppress = 0;
    user_nolist = false;
}

static void list_out(int64_t offset, char *str)
{
    if (strlen(listdata) + strlen(str) > LIST_HEXBIT) {
        strcat(listdata, "-");
        list_emit();
    }
    if (!listdata[0])
        listoffset = offset;
    strcat(listdata, str);
}

static void list_address(int64_t offset, const char *brackets,
			 int64_t addr, int size)
{
    char q[20];
    char *r = q;

    nasm_assert(size <= 8);

    *r++ = brackets[0];
    while (size--) {
	HEX(r, addr);
	addr >>= 8;
	r += 2;
    }
    *r++ = brackets[1];
    *r = '\0';
    list_out(offset, q);
}

static void list_size(int64_t offset, const char *tag, uint64_t size)
{
    char buf[64];
    const char *fmt;

    if (list_option('d'))
        fmt = "<%s %"PRIu64">";
    else
        fmt = "<%s %"PRIX64"h>";

    snprintf(buf, sizeof buf, fmt, tag, size);
    list_out(offset, buf);
}

static void list_output(const struct out_data *data)
{
    char q[24];
    uint64_t size = data->size;
    uint64_t offset = data->loc.offset;
    const uint8_t *p = data->data;


    if (!listfp || suppress || user_nolist)
        return;

    switch (data->type) {
    case OUT_ZERODATA:
        if (size > 16) {
            list_size(offset, "zero", size);
            break;
        } else {
            p = zero_buffer;
        }
        /* fall through */
    case OUT_RAWDATA:
    {
	if (size == 0) {
            if (!listdata[0])
                listoffset = data->loc.offset;
        } else if (p) {
            while (size--) {
                HEX(q, *p);
                q[2] = '\0';
                list_out(offset++, q);
                p++;
            }
        } else {
            /* Used for listing on non-code generation passes with -Lp */
            list_size(offset, "len", size);
        }
	break;
    }
    case OUT_ADDRESS:
        list_address(offset, "[]", data->toffset, size);
	break;
    case OUT_SEGMENT:
        q[0] = '[';
        memset(q+1, 's', size << 1);
        q[(size << 1)+1] = ']';
        q[(size << 1)+2] = '\0';
        list_out(offset, q);
        offset += size;
        break;
    case OUT_RELADDR:
	list_address(offset, "()", data->toffset, size);
	break;
    case OUT_RESERVE:
    {
        if (size > 8) {
            list_size(offset, "res", size);
        } else {
            memset(q, '?', size << 1);
            q[size << 1] = '\0';
            list_out(offset, q);
        }
	break;
    }
    default:
        panic();
    }
}

static void list_line(enum list_type type, int32_t lineno, const char *line)
{
    (void)type;

    if (!listfp)
        return;

    if (user_nolist)
      return;

    list_emit();
    if (lineno >= 0)
        listlineno = lineno;
    listlinep = true;
    strlcpy(listline, line, LIST_MAX_LEN-3);
    memcpy(listline + LIST_MAX_LEN-4, "...", 4);
    listlevel_e = listlevel;
}

static void list_uplevel(enum list_type type, int64_t size)
{
    if (!listfp)
        return;

    switch (type) {
    case LIST_INCBIN:
        suppress |= 1;
        list_size(listoffset, "bin", size);
        break;

    case LIST_TIMES:
        suppress |= 2;
        list_size(listoffset, "rep", size);
        break;

    case LIST_INCLUDE:
        listlevel++;
        break;

    default:
        listlevel++;
        break;
    }
}

static void list_downlevel(enum list_type type)
{
    if (!listfp)
        return;

    switch (type) {
    case LIST_INCBIN:
        suppress &= ~1;
        break;

    case LIST_TIMES:
        suppress &= ~2;
        break;

    default:
        listlevel--;
        break;
    }
}

static void printf_func(2, 3) list_error(errflags severity, const char *fmt, ...)
{
    va_list ap;

    if (!listfp)
	return;

    if (!list_errors)
        list_errors = strlist_alloc(false);

    va_start(ap, fmt);
    strlist_vprintf(list_errors, fmt, ap);
    va_end(ap);
    strlist_tail(list_errors)->pvt.u = severity;

    if ((severity & ERR_MASK) >= ERR_FATAL)
	list_emit();
}

static void list_set_offset(uint64_t offset)
{
    listoffset = offset;
}

/*
 * from_cmdline is passed in to handle -L+ -> LIST_PLUS_OPTIONS,
 * and to prevent active_list_options from being set (that will
 * be done when list_init() is called.)
 *
 * As listing options are assumed non-critical, ignore errors
 * to help forward compatibility.
 */
void list_update_options(const char *str, bool from_cmdline)
{
    unsigned char c;
    uint64_t setmask = LIST_ALL_OPTIONS_MASK;
    uint64_t amask   = from_cmdline ? 0 : setmask;

    while ((c = *str++)) {
        uint64_t mask = 0;
        switch (c) {
        case '+':
            if (from_cmdline && setmask)
                list_update_options(LIST_PLUS_OPTIONS, true);
            setmask = LIST_ALL_OPTIONS_MASK;
            break;
        case '-':
            setmask = 0;
            break;
        default:
            mask = list_option_mask(c);
            list_options = (list_options & ~mask) | (setmask & mask);
            active_list_options =
                (active_list_options & ~mask) |
                (setmask & amask & mask);
            break;
        }
    }
}

enum directive_result list_pragma(const struct pragma *pragma)
{
    switch (pragma->opcode) {
    case D_OPTIONS:
        list_update_options(pragma->tail, false);
        return DIRR_OK;

    default:
        return DIRR_UNKNOWN;
    }
}

static const struct lfmt nasm_list = {
    list_init,
    list_cleanup,
    list_output,
    list_line,
    list_uplevel,
    list_downlevel,
    list_error,
    list_set_offset
};

const struct lfmt *lfmt = &nasm_list;
