/*
    SDL - Simple DirectMedia Layer
    Copyright (C) 1997-2009 Sam Lantinga

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Sam Lantinga
    slouken@libsdl.org
*/

#include "SDL.h"
#include "SDL_assert.h"

#if (SDL_ASSERT_LEVEL > 0)

#ifdef _WINDOWS
#define WIN32_LEAN_AND_MEAN 1
#include <windows.h>
#else  /* fprintf, _exit(), etc. */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#endif

/* We can keep all triggered assertions in a singly-linked list so we can
 *  generate a report later.
 */
#if !SDL_ASSERTION_REPORT_DISABLED
static SDL_assert_data assertion_list_terminator = { 0, 0, 0, 0, 0, 0, 0 };
static SDL_assert_data *triggered_assertions = &assertion_list_terminator;
#endif

static void 
debug_print(const char *fmt, ...)
#ifdef __GNUC__
__attribute__((format (printf, 1, 2)))
#endif
;

static void
debug_print(const char *fmt, ...)
{
#ifdef _WINDOWS
    /* Format into a buffer for OutputDebugStringA(). */
    char buf[1024];
    char *startptr;
    char *ptr;
    int len;
    va_list ap;
    va_start(ap, fmt);
    len = (int) SDL_vsnprintf(buf, sizeof (buf), fmt, ap);
    va_end(ap);

    /* Visual C's vsnprintf() may not null-terminate the buffer. */
    if ((len >= sizeof (buf)) || (len < 0)) {
        buf[sizeof (buf) - 1] = '\0';
    }

    /* Write it, sorting out the Unix newlines... */
    startptr = buf;
    for (ptr = startptr; *ptr; ptr++) {
        if (*ptr == '\n') {
            *ptr = '\0';
            OutputDebugStringA(startptr);
            OutputDebugStringA("\r\n");
            startptr = ptr+1;
        }
    }

    /* catch that last piece if it didn't have a newline... */
    if (startptr != ptr) {
        OutputDebugStringA(startptr);
    }
#else
    /* Unix has it easy. Just dump it to stderr. */
    va_list ap;
    va_start(ap, fmt);
    fprintf(stderr, fmt, ap);
    va_end(ap);
    fflush(stderr);
#endif
}


#ifdef _WINDOWS
static SDL_assert_state SDL_Windows_AssertChoice = SDL_ASSERTION_ABORT;
static const SDL_assert_data *SDL_Windows_AssertData = NULL;

static LRESULT CALLBACK
SDL_Assertion_WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_CREATE:
        {
            /* !!! FIXME: all this code stinks. */
            const SDL_assert_data *data = SDL_Windows_AssertData;
            char buf[1024];
            const int w = 100;
            const int h = 25;
            const int gap = 10;
            int x = gap;
            int y = 50;
            int len;
            int i;
            static const struct { 
                const char *name;
                SDL_assert_state state;
            } buttons[] = {
                {"Abort", SDL_ASSERTION_ABORT },
                {"Break", SDL_ASSERTION_BREAK },
                {"Retry", SDL_ASSERTION_RETRY },
                {"Ignore", SDL_ASSERTION_IGNORE },
                {"Always Ignore", SDL_ASSERTION_ALWAYS_IGNORE },
            };

            len = (int) SDL_snprintf(buf, sizeof (buf), 
                         "Assertion failure at %s (%s:%d), triggered %u time%s:\r\n  '%s'",
                         data->function, data->filename, data->linenum,
                         data->trigger_count, (data->trigger_count == 1) ? "" : "s",
                         data->condition);
            if ((len < 0) || (len >= sizeof (buf))) {
                buf[sizeof (buf) - 1] = '\0';
            }

            CreateWindowA("STATIC", buf,
                         WS_VISIBLE | WS_CHILD | SS_LEFT,
                         x, y, 550, 100,
                         hwnd, (HMENU) 1, NULL, NULL);
            y += 110;

            for (i = 0; i < (sizeof (buttons) / sizeof (buttons[0])); i++) {
                CreateWindowA("BUTTON", buttons[i].name,
                         WS_VISIBLE | WS_CHILD,
                         x, y, w, h,
                         hwnd, (HMENU) buttons[i].state, NULL, NULL);
                x += w + gap;
            }
            break;
        }

        case WM_COMMAND:
            SDL_Windows_AssertChoice = ((SDL_assert_state) (LOWORD(wParam)));
            SDL_Windows_AssertData = NULL;
            break;

        case WM_DESTROY:
            SDL_Windows_AssertData = NULL;
            break;
    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

static SDL_assert_state
SDL_PromptAssertion_windows(const SDL_assert_data *data)
{
    HINSTANCE hInstance = 0;  /* !!! FIXME? */
    HWND hwnd;
    MSG msg;
    WNDCLASS wc = {0};

    SDL_Windows_AssertChoice = SDL_ASSERTION_ABORT;
    SDL_Windows_AssertData = data;

    wc.lpszClassName = TEXT("SDL_assert");
    wc.hInstance = hInstance ;
    wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
    wc.lpfnWndProc = SDL_Assertion_WndProc;
    wc.hCursor = LoadCursor(0, IDC_ARROW);
  
    RegisterClass(&wc);
    hwnd = CreateWindow(wc.lpszClassName, TEXT("SDL assertion failure"),
                 WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                 150, 150, 570, 260, 0, 0, hInstance, 0);  

    while (GetMessage(&msg, NULL, 0, 0) && (SDL_Windows_AssertData != NULL)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    DestroyWindow(hwnd);
    UnregisterClass(wc.lpszClassName, hInstance);
    return SDL_Windows_AssertChoice;
}
#endif


static void SDL_AddAssertionToReport(SDL_assert_data *data)
{
#if !SDL_ASSERTION_REPORT_DISABLED
    /* (data) is always a static struct defined with the assert macros, so
       we don't have to worry about copying or allocating them. */
    if (data->next == NULL) {  /* not yet added? */
        data->next = triggered_assertions;
        triggered_assertions = data;
    }
#endif
}

static void SDL_GenerateAssertionReport(void)
{
#if !SDL_ASSERTION_REPORT_DISABLED
    if (triggered_assertions != &assertion_list_terminator)
    {
        SDL_assert_data *item = triggered_assertions;

        debug_print("\n\nSDL assertion report.\n");
        debug_print("All SDL assertions between last init/quit:\n\n");

        while (item != &assertion_list_terminator) {
            debug_print(
                "'%s'\n"
                "    * %s (%s:%d)\n"
                "    * triggered %u time%s.\n"
                "    * always ignore: %s.\n",
                item->condition, item->function, item->filename,
                item->linenum, item->trigger_count,
                (item->trigger_count == 1) ? "" : "s",
                item->always_ignore ? "yes" : "no");
            item = item->next;
        }
        debug_print("\n");

        triggered_assertions = &assertion_list_terminator;
    }
#endif
}


static void SDL_AbortAssertion(void)
{
    SDL_Quit();
#ifdef _WINDOWS
    ExitProcess(42);
#else
    _exit(42);
#endif
}
    

static SDL_assert_state SDL_PromptAssertion(const SDL_assert_data *data)
{
    const char *envr;
    SDL_assert_state state = SDL_ASSERTION_ABORT;
    SDL_WindowID window;

    debug_print("\n\n"
                "Assertion failure at %s (%s:%d), triggered %u time%s:\n"
                "  '%s'\n"
                "\n",
                data->function, data->filename, data->linenum,
                data->trigger_count, (data->trigger_count == 1) ? "" : "s",
                data->condition);

    /* let env. variable override, so unit tests won't block in a GUI. */
    envr = SDL_getenv("SDL_ASSERT");
    if (envr != NULL) {
        if (SDL_strcmp(envr, "abort") == 0) {
            return SDL_ASSERTION_ABORT;
        } else if (SDL_strcmp(envr, "break") == 0) {
            return SDL_ASSERTION_BREAK;
        } else if (SDL_strcmp(envr, "retry") == 0) {
            return SDL_ASSERTION_RETRY;
        } else if (SDL_strcmp(envr, "ignore") == 0) {
            return SDL_ASSERTION_IGNORE;
        } else if (SDL_strcmp(envr, "always_ignore") == 0) {
            return SDL_ASSERTION_ALWAYS_IGNORE;
        } else {
            return SDL_ASSERTION_ABORT;  /* oh well. */
        }
    }

    /* Leave fullscreen mode, if possible (scary!) */
    window = SDL_GetFocusWindow();
    if (window) {
        if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) {
            SDL_MinimizeWindow(window);
        } else {
            /* No need to mess with the window */
            window = 0;
        }
    }

    /* platform-specific UI... */

#ifdef _WINDOWS
    state = SDL_PromptAssertion_windows(data);

#elif __APPLE__
    /* This has to be done in an Objective-C (*.m) file, so we call out. */
    extern SDL_assert_state SDL_PromptAssertion_cocoa(const SDL_assert_data *);
    state = SDL_PromptAssertion_cocoa(data);

#else
    /* this is a little hacky. */
    for ( ; ; ) {
        char buf[32];
        fprintf(stderr, "Abort/Break/Retry/Ignore/AlwaysIgnore? [abriA] : ");
        fflush(stderr);
        if (fgets(buf, sizeof (buf), stdin) == NULL) {
            break;
        }

        if (SDL_strcmp(buf, "a") == 0) {
            state = SDL_ASSERTION_ABORT;
            break;
        } else if (SDL_strcmp(envr, "b") == 0) {
            state = SDL_ASSERTION_BREAK;
            break;
        } else if (SDL_strcmp(envr, "r") == 0) {
            state = SDL_ASSERTION_RETRY;
            break;
        } else if (SDL_strcmp(envr, "i") == 0) {
            state = SDL_ASSERTION_IGNORE;
            break;
        } else if (SDL_strcmp(envr, "A") == 0) {
            state = SDL_ASSERTION_ALWAYS_IGNORE;
            break;
        }
    }
#endif

    /* Re-enter fullscreen mode */
    if (window) {
        SDL_RestoreWindow(window);
    }

    return state;
}


static SDL_mutex *assertion_mutex = NULL;

SDL_assert_state
SDL_ReportAssertion(SDL_assert_data *data, const char *func, const char *file,
                    int line)
{
    SDL_assert_state state;

    if (SDL_LockMutex(assertion_mutex) < 0) {
        return SDL_ASSERTION_IGNORE;   /* oh well, I guess. */
    }

    /* doing this because Visual C is upset over assigning in the macro. */
    if (data->trigger_count == 0) {
        data->function = func;
        data->filename = file;
        data->linenum = line;
    }

    SDL_AddAssertionToReport(data);

    data->trigger_count++;
    if (data->always_ignore) {
        SDL_UnlockMutex(assertion_mutex);
        return SDL_ASSERTION_IGNORE;
    }

    state = SDL_PromptAssertion(data);

    switch (state)
    {
        case SDL_ASSERTION_ABORT:
            SDL_UnlockMutex(assertion_mutex);  /* in case we assert in quit. */
            SDL_AbortAssertion();
            return SDL_ASSERTION_IGNORE;  /* shouldn't return, but oh well. */

        case SDL_ASSERTION_ALWAYS_IGNORE:
            state = SDL_ASSERTION_IGNORE;
            data->always_ignore = 1;
            break;

        case SDL_ASSERTION_IGNORE:
        case SDL_ASSERTION_RETRY:
        case SDL_ASSERTION_BREAK:
            break;  /* macro handles these. */
    }

    SDL_UnlockMutex(assertion_mutex);

    return state;
}

#endif  /* SDL_ASSERT_LEVEL > 0 */


int SDL_AssertionsInit(void)
{
#if (SDL_ASSERT_LEVEL > 0)
    assertion_mutex = SDL_CreateMutex();
    if (assertion_mutex == NULL) {
        return -1;
    }
#endif
    return 0;
}

void SDL_AssertionsQuit(void)
{
#if (SDL_ASSERT_LEVEL > 0)
    SDL_GenerateAssertionReport();
    SDL_DestroyMutex(assertion_mutex);
    assertion_mutex = NULL;
#endif
}

/* vi: set ts=4 sw=4 expandtab: */
