#include "x11.h"

#include <stdlib.h>
#include <cairo-xlib.h>
#include <X11/extensions/Xinerama.h>

static void
destroy_buffer(struct buffer *buffer)
{
    bm_cairo_destroy(&buffer->cairo);
    memset(buffer, 0, sizeof(struct buffer));
}

static bool
create_buffer(struct window *window, struct buffer *buffer, int32_t width, int32_t height)
{
    cairo_surface_t *surf;
    if (!(surf = cairo_xlib_surface_create(window->display, window->drawable, DefaultVisual(window->display, window->screen), width, height)))
        goto fail;

    cairo_xlib_surface_set_size(surf, width, height);

    if (!bm_cairo_create_for_surface(&buffer->cairo, surf)) {
        cairo_surface_destroy(surf);
        goto fail;
    }

    buffer->width = width;
    buffer->height = height;
    buffer->created = true;
    return true;

fail:
    destroy_buffer(buffer);
    return false;
}

static struct buffer*
next_buffer(struct window *window)
{
    assert(window);

    struct buffer *buffer = &window->buffer;

    if (!buffer)
        return NULL;

    if (window->width != buffer->width || window->height != buffer->height)
        destroy_buffer(buffer);

    if (!buffer->created && !create_buffer(window, buffer, window->width, window->height))
        return NULL;

    return buffer;
}

void
bm_x11_window_render(struct window *window, const struct bm_menu *menu)
{
    assert(window && menu);
    uint32_t oldw = window->width, oldh = window->height;

    struct buffer *buffer;
    for (int32_t tries = 0; tries < 2; ++tries) {
        if (!(buffer = next_buffer(window))) {
            fprintf(stderr, "could not get next buffer");
            exit(EXIT_FAILURE);
        }

        if (!window->notify.render)
            break;

        cairo_push_group(buffer->cairo.cr);
        struct cairo_paint_result result;
        window->notify.render(&buffer->cairo, buffer->width, buffer->height, window->max_height, menu, &result);
        window->displayed = result.displayed;
        cairo_pop_group_to_source(buffer->cairo.cr);

        if (window->height == result.height)
            break;

        window->height = result.height;
        destroy_buffer(buffer);
    }

    if (oldw != window->width || oldh != window->height) {
        if (window->bottom) {
            XMoveResizeWindow(window->display, window->drawable, window->x, window->max_height - window->height, window->width, window->height);
        } else {
            XResizeWindow(window->display, window->drawable, window->width, window->height);
        }
    }

    if (buffer->created) {
        cairo_paint(buffer->cairo.cr);
        cairo_surface_flush(buffer->cairo.surface);
    }
}

void
bm_x11_window_key_press(struct window *window, XKeyEvent *ev)
{
    KeySym keysym = NoSymbol;
    XmbLookupString(window->xic, ev, NULL, 0, &keysym, NULL);
    window->mods = 0;
    if (ev->state & ControlMask) window->mods |= MOD_CTRL;
    if (ev->state & ShiftMask) window->mods |= MOD_SHIFT;
    if (ev->state & Mod1Mask) window->mods |= MOD_ALT;
    window->keysym = keysym;
}

void
bm_x11_window_destroy(struct window *window)
{
    assert(window);
    destroy_buffer(&window->buffer);

    if (window->display && window->drawable)
        XDestroyWindow(window->display, window->drawable);
}

void
bm_x11_window_set_monitor(struct window *window, uint32_t monitor)
{
    if (window->monitor == monitor)
        return;

    Window root = DefaultRootWindow(window->display);

    {
        /* xinerama logic straight from dmenu */
#define INTERSECT(x,y,w,h,r)  (fmax(0, fmin((x)+(w),(r).x_org+(r).width) - fmax((x),(r).x_org)) * fmax(0, fmin((y)+(h),(r).y_org+(r).height) - fmax((y),(r).y_org)))

        int32_t n;
        XineramaScreenInfo *info;
        if ((info = XineramaQueryScreens(window->display, &n))) {
            int32_t x, y, a, j, di, i = 0, area = 0;
            uint32_t du;
            Window w, pw, dw, *dws;
            XWindowAttributes wa;

            XGetInputFocus(window->display, &w, &di);
            if (monitor > 0)
                i = ((int32_t)monitor > n ? n : (int32_t)monitor) - 1;

            if (monitor == 0 && w != root && w != PointerRoot && w != None) {
                /* find top-level window containing current input focus */
                do {
                    if (XQueryTree(window->display, (pw = w), &dw, &w, &dws, &du) && dws)
                        XFree(dws);
                } while(w != root && w != pw);

                /* find xinerama screen with which the window intersects most */
                if (XGetWindowAttributes(window->display, pw, &wa)) {
                    for (j = 0; j < n; j++)
                        if ((a = INTERSECT(wa.x, wa.y, wa.width, wa.height, info[j])) > area) {
                            area = a;
                            i = j;
                        }
                }
            }

            /* no focused window is on screen, so use pointer location instead */
            if (monitor == 0 && !area && XQueryPointer(window->display, root, &dw, &dw, &x, &y, &di, &di, &du)) {
                for (i = 0; i < n; i++) {
                    if (INTERSECT(x, y, 1, 1, info[i]))
                        break;
                }
            }

            window->x = info[i].x_org;
            window->y = info[i].y_org + (window->bottom ? info[i].height - window->height : 0);
            window->width = info[i].width;
            window->max_height = info[i].height;
            XFree(info);
        } else {
            window->max_height = DisplayHeight(window->display, window->screen);
            window->x = 0;
            window->y = (window->bottom ? window->max_height - window->height : 0);
            window->width = DisplayWidth(window->display, window->screen);
        }

#undef INTERSECT
    }

    window->monitor = monitor;
    XMoveResizeWindow(window->display, window->drawable, window->x, window->y, window->width, window->height);
    XFlush(window->display);
}

void
bm_x11_window_set_bottom(struct window *window, bool bottom)
{
    if (window->bottom == bottom)
        return;

    window->bottom = bottom;
    bm_x11_window_set_monitor(window, window->monitor);
}

bool
bm_x11_window_create(struct window *window, Display *display)
{
    assert(window);

    window->display = display;
    window->screen = DefaultScreen(display);
    window->width = window->height = 1;
    window->monitor = -1;

    XSetWindowAttributes wa = {
        .override_redirect = True,
        .event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask
    };

    window->drawable = XCreateWindow(display, DefaultRootWindow(display), 0, 0, window->width, window->height, 0, DefaultDepth(display, window->screen), CopyFromParent, DefaultVisual(display, window->screen), CWOverrideRedirect | CWBackPixel | CWEventMask, &wa);

    XSelectInput(display, window->drawable, ButtonPressMask | KeyPressMask);
    XMapRaised(display, window->drawable);
    window->xim = XOpenIM(display, NULL, NULL, NULL);
    window->xic = XCreateIC(window->xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, window->drawable, XNFocusWindow, window->drawable, NULL);
    return true;
}

/* vim: set ts=8 sw=4 tw=0 :*/