#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_engine_context_c.h"

#include <SDL2/SDL.h>
#include <physfsrwops.h>
#include <stb_ds.h>

#include <stdarg.h>


static struct ProfileItem {
    char const *key;
    struct Profile {
        uint64_t tick_start;
        uint64_t tick_accum;
        uint64_t sample_count;
    } value;
} *profiles;


void cry_impl(const char *file, const int line, const char *title, const char *text) {
    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
                    "TEARS AT %s:%d: %s ... %s", file, line, title, text);
    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, NULL);
}


static void log_impl(const char *restrict format, va_list args, SDL_LogPriority priority) {

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"

    SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION,
                    priority,
                    format,
                    args);

#pragma GCC diagnostic pop
}


void log_info(const char *restrict format, ...) {
    va_list args;
    va_start(args, format);
    log_impl(format, args, SDL_LOG_PRIORITY_INFO);
    va_end(args);
}


void log_critical(const char *restrict format, ...) {
    va_list args;
    va_start(args, format);
    log_impl(format, args, SDL_LOG_PRIORITY_CRITICAL);
    va_end(args);
}


void log_warn(const char *restrict format, ...) {
    va_list args;
    va_start(args, format);
    log_impl(format, args, SDL_LOG_PRIORITY_WARN);
    va_end(args);
}


_Noreturn static void alloc_failure_death(void) {
    log_critical("Allocation failure. Aborting NOW.");
    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
                             "MEMORY ALLOCATION FAILURE",
                             "CRITICAL MEMORY ALLOCATION FAILED. "
                             "EXITING IMMEDIATELY!",
                             NULL);

    die_abruptly();
}


_Noreturn void die_abruptly(void) {
    /* a zombie window will linger if we don't at least try to quit SDL */
    SDL_Quit();
    abort();
}


void *cmalloc(size_t size) {
    void *ptr = SDL_malloc(size);
    if (ptr == NULL)
        alloc_failure_death();
    return ptr;
}


void *crealloc(void *ptr, size_t size) {
    void *out = SDL_realloc(ptr, size);
    if (out == NULL)
        alloc_failure_death();
    return out;
}


void *ccalloc(size_t num, size_t size) {
    void *ptr = SDL_calloc(num, size);
    if (ptr == NULL)
        alloc_failure_death();
    return ptr;
}


double clamp(double d, double min, double max) {
  const double t = d < min ? min : d;
  return t > max ? max : t;
}


float clampf(float f, float min, float max) {
  const float t = f < min ? min : f;
  return t > max ? max : t;
}


int clampi(int i, int min, int max) {
  const int t = i < min ? min : i;
  return t > max ? max : t;
}


int64_t file_to_bytes(const char *path, unsigned char **buf_out) {
    SDL_RWops *handle = PHYSFSRWOPS_openRead(path);

    if (handle == NULL) {
        return -1;
    }

    int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
    SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */

    *buf_out = cmalloc(data_size);
    SDL_RWread(handle, *buf_out, sizeof **buf_out, data_size / sizeof **buf_out);
    SDL_RWclose(handle);    /* we got all we needed from the stream */

    return data_size;
}


char *file_to_str(const char *path) {
    SDL_RWops *handle = PHYSFSRWOPS_openRead(path);

    if (handle == NULL) {
        return NULL;
    }

    int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
    SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */

    char *str_out = cmalloc(data_size + 1); /* data plus final null */
    size_t len = data_size / sizeof *str_out;

    SDL_RWread(handle, str_out, sizeof *str_out, len);
    SDL_RWclose(handle);    /* we got all we needed from the stream */

    str_out[len] = '\0';

    return str_out;
}


bool file_exists(const char *path) {
    return PHYSFS_exists(path);
}


void textures_dump_atlases(void) {
    PHYSFS_mkdir("/dump");

    /* TODO: png instead of bmp */
    const char string_template[] = "/dump/atlas%zd.bmp";

    size_t i = 0;
    for (; i < arrlenu(ctx.texture_cache.atlas_surfaces); ++i) {
        char *buf = NULL;
        SDL_asprintf(&buf, string_template, i);

        SDL_RWops *handle = PHYSFSRWOPS_openWrite(buf);

        if (handle == NULL) {
            CRY("Texture atlas dump failed.", "File could not be opened");
            return;
        }

        SDL_SaveBMP_RW(ctx.texture_cache.atlas_surfaces[i], handle, true);
        log_info("Dumped atlas %zu to %s", i, buf);

        SDL_free(buf);
    }
}


bool strends(const char *str, const char *suffix) {
    size_t str_length = SDL_strlen(str);
    size_t suffix_length = SDL_strlen(suffix);

    if (suffix_length >  str_length)
        return false;

    return SDL_memcmp((str + str_length) - suffix_length, suffix, suffix_length) == 0;
}


/* TODO: have our own */
Rect rect_overlap(const Rect a, const Rect b) {
    SDL_FRect a_sdl = { a.x, a.y, a.w, a.h };
    SDL_FRect b_sdl = { b.x, b.y, b.w, b.h };
    SDL_FRect result_sdl = { 0 };

    (void)SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);

    return (Rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
}


/* TODO: have our own */
bool rect_intersects(const Rect a, const Rect b) {
    SDL_FRect a_sdl = { a.x, a.y, a.w, a.h };
    SDL_FRect b_sdl = { b.x, b.y, b.w, b.h };
    return SDL_HasIntersectionF(&a_sdl, &b_sdl);
}


Vec2 rect_center(Rect rect) {
    return (Vec2){
        .x = rect.x + rect.w / 2,
        .y = rect.y + rect.h / 2,
    };
}


int32_t timer_tick_frames(int32_t frames_left) {
    SDL_assert(frames_left >= 0);
    return MAX(frames_left - 1, 0);
}


float timer_tick_seconds(float seconds_left) {
    SDL_assert(seconds_left >= 0);
    return MAX(seconds_left - ((float)ctx.delta_time / (float)ctx.clocks_per_second), 0.0f);
}


TimerElapseFramesResult timer_elapse_frames(int32_t frames_left, int32_t interval) {
    SDL_assert(frames_left >= 0);
    SDL_assert(interval > 0);

    frames_left -= 1;
    bool elapsed = false;
    if (frames_left <= 0) {
        elapsed = true;
        frames_left += interval;
    }
    return (TimerElapseFramesResult) {
        .elapsed = elapsed,
        .frames_left = frames_left
    };
}


TimerElapseSecondsResult timer_elapse_seconds(float seconds_left, float interval) {
    SDL_assert(seconds_left >= 0);
    SDL_assert(interval > 0);

    seconds_left -= (float)ctx.delta_time / (float)ctx.clocks_per_second;
    bool elapsed = false;
    if (seconds_left <= 0.0f) {
        elapsed = true;
        seconds_left += interval;
    }
    return (TimerElapseSecondsResult) {
        .elapsed = elapsed,
        .seconds_left = seconds_left
    };
}


/* TODO: handle utf8 */
char *expand_asterisk(const char *mask, const char *to) {
    const char *offset = SDL_strchr(mask, '*');
    if (!offset) {
        CRY("Invalid path", "Asterisk should be given.");
        return NULL;
    }
    size_t const mask_len = SDL_strlen(mask);
    size_t const to_len = SDL_strlen(to);
    char *str = SDL_malloc(mask_len + to_len); /* NULL included, replacing the original asterisk */
    SDL_memcpy(str, mask, offset - mask);
    SDL_memcpy(str + (offset - mask), to, to_len);
    /* NULL byte is copied from here */
    SDL_memcpy(str + (offset - mask) + to_len, offset + 1, mask_len - (offset - mask));
    return str;
}


void profile_start(char profile[const static 1]) {
    uint64_t tick_accum = 0, sample_count = 0;

    struct ProfileItem const *p = shgetp_null(profiles, profile);
    if (p) {
        tick_accum = p->value.tick_accum;
        sample_count = p->value.sample_count;
    }

    shput(profiles, profile, ((struct Profile) {
            .tick_start = SDL_GetPerformanceCounter(),
            .tick_accum = tick_accum,
            .sample_count = sample_count,
    }));
}


void profile_end(char profile[const static 1]) {
    struct ProfileItem *p = shgetp_null(profiles, profile);
    if (!p) {
        log_warn("profile %s wasn't started!", profile);
        return;
    }

    p->value.sample_count++;
    p->value.tick_accum += SDL_GetPerformanceCounter() - p->value.tick_start;
}


void profile_list_stats(void) {
    for (size_t i = 0; i < shlenu(profiles); ++i) {
        log_info("profile '%s' on average took: %f seconds",
                 profiles[i].key,
                 (double)profiles[i].value.tick_accum /
                    (double)profiles[i].value.sample_count /
                    (double)(SDL_GetPerformanceFrequency()));
    }
}
