/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * Some of the code in this file is based on:
 *     https://raw.githubusercontent.com/mpaland/printf/master/printf.c
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2014-2019 Marco Paland < info@paland.com >
 *                         PALANDesign Hannover, Germany
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in the
 * Software without restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
 * Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */


#include <eCxx/CTL/BasicOutputWriter.h>


//
// Uncomment this to disable support for long integer data type in vprintf() for AVR
// (it will save ~300 bytes of program code)
//
// #define ECXX_AVR_DISABLE_PRINTF_SUPPORT_LONG


//
// Uncomment this to disable support for pointer data type in vprintf() for AVR
// (it will save ~70 bytes of program code)
//
// #define ECXX_AVR_DISABLE_PRINTF_SUPPORT_POINTER


namespace eCxx {


//
// Helper functions
//
static inline bool isDigit(char ch) __ECXX_ATTR_ALWAYS_INLINE;
static inline bool isDigit(char ch)
{ return (ch >= '0') && (ch <= '9'); }


static inline uint8_t readUInt8(const char*& str) __ECXX_ATTR_ALWAYS_INLINE;
static inline uint8_t readUInt8(const char*& str)
{
    uint8_t i = 0;
    while( isDigit(*str) ) i = i * 10 + static_cast<uint8_t>(*str++ - '0');
    return i;
}


static inline uint8_t strLen(const char* str) __ECXX_ATTR_ALWAYS_INLINE;
static inline uint8_t strLen(const char* str)
{
    uint8_t len = 0;
    while( *str++ && len < static_cast<decltype(len)>(-1) ) ++len;
    return len;
}


//
// vprintf()
//
extern void vprintf(eCxx::function<void(uint8_t)> write, const char* format, va_list va)
{
    // Determine the fixed-size data types to be used for casting
#if !defined(__AVR_ARCH__) || !defined(ECXX_AVR_DISABLE_PRINTF_SUPPORT_POINTER)
    using PtrValueType = eCxx::conditional<
        sizeof(void*) == sizeof(uint64_t),
        uint64_t,
        conditional<
            sizeof(void*) == sizeof(uint32_t),
            uint32_t,
            uint16_t
        >::Type
    >::Type;
#endif

    using LLongValueType = eCxx::conditional<
        sizeof(long long) == sizeof(int64_t),
        int64_t,
        int32_t
    >::Type;

    using LongValueType = eCxx::conditional<
        sizeof(long) == sizeof(int64_t),
        int64_t,
        int32_t
    >::Type;

    using IntValueType = eCxx::conditional<
        sizeof(int) == sizeof(int32_t),
        int32_t,
        int16_t
    >::Type;

    // String buffer for converting number to string
#ifdef __AVR_ARCH__
    #ifndef ECXX_AVR_DISABLE_PRINTF_SUPPORT_LONG
    constexpr uint8_t StrBuffLen = eCxx::maxIntegerDigits<LongValueType >::Dec;
    #else
    constexpr uint8_t StrBuffLen = eCxx::maxIntegerDigits<IntValueType  >::Dec;
    #endif
#else
    constexpr uint8_t StrBuffLen = eCxx::maxIntegerDigits<LLongValueType>::Dec;
#endif

    eCxx::String<StrBuffLen> strCnvBuff;

    // Process the format string and its argument(s)
    for(;;) {

        // Break if we have reached the null terminator
        if(!*format) break;

        // Output it directly if it is not a format specifier, ...
        if(*format != '%') {
            // Character output
            write(*format);
            // Process the next character
            ++format;
            continue;
        }

        // ... otherwise, increment the pointer so it can be evaluated later
        ++format;

        // Evaluate the flag(s)
        bool             done    = false;
        Convert::NumPad  numPad  = Convert::NumPad ::Space;
        Convert::NumSign numSign = Convert::NumSign::None_Minus;
        bool             revPad  = false;
        uint8_t          revPadL = 0;
        do {
            switch(*format) {
                case '0' : numPad  = Convert::NumPad ::Zero;       ++format; break;
                case '+' : numSign = Convert::NumSign::Plus_Minus; ++format; break;
                case ' ' : numSign = Convert::NumSign::Space_Minus;++format; break;
                case '-' : revPad  = true;                         ++format; break;
                default  : done    = true;                                   break;
            }
        } while(!done);

        // Evaluate the width field
        const uint8_t width = readUInt8(format); // This function also increment the format pointer

        // Evaluate the length-modifier field
        bool isLong  = false;
        bool isLLong = false;

        switch(*format) {
#if !defined(__AVR_ARCH__) || !defined(ECXX_AVR_DISABLE_PRINTF_SUPPORT_LONG)
            case 'l' :
                isLong = true;
                ++format;
                break;
#endif
            case 'z' :
                     if( sizeof(size_t) == sizeof(long long) ) isLLong = true;
                else if( sizeof(size_t) == sizeof(long     ) ) isLong  = true;
                ++format;
                break;
            default:
                break;
        } // switch *format

        // Evaluate the specifier
        switch(*format) {
            case 'd' : {
                // Convert signed integer to signed decimal string
                if(isLLong) {
                    Convert::toDecString(
                        strCnvBuff, static_cast<LLongValueType>( va_arg(va, long long) ),
                        numSign, revPad ? 0 : width, numPad
                    );
                }
                else if(isLong) {
                    Convert::toDecString(
                        strCnvBuff, static_cast<LongValueType>( va_arg(va, long) ),
                        numSign, revPad ? 0 : width, numPad
                    );
                }
                else {
                    Convert::toDecString(
                        strCnvBuff, static_cast<IntValueType>( va_arg(va, int) ),
                        numSign, revPad ? 0 : width, numPad
                    );
                }
                // Process the next character
                ++format;
                break;
            }
            case 'u' : {
                // Convert unsigned integer to unsigned decimal string
                if(isLLong) {
                    Convert::toDecString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<LLongValueType>::Type>( va_arg(va, unsigned long long) ),
                        numSign, revPad ? 0 : width, numPad
                    );
                }
                else if(isLong) {
                    Convert::toDecString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<LongValueType>::Type>( va_arg(va, unsigned long) ),
                        numSign, revPad ? 0 : width,
                        numPad );
                }
                else {
                    Convert::toDecString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<IntValueType>::Type>( va_arg(va, unsigned int) ),
                        numSign, revPad ? 0 : width, numPad
                    );
                }
                // Process the next character
                ++format;
                break;
            }
            case 'x' : /* FALLTHROUGH */
            case 'X' : {
                // Convert unsigned integer to unsigned hexadecimal string
                if(isLLong) {
                    Convert::toHexString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<LLongValueType>::Type>( va_arg(va, unsigned long long) ),
                        numSign, revPad ? 0 : width, numPad,
                        (*format == 'x') ? Convert::NumFormat::Lowercase : Convert::NumFormat::Uppercase
                    );
                }
                else if(isLong) {
                    Convert::toHexString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<LongValueType>::Type>( va_arg(va, unsigned long) ),
                        numSign, revPad ? 0 : width, numPad,
                        (*format == 'x') ? Convert::NumFormat::Lowercase : Convert::NumFormat::Uppercase
                    );
                }
                else {
                    Convert::toHexString(
                        strCnvBuff, static_cast<eCxx::makeUnsigned<IntValueType>::Type>( va_arg(va, unsigned int) ),
                        numSign, revPad ? 0 : width, numPad,
                        (*format == 'x') ? Convert::NumFormat::Lowercase : Convert::NumFormat::Uppercase
                    );
                }
                // Process the next character
                ++format;
                break;
            }
            case 'c' : {
                // Pre padding output
                if(!revPad) {
                    uint8_t l = 1;
                    while(l++ < width) write(' ');
                }
                // Character output
                write( static_cast<char>( va_arg(va, int) ) );
                // Calculate the post padding length
                if(revPad) revPadL = (width > 1) ? (width - 1) : 0;
                // Process the next character
                ++format;
                break;
            }
            case 's' : {
                // Determine the string length
                const char*   p   = va_arg(va, char*);
                const uint8_t len = strLen(p);
                // Pre padding output
                if(!revPad) {
                    uint8_t l = len;
                    while(l++ < width) write(' ');
                }
                // String output
                while(*p) write(*p++);
                // Calculate the post padding length
                if(revPad) revPadL = (width > len) ? (width - len) : 0;
                ++format;
                break;
            }
#if !defined(__AVR_ARCH__) || !defined(ECXX_AVR_DISABLE_PRINTF_SUPPORT_POINTER)
            case 'p' : {
                // Pre padding output
                uint8_t len = sizeof(void*) * 2;
                if(!revPad) {
                    if(numPad == Convert::NumPad::Zero) {
                        if(width > len) len = width;
                    }
                    else {
                        uint8_t l = len;
                        while(l++ < width) write(' ');
                    }
                }
                // Convert pointer to string
                Convert::toHexString(
                    strCnvBuff,
                    reinterpret_cast<PtrValueType>( va_arg(va, void*) ),
                    Convert::NumSign::None_Minus,
                    len,
                    Convert::NumPad::Zero,
                    Convert::NumFormat::Lowercase
                );
                // Process the next character
                ++format;
                break;
            }
#endif
            case '%' :
                // Character output
                write('%');
                // Process the next character
                ++format;
                break;
            default :
                // Character output
                write(*format);
                // Process the next character
                ++format;
                break;
        } // switch *format

        // Write the resulting string
        if( !strCnvBuff.empty() ) {
            // Calculate the post padding length
            if(revPad) revPadL = ( width > strCnvBuff.length() ) ? ( width - strCnvBuff.length() ) : 0;
            // String output
            for(auto it = strCnvBuff.cbegin(); it != strCnvBuff.cend(); ++it) write(*it);
            // Clear the conversion buffer
            strCnvBuff.clear();
        }

        // Post padding output
        while(revPadL--) write(' ');

    } // for(;;)
}


} // namespace eCxx
