/**
 * \file    printf.c
 * \brief   printf library of system adaptation layer
 *
 * \par     Copyright (c) 2018 UWE Technologies Corp., Ltd. All Rights Reserved.
 * \author  James
 * \date    2018-03-05
 * \version 
 * 
 * \history 
 *  Date        Note
 *  2018-03-05  Original Creation
 * 
 */

#include <stdarg.h>

#include "types.h"
#include "stdio.h"

#if LIBC_PRINTF_ENABLED

#include "sync.h"
#include "string.h"
#include "stdlib.h"
#include "ctype.h"
#include "drv_uart.h"


#ifndef INCLUDE_PRINT_LONGLONG
#define INCLUDE_PRINT_LONGLONG  0
#endif

#ifndef INCLUDE_PRINT_FLOAT
#define INCLUDE_PRINT_FLOAT     1
#endif

#define ITOA_NUMBER_SIZE 	    32
#define FTOA_BUFFER_SIZE        32

/** Flag definitions */
#define FL_ZEROPAD      0x01
#define FL_PLUS		    0x02
#define FL_SPACE	    0x04
#define FL_LEFT		    0x08
#define FL_HASH         0x10
#define FL_PREC		    0x20
#define FL_LONG		    0x40
#define FL_LONGLONG     0x80
#define FL_HEXUPP	    0x100


/* PROCCHARP(), PROCCHAR():
 * Macros used to conveniently distinguish between buffer-bound
 * and console-bound characters during the formatting process.
 */
#define PROCCHARP(bp,eob,cp)                    \
    if (bp) {                                   \
        if (eob && (bp >= eob)) {               \
            return(-1);                         \
        }                                       \
        *bp++ = *cp++;                          \
    }                                           \
    else {                                      \
        putchar(*cp++);                \
    }                                           

#define PROCCHAR(bp,eob,c)                      \
    if (bp) {                                   \
        if (eob && (bp >= eob)) {               \
            return(-1);                         \
        }                                       \
        *bp++ = c;                              \
    }                                           \
    else if (c) {                               \
        putchar(c);                    \
    }

#define BUFINC(bp,val)      \
    if (bp) {               \
        bp += val;          \
    }


#if INCLUDE_PRINT_LONGLONG

#define ITOA_LL_NUMBER_SIZE     64

#define VA_ARG_GETULONG(ap,lflag)                \
    (((lflag) & FL_LONGLONG) ? (va_arg(ap, unsigned long long)) : (((lflag) & FL_LONG) ? (va_arg(ap, unsigned long)) : (va_arg(ap, unsigned int))))

#define VA_ARG_GETLONG(ap,lflag)                \
    (((lflag) & FL_LONGLONG) ? (va_arg(ap, long long)) : (((lflag) & FL_LONG) ? (va_arg(ap, long)) : (va_arg(ap, int))))

#else

#define VA_ARG_GETULONG(ap,lflag)                \
    ((lflag) ? (va_arg(ap, unsigned long)) : (va_arg(ap, unsigned int)))

#define VA_ARG_GETLONG(ap,lflag)                \
    ((lflag) ? (va_arg(ap, long)) : (va_arg(ap, int)))

#endif


// internal itoa format
static unsigned int ntoa_format(char* buf, char *bufend, char* lbuf, 
    unsigned int len, unsigned int lbufsize, int negative, int base, int prec, 
    int width, int flags)
{
    if (base > 16U) {
        return 0U;
    }

    // pad leading zeros
    while (!(flags & FL_LEFT) && (len < prec) && (len < lbufsize)) {
        lbuf[len++] = '0';
    }
    while (!(flags & FL_LEFT) && (flags & FL_ZEROPAD) && (len < width) && (len < lbufsize)) {
        lbuf[len++] = '0';
    }

    // handle hash
    if (flags & FL_HASH) {
        if (((len == prec) || (len == width)) && (len > 0U)) {
            len--;
            if ((base == 16U) && (len > 0U)) {
                len--;
            }
        }
        if ((base == 16U) && !(flags & FL_HEXUPP) && (len < lbufsize)) {
            lbuf[len++] = 'x';
        }
        if ((base == 16U) &&  (flags & FL_HEXUPP) && (len < lbufsize)) {
            lbuf[len++] = 'X';
        }
        if (len < lbufsize) {
            lbuf[len++] = '0';
        }
    }

    // handle sign
    if ((len == width) && (negative || (flags & FL_PLUS) || (flags & FL_SPACE))) {
        len--;
    }
    if (len < lbufsize) {
        if (negative) {
            lbuf[len++] = '-';
        }
        else if (flags & FL_PLUS) {
            lbuf[len++] = '+';  // ignore the space if the '+' exists
        }
        else if (flags & FL_SPACE) {
            lbuf[len++] = ' ';
        }
    }

    // pad spaces up to given width
    if (!(flags & FL_LEFT) && !(flags & FL_ZEROPAD)) {
        while ((len < width) && (len < lbufsize)) {
            lbuf[len++] = ' ';
        }
    }

    // reverse string
    for (unsigned int i = 0U; i < len; ++i) {
        PROCCHAR(buf,bufend,lbuf[len - i - 1U]);
    }

    // append pad spaces up to given width
    if (flags & FL_LEFT) {
        while (len < width) {
            PROCCHAR(buf,bufend,' ');
            len++;
        }
    }

    return len;
}


// internal print number for 'long' type
static unsigned int print_long(char *buf, char *bufend, unsigned long value, 
    int negative, int base, int prec, int width, int flags)
{
    char lbuf[ITOA_NUMBER_SIZE];
    int letter = (flags & FL_HEXUPP) ? 'A' - 10 : 'a' - 10;
    unsigned int len = 0U;

    // write if precision != 0 and value is != 0
    if (!(flags & FL_PREC) || value) {
        do {
            int digit = (value % base);
            lbuf[len++] = digit < 10 ? '0' + digit : letter + digit;
            value /= base;
        } while ((len < ITOA_NUMBER_SIZE) && value);
    }

    return ntoa_format(buf, bufend, lbuf, len, ITOA_NUMBER_SIZE, negative, 
        base, prec, width, flags);
}


// internal print number for 'long long' type
#if INCLUDE_PRINT_LONGLONG
static unsigned int print_longlong(char *buf, char *bufend, unsigned long long value, 
    int negative, int base, int prec, int width, int flags)
{
    char lbuf[ITOA_LL_NUMBER_SIZE];
    int letter = (flags & FL_HEXUPP) ? 'A' - 10 : 'a' - 10;
    unsigned int len = 0U;

    // write if precision != 0 and value is != 0
    if (!(flags & FL_PREC) || value) {
        do {
            int digit = (value % base);
            lbuf[len++] = digit < 10 ? '0' + digit : letter + digit;
            value /= base;
        } while ((len < ITOA_LL_NUMBER_SIZE) && value);
    }

    return ntoa_format(buf, bufend, lbuf, len, ITOA_LL_NUMBER_SIZE, negative, 
        base, prec, width, flags);
}
#endif  // INCLUDE_PRINT_LONGLONG


#if INCLUDE_PRINT_FLOAT
static unsigned int print_double(char *buf, char *bufend, double num, 
    int prec, int width, int flags)
{
    char lbuf[FTOA_BUFFER_SIZE];
    unsigned int len  = 0U;
    double diff = 0.0;

    int negative = 0;

    // if input is larger than thres_max, revert to exponential
    const double thres_max = (double)0x7FFFFFFF;

    // powers of 10
    static const double pow10[] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
 
    // limit precision
    if (!(flags & FL_PREC)) {
        prec = 6U;  // by default, precesion is 6
    }
    if (prec > 9U) {
        prec = 9U;
    }

    // negative
    if (num < 0)
    {
        negative = 1;
        num = 0 - num;
    }

    // if input is larger than 0x7FFFFFFF, return 0
    if (num > thres_max) {
        return 0U;
    }

    int whole = (int)num;
    double tmp = (num - whole) * pow10[prec];
    unsigned long frac = (unsigned long)tmp;
    diff = tmp - frac;

    if (diff > 0.5) {
        ++frac;
        // handle rollover, e.g. case 0.99 with prec 1 is 1.0
        if (frac >= pow10[prec]) {
            frac = 0;
            ++whole;
        }
    }
    else if ((diff == 0.5) && ((frac == 0) || (frac & 1))) {
        // if halfway, round up if odd, OR if last digit is 0
        ++frac;
    }

    if (prec == 0U) {
        diff = num - whole;
        if (diff > 0.5) {
            // greater than 0.5, round up, e.g. 1.6 -> 2
            ++whole;
        }
        else if ((diff == 0.5) && (whole & 1)) {
            // exactly 0.5 and ODD, then round up
            // 1.5 -> 2, but 2.5 -> 2
            ++whole;
        }
    }
    else {
        int count = prec;
        // now do fractional part, as an unsigned number
        do {
            --count;
            lbuf[len++] = (char)(48U + (frac % 10U));
        } while ((len < FTOA_BUFFER_SIZE) && (frac /= 10U));
        // add extra 0s
        while ((len < FTOA_BUFFER_SIZE) && (count-- > 0U)) {
            lbuf[len++] = '0';
        }
        if (len < FTOA_BUFFER_SIZE) {
            // add decimal
            lbuf[len++] = '.';
        }
    }

    // do whole part, number is reversed
    while (len < FTOA_BUFFER_SIZE) {
        lbuf[len++] = (char)(48 + (whole % 10));
        if (!(whole /= 10)) {
              break;
        }
    }

    // pad leading zeros
    while (!(flags & FL_LEFT) && (len < prec) && (len < FTOA_BUFFER_SIZE)) {
        lbuf[len++] = '0';
    }
    while (!(flags & FL_LEFT) && (flags & FL_ZEROPAD) && (len < width) && (len < FTOA_BUFFER_SIZE)) {
        lbuf[len++] = '0';
    }

    // handle sign
    if (len < FTOA_BUFFER_SIZE) {
        if (negative) {
            lbuf[len++] = '-';
        }
        else if (flags & FL_PLUS) {
            lbuf[len++] = '+';  // ignore the space if the '+' exists
        }
        else if (flags & FL_SPACE) {
            lbuf[len++] = ' ';
        }
    }

    // pad spaces up to given width
    if (!(flags & FL_LEFT) && !(flags & FL_ZEROPAD)) {
        while ((len < width) && (len < FTOA_BUFFER_SIZE)) {
            lbuf[len++] = ' ';
        }
    }

    // reverse string
    for (unsigned int i = 0U; i < len; ++i) {
        PROCCHAR(buf,bufend,lbuf[len - i - 1U]);
    }

    // append pad spaces up to given width
    if (flags & FL_LEFT) {
        while (len < width) {
            PROCCHAR(buf,bufend,' ');
            len++;
        }
    }

    return len;
}
#endif

int vsnprintf(char *buf, int bsize, const char *fmt, va_list ap)
{
    register const char* cp = fmt;
    const char* p;
    int ch;		/* holds a char from the format string */
    int width;
    int prec;
    int flags;
    int base;
    char *bufend;

    if (bsize == 0)
        bufend = 0;
    else
        bufend = buf+(bsize-1);

    bsize = 0;
    while(*cp) {
		if (*cp != '%') {
			PROCCHARP(buf,bufend,cp);
			bsize++;
			continue;
		}
		cp++;
		if (*cp == '%') {
			PROCCHARP(buf,bufend,cp);
			bsize++;
			continue;
		}

        flags = 0;
        do {
          switch (*cp) {
            case '0': flags |= FL_ZEROPAD; cp++; ch = 1U; break;
            case '-': flags |= FL_LEFT;    cp++; ch = 1U; break;
            case '+': flags |= FL_PLUS;    cp++; ch = 1U; break;
            case ' ': flags |= FL_SPACE;   cp++; ch = 1U; break;
            case '#': flags |= FL_HASH;    cp++; ch = 1U; break;
            default :                            ch = 0U; break;
          }
        } while (ch);

        width = 0;
		if (isdigit(*cp)) {
            do 
            {
    		    width = 10*width + (*cp) - '0';
                cp++;
            } while (isdigit(*cp));
		}
        else if (*cp == '*') {
            const int w = va_arg(ap, int);
            if (w < 0) {
                flags |= FL_LEFT;    // reverse padding
                width = -w;
            }
            else {
                width = w;
            }
            cp++;
        }
        
        prec = 0; 
        if (*cp == '.') {
            cp++;
		    flags |= FL_PREC;
            if (isdigit(*cp)) {
                do
                {
                    prec = 10*prec + (*cp) - '0';
                    cp++;
                } while (isdigit(*cp));
            }
            else if (*cp == '*') {
                prec = (unsigned int)va_arg(ap, int);
                cp++;
            }
        }

		if (*cp == 'l') {			/* Ignore the 'long' designator */
            flags |= FL_LONG;
			cp++;
			if (*cp == 'l') {		/* unless its the longlong designator */
				cp++;
#if INCLUDE_PRINT_LONGLONG
                flags |= FL_LONGLONG;
#endif /* #if INCLUDE_LONGLONG */
			}
		}

        base = 0;

		switch(*cp) {
			case 'c':			/* Character conversion */
				ch = va_arg(ap,int);
                p = (const char *)&ch; // little endian
				goto str_pad;
				break;
			case 's':			/* String conversion */
				if ((p = va_arg(ap, const char *)) == NULL)
                {
                    p = "(null)";
                }
                str_pad:
                prec = strnlen (p, (flags & FL_PREC) ? prec : ~0);

        		if (!(flags & FL_LEFT)) {
        		    while (prec < width) {
        			    PROCCHAR(buf,bufend,' ');
                        bsize++;
        			    width--;
        		    }
        		}
        		while (prec > 0) {
        		    PROCCHARP(buf,bufend,p);
        		    if (width > 0) width--;
        		    prec--;
        		}
                while (width > 0)
                {
                    PROCCHAR(buf,bufend,' ');
                    width--;
                    bsize++;
                }
				break;
#if INCLUDE_PRINT_FLOAT
            case 'f':			/* Float conversion */
                {
                	ch = print_double(buf,bufend,va_arg(ap,double),prec,width,flags);
    				BUFINC(buf,ch);
                    bsize += ch;
                }
				break;
#endif
			case 'i':			/* Decimal conversion */
			case 'd':			/* Decimal conversion */
                {
                    flags &= ~FL_HASH;   // no hash for dec format
                    base = 10;
#if INCLUDE_PRINT_LONGLONG
                    if (flags & FL_LONGLONG)
                    {
                        long long value = VA_ARG_GETLONG(ap,flags);
                        ch = print_longlong(buf,bufend,(unsigned long long)(value > 0 ? value : 0 - value),(value < 0),base,prec,width,flags);
                    }
                    else
#endif
                    {
                        long value = VA_ARG_GETLONG(ap,flags);
                        ch = print_long(buf,bufend,(unsigned long)(value > 0 ? value : 0 - value),(value < 0),base,prec,width,flags);
                    }
                    BUFINC(buf,ch);
                    bsize += ch;
    			}
				break;
			case 'p':			/* Hex conversion */
                width = sizeof(void*) * 2U;
                flags |= FL_ZEROPAD;
			case 'X':
                flags |= FL_HEXUPP;
			case 'x':
                base = 16;
                goto print_unsigned;
            case 'b':
                base = 2;
                flags &= ~FL_HASH;   // no hash for bin format
                goto print_unsigned;
            case 'o':
                base = 8;
                goto print_unsigned;
			case 'u':
                base = 10;
                flags &= ~FL_HASH;   // no hash for dec format
                print_unsigned:
                flags &= ~(FL_PLUS | FL_SPACE); // no plus or space flag for u, x, X, o, b
                {
#if INCLUDE_PRINT_LONGLONG
                    if (flags & FL_LONGLONG)
                    {
                        ch = print_longlong(buf,bufend,VA_ARG_GETULONG(ap,flags),FALSE,base,prec,width,flags);
                    }
                    else
#endif
                    {
                        ch = print_long(buf,bufend,VA_ARG_GETULONG(ap,flags),FALSE,base,prec,width,flags);
                    }
                    BUFINC(buf,ch);
                    bsize += ch;
                }
				break;
			default:
				PROCCHAR(buf,bufend,*cp);
				bsize++;
				break;
		}
		cp++;
	}
	PROCCHAR(buf,bufend,0);
	return(bsize);
}

/* snprintf(), sprintf(), printf() :
 * All functions use vsnprintf() to format a string.
 * The string is either transferred to a buffer or transferred
 * directly to this target's console through putchar (refer to
 * the macros at the top of this file).
 *
 * - sprintf() formats to a buffer.
 * - printf() formats to stdio.
 */
int
snprintf(char *buf, int bsize, const char *fmt, ...)
{
    va_list argp;

    va_start(argp,fmt);
    bsize = vsnprintf(buf,bsize,fmt,argp);
    va_end(argp);
    return(bsize);
}

int sprintf(char *buf, const char *fmt, ...)
{
    int total;
    va_list argp;

    va_start(argp,fmt);
    total = vsnprintf(buf,0,fmt,argp);
    va_end(argp);
    return(total);
}

int printf(const char *fmt, ...)
{
    int total;
    va_list argp;

    va_start(argp,fmt);
    total = vsnprintf(0,0,fmt,argp);
    va_end(argp);
    return(total);
}

#endif /* #if LIBC_PRINTF_ENABLED */

int puts(const char *str)
{
	while (*str) {
		putchar(*str++);
	}

	return putchar('\n');
}
