#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdio.h>

#define ASSERT(string,test)  { if (!(test)) { printf("FAILED ASSERT : " string " : " #test "\n" ); exit(1); } }
#define CHECK(string, test)  { if (!(test)) { printf("FAILED TEST : " string " : " #test "\n" ); exit(1); } else { printf("PASSED TEST : " string "\n" ); } }

// ********************************************************************************************

// The alternate strategies to compare:
//
#define SNPRINTF_WRAPPER_FUNCTION (0)   // Properly written wrapper function for Visual Studio
#define SNPRINTF_NAIVE_DEFINE     (1)   // Silly #define of _snprintf() as snprintf()
#define SNPRINTF_BUILTIN          (2)   // On a platform that defines the function properly

// Define this to set the appropriate strategy to test:
#define USING_SNPRINTF (SNPRINTF_BUILTIN)


#if USING_SNPRINTF == SNPRINTF_NAIVE_DEFINE

// Naive snprintf replacement
#define snprintf _snprintf

#endif


#if USING_SNPRINTF == USE_SNPRINTF_WRAPPER

inline int vsnprintf(char *outBuf, size_t size, const char *format, va_list ap)
{
    // _vsnprintf_s does almost what we want from vsnprintf, but it doesn't return the number of 
    // characters that would have been printed if the buffer had been large enough. Instead it returns -1 if
    // _TRUNCATE is specified
    //
    const int VSNPRINTF_S_TRUNCATE_FAILURE_CODE = -1;
    
    int count = VSNPRINTF_S_TRUNCATE_FAILURE_CODE;

    if (size)
        count = _vsnprintf_s(outBuf, size, _TRUNCATE, format, ap);  

    // And what happens in the case that outBuf is null?
    if (count == VSNPRINTF_S_TRUNCATE_FAILURE_CODE)
        count = _vscprintf(format, ap);

    return count;
}

inline int snprintf(char *buffer, size_t count, const char *format, ... )
{
    va_list ap;
    va_start(ap, format);
    int result = vsnprintf(buffer, count, format, ap);
    va_end(ap);
    return result;
}

#endif

int main(int argc, char** argv) 
{
    const int TEST_STRING_LENGTH = 10;
    const int TEST_STRING_LENGTH_WITH_TERMINATOR = TEST_STRING_LENGTH+1;
    const int TEST_BUFFER_LENGTH = TEST_STRING_LENGTH + 6;

    const char TEST_STRING[TEST_STRING_LENGTH_WITH_TERMINATOR] = "0123456789";

    // Puts characters in the buffer to test null termination happens
    #define TEST_STRING_TO_FILL_BUFFER "BUFFERFULLOSTUF"
   
    ASSERT("Test string without null is the length we expect", TEST_STRING_LENGTH == (strlen(TEST_STRING)));
    ASSERT("Test string including null is the length we expect", TEST_STRING_LENGTH_WITH_TERMINATOR == (strlen(TEST_STRING)+1));
    ASSERT("Test buffer is bigger than the test string",TEST_STRING_LENGTH_WITH_TERMINATOR < TEST_BUFFER_LENGTH);

    {
        char ResultBuffer[TEST_BUFFER_LENGTH] = TEST_STRING_TO_FILL_BUFFER;
        const int SimpleLength = snprintf(ResultBuffer,TEST_BUFFER_LENGTH,"%s",TEST_STRING);
        CHECK("With a big enough buffer the whole lot is copied", SimpleLength == TEST_STRING_LENGTH);
        CHECK("And copied correctly", strcmp(ResultBuffer,TEST_STRING) == 0);
    }

    {
        char ResultBuffer[TEST_BUFFER_LENGTH] = TEST_STRING_TO_FILL_BUFFER;
        const int SimpleLength = snprintf(ResultBuffer,TEST_STRING_LENGTH,"%s",TEST_STRING);
        CHECK("Buffer of same length as the string gives expected length return", SimpleLength == TEST_STRING_LENGTH);
        CHECK("However, the copy is truncated to make room for the terminating null", strlen(ResultBuffer) == (TEST_STRING_LENGTH-1));
        // _snprintf() fails this test because it doesn't make room for the terminating null!
        // It's a critical difference between _snprintf() and snprintf() and why you can't do
        // #define snprintf _snprintf and not expect terrible problems
    }

    {
        char ResultBuffer[TEST_BUFFER_LENGTH] = TEST_STRING_TO_FILL_BUFFER;
        const int SHORT_BUFFER_TEST_LENGTH = 2;
        const int SmallBufferLength = snprintf(ResultBuffer,SHORT_BUFFER_TEST_LENGTH,"%s",TEST_STRING);
        CHECK("With a tiny buffer the length is still returned", SmallBufferLength == TEST_STRING_LENGTH);
        CHECK("But copy is truncated", strlen(ResultBuffer) == (SHORT_BUFFER_TEST_LENGTH-1));
    }

    {
        char ResultBuffer[TEST_BUFFER_LENGTH] = TEST_STRING_TO_FILL_BUFFER;
        const int NoBufferLength = snprintf(ResultBuffer,0,"%s",TEST_STRING);
        CHECK("With zero length buffer the length is still returned", NoBufferLength == TEST_STRING_LENGTH);
        CHECK("Passed in buffer is unchanged", strcmp(ResultBuffer, TEST_STRING_TO_FILL_BUFFER) == 0);
    }

    {
        const int NullBufferLength = snprintf(NULL,0,"%s",TEST_STRING);
        CHECK("With a null buffer the length is still returned", NullBufferLength == TEST_STRING_LENGTH);
    }

    // Undefined Behaviour Characterisation Tests 
    // (Run to find out what happens - you of course shouldn't do these in production code)
    // (Undefined behaviour doesn't necessarily cause a crash - as with the null format argument below)

#ifdef RUN_THE_CRASH_TESTS
    {
        // In debug this one triggers a runtime assert then a crash.
        char ResultBuffer[TEST_BUFFER_LENGTH] = {0};
        const int NullFormatStringLength = snprintf(ResultBuffer,TEST_STRING_LENGTH,NULL,TEST_STRING);
        CHECK("With a null format string, who knows what will happen?", !NullFormatStringLength);
    }

    {
        // Passing a null pointer to a string format results in '(null)' being printed to the buffer.
        // This is a characterisation test and was just done for curiosity
        char ResultBuffer[TEST_BUFFER_LENGTH] = {0};
        const int NullFormatArgumentLength = snprintf(ResultBuffer,TEST_STRING_LENGTH,"%s",NULL);
        CHECK("(Characterisation) Passing a null pointer argument doesn't crash and returns a length", NullFormatArgumentLength == 6);
        CHECK("(Characterisation) A null pointer prints as '(null)'", strcmp(ResultBuffer,"(null)") == 0);
    }
#endif
}

