///                MentOS, The Mentoring Operating system project
/// @file vsprintf.c
/// @brief Print formatting routines.
/// @copyright (c) 2014-2021 This file is distributed under the MIT License.
/// See LICENSE.md for details.

#include "math.h"
#include "ctype.h"
#include "string.h"
#include "stdarg.h"
#include "stdbool.h"
#include "stdint.h"
#include "stdio.h"
#include "io/video.h"
#include "fcvt.h"

/// Size of the buffer used to call cvt functions.
#define CVTBUFSIZE 500

#define FLAGS_ZEROPAD   (1U << 0U) ///< Fill zeros before the number.
#define FLAGS_LEFT      (1U << 1U) ///< Left align the value.
#define FLAGS_PLUS      (1U << 2U) ///< Print the plus sign.
#define FLAGS_SPACE     (1U << 3U) ///< If positive add a space instead of the plus sign.
#define FLAGS_HASH      (1U << 4U) ///< Preceed with 0x or 0X, %x or %X respectively.
#define FLAGS_UPPERCASE (1U << 5U) ///< Print uppercase.
#define FLAGS_SIGN      (1U << 6U) ///< Print the sign.

/// The list of digits.
static char *_digits = "0123456789abcdefghijklmnopqrstuvwxyz";

/// The list of uppercase digits.
static char *_upper_digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/// @brief Returns the index of the first non-integer character.
static inline int skip_atoi(const char **s)
{
    int i = 0;
    while (isdigit(**s))
        i = i * 10 + *((*s)++) - '0';
    return i;
}

static char *number(char *str, long num, int base, int size, int32_t precision, unsigned flags)
{
    char c, tmp[66] = { 0 };
    char *dig = _digits;

    if (flags & FLAGS_UPPERCASE) {
        dig = _upper_digits;
    }
    if (flags & FLAGS_LEFT) {
        flags &= ~FLAGS_ZEROPAD;
    }
    if (base < 2 || base > 36) {
        return 0;
    }

    c = (flags & FLAGS_ZEROPAD) ? '0' : ' ';

    // --------------------------------
    // Set the sign.
    // --------------------------------
    char sign = 0;
    if (flags & FLAGS_SIGN) {
        if (num < 0) {
            sign = '-';
            num  = -num;
            size--;
        } else if (flags & FLAGS_PLUS) {
            sign = '+';
            size--;
        } else if (flags & FLAGS_SPACE) {
            sign = ' ';
            size--;
        }
    }
    // Sice I've removed the sign (if negative), i can transform it to unsigned.
    uint32_t uns_num = (uint32_t)num;
    if (flags & FLAGS_HASH) {
        if (base == 16) {
            size -= 2;
        } else if (base == 8) {
            size--;
        }
    }

    int32_t i = 0;
    if (uns_num == 0) {
        tmp[i++] = '0';
    } else {
        while (uns_num != 0) {
            tmp[i++] = dig[((unsigned long)uns_num) % (unsigned)base];
            uns_num  = ((unsigned long)uns_num) / (unsigned)base;
        }
    }
    if (i > precision) {
        precision = i;
    }
    size -= precision;
    if (!(flags & (FLAGS_ZEROPAD | FLAGS_LEFT))) {
        while (size-- > 0)
            *str++ = ' ';
    }
    if (sign) {
        *str++ = sign;
    }
    if (flags & FLAGS_HASH) {
        if (base == 8)
            *str++ = '0';
        else if (base == 16) {
            *str++ = '0';
            *str++ = _digits[33];
        }
    }
    if (!(flags & FLAGS_LEFT)) {
        while (size-- > 0) {
            *str++ = c;
        }
    }
    while (i < precision--) {
        *str++ = '0';
    }
    while (i-- > 0) {
        *str++ = tmp[i];
    }
    while (size-- > 0) {
        *str++ = ' ';
    }
    return str;
}

static char *eaddr(char *str, unsigned char *addr, int size, int precision, unsigned flags)
{
    (void)precision;
    char tmp[24];
    char *dig = _digits;
    int i, len;

    if (flags & FLAGS_UPPERCASE) {
        dig = _upper_digits;
    }

    len = 0;
    for (i = 0; i < 6; i++) {
        if (i != 0) {
            tmp[len++] = ':';
        }
        tmp[len++] = dig[addr[i] >> 4];
        tmp[len++] = dig[addr[i] & 0x0F];
    }

    if (!(flags & FLAGS_LEFT)) {
        while (len < size--) {
            *str++ = ' ';
        }
    }

    for (i = 0; i < len; ++i) {
        *str++ = tmp[i];
    }

    while (len < size--) {
        *str++ = ' ';
    }

    return str;
}

static char *iaddr(char *str, unsigned char *addr, int size, int precision, unsigned flags)
{
    (void)precision;
    char tmp[24];
    int i, n, len;

    len = 0;
    for (i = 0; i < 4; i++) {
        if (i != 0) {
            tmp[len++] = '.';
        }
        n = addr[i];

        if (n == 0) {
            tmp[len++] = _digits[0];
        } else {
            if (n >= 100) {
                tmp[len++] = _digits[n / 100];
                n          = n % 100;
                tmp[len++] = _digits[n / 10];
                n          = n % 10;
            } else if (n >= 10) {
                tmp[len++] = _digits[n / 10];
                n          = n % 10;
            }

            tmp[len++] = _digits[n];
        }
    }

    if (!(flags & FLAGS_LEFT)) {
        while (len < size--) {
            *str++ = ' ';
        }
    }

    for (i = 0; i < len; ++i) {
        *str++ = tmp[i];
    }

    while (len < size--) {
        *str++ = ' ';
    }

    return str;
}

static void cfltcvt(double value, char *buffer, char fmt, int precision)
{
    int decpt, sign, exp, pos;
    char cvtbuf[CVTBUFSIZE];
    char *digits = cvtbuf;
    int capexp   = 0;
    int magnitude;

    if (fmt == 'G' || fmt == 'E') {
        capexp = 1;
        fmt += 'a' - 'A';
    }

    if (fmt == 'g') {
        ecvtbuf(value, precision, &decpt, &sign, cvtbuf, CVTBUFSIZE);
        magnitude = decpt - 1;
        if (magnitude < -4 || magnitude > precision - 1) {
            fmt = 'e';
            precision -= 1;
        } else {
            fmt = 'f';
            precision -= decpt;
        }
    }

    if (fmt == 'e') {
        ecvtbuf(value, precision + 1, &decpt, &sign, cvtbuf, CVTBUFSIZE);

        if (sign) {
            *buffer++ = '-';
        }
        *buffer++ = *digits;
        if (precision > 0) {
            *buffer++ = '.';
        }
        memcpy(buffer, digits + 1, precision);
        buffer += precision;
        *buffer++ = capexp ? 'E' : 'e';

        if (decpt == 0) {
            if (value == 0.0) {
                exp = 0;
            } else {
                exp = -1;
            }
        } else {
            exp = decpt - 1;
        }

        if (exp < 0) {
            *buffer++ = '-';
            exp       = -exp;
        } else {
            *buffer++ = '+';
        }

        buffer[2] = (char)((exp % 10) + '0');
        exp       = exp / 10;
        buffer[1] = (char)((exp % 10) + '0');
        exp       = exp / 10;
        buffer[0] = (char)((exp % 10) + '0');
        buffer += 3;
    } else if (fmt == 'f') {
        fcvtbuf(value, precision, &decpt, &sign, cvtbuf, CVTBUFSIZE);
        if (sign) {
            *buffer++ = '-';
        }
        if (*digits) {
            if (decpt <= 0) {
                *buffer++ = '0';
                *buffer++ = '.';
                for (pos = 0; pos < -decpt; pos++) {
                    *buffer++ = '0';
                }
                while (*digits) {
                    *buffer++ = *digits++;
                }
            } else {
                pos = 0;
                while (*digits) {
                    if (pos++ == decpt) {
                        *buffer++ = '.';
                    }
                    *buffer++ = *digits++;
                }
            }
        } else {
            *buffer++ = '0';
            if (precision > 0) {
                *buffer++ = '.';
                for (pos = 0; pos < precision; pos++) {
                    *buffer++ = '0';
                }
            }
        }
    }

    *buffer = '\0';
}

static void forcdecpt(char *buffer)
{
    while (*buffer) {
        if (*buffer == '.') {
            return;
        }
        if (*buffer == 'e' || *buffer == 'E') {
            break;
        }

        buffer++;
    }

    if (*buffer) {
        long n = (long)strlen(buffer);
        while (n > 0) {
            buffer[n + 1] = buffer[n];
            n--;
        }
        *buffer = '.';
    } else {
        *buffer++ = '.';
        *buffer   = '\0';
    }
}

static void cropzeros(char *buffer)
{
    char *stop;

    while (*buffer && *buffer != '.') {
        buffer++;
    }

    if (*buffer++) {
        while (*buffer && *buffer != 'e' && *buffer != 'E') {
            buffer++;
        }
        stop = buffer--;
        while (*buffer == '0') {
            buffer--;
        }
        if (*buffer == '.') {
            buffer--;
        }
        while ((*++buffer = *stop++)) {}
    }
}

static char *flt(char *str, double num, int size, int precision, char fmt, unsigned flags)
{
    char tmp[80];
    char c, sign;
    int n, i;

    // Left align means no zero padding.
    if (flags & FLAGS_LEFT)
        flags &= ~FLAGS_ZEROPAD;

    // Determine padding and sign char.
    c    = (flags & FLAGS_ZEROPAD) ? '0' : ' ';
    sign = 0;
    if (flags & FLAGS_SIGN) {
        if (num < 0.0) {
            sign = '-';
            num  = -num;
            size--;
        } else if (flags & FLAGS_PLUS) {
            sign = '+';
            size--;
        } else if (flags & FLAGS_SPACE) {
            sign = ' ';
            size--;
        }
    }

    // Compute the precision value.
    if (precision < 0) {
        // Default precision: 6.
        precision = 6;
    } else if (precision == 0 && fmt == 'g') {
        // ANSI specified.
        precision = 1;
    }

    // Convert floating point number to text.
    cfltcvt(num, tmp, fmt, precision);

    // '#' and precision == 0 means force a decimal point.
    if ((flags & FLAGS_HASH) && precision == 0) {
        forcdecpt(tmp);
    }

    // 'g' format means crop zero unless '#' given.
    if (fmt == 'g' && !(flags & FLAGS_HASH)) {
        cropzeros(tmp);
    }

    n = strlen(tmp);

    // Output number with alignment and padding.
    size -= n;
    if (!(flags & (FLAGS_ZEROPAD | FLAGS_LEFT))) {
        while (size-- > 0) {
            *str++ = ' ';
        }
    }

    if (sign) {
        *str++ = sign;
    }

    if (!(flags & FLAGS_LEFT)) {
        while (size-- > 0) {
            *str++ = c;
        }
    }

    for (i = 0; i < n; i++) {
        *str++ = tmp[i];
    }

    while (size-- > 0) {
        *str++ = ' ';
    }

    return str;
}

int vsprintf(char *str, const char *fmt, va_list args)
{
    int base;
    char *tmp;
    char *s;

    // Flags to number().
    unsigned flags;

    // 'h', 'l', or 'L' for integer fields.
    char qualifier;

    for (tmp = str; *fmt; fmt++) {
        if (*fmt != '%') {
            *tmp++ = *fmt;

            continue;
        }

        // Process flags-
        flags = 0;
    repeat:
        // This also skips first '%'.
        fmt++;
        switch (*fmt) {
        case '-':
            flags |= FLAGS_LEFT;
            goto repeat;
        case '+':
            flags |= FLAGS_PLUS;
            goto repeat;
        case ' ':
            flags |= FLAGS_SPACE;
            goto repeat;
        case '#':
            flags |= FLAGS_HASH;
            goto repeat;
        case '0':
            flags |= FLAGS_ZEROPAD;
            goto repeat;
        }

        // Get the width of the output field.
        int32_t field_width;
        field_width = -1;

        if (isdigit(*fmt)) {
            field_width = skip_atoi(&fmt);
        } else if (*fmt == '*') {
            fmt++;
            field_width = va_arg(args, int32_t);
            if (field_width < 0) {
                field_width = -field_width;
                flags |= FLAGS_LEFT;
            }
        }

        /* Get the precision, thus the minimum number of digits for
         * integers; max number of chars for from string.
         */
        int32_t precision = -1;
        if (*fmt == '.') {
            ++fmt;
            if (isdigit(*fmt)) {
                precision = skip_atoi(&fmt);
            } else if (*fmt == '*') {
                ++fmt;
                precision = va_arg(args, int);
            }
            if (precision < 0) {
                precision = 0;
            }
        }

        // Get the conversion qualifier.
        qualifier = -1;
        if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') {
            qualifier = *fmt;
            fmt++;
        }

        // Default base.
        base = 10;

        switch (*fmt) {
        case 'c':
            if (!(flags & FLAGS_LEFT)) {
                while (--field_width > 0) {
                    *tmp++ = ' ';
                }
            }
            *tmp++ = va_arg(args, char);
            while (--field_width > 0) {
                *tmp++ = ' ';
            }
            continue;

        case 's':
            s = va_arg(args, char *);
            if (!s) {
                s = "<NULL>";
            }

            int32_t len = (int32_t)strnlen(s, (uint32_t)precision);
            if (!(flags & FLAGS_LEFT)) {
                while (len < field_width--) {
                    *tmp++ = ' ';
                }
            }

            int32_t it;
            for (it = 0; it < len; ++it) {
                *tmp++ = *s++;
            }
            while (len < field_width--) {
                *tmp++ = ' ';
            }
            continue;

        case 'p':
            if (field_width == -1) {
                field_width = 2 * sizeof(void *);
                flags |= FLAGS_ZEROPAD;
            }
            tmp = number(tmp, (unsigned long)va_arg(args, void *), 16, field_width, precision, flags);
            continue;
        case 'n':
            if (qualifier == 'l') {
                long *ip = va_arg(args, long *);
                *ip      = (tmp - str);
            } else {
                int *ip = va_arg(args, int *);
                *ip     = (tmp - str);
            }
            continue;
        case 'A':
            flags |= FLAGS_UPPERCASE;
            break;
        case 'a':
            if (qualifier == 'l')
                tmp = eaddr(tmp, va_arg(args, unsigned char *), field_width, precision, flags);
            else
                tmp = iaddr(tmp, va_arg(args, unsigned char *), field_width, precision, flags);
            continue;
            // Integer number formats - set up the flags and "break".
        case 'o':
            base = 8;
            break;

        case 'X':
            flags |= FLAGS_UPPERCASE;
            break;

        case 'x':
            base = 16;
            break;

        case 'd':
        case 'i':
            flags |= FLAGS_SIGN;

        case 'u':
            break;
        case 'E':
        case 'G':
        case 'e':
        case 'f':
        case 'g':
            tmp = flt(tmp, va_arg(args, double), field_width, precision, *fmt, flags | FLAGS_SIGN);
            continue;
        default:
            if (*fmt != '%')
                *tmp++ = '%';
            if (*fmt)
                *tmp++ = *fmt;
            else
                --fmt;
            continue;
        }

        if (flags & FLAGS_SIGN) {
            long num;
            if (qualifier == 'l') {
                num = va_arg(args, long);
            } else if (qualifier == 'h') {
                num = va_arg(args, short);
            } else {
                num = va_arg(args, int);
            }
            tmp = number(tmp, num, base, field_width, precision, flags);
        } else {
            unsigned long num;
            if (qualifier == 'l') {
                num = va_arg(args, unsigned long);
            } else if (qualifier == 'h') {
                num = va_arg(args, unsigned short);
            } else {
                num = va_arg(args, unsigned int);
            }
            tmp = number(tmp, num, base, field_width, precision, flags);
        }
    }

    *tmp = '\0';
    return tmp - str;
}

int printf(const char *format, ...)
{
    char buffer[4096];
    va_list ap;
    int len;
    // Start variabile argument's list.
    va_start(ap, format);
    len = vsprintf(buffer, format, ap);
    va_end(ap);
    video_puts(buffer);
    return len;
}

int sprintf(char *str, const char *fmt, ...)
{
    va_list args;
    int len;

    va_start(args, fmt);
    len = vsprintf(str, fmt, args);
    va_end(args);

    return len;
}
