/*
 * DWN - Desktop Window Manager
 * retoor <retoor@molodetz.nl>
 * Client (window) management implementation
 */

#include "client.h"
#include "atoms.h"
#include "config.h"
#include "util.h"
#include "workspace.h"
#include "decorations.h"
#include "notifications.h"
#include "layout.h"
#include "panel.h"
#include "api.h"
#include "rules.h"
#include "marks.h"

#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <X11/Xresource.h>


Client *client_create(Window window)
{
    if (dwn == NULL || dwn->display == NULL) {
        LOG_ERROR("client_create: dwn or display is NULL");
        return NULL;
    }

    if (window == None) {
        LOG_ERROR("client_create: invalid window (None)");
        return NULL;
    }

    Client *client = dwn_calloc(1, sizeof(Client));
    if (client == NULL) {
        LOG_ERROR("client_create: failed to allocate client");
        return NULL;
    }

    client->window = window;
    client->frame = None;
    client->border_width = config_get_border_width();
    client->flags = CLIENT_NORMAL;
    client->workspace = dwn->current_workspace;
    client->next = NULL;
    client->prev = NULL;
    client->mru_next = NULL;
    client->mru_prev = NULL;
    client->snap.horizontal = SNAP_H_NONE;
    client->snap.vertical = SNAP_V_NONE;
    client->snap.timestamp = 0;

    XWindowAttributes wa;
    int orig_width = 640, orig_height = 480;
    if (XGetWindowAttributes(dwn->display, window, &wa)) {
        orig_width = wa.width;
        orig_height = wa.height;
    }

    int work_x = 0;
    int work_y = 0;
    int work_width = dwn->screen_width;
    int work_height = dwn->screen_height;

    if (dwn->config != NULL) {
        if (dwn->config->top_panel_enabled) {
            work_y += config_get_panel_height();
            work_height -= config_get_panel_height();
        }
        if (dwn->config->bottom_panel_enabled) {
            work_height -= config_get_panel_height();
        }
    }

    client->width = orig_width;
    client->height = orig_height;

    if (client->width > work_width - 20) client->width = work_width - 20;
    if (client->height > work_height - 20) client->height = work_height - 20;

    client->x = work_x + (work_width - client->width) / 2;
    client->y = work_y + (work_height - client->height) / 2;

    client->old_x = client->x;
    client->old_y = client->y;
    client->old_width = client->width;
    client->old_height = client->height;

    client_update_title(client);
    client_update_class(client);
    client->taskbar_color = generate_unique_color();

    client->text_glow.phase = (float)(rand() % 628) / 100.0f;
    client->text_glow.speed = 0.8f + (float)(rand() % 40) / 100.0f;
    client->text_glow.base_color = client->taskbar_color;
    client->text_glow.active = true;

    return client;
}

void client_destroy(Client *client)
{
    if (client == NULL) {
        return;
    }

    if (client->frame != None) {
        client_destroy_frame(client);
    }

    dwn_free(client);
}

static inline void client_sync_log(const char *msg)
{
    (void)msg;
}


Client *client_manage(Window window)
{
    client_sync_log("client_manage: START");

    if (dwn == NULL || dwn->display == NULL) {
        client_sync_log("client_manage: dwn NULL");
        return NULL;
    }

    Client *existing = client_find_by_window(window);
    if (existing != NULL) {
        client_sync_log("client_manage: already managed");
        return existing;
    }

    if (client_is_dock(window) || client_is_desktop(window)) {
        LOG_DEBUG("Skipping dock/desktop window: %lu", window);
        client_sync_log("client_manage: skip dock/desktop");
        return NULL;
    }

    LOG_DEBUG("Managing window: %lu", window);
    client_sync_log("client_manage: creating client");

    Client *client = client_create(window);
    if (client == NULL) {
        LOG_ERROR("client_manage: failed to create client for window %lu", window);
        client_sync_log("client_manage: create FAILED");
        return NULL;
    }

    client_sync_log("client_manage: checking dialog");

    if (client_is_dialog(window)) {
        client->flags |= CLIENT_FLOATING;
    }

    client_sync_log("client_manage: creating frame");

    client_create_frame(client);

    client_sync_log("client_manage: verifying window");

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, window, &wa)) {
        LOG_WARN("client_manage: window %lu disappeared before reparenting", window);
        client_sync_log("client_manage: window gone before reparent");
        client_destroy(client);
        return NULL;
    }

    client_sync_log("client_manage: reparenting");
    client_reparent_to_frame(client);

    client_sync_log("client_manage: verifying after reparent");
    XSync(dwn->display, False);

    if (client->frame == None) {
        client_sync_log("client_manage: frame is None after reparent");
        LOG_WARN("client_manage: frame creation failed for window %lu", window);
        client_destroy(client);
        return NULL;
    }

    XWindowAttributes wa_frame;
    if (!XGetWindowAttributes(dwn->display, client->frame, &wa_frame)) {
        client_sync_log("client_manage: frame gone after reparent");
        LOG_WARN("client_manage: frame %lu no longer exists", client->frame);
        client_destroy(client);
        return NULL;
    }

    if (!XGetWindowAttributes(dwn->display, window, &wa)) {
        client_sync_log("client_manage: window gone after reparent");
        LOG_WARN("client_manage: window %lu disappeared after reparent", window);
        client_destroy_frame(client);
        client_destroy(client);
        return NULL;
    }

    client_sync_log("client_manage: configuring");
    client_configure(client);

    client_sync_log("client_manage: adding to list");
    client_add_to_list(client);

    client_sync_log("client_manage: adding to workspace");
    workspace_add_client(dwn->current_workspace, client);

    client_sync_log("client_manage: setting EWMH");
    atoms_set_window_desktop(window, client->workspace);
    atoms_update_client_list();

    client_sync_log("client_manage: verifying window again");
    if (!XGetWindowAttributes(dwn->display, window, &wa)) {
        LOG_WARN("client_manage: window %lu disappeared before event setup", window);
        client_sync_log("client_manage: window gone before events");
        workspace_remove_client(client->workspace, client);
        client_remove_from_list(client);
        client_destroy(client);
        atoms_update_client_list();
        return NULL;
    }

    client_sync_log("client_manage: selecting input");
    XSelectInput(dwn->display, window,
                 EnterWindowMask | FocusChangeMask | PropertyChangeMask |
                 StructureNotifyMask);

    client_sync_log("client_manage: grabbing button");
    XGrabButton(dwn->display, Button1, AnyModifier, window, False,
                ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);

    client_sync_log("client_manage: syncing X");
    XSync(dwn->display, False);

    client_sync_log("client_manage: applying rules");
    bool rules_applied = rules_apply_to_client(client);

    if (rules_applied) {
        client_configure(client);
        if (client->workspace != (unsigned int)dwn->current_workspace) {
            client_hide(client);
        }
    }

    client_sync_log("client_manage: showing");
    if (client->workspace == (unsigned int)dwn->current_workspace) {
        client_show(client);
    }

    client_sync_log("client_manage: focusing");
    if (client->workspace == (unsigned int)dwn->current_workspace) {
        client_focus(client, true);
    }

    api_emit_window_created(client->window, client->title, client->class, client->workspace);

    client_sync_log("client_manage: DONE");
    return client;
}

void client_unmanage(Client *client)
{
    client_sync_log("client_unmanage: START");

    if (client == NULL) {
        client_sync_log("client_unmanage: NULL client");
        return;
    }

    api_emit_window_destroyed(client->window, client->title, client->workspace);

    marks_remove_client(client);

    LOG_DEBUG("Unmanaging window: %lu", client->window);

    if (dwn->alt_tab_client == client) {
        workspace_end_alt_tab();
    }

    client_sync_log("client_unmanage: remove from MRU");
    workspace_mru_remove(client->workspace, client);

    client_sync_log("client_unmanage: remove from workspace");
    workspace_remove_client(client->workspace, client);

    client_sync_log("client_unmanage: remove from list");
    client_remove_from_list(client);

    client_sync_log("client_unmanage: reparent from frame");
    client_reparent_from_frame(client);

    client_sync_log("client_unmanage: update EWMH");
    atoms_update_client_list();

    client_sync_log("client_unmanage: destroy client");
    client_destroy(client);

    client_sync_log("client_unmanage: focus next");
    XGrabServer(dwn->display);

    Workspace *ws = workspace_get_current();
    if (ws != NULL && ws->focused == NULL) {
        Client *next = workspace_mru_get_previous(dwn->current_workspace, NULL);
        if (next != NULL) {
            client_focus(next, true);
        } else {
            XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime);
            atoms_set_active_window(None);
        }
    }

    XUngrabServer(dwn->display);
    XSync(dwn->display, False);

    client_sync_log("client_unmanage: DONE");
}

Client *client_find_by_window(Window window)
{
    if (dwn == NULL || window == None) {
        return NULL;
    }

    for (Client *c = dwn->client_list; c != NULL; c = c->next) {
        if (c->window == window) {
            return c;
        }
    }
    return NULL;
}

Client *client_find_by_frame(Window frame)
{
    if (dwn == NULL || frame == None) {
        return NULL;
    }

    for (Client *c = dwn->client_list; c != NULL; c = c->next) {
        if (c->frame == frame) {
            return c;
        }
    }
    return NULL;
}


void client_focus(Client *client, bool update_mru)
{
    client_sync_log("client_focus: START");

    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        client_sync_log("client_focus: NULL check failed");
        return;
    }

    if (client->window == None) {
        LOG_WARN("client_focus: client has invalid window (None)");
        client_sync_log("client_focus: window is None");
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_WARN("client_focus: window %lu no longer exists", client->window);
        client_sync_log("client_focus: window gone");
        return;
    }

    client_sync_log("client_focus: unfocusing previous");

    Workspace *ws = workspace_get(client->workspace);
    Window prev_window = None;
    if (ws != NULL && ws->focused != NULL && ws->focused != client) {
        prev_window = ws->focused->window;
        client_start_focus_animation(ws->focused, false);
        client_unfocus(ws->focused);
    }

    client_start_focus_animation(client, true);

    client_sync_log("client_focus: XSetInputFocus");

    XSetInputFocus(dwn->display, client->window, RevertToPointerRoot, CurrentTime);
    XSync(dwn->display, False);

    client_sync_log("client_focus: updating workspace");

    if (ws != NULL) {
        ws->focused = client;
        if (update_mru) {
            workspace_mru_push(client->workspace, client);
        }
    }

    client_sync_log("client_focus: raising");

    client_raise(client);

    /* Ungrab buttons so the application receives events directly */
    XUngrabButton(dwn->display, Button1, AnyModifier, client->window);

    client_sync_log("client_focus: decorations");

    decorations_render(client, true);

    client_sync_log("client_focus: EWMH");

    atoms_set_active_window(client->window);

    api_emit_window_focused(client->window, client->title, prev_window);

    client_sync_log("client_focus: DONE");
    LOG_DEBUG("Focused window: %s", client->title);
}

void client_unfocus(Client *client)
{
    if (client == NULL) {
        return;
    }

    /* Grab buttons to intercept clicks for focus */
    if (dwn != NULL && dwn->display != NULL && client->window != None) {
        XGrabButton(dwn->display, Button1, AnyModifier, client->window, False,
                    ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
    }

    decorations_render(client, false);

    api_emit_window_unfocused(client->window, client->title);
}

void client_raise(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    Window win = (client->frame != None) ? client->frame : client->window;
    if (win == None) {
        return;
    }

    XRaiseWindow(dwn->display, win);

    api_emit_window_raised(client->window);

    notifications_raise_all();
    panel_raise_all();
}

void client_lower(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    Window win = (client->frame != None) ? client->frame : client->window;
    if (win == None) {
        return;
    }
    XLowerWindow(dwn->display, win);

    api_emit_window_lowered(client->window);
}

void client_minimize(Client *client)
{
    if (client == NULL) {
        return;
    }

    client->flags |= CLIENT_MINIMIZED;
    client_hide(client);

    api_emit_window_minimized(client->window);

    Workspace *ws = workspace_get(client->workspace);
    if (ws != NULL && ws->focused == client) {
        Client *next = workspace_mru_get_previous(client->workspace, client);
        if (next != NULL) {
            client_focus(next, true);
        } else {
            ws->focused = NULL;
            XSetInputFocus(dwn->display, dwn->root, RevertToPointerRoot, CurrentTime);
            atoms_set_active_window(None);
        }
    }
}

void client_restore(Client *client)
{
    if (client == NULL) {
        return;
    }

    client->flags &= ~CLIENT_MINIMIZED;
    client_show(client);
    client_focus(client, true);

    api_emit_window_restored(client->window);
}


void client_move(Client *client, int x, int y)
{
    if (client == NULL) {
        return;
    }

    int old_x = client->x;
    int old_y = client->y;

    int area_x, area_y, area_w, area_h;
    layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);

    int title_height = (dwn->config && dwn->config->show_decorations) ?
                       config_get_title_height() : 0;
    int border = client->border_width;

    /* Constrain top (title bar) */
    if (y - title_height - border < area_y) {
        y = area_y + title_height + border;
    }

    /* Constrain bottom */
    if (y + client->height + border > area_y + area_h) {
        y = area_y + area_h - client->height - border;
    }

    /* Constrain left */
    if (x - border < area_x) {
        x = area_x + border;
    }

    /* Constrain right */
    if (x + client->width + border > area_x + area_w) {
        x = area_x + area_w - client->width - border;
    }

    client->x = x;
    client->y = y;
    client_configure(client);

    if (old_x != client->x || old_y != client->y) {
        api_emit_window_moved(client->window, old_x, old_y, client->x, client->y);
    }
}

void client_resize(Client *client, int width, int height)
{
    if (client == NULL) {
        return;
    }

    int old_w = client->width;
    int old_h = client->height;

    int area_x, area_y, area_w, area_h;
    layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);

    client_apply_size_hints(client, &width, &height);

    int border = client->border_width;

    /* Constrain height so it doesn't go under bottom panel */
    if (client->y + height + border > area_y + area_h) {
        height = area_y + area_h - client->y - border;
        if (height < 50) height = 50;
    }

    /* Constrain width so it doesn't go off screen right */
    if (client->x + width + border > area_x + area_w) {
        width = area_x + area_w - client->x - border;
        if (width < 50) width = 50;
    }

    client->width = width;
    client->height = height;
    client_configure(client);

    if (old_w != client->width || old_h != client->height) {
        api_emit_window_resized(client->window, old_w, old_h, client->width, client->height);
    }
}

void client_move_resize(Client *client, int x, int y, int width, int height)
{
    if (client == NULL) {
        return;
    }

    int area_x, area_y, area_w, area_h;
    layout_get_usable_area(&area_x, &area_y, &area_w, &area_h);

    client_apply_size_hints(client, &width, &height);

    int title_h = (dwn->config && dwn->config->show_decorations) ?
                  config_get_title_height() : 0;
    int border = client->border_width;

    /* Constrain top */
    if (y - title_h - border < area_y) {
        y = area_y + title_h + border;
    }

    /* Constrain bottom */
    if (y + height + border > area_y + area_h) {
        /* Prefer moving up first, then resizing if still too big */
        y = area_y + area_h - height - border;
        if (y - title_h - border < area_y) {
            y = area_y + title_h + border;
            height = area_y + area_h - y - border;
        }
    }

    /* Constrain left */
    if (x - border < area_x) {
        x = area_x + border;
    }

    /* Constrain right */
    if (x + width + border > area_x + area_w) {
        x = area_x + area_w - width - border;
        if (x - border < area_x) {
            x = area_x + border;
            width = area_x + area_w - x - border;
        }
    }

    if (width < 50) width = 50;
    if (height < 50) height = 50;

    client->x = x;
    client->y = y;
    client->width = width;
    client->height = height;
    client_configure(client);
}

void client_configure(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_DEBUG("client_configure: window no longer exists");
        return;
    }

    int title_height = (dwn->config && dwn->config->show_decorations) ?
                       config_get_title_height() : 0;
    int border = client->border_width;

    if (client->frame != None) {
        XMoveResizeWindow(dwn->display, client->frame,
                          client->x - border,
                          client->y - title_height - border,
                          client->width + 2 * border,
                          client->height + title_height + 2 * border);

        XMoveResizeWindow(dwn->display, client->window,
                          border, title_height + border,
                          client->width, client->height);
    } else {
        XMoveResizeWindow(dwn->display, client->window,
                          client->x, client->y,
                          client->width, client->height);
    }

    XConfigureEvent ce;
    memset(&ce, 0, sizeof(ce));
    ce.type = ConfigureNotify;
    ce.event = client->window;
    ce.window = client->window;
    ce.x = client->x;
    ce.y = client->y;
    ce.width = client->width;
    ce.height = client->height;
    ce.border_width = 0;
    ce.above = None;
    ce.override_redirect = False;

    XSendEvent(dwn->display, client->window, False, StructureNotifyMask, (XEvent *)&ce);
}

void client_apply_size_hints(Client *client, int *width, int *height)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    XSizeHints hints;
    long supplied;

    if (!XGetWMNormalHints(dwn->display, client->window, &hints, &supplied)) {
        return;
    }

    if (hints.flags & PMinSize) {
        if (*width < hints.min_width) *width = hints.min_width;
        if (*height < hints.min_height) *height = hints.min_height;
    }

    if (hints.flags & PMaxSize) {
        if (*width > hints.max_width) *width = hints.max_width;
        if (*height > hints.max_height) *height = hints.max_height;
    }

    if (hints.flags & PResizeInc) {
        int base_w = (hints.flags & PBaseSize) ? hints.base_width : 0;
        int base_h = (hints.flags & PBaseSize) ? hints.base_height : 0;

        *width = base_w + ((*width - base_w) / hints.width_inc) * hints.width_inc;
        *height = base_h + ((*height - base_h) / hints.height_inc) * hints.height_inc;
    }
}


void client_update_title(Client *client)
{
    if (client == NULL) {
        return;
    }

    char old_title[256];
    size_t title_len = strlen(client->title);
    if (title_len >= sizeof(old_title)) title_len = sizeof(old_title) - 1;
    memcpy(old_title, client->title, title_len);
    old_title[title_len] = '\0';

    char *name = atoms_get_window_name(client->window);
    if (name != NULL) {
        strncpy(client->title, name, sizeof(client->title) - 1);
        client->title[sizeof(client->title) - 1] = '\0';
        dwn_free(name);
    } else {
        strncpy(client->title, "Untitled", sizeof(client->title) - 1);
    }

    if (strcmp(old_title, client->title) != 0) {
        api_emit_window_title_changed(client->window, old_title, client->title);
    }

    if (client->frame != None) {
        Workspace *ws = workspace_get(client->workspace);
        bool focused = (ws != NULL && ws->focused == client);
        decorations_render(client, focused);
    }
}

void client_update_class(Client *client)
{
    if (client == NULL) {
        return;
    }

    atoms_get_wm_class(client->window, client->class, NULL, sizeof(client->class));
}

void client_set_fullscreen(Client *client, bool fullscreen)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_WARN("client_set_fullscreen: window %lu no longer exists", client->window);
        return;
    }

    if (fullscreen) {
        if (!(client->flags & CLIENT_FULLSCREEN)) {
            client->old_x = client->x;
            client->old_y = client->y;
            client->old_width = client->width;
            client->old_height = client->height;
        }

        client->flags |= CLIENT_FULLSCREEN;
        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, true);
        api_emit_window_fullscreen(client->window, true);

        client->x = 0;
        client->y = 0;
        client->width = dwn->screen_width;
        client->height = dwn->screen_height;

        if (client->frame != None) {
            XUnmapWindow(dwn->display, client->frame);
            XSync(dwn->display, False);
            XReparentWindow(dwn->display, client->window, dwn->root, 0, 0);
            XSync(dwn->display, False);
        }
        XMoveResizeWindow(dwn->display, client->window,
                          0, 0, client->width, client->height);
        XMapWindow(dwn->display, client->window);
        XRaiseWindow(dwn->display, client->window);
        XSync(dwn->display, False);
    } else {
        client->flags &= ~CLIENT_FULLSCREEN;
        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_FULLSCREEN, false);
        api_emit_window_fullscreen(client->window, false);

        client->x = client->old_x;
        client->y = client->old_y;
        client->width = client->old_width;
        client->height = client->old_height;

        if (client->frame != None) {
            int title_height = config_get_title_height();
            int border = client->border_width;
            XSync(dwn->display, False);
            XReparentWindow(dwn->display, client->window, client->frame,
                            border, title_height + border);
            XSync(dwn->display, False);
            XMapWindow(dwn->display, client->frame);
        }
        client_configure(client);
        decorations_render(client, true);
        client_raise(client);
        XSync(dwn->display, False);
    }
}

void client_toggle_fullscreen(Client *client)
{
    if (client == NULL) {
        return;
    }
    client_set_fullscreen(client, !(client->flags & CLIENT_FULLSCREEN));
}

void client_set_floating(Client *client, bool floating)
{
    if (client == NULL) {
        return;
    }

    if (floating) {
        client->flags |= CLIENT_FLOATING;
    } else {
        client->flags &= ~CLIENT_FLOATING;
    }

    api_emit_window_floating(client->window, floating);
}

void client_toggle_floating(Client *client)
{
    if (client == NULL) {
        return;
    }
    client_set_floating(client, !(client->flags & CLIENT_FLOATING));
}

bool client_is_maximized(Client *client)
{
    return client != NULL && (client->flags & CLIENT_MAXIMIZED);
}

void client_set_maximize(Client *client, bool maximized)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_WARN("client_set_maximize: window %lu no longer exists", client->window);
        return;
    }

    if (maximized) {
        if (!(client->flags & CLIENT_MAXIMIZED)) {
            client->old_x = client->x;
            client->old_y = client->y;
            client->old_width = client->width;
            client->old_height = client->height;
        }

        client->flags |= CLIENT_MAXIMIZED;
        client->flags |= CLIENT_FLOATING;

        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, true);
        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, true);
        api_emit_window_maximized(client->window, true);

        int area_x, area_y, area_width, area_height;
        layout_get_usable_area(&area_x, &area_y, &area_width, &area_height);

        int gap = config_get_gap();
        int title_height = config_get_title_height();
        int border = client->border_width;

        client->x = area_x + gap + border;
        client->y = area_y + gap + title_height + border;
        client->width = area_width - 2 * gap - 2 * border;
        client->height = area_height - 2 * gap - title_height - 2 * border;

        client_configure(client);
        decorations_render(client, true);
        client_raise(client);
        client_focus(client, true);
    } else {
        client->flags &= ~CLIENT_MAXIMIZED;

        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_VERT, false);
        atoms_update_wm_state(client->window, ewmh.NET_WM_STATE_MAXIMIZED_HORZ, false);
        api_emit_window_maximized(client->window, false);

        client->x = client->old_x;
        client->y = client->old_y;
        client->width = client->old_width;
        client->height = client->old_height;

        client_configure(client);
        decorations_render(client, true);
    }
}

void client_toggle_maximize(Client *client)
{
    if (client == NULL) {
        return;
    }
    client_set_maximize(client, !(client->flags & CLIENT_MAXIMIZED));
}


bool client_is_floating(Client *client)
{
    return client != NULL && (client->flags & CLIENT_FLOATING);
}

bool client_is_fullscreen(Client *client)
{
    return client != NULL && (client->flags & CLIENT_FULLSCREEN);
}

bool client_is_minimized(Client *client)
{
    return client != NULL && (client->flags & CLIENT_MINIMIZED);
}

bool client_is_dialog(Window window)
{
    Atom type;
    if (atoms_get_window_type(window, &type)) {
        return type == ewmh.NET_WM_WINDOW_TYPE_DIALOG;
    }

    Window transient_for = None;
    if (XGetTransientForHint(dwn->display, window, &transient_for)) {
        return transient_for != None;
    }

    return false;
}

bool client_is_dock(Window window)
{
    Atom type;
    if (atoms_get_window_type(window, &type)) {
        return type == ewmh.NET_WM_WINDOW_TYPE_DOCK;
    }
    return false;
}

bool client_is_desktop(Window window)
{
    Atom type;
    if (atoms_get_window_type(window, &type)) {
        return type == ewmh.NET_WM_WINDOW_TYPE_DESKTOP;
    }
    return false;
}


void client_create_frame(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        LOG_ERROR("client_create_frame: invalid parameters");
        return;
    }

    if (!dwn->config || !dwn->config->show_decorations) {
        return;
    }

    if (client->window == None) {
        LOG_ERROR("client_create_frame: client has no valid window");
        return;
    }

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

    int frame_width = client->width + 2 * border;
    int frame_height = client->height + title_height + 2 * border;
    if (frame_width <= 0 || frame_height <= 0) {
        LOG_ERROR("client_create_frame: invalid dimensions (%dx%d)", frame_width, frame_height);
        return;
    }

    XSetWindowAttributes swa;
    memset(&swa, 0, sizeof(swa));
    swa.override_redirect = True;
    swa.background_pixel = dwn->config->colors.title_unfocused_bg;
    swa.border_pixel = dwn->config->colors.border_unfocused;
    swa.event_mask = SubstructureRedirectMask | SubstructureNotifyMask |
                     ButtonPressMask | ButtonReleaseMask | PointerMotionMask |
                     ExposureMask | EnterWindowMask;

    client->frame = XCreateWindow(dwn->display, dwn->root,
                                  client->x - border,
                                  client->y - title_height - border,
                                  frame_width,
                                  frame_height,
                                  0,
                                  CopyFromParent, InputOutput, CopyFromParent,
                                  CWOverrideRedirect | CWBackPixel | CWBorderPixel | CWEventMask,
                                  &swa);

    if (client->frame == None) {
        LOG_ERROR("client_create_frame: XCreateWindow failed for window %lu", client->window);
        return;
    }

    XSetWindowBorderWidth(dwn->display, client->frame, border);
    XSync(dwn->display, False);

    LOG_DEBUG("Created frame %lu for window %lu", client->frame, client->window);
}

void client_destroy_frame(Client *client)
{
    if (client == NULL || client->frame == None) {
        return;
    }

    XDestroyWindow(dwn->display, client->frame);
    client->frame = None;
}

void client_reparent_to_frame(Client *client)
{
    if (client == NULL || client->frame == None) {
        return;
    }

    if (dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        LOG_WARN("client_reparent_to_frame: client has no valid window");
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_WARN("client_reparent_to_frame: window %lu no longer exists", client->window);
        return;
    }

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

    XSync(dwn->display, False);

    XReparentWindow(dwn->display, client->window, client->frame,
                    border, title_height + border);

    XSync(dwn->display, False);

    XSaveContext(dwn->display, client->window, XUniqueContext(), (XPointer)client);
}

void client_reparent_from_frame(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->frame == None || client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_WARN("client_reparent_from_frame: window %lu no longer exists", client->window);
        return;
    }

    XSync(dwn->display, False);

    XReparentWindow(dwn->display, client->window, dwn->root,
                    client->x, client->y);

    XSync(dwn->display, False);
}


void client_show(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_DEBUG("client_show: window no longer exists");
        return;
    }

    if (client->frame != None) {
        XMapWindow(dwn->display, client->frame);
    }
    XMapWindow(dwn->display, client->window);

    panel_raise_all();
}

void client_hide(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_DEBUG("client_hide: window no longer exists");
        return;
    }

    if (client->frame != None) {
        XUnmapWindow(dwn->display, client->frame);
    }
    XUnmapWindow(dwn->display, client->window);
}

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

    if (client->flags & CLIENT_MINIMIZED) {
        return false;
    }

    if (!(client->flags & CLIENT_STICKY) &&
        client->workspace != (unsigned int)dwn->current_workspace) {
        return false;
    }

    return true;
}


void client_close(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XWindowAttributes wa;
    if (!XGetWindowAttributes(dwn->display, client->window, &wa)) {
        LOG_DEBUG("client_close: window no longer exists");
        return;
    }

    if (atoms_window_supports_protocol(client->window, icccm.WM_DELETE_WINDOW)) {
        atoms_send_protocol(client->window, icccm.WM_DELETE_WINDOW, CurrentTime);
    } else {
        client_kill(client);
    }
}

void client_kill(Client *client)
{
    if (client == NULL || dwn == NULL || dwn->display == NULL) {
        return;
    }

    if (client->window == None) {
        return;
    }

    XKillClient(dwn->display, client->window);
}


void client_add_to_list(Client *client)
{
    if (client == NULL || dwn == NULL) {
        return;
    }

    client->next = dwn->client_list;
    client->prev = NULL;

    if (dwn->client_list != NULL) {
        dwn->client_list->prev = client;
    }

    dwn->client_list = client;
    dwn->client_count++;
}

void client_remove_from_list(Client *client)
{
    if (client == NULL || dwn == NULL) {
        return;
    }

    if (client->prev != NULL) {
        client->prev->next = client->next;
    } else {
        dwn->client_list = client->next;
    }

    if (client->next != NULL) {
        client->next->prev = client->prev;
    }

    dwn->client_count--;
}

int client_count(void)
{
    return dwn != NULL ? dwn->client_count : 0;
}

int client_count_on_workspace(int workspace)
{
    int count = 0;
    for (Client *c = dwn->client_list; c != NULL; c = c->next) {
        if (c->workspace == (unsigned int)workspace) {
            count++;
        }
    }
    return count;
}


Client *client_get_next(Client *client)
{
    return client != NULL ? client->next : NULL;
}

Client *client_get_prev(Client *client)
{
    return client != NULL ? client->prev : NULL;
}

Client *client_get_first(void)
{
    return dwn != NULL ? dwn->client_list : NULL;
}

Client *client_get_last(void)
{
    if (dwn == NULL || dwn->client_list == NULL) {
        return NULL;
    }

    Client *c = dwn->client_list;
    while (c->next != NULL) {
        c = c->next;
    }
    return c;
}


#define FOCUS_ANIMATION_DURATION_MS 400

void client_start_focus_animation(Client *client, bool gaining_focus)
{
    if (client == NULL) {
        return;
    }

    unsigned long full_color = client->taskbar_color;
    unsigned long dimmed_color = dim_color(client->taskbar_color, 0.35f);

    if (gaining_focus) {
        client->title_anim.from_color = dimmed_color;
        client->title_anim.to_color = full_color;
    } else {
        client->title_anim.from_color = full_color;
        client->title_anim.to_color = dimmed_color;
    }

    client->title_anim.start_time = get_time_ms();
    client->title_anim.progress = 0.0f;
    client->title_anim.active = true;
}

unsigned long client_get_animated_title_color(Client *client)
{
    if (client == NULL) {
        return 0;
    }

    Workspace *ws = workspace_get(client->workspace);
    bool is_focused = (ws != NULL && ws->focused == client);

    const ColorScheme *colors = config_get_colors();
    return is_focused ? colors->title_focused_bg : colors->title_unfocused_bg;
}

unsigned long client_get_glow_text_color(Client *client)
{
    if (client == NULL || !client->text_glow.active) {
        return 0xFFFFFF;
    }
    float phase = dwn->ambient_phase + client->text_glow.phase;
    return glow_color(client->text_glow.base_color, phase);
}

#define TEXT_GLOW_THROTTLE_MS 100

void client_update_animations(void)
{
    if (dwn == NULL) {
        return;
    }

    static long last_glow_update = 0;
    long now = get_time_ms();
    bool should_update_glow = (now - last_glow_update >= TEXT_GLOW_THROTTLE_MS);
    bool needs_redraw = false;

    for (Client *c = dwn->client_list; c != NULL; c = c->next) {
        if (c->title_anim.active) {
            long elapsed = now - c->title_anim.start_time;
            c->title_anim.progress = (float)elapsed / (float)FOCUS_ANIMATION_DURATION_MS;

            if (c->title_anim.progress >= 1.0f) {
                c->title_anim.progress = 1.0f;
                c->title_anim.active = false;
            }
            needs_redraw = true;
        }

        if (should_update_glow && c->text_glow.active &&
            c->workspace == (unsigned int)dwn->current_workspace) {
            c->text_glow.phase += 0.05f * c->text_glow.speed;
            if (c->text_glow.phase > 6.283f) {
                c->text_glow.phase -= 6.283f;
            }
            needs_redraw = true;
        }
    }

    if (should_update_glow) {
        last_glow_update = now;
    }

    if (needs_redraw) {
        panel_invalidate_taskbar();
    }
}
