/**
 * @file format.c
 * @brief Implements `PRINT USING` field formatting.
 *
 * One field per call, with whatever literal text surrounds it copied through.
 * The work splits three ways: find the field, measure it, then fill it.
 */

#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

#include <akerror.h>
#include <akstdlib.h>

#include <akbasic/error.h>
#include <akbasic/format.h>

/** @brief The PUDEF defaults, in PUDEF's own order. */
static const char PUDEF_DEFAULTS[AKBASIC_PUDEF_CHARS] = { ' ', ',', '.', '$' };

/** @brief True for a character that can only appear inside a numeric field. */
static bool is_numeric_field_char(char c)
{
    return (c == '#' || c == ',' || c == '.');
}

/** @brief True for a character that can only appear inside a string field. */
static bool is_string_field_char(char c)
{
    return (c == '=' || c == '>');
}

akerr_ErrorContext *akbasic_format_state_init(akbasic_FormatState *obj)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL format state in init");
    PASS(errctx, aksl_memcpy(obj->chars, PUDEF_DEFAULTS, sizeof(obj->chars)));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_format_pudef(akbasic_FormatState *obj, const char *chars)
{
    PREPARE_ERROR(errctx);
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && chars != NULL), AKERR_NULLPOINTER,
		     "NULL argument in pudef");
    /*
     * Only as many as were given. `PUDEF "*"` redefines the leading blank and
     * leaves the comma, the point and the dollar alone, which is BASIC 7.0's
     * rule and is what makes the one-character form useful.
     */
    for ( i = 0; i < AKBASIC_PUDEF_CHARS && chars[i] != '\0'; i++ ) {
	obj->chars[i] = chars[i];
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Locate the first field in a format string.
 *
 * @param format The format string.
 * @param start Output: index of the field's first character.
 * @param length Output: how many characters the field spans.
 * @param numeric Output: true for a numeric field, false for a string field.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_SYNTAX When there is no field.
 */
static akerr_ErrorContext *find_field(const char *format, size_t *start, size_t *length, bool *numeric)
{
    PREPARE_ERROR(errctx);
    size_t i = 0;

    for ( i = 0; format[i] != '\0'; i++ ) {
	if ( format[i] == '#' || format[i] == '.' ) {
	    *numeric = true;
	    break;
	}
	if ( is_string_field_char(format[i]) ) {
	    *numeric = false;
	    break;
	}
    }
    FAIL_ZERO_RETURN(errctx, (format[i] != '\0'), AKBASIC_ERR_SYNTAX,
		     "PRINT USING format \"%s\" contains no field", format);

    /*
     * A `$`, `+` or `-` immediately before the field belongs to it. Scanning
     * backwards is how a leading sign or currency sign is told apart from
     * literal text: `"COST $###"` has a field of `$###`, and `"A + B ###"` does
     * not, because the `+` is not adjacent.
     */
    *start = i;
    while ( *start > 0 && (format[*start - 1] == '$' || format[*start - 1] == '+' ||
			   format[*start - 1] == '-') ) {
	*start -= 1;
    }

    for ( ; format[i] != '\0'; i++ ) {
	if ( *numeric && is_numeric_field_char(format[i]) ) {
	    continue;
	}
	if ( !(*numeric) && is_string_field_char(format[i]) ) {
	    continue;
	}
	break;
    }
    /* A trailing sign belongs to the field too. */
    if ( format[i] == '+' || format[i] == '-' ) {
	i += 1;
    }
    *length = i - *start;
    SUCCEED_RETURN(errctx);
}

/** @brief Measure a numeric field: digits before and after the point, and its decorations. */
static void measure_numeric(const char *field, size_t length, int *before, int *after,
			    bool *commas, bool *dollar, bool *leadsign, bool *trailsign)
{
    size_t i = 0;
    bool seenpoint = false;

    *before = 0;
    *after = 0;
    *commas = false;
    *dollar = false;
    *leadsign = false;
    *trailsign = false;

    for ( i = 0; i < length; i++ ) {
	switch ( field[i] ) {
	case '#':
	    if ( seenpoint ) {
		*after += 1;
	    } else {
		*before += 1;
	    }
	    break;
	case '.':
	    seenpoint = true;
	    break;
	case ',':
	    *commas = true;
	    break;
	case '$':
	    *dollar = true;
	    break;
	case '+':
	case '-':
	    if ( i == 0 ) {
		*leadsign = true;
	    } else {
		*trailsign = true;
	    }
	    break;
	default:
	    break;
	}
    }
}

/**
 * @brief Fill @p dest with @p width copies of `*`, the overflow marker.
 *
 * Returns a context rather than `void` because the fill can fail and there was
 * nowhere to say so. Both call sites are already inside `render_numeric`, which
 * returns one. See libakstdlib #38.
 */
static akerr_ErrorContext *overflow(char *dest, size_t width)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, aksl_memset(dest, '*', width));
    dest[width] = '\0';
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Render a number into a measured numeric field.
 *
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *field, size_t length,
					  double number, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    char digits[AKBASIC_MAX_STRING_LENGTH];
    char grouped[AKBASIC_MAX_STRING_LENGTH];
    char *point = NULL;
    size_t used = 0;
    size_t i = 0;
    size_t intlen = 0;
    int before = 0;
    int after = 0;
    int group = 0;
    bool commas = false;
    bool dollar = false;
    bool leadsign = false;
    bool trailsign = false;
    bool negative = (number < 0.0);
    int written = 0;

    measure_numeric(field, length, &before, &after, &commas, &dollar, &leadsign, &trailsign);
    FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS,
		     "PRINT USING field of %zu characters does not fit", length);

    PASS(errctx, aksl_snprintf(&written, digits, sizeof(digits), "%.*f",
			       after, (negative ? -number : number)));

    PASS(errctx, aksl_strchr(digits, '.', &point));
    if ( point != NULL ) {
	intlen = (size_t)(point - digits);
    } else {
	PASS(errctx, aksl_strlen(digits, &intlen));
    }

    /* Group the integer part, if the field asked for separators. */
    used = 0;
    if ( commas ) {
	for ( i = 0; i < intlen; i++ ) {
	    if ( i > 0 && ((intlen - i) % 3) == 0 ) {
		grouped[used] = obj->chars[AKBASIC_PUDEF_COMMA];
		used += 1;
	    }
	    grouped[used] = digits[i];
	    used += 1;
	}
    } else {
	PASS(errctx, aksl_memcpy(grouped, digits, intlen));
	used = intlen;
    }
    grouped[used] = '\0';
    group = (int)used;

    /*
     * Does it fit? The field's `#` positions before the point are the budget,
     * and a separator or a sign that the field asked for costs nothing extra
     * because it was written into the field. Overflow fills with `*` rather than
     * printing wider than asked, which would misalign every later column.
     */
    if ( (int)intlen > before ) {
	PASS(errctx, overflow(dest, length));
	SUCCEED_RETURN(errctx);
    }

    used = 0;
    if ( leadsign ) {
	dest[used] = (negative ? '-' : '+');
	used += 1;
    }
    if ( dollar ) {
	dest[used] = obj->chars[AKBASIC_PUDEF_DOLLAR];
	used += 1;
    }
    /*
     * Right-justify inside the digit positions, padding with the PUDEF blank.
     * The width to pad to counts the separators the field will actually use, so
     * `###,###` holding 1234 pads to the same column every time.
     */
    {
	int width = before + (commas ? (before - 1) / 3 : 0);

	for ( i = (size_t)group; (int)i < width; i++ ) {
	    dest[used] = obj->chars[AKBASIC_PUDEF_BLANK];
	    used += 1;
	}
    }
    PASS(errctx, aksl_memcpy(dest + used, grouped, (size_t)group));
    used += (size_t)group;

    if ( after > 0 ) {
	dest[used] = obj->chars[AKBASIC_PUDEF_POINT];
	used += 1;
	PASS(errctx, aksl_memcpy(dest + used, (point != NULL ? point + 1 : ""), (size_t)after));
	used += (size_t)after;
    }
    if ( trailsign ) {
	dest[used] = (negative ? '-' : '+');
	used += 1;
    }
    /*
     * A negative number in a field with no sign position still has to say so.
     * BASIC 7.0 overflows the field in that case rather than dropping the minus,
     * because a printed -5 that reads as 5 is worse than a row of stars.
     */
    if ( negative && !leadsign && !trailsign ) {
	PASS(errctx, overflow(dest, length));
	SUCCEED_RETURN(errctx);
    }
    dest[used] = '\0';
    SUCCEED_RETURN(errctx);
}

/** @brief Render a string into a measured string field: `=` centres, `>` right-justifies. */
static akerr_ErrorContext *render_string(const char *field, size_t length, const char *text,
					 char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    size_t textlen = 0;
    size_t pad = 0;

    PASS(errctx, aksl_strlen(text, &textlen));
    FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS,
		     "PRINT USING field of %zu characters does not fit", length);
    if ( textlen > length ) {
	/* Truncated, not starred: BASIC 7.0 cuts a string to its field. */
	PASS(errctx, aksl_memcpy(dest, text, length));
	dest[length] = '\0';
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, aksl_memset(dest, ' ', length));
    dest[length] = '\0';
    if ( field[0] == '=' ) {
	pad = (length - textlen) / 2;
    } else {
	pad = length - textlen;
    }
    PASS(errctx, aksl_memcpy(dest + pad, text, textlen));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_format_using(akbasic_FormatState *obj, const char *format, akbasic_Value *value, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    char rendered[AKBASIC_MAX_STRING_LENGTH];
    size_t start = 0;
    size_t length = 0;
    size_t used = 0;
    size_t renderedlen = 0;
    size_t formatlen = 0;
    int written = 0;
    bool numeric = false;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && format != NULL && value != NULL && dest != NULL),
		     AKERR_NULLPOINTER, "NULL argument in format_using");
    FAIL_ZERO_RETURN(errctx, (len > 0), AKBASIC_ERR_BOUNDS, "Zero-length destination");

    PASS(errctx, find_field(format, &start, &length, &numeric));

    if ( numeric ) {
	double number = 0.0;

	FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			    "PRINT USING: a numeric field cannot print a string");
	if ( value->valuetype == AKBASIC_TYPE_FLOAT ) {
	    number = value->floatval;
	} else if ( value->valuetype == AKBASIC_TYPE_BOOLEAN ) {
	    number = (double)value->boolvalue;
	} else {
	    number = (double)value->intval;
	}
	PASS(errctx, render_numeric(obj, format + start, length, number,
				    rendered, sizeof(rendered)));
    } else {
	FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			    "PRINT USING: a string field cannot print a number");
	PASS(errctx, render_string(format + start, length, value->stringval,
				   rendered, sizeof(rendered)));
    }

    /* Literal text before the field, the field, then literal text after it. */
    PASS(errctx, aksl_strlen(rendered, &renderedlen));
    PASS(errctx, aksl_strlen(format, &formatlen));
    used = renderedlen + formatlen - length;
    FAIL_ZERO_RETURN(errctx, (used + 1 <= len), AKBASIC_ERR_BOUNDS,
		     "PRINT USING result of %zu characters does not fit", used);
    PASS(errctx, aksl_memcpy(dest, format, start));
    PASS(errctx, aksl_memcpy(dest + start, rendered, renderedlen));
    /* The check above already proved the tail fits, so this cannot truncate. */
    PASS(errctx, aksl_snprintf(&written, dest + start + renderedlen,
			       len - start - renderedlen, "%s", format + start + length));
    SUCCEED_RETURN(errctx);
}
