/**
 * @file os_api.c
 * @brief Operating system abstraction API.
 * 
 * The OS API implements an overridable interface for implementing functions 
 * that are operating system specific, in addition to a number of hooks which
 * allow for customization by the user, like logging.
 */

#include "private_api.h"
#include <ctype.h>
#include <time.h>

void ecs_os_api_impl(ecs_os_api_t *api);

static bool ecs_os_api_initialized = false;
static bool ecs_os_api_initializing = false;
static int ecs_os_api_init_count = 0;

ecs_os_api_t ecs_os_api = {
    .flags_ = EcsOsApiHighResolutionTimer | EcsOsApiLogWithColors,
    .log_level_ = -1 /* Disable tracing by default, but log warnings/errors */
};

int64_t ecs_os_api_malloc_count = 0;
int64_t ecs_os_api_realloc_count = 0;
int64_t ecs_os_api_calloc_count = 0;
int64_t ecs_os_api_free_count = 0;

void ecs_os_set_api(
    ecs_os_api_t *os_api)
{
    if (!ecs_os_api_initialized) {
        ecs_os_api = *os_api;
        ecs_os_api_initialized = true;
    }
}

ecs_os_api_t ecs_os_get_api(void) {
    return ecs_os_api;
}

void ecs_os_init(void)
{
    if (!ecs_os_api_initialized) {
        ecs_os_set_api_defaults();
    }
    
    if (!(ecs_os_api_init_count ++)) {
        if (ecs_os_api.init_) {
            ecs_os_api.init_();
        }
    }
}

void ecs_os_fini(void) {
    if (!--ecs_os_api_init_count) {
        if (ecs_os_api.fini_) {
            ecs_os_api.fini_();
        }
    }
}

/* Assume every non-glibc Linux target has no execinfo.
   This mainly fixes musl support, as musl doesn't define any preprocessor macro specifying its presence. */ 
#if defined(ECS_TARGET_LINUX) && !defined(__GLIBC__)
#define HAVE_EXECINFO 0
#elif !defined(ECS_TARGET_WINDOWS) && !defined(ECS_TARGET_EM) && !defined(ECS_TARGET_ANDROID)
#define HAVE_EXECINFO 1
#else
#define HAVE_EXECINFO 0
#endif

#if HAVE_EXECINFO
#include <execinfo.h>
#define ECS_BT_BUF_SIZE 100

void flecs_dump_backtrace(
    void *stream) 
{
    int nptrs;
    void *buffer[ECS_BT_BUF_SIZE];
    char **strings;

    nptrs = backtrace(buffer, ECS_BT_BUF_SIZE);

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        return;
    }

    for (int j = 1; j < nptrs; j++) {
        fprintf(stream, "%s\n", strings[j]);
    }

    free(strings);
}
#else
void flecs_dump_backtrace(
    void *stream)
{ 
    (void)stream;
}
#endif
#undef HAVE_EXECINFO_H

static
void flecs_log_msg(
    int32_t level,
    const char *file, 
    int32_t line,  
    const char *msg)
{
    FILE *stream;
    if (level >= 0) {
        stream = stdout;
    } else {
        stream = stderr;
    }

    bool use_colors = ecs_os_api.flags_ & EcsOsApiLogWithColors;
    bool timestamp = ecs_os_api.flags_ & EcsOsApiLogWithTimeStamp;
    bool deltatime = ecs_os_api.flags_ & EcsOsApiLogWithTimeDelta;

    time_t now = 0;

    if (deltatime) {
        now = time(NULL);
        int64_t delta = 0;
        if (ecs_os_api.log_last_timestamp_) {
            delta = now - ecs_os_api.log_last_timestamp_;
        }
        ecs_os_api.log_last_timestamp_ = (int64_t)now;

        if (delta) {
            if (delta < 10) {
                fputs(" ", stream);
            }
            if (delta < 100) {
                fputs(" ", stream);
            }
            char time_buf[20];
            ecs_os_sprintf(time_buf, "%u", (uint32_t)delta);
            fputs("+", stream);
            fputs(time_buf, stream);
            fputs(" ", stream);
        } else {
            fputs("     ", stream);
        }
    }

    if (timestamp) {
        if (!now) {
            now = time(NULL);
        }
        char time_buf[20];
        ecs_os_sprintf(time_buf, "%u", (uint32_t)now);
        fputs(time_buf, stream);
        fputs(" ", stream);
    }

    if (level >= 4) {
        if (use_colors) fputs(ECS_NORMAL, stream);
        fputs("jrnl", stream);
    } else if (level >= 0) {
        if (level == 0) {
            if (use_colors) fputs(ECS_MAGENTA, stream);
        } else {
            if (use_colors) fputs(ECS_GREY, stream);
        }
        fputs("info", stream);
    } else if (level == -2) {
        if (use_colors) fputs(ECS_YELLOW, stream);
        fputs("warning", stream);
    } else if (level == -3) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("error", stream);
    } else if (level == -4) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("fatal", stream);
    }

    if (use_colors) fputs(ECS_NORMAL, stream);
    fputs(": ", stream);

    if (level >= 0) {
        if (ecs_os_api.log_indent_) {
            char indent[32];
            int i, indent_count = ecs_os_api.log_indent_;
            if (indent_count > 15) indent_count = 15;

            for (i = 0; i < indent_count; i ++) {
                indent[i * 2] = '|';
                indent[i * 2 + 1] = ' ';
            }

            if (ecs_os_api.log_indent_ != indent_count) {
                indent[i * 2 - 2] = '+';
            }

            indent[i * 2] = '\0';

            fputs(indent, stream);
        }
    }

    if (level < 0) {
        if (file) {
            const char *file_ptr = strrchr(file, '/');
            if (!file_ptr) {
                file_ptr = strrchr(file, '\\');
            }

            if (file_ptr) {
                file = file_ptr + 1;
            }

            fputs(file, stream);
            fputs(": ", stream);
        }

        if (line) {
            fprintf(stream, "%d: ", line);
        }
    }

    fputs(msg, stream);

    fputs("\n", stream);

    if (level == -4) {
        flecs_dump_backtrace(stream);
    }
}

void ecs_os_dbg(
    const char *file, 
    int32_t line, 
    const char *msg)
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(1, file, line, msg);
    }
}

void ecs_os_trace(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(0, file, line, msg);
    }
}

void ecs_os_warn(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-2, file, line, msg);
    }
}

void ecs_os_err(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-3, file, line, msg);
    }
}

void ecs_os_fatal(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-4, file, line, msg);
    }
}

static
void ecs_os_gettime(ecs_time_t *time) {
    ecs_assert(ecs_os_has_time() == true, ECS_MISSING_OS_API, NULL);
    
    uint64_t now = ecs_os_now();
    uint64_t sec = now / 1000000000;

    assert(sec < UINT32_MAX);
    assert((now - sec * 1000000000) < UINT32_MAX);

    time->sec = (uint32_t)sec;
    time->nanosec = (uint32_t)(now - sec * 1000000000);
}

static
void* ecs_os_api_malloc(ecs_size_t size) {
    ecs_os_linc(&ecs_os_api_malloc_count);
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return malloc((size_t)size);
}

static
void* ecs_os_api_calloc(ecs_size_t size) {
    ecs_os_linc(&ecs_os_api_calloc_count);
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return calloc(1, (size_t)size);
}

static
void* ecs_os_api_realloc(void *ptr, ecs_size_t size) {
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);

    if (ptr) {
        ecs_os_linc(&ecs_os_api_realloc_count);
    } else {
        /* If not actually reallocing, treat as malloc */
        ecs_os_linc(&ecs_os_api_malloc_count);
    }
    
    return realloc(ptr, (size_t)size);
}

static
void ecs_os_api_free(void *ptr) {
    if (ptr) {
        ecs_os_linc(&ecs_os_api_free_count);
    }
    free(ptr);
}

static
char* ecs_os_api_strdup(const char *str) {
    if (str) {
        int len = ecs_os_strlen(str);
        char *result = ecs_os_malloc(len + 1);
        ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);
        ecs_os_strcpy(result, str);
        return result;
    } else {
        return NULL;
    }
}

void ecs_os_strset(char **str, const char *value) {
    char *old = str[0];
    str[0] = ecs_os_strdup(value);
    ecs_os_free(old);
}

/* Replace dots with underscores */
static
char *module_file_base(const char *module, char sep) {
    char *base = ecs_os_strdup(module);
    ecs_size_t i, len = ecs_os_strlen(base);
    for (i = 0; i < len; i ++) {
        if (base[i] == '.') {
            base[i] = sep;
        }
    }

    return base;
}

static
char* ecs_os_api_module_to_dl(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with underscores + OS library extension */
    char *file_base = module_file_base(module, '_');

#   if defined(ECS_TARGET_LINUX) || defined(ECS_TARGET_FREEBSD)
    ecs_strbuf_appendlit(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".so");
#   elif defined(ECS_TARGET_DARWIN)
    ecs_strbuf_appendlit(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".dylib");
#   elif defined(ECS_TARGET_WINDOWS)
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".dll");
#   endif

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

static
char* ecs_os_api_module_to_etc(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with dashes + /etc */
    char *file_base = module_file_base(module, '-');

    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, "/etc");

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

void ecs_os_set_api_defaults(void)
{
    /* Don't overwrite if already initialized */
    if (ecs_os_api_initialized != 0) {
        return;
    }

    if (ecs_os_api_initializing != 0) {
        return;
    }

    ecs_os_api_initializing = true;
    
    /* Memory management */
    ecs_os_api.malloc_ = ecs_os_api_malloc;
    ecs_os_api.free_ = ecs_os_api_free;
    ecs_os_api.realloc_ = ecs_os_api_realloc;
    ecs_os_api.calloc_ = ecs_os_api_calloc;

    /* Strings */
    ecs_os_api.strdup_ = ecs_os_api_strdup;

    /* Time */
    ecs_os_api.get_time_ = ecs_os_gettime;

    /* Logging */
    ecs_os_api.log_ = flecs_log_msg;

    /* Modules */
    if (!ecs_os_api.module_to_dl_) {
        ecs_os_api.module_to_dl_ = ecs_os_api_module_to_dl;
    }

    if (!ecs_os_api.module_to_etc_) {
        ecs_os_api.module_to_etc_ = ecs_os_api_module_to_etc;
    }

    ecs_os_api.abort_ = abort;

#   ifdef FLECS_OS_API_IMPL
    /* Initialize defaults to OS API IMPL addon, but still allow for overriding
     * by the application */
    ecs_set_os_api_impl();
    ecs_os_api_initialized = false;
#   endif

    ecs_os_api_initializing = false;
}

bool ecs_os_has_heap(void) {
    return 
        (ecs_os_api.malloc_ != NULL) &&
        (ecs_os_api.calloc_ != NULL) &&
        (ecs_os_api.realloc_ != NULL) &&
        (ecs_os_api.free_ != NULL);
}

bool ecs_os_has_threading(void) {
    return
        (ecs_os_api.mutex_new_ != NULL) &&
        (ecs_os_api.mutex_free_ != NULL) &&
        (ecs_os_api.mutex_lock_ != NULL) &&
        (ecs_os_api.mutex_unlock_ != NULL) &&
        (ecs_os_api.cond_new_ != NULL) &&
        (ecs_os_api.cond_free_ != NULL) &&
        (ecs_os_api.cond_wait_ != NULL) &&
        (ecs_os_api.cond_signal_ != NULL) &&
        (ecs_os_api.cond_broadcast_ != NULL) &&
        (ecs_os_api.thread_new_ != NULL) &&
        (ecs_os_api.thread_join_ != NULL) &&
        (ecs_os_api.thread_self_ != NULL);
}

bool ecs_os_has_task_support(void) {
    return
        (ecs_os_api.mutex_new_ != NULL) &&
        (ecs_os_api.mutex_free_ != NULL) &&
        (ecs_os_api.mutex_lock_ != NULL) &&
        (ecs_os_api.mutex_unlock_ != NULL) &&
        (ecs_os_api.cond_new_ != NULL) &&
        (ecs_os_api.cond_free_ != NULL) &&
        (ecs_os_api.cond_wait_ != NULL) &&
        (ecs_os_api.cond_signal_ != NULL) &&
        (ecs_os_api.cond_broadcast_ != NULL) &&
        (ecs_os_api.task_new_ != NULL) &&
        (ecs_os_api.task_join_ != NULL);
}

bool ecs_os_has_time(void) {
    return 
        (ecs_os_api.get_time_ != NULL) &&
        (ecs_os_api.sleep_ != NULL) &&
        (ecs_os_api.now_ != NULL);
}

bool ecs_os_has_logging(void) {
    return (ecs_os_api.log_ != NULL);
}

bool ecs_os_has_dl(void) {
    return 
        (ecs_os_api.dlopen_ != NULL) &&
        (ecs_os_api.dlproc_ != NULL) &&
        (ecs_os_api.dlclose_ != NULL);  
}

bool ecs_os_has_modules(void) {
    return 
        (ecs_os_api.module_to_dl_ != NULL) &&
        (ecs_os_api.module_to_etc_ != NULL);
}

#if defined(ECS_TARGET_WINDOWS)
static char error_str[255];
#endif

const char* ecs_os_strerror(int err) {
#   if defined(ECS_TARGET_WINDOWS)
    strerror_s(error_str, 255, err);
    return error_str;
#   else
    return strerror(err);
#   endif
}
