/*
 * DWN - Desktop Window Manager
 * retoor <retoor@molodetz.nl>
 * Utility functions implementation
 */

#include "util.h"
#include "dwn.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <pwd.h>
#include <pthread.h>
#include <stdatomic.h>
#include <fcntl.h>
#include <math.h>


#define LOG_RING_SIZE 256
#define LOG_MSG_MAX_LEN 512
#define LOG_MAX_FILE_SIZE (5 * 1024 * 1024)
#define LOG_FLUSH_INTERVAL_MS 100

typedef struct {
    char message[LOG_MSG_MAX_LEN];
    LogLevel level;
    _Atomic int ready;
} LogEntry;

static LogEntry log_ring[LOG_RING_SIZE];
static _Atomic size_t log_write_idx = 0;
static _Atomic size_t log_read_idx = 0;
static _Atomic int log_running = 0;
static pthread_t log_thread;
static int log_fd = -1;
static char *log_path = NULL;
static LogLevel min_level = LOG_INFO;
static _Atomic size_t log_file_size = 0;

static const char *level_names[] = {
    "DEBUG",
    "INFO",
    "WARN",
    "ERROR"
};

static const char *level_colors[] = {
    "\033[36m",
    "\033[32m",
    "\033[33m",
    "\033[31m"
};

static void log_rotate_if_needed(void);
static void *log_writer_thread(void *arg);


static void log_rotate_if_needed(void)
{
    if (log_fd < 0 || log_path == NULL) {
        return;
    }

    size_t current_size = atomic_load(&log_file_size);
    if (current_size < LOG_MAX_FILE_SIZE) {
        return;
    }

    close(log_fd);
    log_fd = -1;

    size_t path_len = strlen(log_path);
    char *backup_path = malloc(path_len + 8);
    if (backup_path != NULL) {
        snprintf(backup_path, path_len + 8, "%s.old", log_path);
        unlink(backup_path);
        rename(log_path, backup_path);
        free(backup_path);
    }

    log_fd = open(log_path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
    atomic_store(&log_file_size, 0);
}

static void *log_writer_thread(void *arg)
{
    (void)arg;

    while (atomic_load(&log_running)) {
        size_t read_idx = atomic_load(&log_read_idx);
        size_t write_idx = atomic_load(&log_write_idx);

        while (read_idx != write_idx) {
            size_t idx = read_idx % LOG_RING_SIZE;
            LogEntry *entry = &log_ring[idx];

            int attempts = 0;
            while (!atomic_load(&entry->ready) && attempts < 100) {
                attempts++;
                struct timespec ts = {0, 1000};
                nanosleep(&ts, NULL);
            }

            if (atomic_load(&entry->ready)) {
                if (log_fd >= 0) {
                    log_rotate_if_needed();
                    if (log_fd >= 0) {
                        ssize_t written = write(log_fd, entry->message, strlen(entry->message));
                        if (written > 0) {
                            atomic_fetch_add(&log_file_size, (size_t)written);
                        }
                    }
                }

                atomic_store(&entry->ready, 0);
            }

            read_idx++;
            atomic_store(&log_read_idx, read_idx);
        }

        struct timespec ts = {0, LOG_FLUSH_INTERVAL_MS * 1000000L};
        nanosleep(&ts, NULL);
    }

    return NULL;
}

void log_init(const char *path)
{
    memset(log_ring, 0, sizeof(log_ring));
    atomic_store(&log_write_idx, 0);
    atomic_store(&log_read_idx, 0);

    if (path != NULL) {
        char *expanded = expand_path(path);
        if (expanded != NULL) {
            char *dir = strdup(expanded);
            if (dir != NULL) {
                char *last_slash = strrchr(dir, '/');
                if (last_slash != NULL) {
                    *last_slash = '\0';
                    struct stat st;
                    if (stat(dir, &st) != 0) {
                        char cmd[512];
                        snprintf(cmd, sizeof(cmd), "mkdir -p '%s' 2>/dev/null", dir);
                        int ret = system(cmd);
                        (void)ret;
                    }
                }
                free(dir);
            }

            log_fd = open(expanded, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644);
            if (log_fd < 0) {
                fprintf(stderr, "Warning: Could not open log file: %s\n", expanded);
            } else {
                struct stat st;
                if (fstat(log_fd, &st) == 0) {
                    atomic_store(&log_file_size, (size_t)st.st_size);
                }
            }

            log_path = expanded;
        }
    }

    atomic_store(&log_running, 1);
    if (pthread_create(&log_thread, NULL, log_writer_thread, NULL) != 0) {
        fprintf(stderr, "Warning: Could not create log writer thread\n");
        atomic_store(&log_running, 0);
    }
}

void log_close(void)
{
    if (!atomic_load(&log_running)) {
        goto cleanup;
    }
    atomic_store(&log_running, 0);

    pthread_join(log_thread, NULL);

cleanup:
    if (log_fd >= 0) {
        close(log_fd);
        log_fd = -1;
    }

    if (log_path != NULL) {
        free(log_path);
        log_path = NULL;
    }
}

void log_set_level(LogLevel level)
{
    min_level = level;
}

void log_flush(void)
{
    if (!atomic_load(&log_running) || log_fd < 0) {
        return;
    }

    size_t read_idx = atomic_load(&log_read_idx);
    size_t write_idx = atomic_load(&log_write_idx);

    while (read_idx != write_idx) {
        size_t idx = read_idx % LOG_RING_SIZE;
        LogEntry *entry = &log_ring[idx];

        if (atomic_load(&entry->ready)) {
            ssize_t written = write(log_fd, entry->message, strlen(entry->message));
            if (written > 0) {
                atomic_fetch_add(&log_file_size, (size_t)written);
            }
            atomic_store(&entry->ready, 0);
        }

        read_idx++;
        atomic_store(&log_read_idx, read_idx);
    }

    fsync(log_fd);
}

void log_msg(LogLevel level, const char *fmt, ...)
{
    if (level < min_level) {
        return;
    }

    time_t now = time(NULL);
    struct tm tm_info;
    localtime_r(&now, &tm_info);
    char time_buf[32];
    strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &tm_info);

    char msg_buf[LOG_MSG_MAX_LEN - 64];
    va_list args;
    va_start(args, fmt);
    vsnprintf(msg_buf, sizeof(msg_buf), fmt, args);
    va_end(args);

    fprintf(stderr, "%s[%s] %s: %s%s\n",
            level_colors[level], time_buf, level_names[level], "\033[0m", msg_buf);

    if (atomic_load(&log_running)) {
        size_t write_idx = atomic_fetch_add(&log_write_idx, 1);
        size_t idx = write_idx % LOG_RING_SIZE;
        LogEntry *entry = &log_ring[idx];

        if (!atomic_load(&entry->ready)) {
            entry->level = level;
            snprintf(entry->message, sizeof(entry->message),
                     "[%s] %s: %s\n", time_buf, level_names[level], msg_buf);
            atomic_store(&entry->ready, 1);
        }
    }
}


void *dwn_malloc(size_t size)
{
    void *ptr = malloc(size);
    if (ptr == NULL && size > 0) {
        LOG_ERROR("Memory allocation failed for %zu bytes", size);
        exit(EXIT_FAILURE);
    }
    return ptr;
}

void *dwn_calloc(size_t nmemb, size_t size)
{
    void *ptr = calloc(nmemb, size);
    if (ptr == NULL && nmemb > 0 && size > 0) {
        LOG_ERROR("Memory allocation failed for %zu elements", nmemb);
        exit(EXIT_FAILURE);
    }
    return ptr;
}

void *dwn_realloc(void *ptr, size_t size)
{
    void *new_ptr = realloc(ptr, size);
    if (new_ptr == NULL && size > 0) {
        LOG_ERROR("Memory reallocation failed for %zu bytes", size);
        exit(EXIT_FAILURE);
    }
    return new_ptr;
}

char *dwn_strdup(const char *s)
{
    if (s == NULL) {
        return NULL;
    }
    char *dup = strdup(s);
    if (dup == NULL) {
        LOG_ERROR("String duplication failed");
        exit(EXIT_FAILURE);
    }
    return dup;
}

void dwn_free(void *ptr)
{
    free(ptr);
}

void secure_wipe(void *ptr, size_t size)
{
    if (ptr == NULL || size == 0) {
        return;
    }
#if defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 25)
    explicit_bzero(ptr, size);
#else
    volatile unsigned char *p = (volatile unsigned char *)ptr;
    while (size--) {
        *p++ = 0;
    }
#endif
}


char *str_trim(char *str)
{
    if (str == NULL) {
        return NULL;
    }

    while (isspace((unsigned char)*str)) {
        str++;
    }

    if (*str == '\0') {
        return str;
    }

    char *end = str + strlen(str) - 1;
    while (end > str && isspace((unsigned char)*end)) {
        end--;
    }
    end[1] = '\0';

    return str;
}

bool str_starts_with(const char *str, const char *prefix)
{
    if (str == NULL || prefix == NULL) {
        return false;
    }
    return strncmp(str, prefix, strlen(prefix)) == 0;
}

bool str_ends_with(const char *str, const char *suffix)
{
    if (str == NULL || suffix == NULL) {
        return false;
    }
    size_t str_len = strlen(str);
    size_t suffix_len = strlen(suffix);
    if (suffix_len > str_len) {
        return false;
    }
    return strcmp(str + str_len - suffix_len, suffix) == 0;
}

int str_split(char *str, char delim, char **parts, int max_parts)
{
    if (str == NULL || parts == NULL || max_parts <= 0) {
        return 0;
    }

    int count = 0;
    char *start = str;

    while (*str && count < max_parts) {
        if (*str == delim) {
            *str = '\0';
            parts[count++] = start;
            start = str + 1;
        }
        str++;
    }

    if (count < max_parts && *start) {
        parts[count++] = start;
    }

    return count;
}

char *shell_escape(const char *str)
{
    if (str == NULL) {
        return dwn_strdup("");
    }

    size_t quotes = 0;
    for (const char *p = str; *p; p++) {
        if (*p == '\'') quotes++;
    }

    size_t len = strlen(str) + quotes * 3 + 3;
    char *escaped = dwn_malloc(len);
    char *dst = escaped;

    *dst++ = '\'';
    for (const char *src = str; *src; src++) {
        if (*src == '\'') {
            *dst++ = '\'';
            *dst++ = '\\';
            *dst++ = '\'';
            *dst++ = '\'';
        } else {
            *dst++ = *src;
        }
    }
    *dst++ = '\'';
    *dst = '\0';

    return escaped;
}


bool file_exists(const char *path)
{
    if (path == NULL) {
        return false;
    }
    struct stat st;
    return stat(path, &st) == 0;
}

char *file_read_all(const char *path)
{
    FILE *f = fopen(path, "r");
    if (f == NULL) {
        return NULL;
    }

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    char *content = dwn_malloc(size + 1);
    size_t read = fread(content, 1, size, f);
    content[read] = '\0';

    fclose(f);
    return content;
}

bool file_write_all(const char *path, const char *content)
{
    FILE *f = fopen(path, "w");
    if (f == NULL) {
        return false;
    }

    size_t len = strlen(content);
    size_t written = fwrite(content, 1, len, f);
    fclose(f);

    return written == len;
}

char *expand_path(const char *path)
{
    if (path == NULL) {
        return NULL;
    }

    if (path[0] == '~') {
        const char *home = getenv("HOME");
        if (home == NULL) {
            struct passwd *pw = getpwuid(getuid());
            if (pw != NULL) {
                home = pw->pw_dir;
            }
        }
        if (home != NULL) {
            size_t len = strlen(home) + strlen(path);
            char *expanded = dwn_malloc(len);
            snprintf(expanded, len, "%s%s", home, path + 1);
            return expanded;
        }
    }

    return dwn_strdup(path);
}


unsigned long parse_color(const char *color_str)
{
    if (color_str == NULL || dwn == NULL || dwn->display == NULL) {
        return 0;
    }

    XColor color, exact;
    if (XAllocNamedColor(dwn->display, dwn->colormap, color_str, &color, &exact)) {
        return color.pixel;
    }

    if (color_str[0] == '#') {
        unsigned int r, g, b;
        if (sscanf(color_str + 1, "%02x%02x%02x", &r, &g, &b) == 3) {
            color.red = r << 8;
            color.green = g << 8;
            color.blue = b << 8;
            color.flags = DoRed | DoGreen | DoBlue;
            if (XAllocColor(dwn->display, dwn->colormap, &color)) {
                return color.pixel;
            }
        }
    }

    LOG_WARN("Could not parse color: %s", color_str);
    return 0;
}

void color_to_rgb(unsigned long color, int *r, int *g, int *b)
{
    XColor xc;
    xc.pixel = color;
    XQueryColor(dwn->display, dwn->colormap, &xc);
    *r = xc.red >> 8;
    *g = xc.green >> 8;
    *b = xc.blue >> 8;
}


long get_time_ms(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (long)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}

void sleep_ms(int ms)
{
    struct timespec ts;
    ts.tv_sec = ms / 1000;
    ts.tv_nsec = (ms % 1000) * 1000000L;
    nanosleep(&ts, NULL);
}


int spawn(const char *cmd)
{
    if (cmd == NULL) {
        return -1;
    }

    LOG_DEBUG("Spawning: %s", cmd);

    pid_t pid = fork();
    if (pid == 0) {
        setsid();
        execl("/bin/sh", "sh", "-c", cmd, NULL);
        _exit(EXIT_FAILURE);
    } else if (pid < 0) {
        LOG_ERROR("Fork failed: %s", strerror(errno));
        return -1;
    }

    int status;
    waitpid(pid, &status, 0);
    return WEXITSTATUS(status);
}

int spawn_async(const char *cmd)
{
    if (cmd == NULL) {
        return -1;
    }

    LOG_DEBUG("Spawning async: %s", cmd);

    pid_t pid = fork();
    if (pid == 0) {
        setsid();

        pid_t pid2 = fork();
        if (pid2 == 0) {
            execl("/bin/sh", "sh", "-c", cmd, NULL);
            _exit(EXIT_FAILURE);
        }
        _exit(EXIT_SUCCESS);
    } else if (pid < 0) {
        LOG_ERROR("Fork failed: %s", strerror(errno));
        return -1;
    }

    int status;
    waitpid(pid, &status, 0);
    return 0;
}

char *spawn_capture(const char *cmd)
{
    if (cmd == NULL) {
        return NULL;
    }

    LOG_DEBUG("Spawning capture: %s", cmd);

    FILE *fp = popen(cmd, "r");
    if (fp == NULL) {
        LOG_ERROR("popen failed: %s", strerror(errno));
        return NULL;
    }

    size_t buf_size = 1024;
    size_t len = 0;
    char *output = dwn_malloc(buf_size);
    output[0] = '\0';

    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        size_t line_len = strlen(line);
        if (len + line_len + 1 > buf_size) {
            buf_size *= 2;
            output = dwn_realloc(output, buf_size);
        }
        memcpy(output + len, line, line_len + 1);
        len += line_len;
    }

    int status = pclose(fp);
    if (status != 0) {
        LOG_DEBUG("Command exited with status %d", status);
    }

    if (len > 0 && output[len - 1] == '\n') {
        output[len - 1] = '\0';
    }

    return output;
}


static void hsl_to_rgb(double h, double s, double l, int *r, int *g, int *b)
{
    double c = (1.0 - fabs(2.0 * l - 1.0)) * s;
    double x = c * (1.0 - fabs(fmod(h / 60.0, 2.0) - 1.0));
    double m = l - c / 2.0;

    double rp = 0, gp = 0, bp = 0;

    if (h < 60) {
        rp = c; gp = x; bp = 0;
    } else if (h < 120) {
        rp = x; gp = c; bp = 0;
    } else if (h < 180) {
        rp = 0; gp = c; bp = x;
    } else if (h < 240) {
        rp = 0; gp = x; bp = c;
    } else if (h < 300) {
        rp = x; gp = 0; bp = c;
    } else {
        rp = c; gp = 0; bp = x;
    }

    *r = (int)((rp + m) * 255);
    *g = (int)((gp + m) * 255);
    *b = (int)((bp + m) * 255);
}

unsigned long rgb_to_pixel(int r, int g, int b)
{
    if (dwn == NULL || dwn->display == NULL) {
        return 0;
    }

    XColor color;
    color.red = (unsigned short)(r << 8);
    color.green = (unsigned short)(g << 8);
    color.blue = (unsigned short)(b << 8);
    color.flags = DoRed | DoGreen | DoBlue;

    if (XAllocColor(dwn->display, dwn->colormap, &color)) {
        return color.pixel;
    }

    return (unsigned long)((r << 16) | (g << 8) | b);
}

unsigned long generate_unique_color(void)
{
    static unsigned int color_index = 0;
    double golden_angle = 137.5;
    double hue = fmod(color_index * golden_angle, 360.0);
    color_index++;

    double saturation = 0.75;
    double lightness = 0.65;

    int r, g, b;
    hsl_to_rgb(hue, saturation, lightness, &r, &g, &b);

    return rgb_to_pixel(r, g, b);
}

static double calculate_luminance(int r, int g, int b)
{
    double rs = r / 255.0;
    double gs = g / 255.0;
    double bs = b / 255.0;

    rs = (rs <= 0.03928) ? rs / 12.92 : pow((rs + 0.055) / 1.055, 2.4);
    gs = (gs <= 0.03928) ? gs / 12.92 : pow((gs + 0.055) / 1.055, 2.4);
    bs = (bs <= 0.03928) ? bs / 12.92 : pow((bs + 0.055) / 1.055, 2.4);

    return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

unsigned long adjust_color_for_background(unsigned long color, unsigned long bg)
{
    int cr, cg, cb;
    int br, bg_g, bb;

    color_to_rgb(color, &cr, &cg, &cb);
    color_to_rgb(bg, &br, &bg_g, &bb);

    double color_lum = calculate_luminance(cr, cg, cb);
    double bg_lum = calculate_luminance(br, bg_g, bb);

    double lighter = (color_lum > bg_lum) ? color_lum : bg_lum;
    double darker = (color_lum > bg_lum) ? bg_lum : color_lum;
    double contrast = (lighter + 0.05) / (darker + 0.05);

    if (contrast < 3.0) {
        double factor = (bg_lum > 0.5) ? 0.3 : 1.8;
        cr = (int)(cr * factor);
        cg = (int)(cg * factor);
        cb = (int)(cb * factor);

        if (cr > 255) cr = 255;
        if (cg > 255) cg = 255;
        if (cb > 255) cb = 255;
        if (cr < 0) cr = 0;
        if (cg < 0) cg = 0;
        if (cb < 0) cb = 0;
    }

    return rgb_to_pixel(cr, cg, cb);
}

unsigned long interpolate_color(unsigned long from, unsigned long to, float progress)
{
    if (progress <= 0.0f) return from;
    if (progress >= 1.0f) return to;

    int fr, fg_c, fb;
    int tr, tg, tb;

    color_to_rgb(from, &fr, &fg_c, &fb);
    color_to_rgb(to, &tr, &tg, &tb);

    int r = fr + (int)((tr - fr) * progress);
    int g = fg_c + (int)((tg - fg_c) * progress);
    int b = fb + (int)((tb - fb) * progress);

    return rgb_to_pixel(r, g, b);
}

unsigned long dim_color(unsigned long color, float factor)
{
    int r, g, b;
    color_to_rgb(color, &r, &g, &b);

    r = (int)(r * factor);
    g = (int)(g * factor);
    b = (int)(b * factor);

    if (r > 255) r = 255;
    if (g > 255) g = 255;
    if (b > 255) b = 255;
    if (r < 0) r = 0;
    if (g < 0) g = 0;
    if (b < 0) b = 0;

    return rgb_to_pixel(r, g, b);
}

#define SINE_TABLE_SIZE 256
#define TWO_PI 6.283185307f

static float sine_table[SINE_TABLE_SIZE];
static bool sine_table_initialized = false;

static float fast_sin(float phase)
{
    if (!sine_table_initialized) {
        for (int i = 0; i < SINE_TABLE_SIZE; i++) {
            sine_table[i] = sinf((float)i * TWO_PI / (float)SINE_TABLE_SIZE);
        }
        sine_table_initialized = true;
    }

    while (phase < 0.0f) phase += TWO_PI;
    while (phase >= TWO_PI) phase -= TWO_PI;

    int idx = (int)(phase * (float)SINE_TABLE_SIZE / TWO_PI);
    if (idx >= SINE_TABLE_SIZE) idx = SINE_TABLE_SIZE - 1;
    if (idx < 0) idx = 0;

    return sine_table[idx];
}

unsigned long glow_color(unsigned long base, float phase)
{
    int r, g, b;
    color_to_rgb(base, &r, &g, &b);

    float mod_r = 0.25f * fast_sin(phase);
    float mod_g = 0.25f * fast_sin(phase + 2.094f);
    float mod_b = 0.25f * fast_sin(phase + 4.189f);

    r = r + (int)(r * mod_r);
    g = g + (int)(g * mod_g);
    b = b + (int)(b * mod_b);

    if (r > 255) r = 255;
    if (r < 0) r = 0;
    if (g > 255) g = 255;
    if (g < 0) g = 0;
    if (b > 255) b = 255;
    if (b < 0) b = 0;

    return rgb_to_pixel(r, g, b);
}

unsigned long ambient_glow_bg(unsigned long base, float phase)
{
    int r, g, b;
    color_to_rgb(base, &r, &g, &b);

    float mod = 0.25f * fast_sin(phase);

    r = r + (int)(r * mod);
    g = g + (int)(g * mod);
    b = b + (int)(b * mod);

    if (r > 255) r = 255;
    if (r < 0) r = 0;
    if (g > 255) g = 255;
    if (g < 0) g = 0;
    if (b > 255) b = 255;
    if (b < 0) b = 0;

    return rgb_to_pixel(r, g, b);
}

unsigned long ambient_glow_accent(unsigned long base, float phase)
{
    int r, g, b;
    color_to_rgb(base, &r, &g, &b);

    float mod_r = 0.45f * fast_sin(phase);
    float mod_g = 0.45f * fast_sin(phase + 2.094f);
    float mod_b = 0.45f * fast_sin(phase + 4.189f);

    r = r + (int)(r * mod_r);
    g = g + (int)(g * mod_g);
    b = b + (int)(b * mod_b);

    if (r > 255) r = 255;
    if (r < 0) r = 0;
    if (g > 255) g = 255;
    if (g < 0) g = 0;
    if (b > 255) b = 255;
    if (b < 0) b = 0;

    return rgb_to_pixel(r, g, b);
}
