/** Copyright 2011-2013 Thorsten Wißmann. All rights reserved.
 *
 * This software is licensed under the "Simplified BSD License".
 * See LICENSE for details */

#include "mouse.h"
#include "globals.h"
#include "clientlist.h"
#include "layout.h"
#include "key.h"
#include "ipc-protocol.h"
#include "utils.h"
#include "settings.h"
#include "command.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "glib-backports.h"

// gui
#include <X11/Xlib.h>
#include <X11/Xproto.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>

static Point2D          g_button_drag_start;
static Rectangle        g_win_drag_start;
static HSClient*        g_win_drag_client = NULL;
static HSMonitor*       g_drag_monitor = NULL;
static MouseDragFunction g_drag_function = NULL;

static Cursor g_cursor;
static GList* g_mouse_binds = NULL;
static unsigned int* g_numlockmask_ptr;
static int* g_snap_distance;
static int* g_snap_gap;

#define CLEANMASK(mask)         ((mask) & ~(*g_numlockmask_ptr|LockMask))
#define REMOVEBUTTONMASK(mask) ((mask) & \
    ~( Button1Mask \
     | Button2Mask \
     | Button3Mask \
     | Button4Mask \
     | Button5Mask ))

void mouse_init() {
    g_numlockmask_ptr = get_numlockmask_ptr();
    g_snap_distance = &(settings_find("snap_distance")->value.i);
    g_snap_gap = &(settings_find("snap_gap")->value.i);
    /* set cursor theme */
    g_cursor = XCreateFontCursor(g_display, XC_left_ptr);
    XDefineCursor(g_display, g_root, g_cursor);
}

void mouse_destroy() {
    mouse_unbind_all();
    XFreeCursor(g_display, g_cursor);
}

void mouse_handle_event(XEvent* ev) {
    XButtonEvent* be = &(ev->xbutton);
    MouseBinding* b = mouse_binding_find(be->state, be->button);
    HSClient* client = get_client_from_window(ev->xbutton.window);
    if (!b || !client) {
        // there is no valid bind for this type of mouse event
        return;
    }
    b->action(client, b->argc, b->argv);
}

void mouse_initiate_move(HSClient* client, int argc, char** argv) {
    (void) argc; (void) argv;
    mouse_initiate_drag(client, mouse_function_move);
}

void mouse_initiate_zoom(HSClient* client, int argc, char** argv) {
    (void) argc; (void) argv;
    mouse_initiate_drag(client, mouse_function_zoom);
}

void mouse_initiate_resize(HSClient* client, int argc, char** argv) {
    (void) argc; (void) argv;
    mouse_initiate_drag(client, mouse_function_resize);
}

void mouse_call_command(struct HSClient* client, int argc, char** argv) {
    // TODO: add completion
    client_set_dragged(client, true);
    call_command_no_output(argc, argv);
    client_set_dragged(client, false);
}


void mouse_initiate_drag(HSClient* client, MouseDragFunction function) {
    g_drag_function = function;
    g_win_drag_client = client;
    g_drag_monitor = find_monitor_with_tag(client->tag);
    if (!g_drag_monitor || client->tag->floating == false) {
        // only can drag wins in  floating mode
        g_win_drag_client = NULL;
        g_drag_function = NULL;
        return;
    }
    client_set_dragged(g_win_drag_client, true);
    g_win_drag_start = g_win_drag_client->float_size;
    g_button_drag_start = get_cursor_position();
    XGrabPointer(g_display, client->window, True,
        PointerMotionMask|ButtonReleaseMask, GrabModeAsync,
            GrabModeAsync, None, None, CurrentTime);
}

void mouse_stop_drag() {
    if (g_win_drag_client) {
        client_set_dragged(g_win_drag_client, false);
        // resend last size
        monitor_apply_layout(g_drag_monitor);
    }
    g_win_drag_client = NULL;
    g_drag_function = NULL;
    XUngrabPointer(g_display, CurrentTime);
    // remove all enternotify-events from the event queue that were
    // generated by the XUngrabPointer
    XEvent ev;
    XSync(g_display, False);
    while(XCheckMaskEvent(g_display, EnterWindowMask, &ev));
}

void handle_motion_event(XEvent* ev) {
    if (!g_drag_monitor) { return; }
    if (!g_win_drag_client) return;
    if (!g_drag_function) return;
    if (ev->type != MotionNotify) return;
    // get newest motion notification
    while (XCheckMaskEvent(g_display, ButtonMotionMask, ev));
    // call function that handles it
    g_drag_function(&(ev->xmotion));
}

bool mouse_is_dragging() {
    return g_drag_function != NULL;
}

static void mouse_binding_free(void* voidmb) {
    MouseBinding* mb = voidmb;
    if (!mb) return;
    argv_free(mb->argc, mb->argv);
    g_free(mb);
}

int mouse_unbind_all() {
    g_list_free_full(g_mouse_binds, mouse_binding_free);
    g_mouse_binds = NULL;
    HSClient* client = get_current_client();
    if (client) {
        grab_client_buttons(client, true);
    }
    return 0;
}

int mouse_binding_equals(MouseBinding* a, MouseBinding* b) {
    if((REMOVEBUTTONMASK(CLEANMASK(a->modifiers))
        == REMOVEBUTTONMASK(CLEANMASK(b->modifiers)))
        && (a->button == b->button)) {
        return 0;
    } else {
        return -1;
    }
}

int mouse_bind_command(int argc, char** argv, GString* output) {
    if (argc < 3) {
        return HERBST_NEED_MORE_ARGS;
    }
    unsigned int modifiers = 0;
    char* string = argv[1];
    if (!string2modifiers(string, &modifiers)) {
        g_string_append_printf(output,
            "%s: Modifier \"%s\" does not exist\n", argv[0], string);
        return HERBST_INVALID_ARGUMENT;
    }
    // last one is the mouse button
    char* last_token = strlasttoken(string, KEY_COMBI_SEPARATORS);
    unsigned int button = string2button(last_token);
    if (button == 0) {
        g_string_append_printf(output,
            "%s: Unknown mouse button \"%s\"\n", argv[0], last_token);
        return HERBST_INVALID_ARGUMENT;
    }
    MouseFunction function = string2mousefunction(argv[2]);
    if (!function) {
        g_string_append_printf(output,
            "%s: Unknown mouse action \"%s\"\n", argv[0], argv[2]);
        return HERBST_INVALID_ARGUMENT;
    }

    // actually create a binding
    MouseBinding* mb = g_new(MouseBinding, 1);
    mb->button = button;
    mb->modifiers = modifiers;
    mb->action = function;
    mb->argc = argc - 3;
    mb->argv = argv_duplicate(argc - 3, argv + 3);;
    g_mouse_binds = g_list_prepend(g_mouse_binds, mb);
    HSClient* client = get_current_client();
    if (client) {
        grab_client_buttons(client, true);
    }
    return 0;
}

MouseFunction string2mousefunction(char* name) {
    static struct {
        char* name;
        MouseFunction function;
    } table[] = {
        { "move",       mouse_initiate_move },
        { "zoom",       mouse_initiate_zoom },
        { "resize",     mouse_initiate_resize },
        { "call",       mouse_call_command },
    };
    int i;
    for (i = 0; i < LENGTH(table); i++) {
        if (!strcmp(table[i].name, name)) {
            return table[i].function;
        }
    }
    return NULL;
}

static struct {
    char* name;
    unsigned int button;
} string2button_table[] = {
    { "Button1",       Button1 },
    { "Button2",       Button2 },
    { "Button3",       Button3 },
    { "Button4",       Button4 },
    { "Button5",       Button5 },
    { "B1",       Button1 },
    { "B2",       Button2 },
    { "B3",       Button3 },
    { "B4",       Button4 },
    { "B5",       Button5 },
};
unsigned int string2button(char* name) {
    for (int i = 0; i < LENGTH(string2button_table); i++) {
        if (!strcmp(string2button_table[i].name, name)) {
            return string2button_table[i].button;
        }
    }
    return 0;
}


void complete_against_mouse_buttons(char* needle, char* prefix, GString* output) {
    for (int i = 0; i < LENGTH(string2button_table); i++) {
        char* buttonname = string2button_table[i].name;
        try_complete_prefix(needle, buttonname, prefix, output);
    }
}

MouseBinding* mouse_binding_find(unsigned int modifiers, unsigned int button) {
    MouseBinding mb = { .modifiers = modifiers, .button = button, 0};
    GList* elem = g_list_find_custom(g_mouse_binds, &mb,
                                     (GCompareFunc)mouse_binding_equals);
    return elem ? elem->data : NULL;
}

static void grab_client_button(MouseBinding* bind, HSClient* client) {
    unsigned int modifiers[] = { 0, LockMask, *g_numlockmask_ptr, *g_numlockmask_ptr|LockMask };
    for(int j = 0; j < LENGTH(modifiers); j++) {
        XGrabButton(g_display, bind->button,
                    bind->modifiers | modifiers[j],
                    client->window, False, ButtonPressMask | ButtonReleaseMask,
                    GrabModeAsync, GrabModeSync, None, None);
    }
}

void grab_client_buttons(HSClient* client, bool focused) {
    update_numlockmask();
    XUngrabButton(g_display, AnyButton, AnyModifier, client->window);
    if (focused) {
        g_list_foreach(g_mouse_binds, (GFunc)grab_client_button, client);
    }
    unsigned int btns[] = { Button1, Button2, Button3 };
    for (int i = 0; i < LENGTH(btns); i++) {
        XGrabButton(g_display, btns[i], AnyModifier, client->window, False,
                    ButtonPressMask|ButtonReleaseMask, GrabModeSync,
                    GrabModeSync, None, None);
    }
}

void mouse_function_move(XMotionEvent* me) {
    int x_diff = me->x_root - g_button_drag_start.x;
    int y_diff = me->y_root - g_button_drag_start.y;
    g_win_drag_client->float_size = g_win_drag_start;
    g_win_drag_client->float_size.x += x_diff;
    g_win_drag_client->float_size.y += y_diff;
    // snap it to other windows
    int dx, dy;
    client_snap_vector(g_win_drag_client, g_drag_monitor,
                       SNAP_EDGE_ALL, &dx, &dy);
    g_win_drag_client->float_size.x += dx;
    g_win_drag_client->float_size.y += dy;
    client_resize_floating(g_win_drag_client, g_drag_monitor);
}

void mouse_function_resize(XMotionEvent* me) {
    int x_diff = me->x_root - g_button_drag_start.x;
    int y_diff = me->y_root - g_button_drag_start.y;
    g_win_drag_client->float_size = g_win_drag_start;
    // relative x/y coords in drag window
    HSMonitor* m = g_drag_monitor;
    int rel_x = monitor_get_relative_x(m, g_button_drag_start.x) - g_win_drag_start.x;
    int rel_y = monitor_get_relative_y(m, g_button_drag_start.y) - g_win_drag_start.y;
    bool top = false;
    bool left = false;
    if (rel_y < g_win_drag_start.height/2) {
        top = true;
        y_diff *= -1;
    }
    if (rel_x < g_win_drag_start.width/2) {
        left = true;
        x_diff *= -1;
    }
    // avoid an overflow
    int new_width  = g_win_drag_client->float_size.width + x_diff;
    int new_height = g_win_drag_client->float_size.height + y_diff;
    int min_width = WINDOW_MIN_WIDTH;
    int min_height = WINDOW_MIN_HEIGHT;
    HSClient* client = g_win_drag_client;
    if (client->sizehints_floating) {
        min_width = MAX(WINDOW_MIN_WIDTH, client->minw);
        min_height = MAX(WINDOW_MIN_HEIGHT, client->minh);
    }
    if (new_width <  min_width) {
        new_width = min_width;
        x_diff = new_width - g_win_drag_client->float_size.width;
    }
    if (new_height < min_height) {
        new_height = min_height;
        y_diff = new_height - g_win_drag_client->float_size.height;
    }
    if (left)   g_win_drag_client->float_size.x -= x_diff;
    if (top)    g_win_drag_client->float_size.y -= y_diff;
    g_win_drag_client->float_size.width  = new_width;
    g_win_drag_client->float_size.height = new_height;
    // snap it to other windows
    int dx, dy;
    int snap_flags = 0;
    if (left)   snap_flags |= SNAP_EDGE_LEFT;
    else        snap_flags |= SNAP_EDGE_RIGHT;
    if (top)    snap_flags |= SNAP_EDGE_TOP;
    else        snap_flags |= SNAP_EDGE_BOTTOM;
    client_snap_vector(g_win_drag_client, g_drag_monitor,
                       snap_flags, &dx, &dy);
    if (left) {
        g_win_drag_client->float_size.x += dx;
        dx *= -1;
    }
    if (top) {
        g_win_drag_client->float_size.y += dy;
        dy *= -1;
    }
    g_win_drag_client->float_size.width += dx;
    g_win_drag_client->float_size.height += dy;
    client_resize_floating(g_win_drag_client, g_drag_monitor);
}

void mouse_function_zoom(XMotionEvent* me) {
    // stretch, where center stays at the same position
    int x_diff = me->x_root - g_button_drag_start.x;
    int y_diff = me->y_root - g_button_drag_start.y;
    // relative x/y coords in drag window
    HSMonitor* m = g_drag_monitor;
    int rel_x = monitor_get_relative_x(m, g_button_drag_start.x) - g_win_drag_start.x;
    int rel_y = monitor_get_relative_y(m, g_button_drag_start.y) - g_win_drag_start.y;
    int cent_x = g_win_drag_start.x + g_win_drag_start.width  / 2;
    int cent_y = g_win_drag_start.y + g_win_drag_start.height / 2;
    if (rel_x < g_win_drag_start.width/2) {
        x_diff *= -1;
    }
    if (rel_y < g_win_drag_start.height/2) {
        y_diff *= -1;
    }
    HSClient* client = g_win_drag_client;

    // avoid an overflow
    int new_width  = g_win_drag_start.width  + 2 * x_diff;
    int new_height = g_win_drag_start.height + 2 * y_diff;
    // apply new rect
    client->float_size = g_win_drag_start;
    client->float_size.x = cent_x - new_width / 2;
    client->float_size.y = cent_y - new_height / 2;
    client->float_size.width = new_width;
    client->float_size.height = new_height;
    // snap it to other windows
    int right_dx, bottom_dy;
    int left_dx, top_dy;
    // we have to distinguish the direction in which we zoom
    client_snap_vector(g_win_drag_client, m,
                     SNAP_EDGE_BOTTOM | SNAP_EDGE_RIGHT, &right_dx, &bottom_dy);
    client_snap_vector(g_win_drag_client, m,
                       SNAP_EDGE_TOP | SNAP_EDGE_LEFT, &left_dx, &top_dy);
    // e.g. if window snaps by vector (3,3) at topleft, window has to be shrinked
    // but if the window snaps by vector (3,3) at bottomright, window has to grow
    if (abs(right_dx) < abs(left_dx)) {
        right_dx = -left_dx;
    }
    if (abs(bottom_dy) < abs(top_dy)) {
        bottom_dy = -top_dy;
    }
    new_width += 2 * right_dx;
    new_height += 2 * bottom_dy;
    applysizehints(client, &new_width, &new_height);
    // center window again
    client->float_size.width = new_width;
    client->float_size.height = new_height;
    client->float_size.x = cent_x - new_width / 2;
    client->float_size.y = cent_y - new_height / 2;
    client_resize_floating(g_win_drag_client, g_drag_monitor);
}

struct SnapData {
    HSClient*       client;
    Rectangle      rect;
    enum SnapFlags  flags;
    int             dx, dy; // the vector from client to other to make them snap
};

bool is_point_between(int point, int left, int right) {
    return (point < right && point >= left);
}

// tells if the intervals [a_left, a_right) [b_left, b_right) intersect
bool intervals_intersect(int a_left, int a_right, int b_left, int b_right) {
    return (b_left < a_right) && (a_left < b_right);
}

// compute vector to snap a point to an edge
static void snap_1d(int x, int edge, int* delta) {
    // whats the vector from subject to edge?
    int cur_delta = edge - x;
    // if distance is smaller then all other deltas
    if (abs(cur_delta) < abs(*delta)) {
        // then snap it, i.e. save vector
        *delta = cur_delta;
    }
}

static int client_snap_helper(HSClient* candidate, struct SnapData* d) {
    if (candidate == d->client) {
        return 0;
    }
    Rectangle subject  = d->rect;
    Rectangle other    = candidate->dec.last_outer_rect;
    // increase other by snap gap
    other.x -= *g_snap_gap;
    other.y -= *g_snap_gap;
    other.width += *g_snap_gap * 2;
    other.height += *g_snap_gap * 2;
    if (intervals_intersect(other.y, other.y + other.height, subject.y, subject.y + subject.height)) {
        // check if x can snap to the right
        if (d->flags & SNAP_EDGE_RIGHT) {
            snap_1d(subject.x + subject.width, other.x, &d->dx);
        }
        // or to the left
        if (d->flags & SNAP_EDGE_LEFT) {
            snap_1d(subject.x, other.x + other.width, &d->dx);
        }
    }
    if (intervals_intersect(other.x, other.x + other.width, subject.x, subject.x + subject.width)) {
        // if we can snap to the top
        if (d->flags & SNAP_EDGE_TOP) {
            snap_1d(subject.y, other.y + other.height, &d->dy);
        }
        // or to the bottom
        if (d->flags & SNAP_EDGE_BOTTOM) {
            snap_1d(subject.y + subject.height, other.y, &d->dy);
        }
    }
    return 0;
}

// get the vector to snap a client to it's neighbour
void client_snap_vector(struct HSClient* client, struct HSMonitor* monitor,
                        enum SnapFlags flags,
                        int* return_dx, int* return_dy) {
    struct SnapData d;
    HSTag* tag = monitor->tag;
    int distance = (*g_snap_distance > 0) ? *g_snap_distance : 0;
    // init delta
    *return_dx = 0;
    *return_dy = 0;
    if (!distance) {
        // nothing to do
        return;
    }
    d.client    = client;
    // translate client rectangle to global coordinates
    d.rect      = client_outer_floating_rect(client);
    d.rect.x += monitor->rect.x + monitor->pad_left;
    d.rect.y += monitor->rect.y + monitor->pad_up;
    d.flags     = flags;
    d.dx        = distance;
    d.dy        = distance;

    // snap to monitor edges
    HSMonitor* m = g_drag_monitor;
    if (flags & SNAP_EDGE_TOP) {
        snap_1d(d.rect.y, m->rect.y + m->pad_up + *g_snap_gap, &d.dy);
    }
    if (flags & SNAP_EDGE_LEFT) {
        snap_1d(d.rect.x, m->rect.x + m->pad_left + *g_snap_gap, &d.dx);
    }
    if (flags & SNAP_EDGE_RIGHT) {
        snap_1d(d.rect.x + d.rect.width, m->rect.x + m->rect.width - m->pad_right - *g_snap_gap, &d.dx);
    }
    if (flags & SNAP_EDGE_BOTTOM) {
        snap_1d(d.rect.y + d.rect.height, m->rect.y + m->rect.height - m->pad_down - *g_snap_gap, &d.dy);
    }

    // snap to other clients
    frame_foreach_client(tag->frame, (ClientAction)client_snap_helper, &d);

    // write back results
    if (abs(d.dx) < abs(distance)) {
        *return_dx = d.dx;
    }
    if (abs(d.dy) < abs(distance)) {
        *return_dy = d.dy;
    }
}

