/* Copyright (C) 2018-2021  Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library 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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#include <igloo/tap.h>
#include <igloo/error.h>
#include <igloo/ro.h>

static pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

static struct {
    igloo_tap_stats_t   stats;
    bool                can_continue;
    const char *        test_group;
    unsigned int        exit_flags;
    void                (*on_exit_cleanup)(void);
    bool                exit_pending;
} * state = NULL;

static igloo_error_t    igloo_tap_diagnostic_locked(const char *line);

#define begin() \
    pthread_mutex_lock(&mutex); \
    if (!state) { \
        pthread_mutex_unlock(&mutex); \
        return igloo_ERROR_BADSTATE; \
    } \
    (void)0

#define end() \
    pthread_mutex_unlock(&mutex); \
    return igloo_ERROR_NONE

#define destroy() \
    printf("1..%zu\n", state->stats.count); \
    free(state); \
    state = NULL; \
    pthread_mutex_unlock(&mutex);

static inline void update_exit_status(void)
{
    if (state->stats.failed) {
        state->stats.exit_status = EXIT_FAILURE;
    } else {
        state->stats.exit_status = EXIT_SUCCESS;
    }
}

static void try_exit(unsigned int reason)
{
    int status;

    // check if we need to exit.
    switch (reason) {
        case igloo_TAP_EXIT_AFTER_GROUP:
            // no-op
        break;
        case igloo_TAP_EXIT_ON_FIN:
            if (reason & state->exit_flags)
                state->exit_pending = true;
        break;

        default:
            if (!(reason & state->exit_flags))
                return;

            state->exit_pending = true;

            if (state->exit_flags & igloo_TAP_EXIT_AFTER_GROUP)
                return;
        break;
    }

    if (!state->exit_pending)
        return;

    // we should exit.

    if (state->on_exit_cleanup) {
        // if we have, run cleanup. run in un-locked so it can call
        // e.g. igloo_tap_diagnostic().
        pthread_mutex_unlock(&mutex);
        state->on_exit_cleanup();
        pthread_mutex_lock(&mutex);
    }

    update_exit_status();
    status = state->stats.exit_status;
    destroy();
    exit(status);
}

igloo_error_t   igloo_tap_init(void)
{
    pthread_mutex_lock(&mutex);
    state = calloc(1, sizeof(*state));
    if (!state) {
        pthread_mutex_unlock(&mutex);
        return igloo_ERROR_NOMEM;
    }
    state->can_continue = true;
    pthread_mutex_unlock(&mutex);

    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_tap_fin(void)
{
    begin();

    try_exit(igloo_TAP_EXIT_ON_FIN);

    destroy();
    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_tap_exit_on(unsigned int flags, void (*cleanup)(void))
{
    begin();

    state->exit_flags = flags;
    state->on_exit_cleanup = cleanup;

    end();
}

igloo_error_t   igloo_tap_test(const char *desc, bool res)
{
    const char *prefix = NULL;

    begin();

    if (!state->can_continue) {
        pthread_mutex_unlock(&mutex);
        return igloo_ERROR_NONE;
    }

    state->stats.count++;

    if (res) {
        prefix = "ok";
        state->stats.passed++;
    } else {
        prefix = "not ok";
        state->stats.failed++;
    }

    if (desc || state->test_group) {
        if (state->test_group) {
            printf("%s %zu %s/%s\n", prefix, state->stats.count, state->test_group, desc ? desc : "*");
        } else {
            printf("%s %zu %s\n", prefix, state->stats.count, desc);
        }
    } else {
        printf("%s %zu\n", prefix, state->stats.count);
    }

    if (!res)
        try_exit(igloo_TAP_EXIT_ON_FAIL);

    end();
}

igloo_error_t   igloo_tap_test_error(const char *desc, igloo_error_t expected, igloo_error_t got)
{
    // for now, maybe do something better later on.
    if (got != expected) {
#define _p(x) \
        if (1) { \
            const igloo_error_desc_t * k = igloo_error_get_description((x)); \
            if (k) { \
                printf("%" igloo_PRIerror " (%s: %s)\n", (x), k->name, k->message); \
            } else { \
                printf("%" igloo_PRIerror "\n", (x)); \
            } \
        }
        printf("# Expected: ");
        _p(expected);
        printf("# Got:      ");
        _p(got);
    }
    return igloo_tap_test(desc, got == expected);
}

igloo_error_t   igloo_tap_diagnostic(const char *line)
{
    igloo_error_t error;

    begin();

    error = igloo_tap_diagnostic_locked(line);
    if (error != igloo_ERROR_NONE) {
        pthread_mutex_unlock(&mutex);
        return error;
    }

    end();
}

static igloo_error_t    igloo_tap_diagnostic_locked(const char *line)
{
    while (line && *line) {
        const char *nl = strchr(line, '\n');
        const char *cr = strchr(line, '\r');
        const char *end;
        size_t len;

        end = (!cr || (nl && cr && nl < cr)) ? nl : cr;

        if (end) {
            len = end - line;
        } else {
            len = strlen(line);
        }

        printf("# %.*s\n", (int)len, line);

        line = end;
        for (; line && (*line == '\n' || *line == '\r'); line++);
    }

    return igloo_ERROR_NONE;
}

igloo_error_t   igloo_tap_bail_out(const char *reason)
{
    begin();

    state->can_continue = false;

    if (reason) {
        printf("Bail out! %s\n", reason);
    } else {
        printf("Bail out!\n");
    }

    try_exit(igloo_TAP_EXIT_ON_BAIL_OUT);

    end();
}

bool            igloo_tap_can_continue(igloo_error_t *error)
{
    bool ret = false;

    pthread_mutex_lock(&mutex);
    ret = state && state->can_continue;

    if (error) {
        if (state) {
            *error = igloo_ERROR_NONE;
        } else {
            *error = igloo_ERROR_BADSTATE;
        }
    }

    pthread_mutex_unlock(&mutex);

    return ret;
}

igloo_error_t   igloo_tap_group_begin(const char *group)
{
    begin();
    state->test_group = group;
    end();
}

igloo_error_t   igloo_tap_group_end(void)
{
    begin();
    state->test_group = NULL;
    try_exit(igloo_TAP_EXIT_AFTER_GROUP);
    end();
}

igloo_error_t   igloo_tap_group_run(const char *group, void (*func)(void))
{
    begin();
    if (state->can_continue) {
        state->test_group = group;
        pthread_mutex_unlock(&mutex);
        func();
        pthread_mutex_lock(&mutex);
        state->test_group = NULL;
    } else {
        char buf[128];
        snprintf(buf, sizeof(buf), "Skipping group \"%s\" at %p: already bailed out", group, func);
        igloo_tap_diagnostic_locked(buf);
    }
    try_exit(igloo_TAP_EXIT_AFTER_GROUP);
    end();
}


igloo_error_t   igloo_tap_dump_ro(igloo_ro_t object, const char *name)
{
    char *as_object_string = NULL;
    igloo_error_t error;
    char buf[128];

    begin();

    if (name) {
        snprintf(buf, sizeof(buf), "--- Dump of ro \"%s\" at %p ---", name, igloo_RO__GETSTUB(object));
    } else {
        snprintf(buf, sizeof(buf), "--- Dump of ro <no name> at %p ---", igloo_RO__GETSTUB(object));
    }

    igloo_tap_diagnostic_locked(buf);

    error = igloo_ro_stringify(object, &as_object_string, igloo_RO_SY_OBJECT);
    if (error == igloo_ERROR_NONE) {
        igloo_tap_diagnostic_locked("Stringifyed as object:");
        igloo_tap_diagnostic_locked(as_object_string);
    } else {
        const igloo_error_desc_t * k = igloo_error_get_description(error);

        if (k) {
            snprintf(buf, sizeof(buf), "Can not stringify as object: %" igloo_PRIerror " (%s: %s)\n", error, k->name, k->message);
        } else {
            printf(buf, sizeof(buf), "Can not stringify as object: %" igloo_PRIerror "\n", error);
        }

        igloo_tap_diagnostic_locked(buf);
    }
    free(as_object_string);

    igloo_tap_diagnostic_locked("--- End of Dump ---");

    end();
}

igloo_error_t   igloo_tap_get_stats_real(igloo_tap_stats_t *stats, size_t len)
{
    if (stats == NULL || len < 1)
        return igloo_ERROR_FAULT;

    if (len > sizeof(igloo_tap_stats_t))
        return igloo_ERROR_FAULT;

    begin();

    update_exit_status();

    memcpy(stats, &(state->stats), len);

    end();
}
