/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2002 by Gary Czvitkovicz
 * Copyright (C) 2017 by William Wilgus
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/


#include <stdarg.h>
#include <stdbool.h>
#include <limits.h>
#include <string.h>
#include "file.h"
#include "format.h"

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

/* smaller compressed binary without inline but 18% slower */
#define FMT_DECL static inline

FMT_DECL int fmt_width_precision(int *ch, const char **fmt, char **str, va_list *ap)
{
    int value = 0;
    (void) str;
    (void) ap;

    while (*ch >= '0' && *ch <= '9')
    {
        value = 10 * value + (*ch - '0');
        *ch = *(*fmt)++;
    }
    return value;
}

FMT_DECL int fmt_integer_signed(int *ch, const char **fmt, char **str, va_list *ap)
{
    int val, rem, sign;
    (void) ch;
    (void) fmt;

    val = sign = va_arg(*ap, int);
    if (val < 0)
        val = -val;
    do {
        rem = val % 10;
        val /= 10;
        *--(*str) = rem + '0';

    } while (val > 0);

    if (sign < 0)
        *--(*str) = '-';
    return 0;
}

FMT_DECL int fmt_integer_unsigned(int *ch, const char **fmt, char **str, va_list *ap)
{
    unsigned int uval, urem;
    (void) ch;
    (void) fmt;

    uval = va_arg(*ap, unsigned int);
    do {
        urem = uval % 10;
        uval /= 10;
        *--(*str) = urem + '0';
    } while (uval > 0);
    return 0;
}

FMT_DECL int fmt_long(int *ch, const char **fmt, char **str, va_list *ap)
{
    int pad = 0;
    long lval, lrem, lsign = 0;
    unsigned long ulval, ulrem;
    char ch_l = *ch;

    *ch = *(*fmt)++;
    if (*ch == 'd') {
        lval = lsign = va_arg(*ap, long);

        if (lval < 0)
            lval = -lval;
        do {
            lrem = lval % 10;
            lval /= 10;
            *--(*str) = lrem + '0';
        } while (lval > 0);

        if (lsign < 0)
            *--(*str) = '-';
    }
    else if (*ch == 'u') {
        ulval = va_arg(*ap, unsigned long);
        do {
            ulrem = ulval % 10;
            ulval /= 10;
            *--(*str) = ulrem + '0';
        } while (ulval > 0);
    }
    else if (*ch == 'x' || *ch == 'X') {
        pad++;
        ulval = va_arg(*ap, long);
        do {
            *--(*str) = hexdigit[ulval & 0xf];
            ulval >>= 4;
        } while (ulval > 0);
    }
    else {
        *--(*str) = ch_l;
        *--(*str) = *ch;
    }
    return pad;
}

FMT_DECL int fmt_character(int *ch, const char **fmt, char **str, va_list *ap)
{
    (void) ch;
    (void) fmt;

    *--(*str) = va_arg(*ap, int);
    return 0;
}

FMT_DECL int fmt_string(int *ch, const char **fmt, char **str, va_list *ap)
{
    (void) ch;
    (void) fmt;

    *str = va_arg (*ap, char*);
    return 0;
}

FMT_DECL int fmt_hex_unsigned(int *ch, const char **fmt, char **str, va_list *ap)
{
    unsigned int uval;
    (void) ch;
    (void) fmt;

    uval = va_arg(*ap, int);
    do {
        *--(*str) = hexdigit[uval & 0xf];
        uval >>= 4;
    } while (uval > 0);
    return 1;
}

FMT_DECL int fmt_pointer(int *ch, const char **fmt, char **str, va_list *ap)
{
    int pad = fmt_hex_unsigned(ch, fmt, str, ap);
    /* for pointers prepend 0x and act like 'X' */
    *--(*str) = 'x';
    *--(*str) = '0';
    return pad;
}

FMT_DECL int fmt_sizet(int *ch, const char **fmt, char **str, va_list *ap)
{
    size_t uszval, uszrem;
    ssize_t szval, szrem, szsign;
    char ch_z = *ch;
    *ch = *(*fmt)++;

    if (*ch == 'd') {
        szval = szsign = va_arg(*ap, ssize_t);
        if (szval < 0)
            szval = -szval;
        do {
            szrem = szval % 10;
            szval /= 10;
            *--(*str) = szrem + '0';
        } while (szval > 0);

        if (szsign < 0)
            *--(*str) = '-';
    }
    else if (*ch == 'u') {
        uszval = va_arg(*ap, size_t);
        do {
            uszrem = uszval % 10;
            uszval /= 10;
            *--(*str) = uszrem + '0';
        } while (uszval > 0);
    }
    else {
        *--(*str) = ch_z;
        *--(*str) = *ch;
    }
    return 0;
}

static inline int fmt_next_char(int *ch, const char **fmt, char **str, va_list *ap)
{
    (void) fmt;
    (void) ap;

    *--(*str) = *ch;
    return 0;
}



void format(
    /* call 'push()' for each output letter */
    int (*push)(void *userp, unsigned char data),
    void *userp,
    const char *fmt,
    va_list ap)
{
    bool ok = true;
    char *str;
    char tmpbuf[12], pad;
    int ch, width, precision, padded;


    ch = *fmt++;
    tmpbuf[sizeof tmpbuf - 1] = '\0';

    do
    {
        if (ch == '%')
        {
            str = tmpbuf + sizeof tmpbuf - 1;
            ch = *fmt++;
            padded = (ch == '0' ? 1 : 0);
            width = fmt_width_precision(&ch, &fmt, &str, &ap);

            precision = INT_MAX;
            if(ch == '.')
            {
                ch = *fmt++;
                precision = fmt_width_precision(&ch, &fmt, &str, &ap);
            }

            if (ch == 'd')
                fmt_integer_signed(&ch, &fmt, &str, &ap);
            else if (ch == 'u')
                fmt_integer_unsigned(&ch, &fmt, &str, &ap);
            else if (ch == 'l')
                padded += fmt_long(&ch, &fmt, &str, &ap);
            else if (ch == 'c')
                fmt_character(&ch, &fmt, &str, &ap);
            else if (ch == 's')
                fmt_string(&ch, &fmt, &str, &ap);
            else if (ch == 'x' || ch == 'X')
                padded += fmt_hex_unsigned(&ch, &fmt, &str, &ap);
            else if (ch == 'p' || ch == 'P')
                padded += fmt_pointer(&ch, &fmt, &str, &ap);
#if 0
            else if (ch == 'z')
                fmt_sizet(&ch, &fmt, &str, &ap);
#endif
            else
                fmt_next_char(&ch, &fmt, &str, &ap);

            width -= strlen (str);
            if (width > 0)
            {
                pad = (padded ? '0' : ' ');
                while (width-- > 0 && ok)
                    ok=push(userp, pad);
            }
            while(*str != '\0' && ok && precision--)
                    ok=push(userp, *str++);
        }
        else
            ok=push(userp, ch);

    } while ((ch = *fmt++) != '\0' && ok);
}

struct for_fprintf {
    int fd;    /* where to store it */
    int bytes; /* amount stored */
};

static int fprfunc(void *pr, unsigned char letter)
{
    struct for_fprintf *fpr  = (struct for_fprintf *)pr;
    int rc = write(fpr->fd, &letter, 1);

    if(rc > 0) {
        fpr->bytes++; /* count them */
        return true;  /* we are ok */
    }

    return false; /* failure */
}


int fdprintf(int fd, const char *fmt, ...)
{
    va_list ap;
    struct for_fprintf fpr;

    fpr.fd=fd;
    fpr.bytes=0;

    va_start(ap, fmt);
    format(fprfunc, &fpr, fmt, ap);
    va_end(ap);

    return fpr.bytes; /* return 0 on error */
}

void vuprintf(int (*push)(void *userp, unsigned char data), void *userp, const char *fmt, va_list ap)
{
    format(push, userp, fmt, ap);
}
