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


#include <ctype.h>

#include <eCxx/CTL/Convert.h>
#include <eCxx/CTL/NumPunct.h>


namespace eCxx {


//
// Internal helper functions
//
static inline Convert::StringPointer padWithZero(Convert::StringPointer ptr, Convert::StringPointer ptrEnd, Convert::NumPad numPad, Convert::StringSizeType padCount) noexcept
{
    if(padCount && numPad == Convert::NumPad::Zero) {
        for(Convert::StringSizeType i = 0; i < padCount; ++i) {
            *ptr++ = '0';
            if(ptr > ptrEnd) return nullptr;
        }
    }

    return ptr;
}


static inline Convert::StringPointer padWithSpace(Convert::StringPointer ptr, Convert::StringPointer ptrEnd, Convert::NumPad numPad, Convert::StringSizeType padCount) noexcept
{
    if(padCount && numPad == Convert::NumPad::Space) {
        for(Convert::StringSizeType i = 0; i < padCount; ++i) {
            *ptr++ = ' ';
            if(ptr > ptrEnd) return nullptr;
        }
    }

    return ptr;
}


static inline Convert::StringPointer addNumSign(Convert::StringPointer ptr, Convert::NumSign numSign, bool negativeNumber) noexcept
{
    if(numSign == Convert::NumSign::Space_Minus) {
        if(negativeNumber) *ptr++ = '-';
        else               *ptr++ = ' ';
    }
    else if(numSign == Convert::NumSign::Plus_Minus) {
        if(negativeNumber) *ptr++ = '-';
        else               *ptr++ = '+';
    }
    else { // NumSign::None_Minus
        if(negativeNumber) *ptr++ = '-';
    }

    return ptr;
}


static inline void reverseCharacters(Convert::StringPointer ptr, Convert::StringSizeType length) noexcept __ECXX_ATTR_ALWAYS_INLINE;
static inline void reverseCharacters(Convert::StringPointer ptr, Convert::StringSizeType length) noexcept
{
    for(Convert::StringSizeType i = 0; i < (length / 2); ++i) {
        eCxx::swap( ptr[i], ptr[length - i - 1] );
    }
}


template <typename ValueT>
static void uintToGenNumString(
    uint8_t                  base,
    Convert::StringPointer   stringPtrBeg,
    Convert::StringSizeType& resultLength,  Convert::StringSizeType stringCapacity,
    ValueT                   value,         Convert::NumSign        numSign,
    uint8_t                  minFieldWidth, Convert::NumPad         numPad,
    Convert::NumFormat       format,        bool                    thousandsSeparator_,
    bool                     negativeNumber
) noexcept
{
    // Store the pointers
    Convert::StringPointer ptr    = stringPtrBeg;
    Convert::StringPointer ptrBeg = stringPtrBeg;
    Convert::StringPointer ptrEnd = ptr + stringCapacity;

    // Determine the grouping and thousands separator
    uint8_t grouping           = 0;
    char    thousandsSeparator = 0;
    if(thousandsSeparator_) {
        grouping           = NumPunct::grouping();
        thousandsSeparator = NumPunct::thousandsSeparator();
    }

    // Set the resulting length to zero first
    resultLength = 0;

    // Extract the format
    const bool uppercase = (format == Convert::NumFormat::Uppercase          ) || (format == Convert::NumFormat::UppercaseWithPrefix);
    const bool usePrefix = (format == Convert::NumFormat::LowercaseWithPrefix) || (format == Convert::NumFormat::UppercaseWithPrefix);

    // Process the number into a string (reversed)
    uint8_t tsCnt = 0;
    do {
        // Get the digit
        const char  digit = (value % base);
        const char xdigit = (digit < 10) ? (digit + '0') : ( uppercase ? (digit - 10 + 'A') : (digit - 10 + 'a') );
        value /= base;
        *ptr++ = xdigit;
        if(ptr > ptrEnd) return;
        // Put thousands separator(s) as needed
        if(grouping && ++tsCnt == grouping) {
            tsCnt = 0;
            *ptr++ = thousandsSeparator;
            if(ptr > ptrEnd) return;
        }
    } while(value != 0);

    // If the last character is the thousands separator, remove it
    if(thousandsSeparator && *(ptr - 1) == thousandsSeparator) --ptr;

    // Calculate the resulting length so far
    resultLength = ptr - ptrBeg;

    // Calculate the number of padding character(s) that need to be added
    Convert::StringSizeType padCount = 0;

    if(minFieldWidth > resultLength) {
       padCount = minFieldWidth - resultLength;
       if(negativeNumber || numSign != Convert::NumSign::None_Minus) --padCount;
       if(usePrefix) {
           if(base == 8) {
               if(padCount >= 1) padCount -= 1;
           }
           else {
               if(padCount >= 2) padCount -= 2;
           }
       }
    }

    // Pad with zeroes
    ptr = padWithZero(ptr, ptrEnd, numPad, padCount);
    if(!ptr) return;

    // Add the prefix as needed
    if(usePrefix) {
        if(uppercase) {
                 if(base ==  2) *ptr++ = 'B';
            else if(base == 16) *ptr++ = 'X';
        }
        else {
                 if(base ==  2) *ptr++ = 'b';
            else if(base == 16) *ptr++ = 'x';
        }
        if(ptr > ptrEnd) return;
        *ptr++ = '0';
        if(ptr > ptrEnd) return;
    }

    // Add the sign marker as needed
    ptr = addNumSign(ptr, numSign, negativeNumber);
    if(ptr > ptrEnd) return;

    // Pad with spaces
    ptr = padWithSpace(ptr, ptrEnd, numPad, padCount);
    if(!ptr) return;

    // Calculate the final resulting length
    resultLength = ptr - ptrBeg;

    // Reverse the characters
    reverseCharacters(ptrBeg, resultLength);
}


//
// Various conversion utilities
//
void Convert::_reverseCharacters(StringPointer ptr, StringSizeType length) noexcept
{ eCxx::reverseCharacters(ptr, length); }


void Convert::_toLowercase(StringPointer ptr, StringSizeType length) noexcept
{
    for(StringSizeType i = 0; i < length; ++i) {
        *ptr = tolower(*ptr);
        ++ptr;
    }
}


void Convert::_toUppercase(StringPointer ptr, StringSizeType length) noexcept
{
    for(StringSizeType i = 0; i < length; ++i) {
        *ptr = toupper(*ptr);
        ++ptr;
    }
}


void Convert::_toDecString(
    StringPointer     stringPtrBeg,
    StringSizeType&   resultLength,        StringSizeType stringCapacity,
    const FixedPoint& value,               NumSign        numSign,
    uint8_t           minFieldWidth,       NumPad         numPad,
    bool              thousandsSeparator_
) noexcept
{
    // Decompose the fixed point number
    bool                               negativeNumber;
    FixedPoint::UnsignedHalfValueType  integer;
    FixedPoint::UnsignedQuartValueType fractionalScaled;

    value.decomposeToDecimal(negativeNumber, integer, fractionalScaled);

    // Store the pointers
    StringPointer ptr    = stringPtrBeg;
    StringPointer ptrBeg = stringPtrBeg;
    StringPointer ptrEnd = ptr + stringCapacity;

    // Determine the grouping, thousands separator, and decimal point
    uint8_t grouping           = 0;
    char    thousandsSeparator = 0;
    char    decimalPoint       = NumPunct::decimalPoint();
    if(thousandsSeparator_) {
        grouping           = NumPunct::grouping();
        thousandsSeparator = NumPunct::thousandsSeparator();
    }
    if(!decimalPoint) decimalPoint = '.';

    // Set the resulting length to zero first
    resultLength = 0;

    // Process the fractional part into string (reversed)
    if(sizeof(FixedPoint::UnsignedQuartValueType) > 1) {
        // Process into a string
        uint8_t numDigits = 0;
        do {
            // Get the digit
            const char digit = (fractionalScaled % 10) + '0';
            fractionalScaled /= 10;
            *ptr++ = digit;
            if(ptr > ptrEnd) return;
            // Increment the counters
            ++numDigits;
        } while(fractionalScaled != 0);
        // Pad with zeroes with zeroes as needed
        if(numDigits < internal::getFPSpec<FixedPoint::ValueType>::FractionalLen) {
            const uint8_t pc = internal::getFPSpec<FixedPoint::ValueType>::FractionalLen - numDigits;
            for(uint8_t i = 0; i < pc; ++i) {
                *ptr++ = '0';
                if(ptr > ptrEnd) return;
            }
        }
    }
    else {
        if(fractionalScaled >= 10) {
            *ptr++ = (fractionalScaled % 10) + '0'; if(ptr > ptrEnd) return;
            *ptr++ = (fractionalScaled / 10) + '0'; if(ptr > ptrEnd) return;
        }
        else {
            *ptr++ = fractionalScaled + '0'; if(ptr > ptrEnd) return;
            *ptr++ = '0';                    if(ptr > ptrEnd) return;
        }
    }

    // Put the decimal point
    *ptr++ = decimalPoint;
    if(ptr > ptrEnd) return;

    // Process the integer part into string (reversed)
    uint8_t tsCnt = 0;
    do {
        // Get the digit
        const char digit = (integer % 10) + '0';
        integer /= 10;
        *ptr++ = digit;
        if(ptr > ptrEnd) return;
        // Put thousands separator(s) as needed
        if(grouping && ++tsCnt == grouping) {
            tsCnt = 0;
            *ptr++ = thousandsSeparator;
            if(ptr > ptrEnd) return;
        }
    } while(integer != 0);

    // If the last character is the thousands separator, remove it
    if(thousandsSeparator && *(ptr - 1) == thousandsSeparator) --ptr;

    // Calculate the resulting length so far
    resultLength = ptr - ptrBeg;

    // Calculate the number of padding character(s) that need to be added
    StringSizeType padCount = 0;

    if(minFieldWidth > resultLength) {
       padCount = minFieldWidth - resultLength;
       if(negativeNumber || numSign != NumSign::None_Minus) --padCount;
    }

    // Pad with zeroes
    ptr = eCxx::padWithZero(ptr, ptrEnd, numPad, padCount);
    if(!ptr) return;

    // Add the sign marker as needed
    ptr = eCxx::addNumSign(ptr, numSign, negativeNumber);
    if(ptr > ptrEnd) return;

    // Pad with spaces
    ptr = eCxx::padWithSpace(ptr, ptrEnd, numPad, padCount);
    if(!ptr) return;

    // Calculate the final resulting length
    resultLength = ptr - ptrBeg;

    // Reverse the characters
    eCxx::reverseCharacters(ptrBeg, resultLength);
}


void Convert::_toBinString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint16_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        2,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toBinString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint32_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        2,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toBinString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint64_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        2,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toOctString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint16_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        8,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toOctString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint32_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        8,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toOctString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint64_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        8,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toDecString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,       StringSizeType stringCapacity,
    uint16_t        value,              NumSign        numSign,
    uint8_t         minFieldWidth,      NumPad         numPad,
    bool            thousandsSeparator, bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        10,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        NumFormat::Lowercase,
        thousandsSeparator,
        negativeNumber
    );
}


void Convert::_toDecString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,       StringSizeType stringCapacity,
    uint32_t        value,              NumSign        numSign,
    uint8_t         minFieldWidth,      NumPad         numPad,
    bool            thousandsSeparator, bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        10,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        NumFormat::Lowercase,
        thousandsSeparator,
        negativeNumber
    );
}


void Convert::_toDecString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,       StringSizeType stringCapacity,
    uint64_t        value,              NumSign        numSign,
    uint8_t         minFieldWidth,      NumPad         numPad,
    bool            thousandsSeparator, bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        10,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        NumFormat::Lowercase,
        thousandsSeparator,
        negativeNumber
    );
}


void Convert::_toHexString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint16_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        16,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toHexString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint32_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        16,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


void Convert::_toHexString(
    StringPointer   stringPtrBeg,
    StringSizeType& resultLength,  StringSizeType stringCapacity,
    uint64_t        value,         NumSign        numSign,
    uint8_t         minFieldWidth, NumPad         numPad,
    NumFormat       format,        bool           negativeNumber
) noexcept
{
    uintToGenNumString(
        16,
        stringPtrBeg,
        resultLength,
        stringCapacity,
        value,
        numSign,
        minFieldWidth,
        numPad,
        format,
        false,
        negativeNumber
    );
}


} // namespace eCxx
