/*
 * DWN - Desktop Window Manager
 * retoor <retoor@molodetz.nl>
 * D-Bus Notification daemon implementation
 */

#include "notifications.h"
#include "config.h"
#include "util.h"
#include "api.h"

#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <X11/Xft/Xft.h>

static sigjmp_buf dbus_timeout_jmp;
static volatile sig_atomic_t dbus_timeout_occurred = 0;

static void dbus_timeout_handler(int sig)
{
    (void)sig;
    dbus_timeout_occurred = 1;
    siglongjmp(dbus_timeout_jmp, 1);
}

DBusConnection *dbus_conn = NULL;

static Notification *notification_list = NULL;
static uint32_t next_notification_id = 1;

#define NOTIFICATION_MIN_WIDTH 280
#define NOTIFICATION_PADDING 12
#define NOTIFICATION_MARGIN 10

static int notification_max_width(void)
{
    if (dwn == NULL) return 500;
    return dwn->screen_width / 3;
}

static int notification_max_height(void)
{
    if (dwn == NULL) return 600;
    return dwn->screen_height / 2;
}

#define DEFAULT_TIMEOUT 5000


static int notif_text_width(const char *text, int len)
{
    if (text == NULL || dwn == NULL) return len * 7;

    if (dwn->xft_font != NULL) {
        XGlyphInfo extents;
        XftTextExtentsUtf8(dwn->display, dwn->xft_font,
                           (const FcChar8 *)text, len, &extents);
        return extents.xOff;
    }

    if (dwn->font != NULL) {
        return XTextWidth(dwn->font, text, len);
    }

    return len * 7;
}

static int notif_line_height(void)
{
    if (dwn == NULL) return 14;

    if (dwn->xft_font != NULL) {
        return dwn->xft_font->ascent + dwn->xft_font->descent + 2;
    }

    if (dwn->font != NULL) {
        return dwn->font->ascent + dwn->font->descent + 2;
    }

    return 14;
}

static int notif_font_ascent(void)
{
    if (dwn == NULL) return 12;

    if (dwn->xft_font != NULL) {
        return dwn->xft_font->ascent;
    }

    if (dwn->font != NULL) {
        return dwn->font->ascent;
    }

    return 12;
}

static void notif_draw_text(Window win, int x, int y, const char *text,
                            int len, unsigned long color)
{
    if (text == NULL || dwn == NULL || dwn->display == NULL) return;

    if (dwn->xft_font != NULL) {
        XftDraw *xft_draw = XftDrawCreate(dwn->display, win,
                                          DefaultVisual(dwn->display, dwn->screen),
                                          dwn->colormap);
        if (xft_draw != NULL) {
            XftColor xft_color;
            XRenderColor render_color;
            render_color.red = ((color >> 16) & 0xFF) * 257;
            render_color.green = ((color >> 8) & 0xFF) * 257;
            render_color.blue = (color & 0xFF) * 257;
            render_color.alpha = 0xFFFF;

            XftColorAllocValue(dwn->display, DefaultVisual(dwn->display, dwn->screen),
                               dwn->colormap, &render_color, &xft_color);

            XftDrawStringUtf8(xft_draw, &xft_color, dwn->xft_font,
                              x, y, (const FcChar8 *)text, len);

            XftColorFree(dwn->display, DefaultVisual(dwn->display, dwn->screen),
                         dwn->colormap, &xft_color);
            XftDrawDestroy(xft_draw);
            return;
        }
    }

    XSetForeground(dwn->display, dwn->gc, color);
    XDrawString(dwn->display, win, dwn->gc, x, y, text, len);
}


bool notifications_init(void)
{
    DBusError err;
    dbus_error_init(&err);

    dbus_timeout_occurred = 0;

    struct sigaction sa, old_sa;
    sa.sa_handler = dbus_timeout_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, &old_sa);

    if (sigsetjmp(dbus_timeout_jmp, 1) != 0) {
        sigaction(SIGALRM, &old_sa, NULL);
        alarm(0);
        LOG_WARN("D-Bus connection timed out (5s) - notifications disabled");
        return false;
    }

    alarm(5);
    dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
    alarm(0);

    sigaction(SIGALRM, &old_sa, NULL);

    if (dbus_error_is_set(&err)) {
        LOG_ERROR("D-Bus connection error: %s", err.message);
        dbus_error_free(&err);
        return false;
    }

    if (dbus_conn == NULL) {
        LOG_ERROR("Failed to connect to D-Bus session bus");
        return false;
    }

    if (!notifications_register_service()) {
        LOG_WARN("Could not register notification service (another daemon running?)");
    }

    LOG_INFO("Notification daemon initialized");
    return true;
}

void notifications_cleanup(void)
{
    notification_close_all();

    dbus_conn = NULL;
}


bool notifications_register_service(void)
{
    if (dbus_conn == NULL) {
        return false;
    }

    DBusError err;
    dbus_error_init(&err);

    int result = dbus_bus_request_name(dbus_conn, "org.freedesktop.Notifications",
                                       DBUS_NAME_FLAG_REPLACE_EXISTING, &err);

    if (dbus_error_is_set(&err)) {
        LOG_ERROR("D-Bus name request error: %s", err.message);
        dbus_error_free(&err);
        return false;
    }

    if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
        LOG_WARN("Could not become primary owner of notification service");
        return false;
    }

    dbus_connection_add_filter(dbus_conn, notifications_handle_message, NULL, NULL);

    LOG_INFO("Registered as org.freedesktop.Notifications");
    return true;
}

void notifications_process_messages(void)
{
    if (dbus_conn == NULL) {
        return;
    }

    dbus_connection_read_write(dbus_conn, 0);

    while (dbus_connection_dispatch(dbus_conn) == DBUS_DISPATCH_DATA_REMAINS) {
    }
}

DBusHandlerResult notifications_handle_message(DBusConnection *conn,
                                                DBusMessage *msg,
                                                void *user_data)
{
    (void)user_data;

    const char *interface = dbus_message_get_interface(msg);
    const char *member = dbus_message_get_member(msg);

    if (interface == NULL || member == NULL) {
        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    }

    if (strcmp(interface, "org.freedesktop.Notifications") != 0) {
        return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
    }

    if (strcmp(member, "Notify") == 0) {
        DBusMessageIter args;
        if (!dbus_message_iter_init(msg, &args)) {
            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
        }

        char *app_name = "";
        uint32_t replaces_id = 0;
        char *icon = "";
        char *summary = "";
        char *body = "";
        int32_t timeout = -1;

        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
            dbus_message_iter_get_basic(&args, &app_name);
            dbus_message_iter_next(&args);
        }
        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) {
            dbus_message_iter_get_basic(&args, &replaces_id);
            dbus_message_iter_next(&args);
        }
        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
            dbus_message_iter_get_basic(&args, &icon);
            dbus_message_iter_next(&args);
        }
        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
            dbus_message_iter_get_basic(&args, &summary);
            dbus_message_iter_next(&args);
        }
        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_STRING) {
            dbus_message_iter_get_basic(&args, &body);
            dbus_message_iter_next(&args);
        }

        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) {
            dbus_message_iter_next(&args);
        }

        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_ARRAY) {
            dbus_message_iter_next(&args);
        }

        if (dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_INT32) {
            dbus_message_iter_get_basic(&args, &timeout);
        }

        uint32_t id = notification_show(app_name, summary, body, icon, timeout);

        DBusMessage *reply = dbus_message_new_method_return(msg);
        dbus_message_append_args(reply, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID);
        dbus_connection_send(conn, reply, NULL);
        dbus_message_unref(reply);

        return DBUS_HANDLER_RESULT_HANDLED;
    }

    if (strcmp(member, "CloseNotification") == 0) {
        DBusMessageIter args;
        if (dbus_message_iter_init(msg, &args) &&
            dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_UINT32) {
            uint32_t id;
            dbus_message_iter_get_basic(&args, &id);
            notification_close(id);
        }

        DBusMessage *reply = dbus_message_new_method_return(msg);
        dbus_connection_send(conn, reply, NULL);
        dbus_message_unref(reply);

        return DBUS_HANDLER_RESULT_HANDLED;
    }

    if (strcmp(member, "GetCapabilities") == 0) {
        DBusMessage *reply = dbus_message_new_method_return(msg);
        DBusMessageIter args_iter, array_iter;

        dbus_message_iter_init_append(reply, &args_iter);
        dbus_message_iter_open_container(&args_iter, DBUS_TYPE_ARRAY, "s", &array_iter);

        const char *caps[] = { "body", "body-markup" };
        for (size_t i = 0; i < sizeof(caps) / sizeof(caps[0]); i++) {
            dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_STRING, &caps[i]);
        }

        dbus_message_iter_close_container(&args_iter, &array_iter);
        dbus_connection_send(conn, reply, NULL);
        dbus_message_unref(reply);

        return DBUS_HANDLER_RESULT_HANDLED;
    }

    if (strcmp(member, "GetServerInformation") == 0) {
        DBusMessage *reply = dbus_message_new_method_return(msg);

        const char *name = "DWN";
        const char *vendor = "DWN Project";
        const char *version = DWN_VERSION;
        const char *spec_version = "1.2";

        dbus_message_append_args(reply,
                                 DBUS_TYPE_STRING, &name,
                                 DBUS_TYPE_STRING, &vendor,
                                 DBUS_TYPE_STRING, &version,
                                 DBUS_TYPE_STRING, &spec_version,
                                 DBUS_TYPE_INVALID);

        dbus_connection_send(conn, reply, NULL);
        dbus_message_unref(reply);

        return DBUS_HANDLER_RESULT_HANDLED;
    }

    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}


#define MAX_VISIBLE_NOTIFICATIONS 3

static void notification_calculate_size(const char *summary, const char *body,
                                         int *out_width, int *out_height)
{
    int max_width = notification_max_width();
    int max_height = notification_max_height();
    int min_height = 80;
    int content_max_width = max_width - 2 * NOTIFICATION_PADDING;

    int width = NOTIFICATION_MIN_WIDTH;
    int height = NOTIFICATION_PADDING * 2;
    int line_height = notif_line_height();

    if (summary != NULL && summary[0] != '\0') {
        int summary_width = NOTIFICATION_PADDING * 2;
        summary_width += notif_text_width(summary, strlen(summary));
        if (summary_width > width) width = summary_width;
        height += line_height + 4;
    }

    if (body != NULL && body[0] != '\0') {
        const char *p = body;
        int wrapped_line_count = 0;

        while (*p != '\0') {
            const char *line_start = p;
            while (*p != '\0' && *p != '\n') p++;
            size_t logical_line_len = p - line_start;

            if (logical_line_len == 0) {
                wrapped_line_count++;
            } else {
                const char *lp = line_start;
                while (lp < line_start + logical_line_len) {
                    size_t remaining = (line_start + logical_line_len) - lp;
                    size_t fit_len = 0;

                    for (size_t i = 0; i < remaining; i++) {
                        if ((lp[i] & 0xC0) == 0x80) continue;
                        int w = notif_text_width(lp, i + 1);
                        if (w > content_max_width) break;
                        fit_len = i + 1;
                    }

                    while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) {
                        fit_len++;
                    }

                    if (fit_len == 0 && remaining > 0) {
                        fit_len = 1;
                        while (fit_len < remaining && (lp[fit_len] & 0xC0) == 0x80) {
                            fit_len++;
                        }
                    }

                    if (fit_len == 0) break;

                    lp += fit_len;
                    while (lp < line_start + logical_line_len && *lp == ' ') lp++;

                    wrapped_line_count++;

                    if (height + (wrapped_line_count * line_height) > max_height - line_height - NOTIFICATION_PADDING) {
                        goto done_counting;
                    }
                }
            }

            if (*p == '\n') p++;
        }

done_counting:
        height += wrapped_line_count * line_height;
    }

    height += line_height + NOTIFICATION_PADDING;

    width = max_width;

    if (width < NOTIFICATION_MIN_WIDTH) width = NOTIFICATION_MIN_WIDTH;
    if (width > max_width) width = max_width;
    if (height < min_height) height = min_height;
    if (height > max_height) height = max_height;

    *out_width = width;
    *out_height = height;
}

uint32_t notification_show(const char *app_name, const char *summary,
                           const char *body, const char *icon, int timeout)
{
    int count = 0;
    Notification *oldest = NULL;
    for (Notification *n = notification_list; n != NULL; n = n->next) {
        count++;
        oldest = n;
    }

    if (count >= MAX_VISIBLE_NOTIFICATIONS && oldest != NULL) {
        notification_close(oldest->id);
    }

    Notification *notif = dwn_calloc(1, sizeof(Notification));
    if (notif == NULL) {
        LOG_ERROR("Failed to allocate notification");
        return 0;
    }

    notif->id = next_notification_id++;

    if (app_name) {
        strncpy(notif->app_name, app_name, sizeof(notif->app_name) - 1);
        notif->app_name[sizeof(notif->app_name) - 1] = '\0';
    }
    if (summary) {
        strncpy(notif->summary, summary, sizeof(notif->summary) - 1);
        notif->summary[sizeof(notif->summary) - 1] = '\0';
    }

    if (body != NULL && body[0] != '\0') {
        notif->body_len = strlen(body);
        notif->body = dwn_malloc(notif->body_len + 1);
        if (notif->body == NULL) {
            LOG_ERROR("Failed to allocate notification body");
            dwn_free(notif);
            return 0;
        }
        memcpy(notif->body, body, notif->body_len + 1);
    } else {
        notif->body = NULL;
        notif->body_len = 0;
    }

    if (icon) {
        strncpy(notif->icon, icon, sizeof(notif->icon) - 1);
        notif->icon[sizeof(notif->icon) - 1] = '\0';
    }

    notif->timeout = (timeout < 0) ? DEFAULT_TIMEOUT : timeout;
    notif->urgency = NOTIFY_URGENCY_NORMAL;
    notif->expire_time = (notif->timeout > 0) ?
                         get_time_ms() + notif->timeout : 0;

    int notif_width, notif_height;
    notification_calculate_size(summary, body, &notif_width, &notif_height);
    notif->width = notif_width;
    notif->height = notif_height;

    if (dwn != NULL && dwn->display != NULL) {
        XSetWindowAttributes swa;
        swa.override_redirect = True;
        swa.background_pixel = dwn->config->colors.notification_bg;
        swa.event_mask = ExposureMask | ButtonPressMask;

        notif->window = XCreateWindow(dwn->display, dwn->root,
                                      0, 0,
                                      notif->width, notif->height,
                                      0,
                                      CopyFromParent, InputOutput, CopyFromParent,
                                      CWOverrideRedirect | CWBackPixel | CWEventMask,
                                      &swa);
    }

    notif->next = notification_list;
    notification_list = notif;

    notifications_position();
    notification_render(notif);

    if (notif->window != None) {
        XMapRaised(dwn->display, notif->window);
    }

    LOG_DEBUG("Notification %u (%dx%d): %s - %s", notif->id,
              notif->width, notif->height, notif->summary, 
              notif->body ? notif->body : "(no body)");

    api_emit_notification_shown(notif->id, notif->summary, notif->body);

    return notif->id;
}

void notification_close(uint32_t id)
{
    Notification *prev = NULL;
    Notification *notif = notification_list;

    while (notif != NULL) {
        if (notif->id == id) {
            if (prev != NULL) {
                prev->next = notif->next;
            } else {
                notification_list = notif->next;
            }

            if (notif->window != None && dwn != NULL && dwn->display != NULL) {
                XDestroyWindow(dwn->display, notif->window);
            }

            if (notif->body != NULL) {
                dwn_free(notif->body);
                notif->body = NULL;
            }

            api_emit_notification_closed(id);

            dwn_free(notif);

            notifications_position();
            return;
        }

        prev = notif;
        notif = notif->next;
    }
}

void notification_close_all(void)
{
    while (notification_list != NULL) {
        notification_close(notification_list->id);
    }
}

Notification *notification_find(uint32_t id)
{
    for (Notification *n = notification_list; n != NULL; n = n->next) {
        if (n->id == id) {
            return n;
        }
    }
    return NULL;
}

Notification *notification_find_by_window(Window window)
{
    for (Notification *n = notification_list; n != NULL; n = n->next) {
        if (n->window == window) {
            return n;
        }
    }
    return NULL;
}


void notification_render(Notification *notif)
{
    if (notif == NULL || notif->window == None) {
        LOG_DEBUG("notification_render: notif or window is NULL");
        return;
    }

    if (dwn == NULL || dwn->display == NULL) {
        LOG_DEBUG("notification_render: dwn or display is NULL");
        return;
    }

    if (dwn->xft_font == NULL && dwn->font == NULL) {
        LOG_ERROR("notification_render: no font available - cannot render text");
        return;
    }

    Display *dpy = dwn->display;
    const ColorScheme *colors = config_get_colors();

    if (colors == NULL) {
        LOG_ERROR("notification_render: colors is NULL");
        return;
    }

    LOG_DEBUG("Rendering notification: summary='%s', body_len=%zu",
              notif->summary, notif->body_len);

    if (dwn->font != NULL) {
        XSetFont(dpy, dwn->gc, dwn->font->fid);
    }

    XSetForeground(dpy, dwn->gc, colors->notification_bg);
    XFillRectangle(dpy, notif->window, dwn->gc, 0, 0,
                   notif->width, notif->height);

    int line_height = notif_line_height();
    int y = NOTIFICATION_PADDING + notif_font_ascent();
    notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
                    notif->summary, strlen(notif->summary),
                    colors->notification_fg);

    y += line_height + 4;

    int max_width = notif->width - 2 * NOTIFICATION_PADDING;
    int max_y = notif->height - line_height - NOTIFICATION_PADDING;

    if (notif->body != NULL && notif->body_len > 0) {
        char *body_copy = dwn_malloc(notif->body_len + 1);
        if (body_copy != NULL) {
            memcpy(body_copy, notif->body, notif->body_len + 1);

            char *line = body_copy;
            char *next;

            while (line != NULL && *line != '\0' && y < max_y) {
                next = strchr(line, '\n');
                if (next != NULL) {
                    *next = '\0';
                    next++;
                }

                if (*line == '\0') {
                    y += line_height / 2;
                    line = next;
                    continue;
                }

                size_t line_len = strlen(line);
                const char *p = line;

                while (*p != '\0' && y < max_y) {
                    size_t fit_len = 0;
                    size_t last_space = 0;

                    for (size_t i = 0; p[i] != '\0'; i++) {
                        if ((p[i] & 0xC0) == 0x80) continue;

                        int width = notif_text_width(p, i + 1);
                        if (width > max_width) {
                            break;
                        }
                        fit_len = i + 1;

                        if (p[i] == ' ') {
                            last_space = i;
                        }
                    }

                    while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
                        fit_len++;
                    }

                    if (fit_len == 0 && *p != '\0') {
                        fit_len = 1;
                        while (fit_len < line_len && (p[fit_len] & 0xC0) == 0x80) {
                            fit_len++;
                        }
                    }

                    if (fit_len < strlen(p) && last_space > 0 && last_space > fit_len / 2) {
                        fit_len = last_space + 1;
                    }

                    if (fit_len > 0) {
                        char *segment = dwn_malloc(fit_len + 1);
                        if (segment != NULL) {
                            memcpy(segment, p, fit_len);
                            segment[fit_len] = '\0';

                            notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
                                            segment, strlen(segment), colors->panel_fg);

                            dwn_free(segment);
                        }

                        p += fit_len;
                        while (*p == ' ') p++;

                        y += line_height;
                    } else {
                        break;
                    }
                }

                line = next;
            }

            dwn_free(body_copy);
        }
    }

    if (notif->app_name[0] != '\0') {
        y = notif->height - NOTIFICATION_PADDING;
        notif_draw_text(notif->window, NOTIFICATION_PADDING, y,
                        notif->app_name, strlen(notif->app_name),
                        colors->workspace_inactive);
    }

    XFlush(dpy);
}

void notifications_render_all(void)
{
    for (Notification *n = notification_list; n != NULL; n = n->next) {
        notification_render(n);
    }
}

void notifications_update(void)
{
    long now = get_time_ms();

    Notification *notif = notification_list;
    while (notif != NULL) {
        Notification *next = notif->next;

        if (notif->expire_time > 0 && now >= notif->expire_time) {
            notification_close(notif->id);
        }

        notif = next;
    }
}

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

    int y = NOTIFICATION_MARGIN;

    if (dwn->config && dwn->config->top_panel_enabled) {
        y += config_get_panel_height();
    }

    int max_y = dwn->screen_height - NOTIFICATION_MARGIN;

    for (Notification *n = notification_list; n != NULL; n = n->next) {
        if (y + n->height > max_y) {
            if (n->window != None) {
                XUnmapWindow(dwn->display, n->window);
            }
            continue;
        }
        if (n->window != None) {
            int x = dwn->screen_width - n->width - NOTIFICATION_MARGIN;
            XMoveWindow(dwn->display, n->window, x, y);
            XMapRaised(dwn->display, n->window);
        }
        y += n->height + NOTIFICATION_MARGIN;
    }
}

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

    for (Notification *n = notification_list; n != NULL; n = n->next) {
        if (n->window != None) {
            XRaiseWindow(dwn->display, n->window);
        }
    }
}


void notifications_get_server_info(const char **name, const char **vendor,
                                   const char **version, const char **spec_version)
{
    if (name) *name = "DWN";
    if (vendor) *vendor = "DWN Project";
    if (version) *version = DWN_VERSION;
    if (spec_version) *spec_version = "1.2";
}

void notifications_get_capabilities(const char ***caps, int *count)
{
    static const char *capabilities[] = { "body", "body-markup" };
    if (caps) *caps = capabilities;
    if (count) *count = 2;
}
