/*
 * DWN - Desktop Window Manager
 * retoor <retoor@molodetz.nl>
 * Window rules engine implementation
 */

#include "rules.h"
#include "config.h"
#include "client.h"
#include "workspace.h"
#include "util.h"
#include "layout.h"

#define _GNU_SOURCE
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <fnmatch.h>

static WindowRule rules[MAX_RULES];
static int rule_count = 0;

void rules_init(void)
{
    rule_count = 0;
    memset(rules, 0, sizeof(rules));
    LOG_INFO("Window rules engine initialized");
}

void rules_cleanup(void)
{
    rules_clear();
}

void rules_add(const WindowRule *rule)
{
    if (rule == NULL) {
        return;
    }

    if (rule_count >= MAX_RULES) {
        LOG_WARN("Maximum number of window rules reached (%d)", MAX_RULES);
        return;
    }

    memcpy(&rules[rule_count], rule, sizeof(WindowRule));
    rule_count++;

    LOG_DEBUG("Added window rule: class='%s' title='%s'",
              rule->class_pattern, rule->title_pattern);
}

void rules_clear(void)
{
    rule_count = 0;
    memset(rules, 0, sizeof(rules));
}

int rules_count(void)
{
    return rule_count;
}

bool rules_match_pattern(const char *str, const char *pattern)
{
    if (str == NULL || pattern == NULL) {
        return false;
    }

    if (pattern[0] == '\0') {
        return true;
    }

    return fnmatch(pattern, str, FNM_CASEFOLD) == 0;
}

static bool rule_matches(const WindowRule *rule, const Client *client)
{
    if (rule == NULL || client == NULL) {
        return false;
    }

    switch (rule->match_type) {
    case RULE_MATCH_CLASS:
        return rules_match_pattern(client->class, rule->class_pattern);

    case RULE_MATCH_TITLE:
        return rules_match_pattern(client->title, rule->title_pattern);

    case RULE_MATCH_CLASS_AND_TITLE:
        return rules_match_pattern(client->class, rule->class_pattern) &&
               rules_match_pattern(client->title, rule->title_pattern);
    }

    return false;
}

static void apply_rule(const WindowRule *rule, Client *client)
{
    if (rule == NULL || client == NULL) {
        return;
    }

    LOG_DEBUG("Applying rule to window '%s' (class: %s)",
              client->title, client->class);

    if (rule->has_workspace && rule->workspace >= 0 && rule->workspace < MAX_WORKSPACES) {
        if (client->workspace != (unsigned int)rule->workspace) {
            workspace_remove_client((int)client->workspace, client);
            client->workspace = (unsigned int)rule->workspace;
            workspace_add_client(rule->workspace, client);
            LOG_DEBUG("  Moved to workspace %d", rule->workspace + 1);
        }
    }

    if (rule->has_floating) {
        if (rule->floating) {
            client->flags |= CLIENT_FLOATING;
        } else {
            client->flags &= ~CLIENT_FLOATING;
        }
        LOG_DEBUG("  Set floating: %s", rule->floating ? "true" : "false");
    }

    if (rule->has_sticky) {
        if (rule->sticky) {
            client->flags |= CLIENT_STICKY;
        } else {
            client->flags &= ~CLIENT_STICKY;
        }
        LOG_DEBUG("  Set sticky: %s", rule->sticky ? "true" : "false");
    }

    if (rule->has_fullscreen) {
        if (rule->fullscreen) {
            client->flags |= CLIENT_FULLSCREEN;
        } else {
            client->flags &= ~CLIENT_FULLSCREEN;
        }
        LOG_DEBUG("  Set fullscreen: %s", rule->fullscreen ? "true" : "false");
    }

    if (rule->has_size) {
        if (rule->width > 0) {
            client->width = rule->width;
        }
        if (rule->height > 0) {
            client->height = rule->height;
        }
        LOG_DEBUG("  Set size: %dx%d", client->width, client->height);
    }

    if (rule->has_position) {
        int area_x, area_y, area_w, area_h;
        layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);

        int title_h = config_get_title_height();
        int border = client->border_width;

        client->x = area_x + rule->x + border;
        client->y = area_y + rule->y + title_h + border;
        LOG_DEBUG("  Set position: %d,%d", client->x, client->y);
    }
}

bool rules_apply_to_client(Client *client)
{
    if (client == NULL) {
        return false;
    }

    bool applied = false;

    for (int i = 0; i < rule_count; i++) {
        if (rule_matches(&rules[i], client)) {
            apply_rule(&rules[i], client);
            applied = true;
        }
    }

    return applied;
}

static char *str_trim_whitespace(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;
}

static bool parse_bool(const char *value)
{
    if (value == NULL) {
        return false;
    }
    return strcmp(value, "true") == 0 ||
           strcmp(value, "1") == 0 ||
           strcmp(value, "yes") == 0;
}

static void parse_rule_value(WindowRule *rule, const char *key, const char *value)
{
    if (rule == NULL || key == NULL || value == NULL) {
        return;
    }

    if (strcmp(key, "workspace") == 0) {
        rule->has_workspace = true;
        rule->workspace = atoi(value) - 1;
        if (rule->workspace < 0) rule->workspace = 0;
        if (rule->workspace >= MAX_WORKSPACES) rule->workspace = MAX_WORKSPACES - 1;
    } else if (strcmp(key, "floating") == 0) {
        rule->has_floating = true;
        rule->floating = parse_bool(value);
    } else if (strcmp(key, "sticky") == 0) {
        rule->has_sticky = true;
        rule->sticky = parse_bool(value);
    } else if (strcmp(key, "fullscreen") == 0) {
        rule->has_fullscreen = true;
        rule->fullscreen = parse_bool(value);
    } else if (strcmp(key, "width") == 0) {
        rule->has_size = true;
        rule->width = atoi(value);
    } else if (strcmp(key, "height") == 0) {
        rule->has_size = true;
        rule->height = atoi(value);
    } else if (strcmp(key, "x") == 0) {
        rule->has_position = true;
        rule->x = atoi(value);
    } else if (strcmp(key, "y") == 0) {
        rule->has_position = true;
        rule->y = atoi(value);
    } else if (strcmp(key, "opacity") == 0) {
        rule->has_opacity = true;
        rule->opacity = (float)atof(value);
        if (rule->opacity < 0.0f) rule->opacity = 0.0f;
        if (rule->opacity > 1.0f) rule->opacity = 1.0f;
    }
}

static void parse_rule_line(const char *key, const char *value)
{
    WindowRule rule;
    memset(&rule, 0, sizeof(rule));

    char key_copy[RULE_PATTERN_SIZE];
    safe_strncpy(key_copy, key, sizeof(key_copy));

    if (strncmp(key_copy, "class:", 6) == 0) {
        rule.match_type = RULE_MATCH_CLASS;
        safe_strncpy(rule.class_pattern, key_copy + 6, sizeof(rule.class_pattern));
    } else if (strncmp(key_copy, "title:", 6) == 0) {
        rule.match_type = RULE_MATCH_TITLE;
        safe_strncpy(rule.title_pattern, key_copy + 6, sizeof(rule.title_pattern));
    } else {
        rule.match_type = RULE_MATCH_CLASS;
        safe_strncpy(rule.class_pattern, key_copy, sizeof(rule.class_pattern));
    }

    char value_copy[512];
    safe_strncpy(value_copy, value, sizeof(value_copy));

    char *token = strtok(value_copy, ",");
    while (token != NULL) {
        char *trimmed = str_trim_whitespace(token);

        char *eq = strchr(trimmed, ':');
        if (eq != NULL) {
            *eq = '\0';
            char *prop_key = str_trim_whitespace(trimmed);
            char *prop_val = str_trim_whitespace(eq + 1);
            parse_rule_value(&rule, prop_key, prop_val);
        }

        token = strtok(NULL, ",");
    }

    rules_add(&rule);
}

static void rules_config_callback(const char *section, const char *key,
                                   const char *value, void *user_data)
{
    (void)user_data;

    if (strcmp(section, "rules") != 0) {
        return;
    }

    parse_rule_line(key, value);
}

bool rules_load(const char *path)
{
    if (path == NULL) {
        if (dwn == NULL || dwn->config == NULL) {
            return false;
        }
        path = dwn->config->config_path;
    }

    int prev_count = rule_count;

    config_parse_ini(path, rules_config_callback, NULL);

    int loaded = rule_count - prev_count;
    if (loaded > 0) {
        LOG_INFO("Loaded %d window rules from config", loaded);
    }

    return loaded > 0;
}
