// SPDX-FileCopyrightText: 2023 KylinSoft Co., Ltd.
//
// SPDX-License-Identifier: GPL-1.0-or-later

#include <stdlib.h>

#include <linux/input-event-codes.h>
#include <wlr/types/wlr_seat.h>

#include "effect/move.h"
#include "effect/node_transform.h"
#include "input/cursor.h"
#include "input/seat.h"
#include "output.h"
#include "scene/surface.h"
#include "theme.h"
#include "util/color.h"
#include "util/macros.h"
#include "util/time.h"
#include "view/action.h"
#include "view/workspace.h"
#include "view_p.h"

#define VIEW_EDGE_GAP 20
#define VIEW_TOP_GAP 5
#define VIEW_BOTTOM_GAP 100
#define VIEW_LEFT_GAP 100
#define VIEW_RIGHT_GAP 100
#define VIEW_MIN_WIDTH 200
#define VIEW_MIN_HEIGHT 100
#define VIEW_MOVE_STEP 10
#define VIEW_RESIZE_STEP 10
#define SNAP_BOX_FILTER 200
#define SNAP_BORDER_CORNER_RATIO 0.25
#define EDGE_OFFSET 10

enum interactive_mode {
    INTERACTIVE_MODE_NONE = 0,
    INTERACTIVE_MODE_MOVE,
    INTERACTIVE_MODE_RESIZE,
    INTERACTIVE_MODE_TILE,
    INTERACTIVE_MODE_TILE_HALF_SCREEN,
};

enum tile_state {
    TILE_NONE = 0,
    TILE_TOP,
    TILE_BOTTOM,
    TILE_LEFT,
    TILE_RIGHT,
    TILE_TOP_LEFT,
    TILE_BOTTOM_LEFT,
    TILE_TOP_RIGHT,
    TILE_BOTTOM_RIGHT,
    TILE_MINIMIZE,
    TILE_MAXIMIZE,
};

struct interactive_tile_state {
    int key_up;
    int key_down;
    int key_left;
    int key_right;
};

const struct interactive_tile_state tile_states[] = {
    { TILE_MAXIMIZE, TILE_MINIMIZE, TILE_LEFT, TILE_RIGHT },            // TILE_NONE
    { TILE_TOP, TILE_NONE, TILE_TOP_LEFT, TILE_TOP_RIGHT },             // TILE_TOP
    { TILE_NONE, TILE_MINIMIZE, TILE_BOTTOM_LEFT, TILE_BOTTOM_RIGHT },  // TILE_BOTTOM
    { TILE_TOP_LEFT, TILE_BOTTOM_LEFT, TILE_RIGHT, TILE_NONE },         // TILE_LEFT
    { TILE_TOP_RIGHT, TILE_BOTTOM_RIGHT, TILE_NONE, TILE_LEFT },        // TILE_RIGHT
    { TILE_MAXIMIZE, TILE_LEFT, TILE_TOP_RIGHT, TILE_TOP_RIGHT },       // TILE_TOP_LEFT
    { TILE_LEFT, TILE_MINIMIZE, TILE_BOTTOM_RIGHT, TILE_BOTTOM_RIGHT }, // TILE_BOTTOM_LEFT
    { TILE_MAXIMIZE, TILE_RIGHT, TILE_TOP_LEFT, TILE_TOP_LEFT },        // TILE_TOP_RIGHT
    { TILE_RIGHT, TILE_MINIMIZE, TILE_BOTTOM_LEFT, TILE_BOTTOM_LEFT },  // TILE_BOTTOM_RIGHT
    { TILE_NONE, TILE_MINIMIZE, TILE_MINIMIZE, TILE_MINIMIZE },         // TILE_MINIMIZE
    { TILE_TOP, TILE_NONE, TILE_LEFT, TILE_RIGHT },                     // TILE_MAXIMIZE
};

struct interactive_grab {
    struct seat_pointer_grab pointer_grab;
    struct seat_keyboard_grab keyboard_grab;
    struct seat_touch_grab touch_grab;

    struct seat *seat;
    // struct wl_listener *seat_destroy;
    struct output *output;

    /* view being moved or resized */
    struct view *view;
    struct wl_listener view_unmap;

    /* move or resize */
    enum interactive_mode mode;
    /* moved or resized actually */
    bool ongoing;
    /* tile view resize linkage */
    bool has_linkage;
    /* cursor position before move or resize */
    double cursor_x, cursor_y;
    /* view position and size before move or resize */
    struct kywc_box geo;
    /* resize edges */
    uint32_t resize_edges;
    uint32_t last_time;

    /* snap_box */
    struct ky_scene_node *snap_node;
    struct ky_scene_rect *snap_rect;
    struct output *snap_output;
    enum kywc_tile snap_mode;
    struct wl_event_source *filter;
    bool filter_enabled;

    /* move effect */
    struct move_proxy *proxy;
    struct wl_listener proxy_destroy;

    struct kywc_box saved_usable_area;
    bool update_saved_geo;
};

static enum kywc_tile get_kywc_tile_mode(struct interactive_grab *grab)
{
    double cur_x = grab->seat->cursor->lx;
    double cur_y = grab->seat->cursor->ly;
    enum kywc_tile mode = KYWC_TILE_NONE;
    /* current output usable area */
    struct kywc_box *usable = &grab->output->usable_area;
    int32_t y1 = cur_y - usable->y;

    /* left */
    if (cur_x - usable->x < VIEW_EDGE_GAP) {
        /* top left*/
        if (y1 < usable->height * SNAP_BORDER_CORNER_RATIO) {
            mode = KYWC_TILE_TOP_LEFT;
            /* bottom left */
        } else if (y1 > usable->height - usable->height * SNAP_BORDER_CORNER_RATIO) {
            mode = KYWC_TILE_BOTTOM_LEFT;
            /* left */
        } else {
            mode = KYWC_TILE_LEFT;
        }
        /* right */
    } else if (usable->x + usable->width - cur_x < VIEW_EDGE_GAP) {
        /* top right*/
        if (y1 < usable->height * SNAP_BORDER_CORNER_RATIO) {
            mode = KYWC_TILE_TOP_RIGHT;
            /* bottom right */
        } else if (y1 > usable->height - usable->height * SNAP_BORDER_CORNER_RATIO) {
            mode = KYWC_TILE_BOTTOM_RIGHT;
            /* right */
        } else {
            mode = KYWC_TILE_RIGHT;
        }
        /* all */
    } else if (y1 < VIEW_TOP_GAP) {
        if (KYWC_VIEW_IS_MAXIMIZABLE(&grab->view->base)) {
            mode = KYWC_TILE_ALL;
        }
    }

    return mode;
}

static void snap_box_update(struct interactive_grab *grab, enum kywc_tile mode)
{
    if (grab->snap_mode == mode && grab->snap_output == grab->output) {
        return;
    }

    struct view *view = grab->view;
    bool need_source_box = grab->snap_mode == KYWC_TILE_NONE;

    if (!grab->snap_rect) {
        struct ky_scene_node *sibling = &view->tree->node;
        float color[4];
        struct theme *theme = theme_manager_get_theme();
        color_float_pax(color, theme->active_bg_color, theme->opacity / 100.0);
        grab->snap_rect = ky_scene_rect_create(sibling->parent, 0, 0, color);
        grab->snap_node = &grab->snap_rect->node;
        /* add blur */
        pixman_region32_t region;
        pixman_region32_init(&region);
        ky_scene_node_set_blur_region(grab->snap_node, theme->opacity != 100 ? &region : NULL);
        pixman_region32_fini(&region);

        ky_scene_node_place_below(grab->snap_node, sibling);
        need_source_box = true;
    }

    grab->snap_mode = mode;
    grab->snap_output = grab->output;
    ky_scene_node_set_enabled(grab->snap_node, mode != KYWC_TILE_NONE);
    if (mode == KYWC_TILE_NONE) {
        return;
    }

    struct kywc_box geo = { 0 };
    view_get_tiled_geometry(view, &geo, &grab->output->base, mode);
    geo.x -= view->base.margin.off_x;
    geo.y -= view->base.margin.off_y;
    geo.width += view->base.margin.off_width;
    geo.height += view->base.margin.off_height;

    if (need_source_box) {
        struct wlr_box output_box = { geo.x, geo.y, geo.width, geo.height };
        struct wlr_box view_box = { view->base.geometry.x - view->base.margin.off_x,
                                    view->base.geometry.y - view->base.margin.off_y,
                                    view->base.geometry.width + view->base.margin.off_x,
                                    view->base.geometry.height + view->base.margin.off_y };
        struct wlr_box rect_box;
        wlr_box_intersection(&rect_box, &output_box, &view_box);
        ky_scene_rect_set_size(grab->snap_rect, rect_box.width, rect_box.height);
        ky_scene_node_set_position(grab->snap_node, rect_box.x, rect_box.y);
    }

    struct kywc_box start_geo = { grab->snap_rect->node.x, grab->snap_rect->node.y,
                                  grab->snap_rect->width, grab->snap_rect->height };
    struct transform_options options = {
        .start_time = current_time_msec(),
        .duration = 200,
        .start = { .geometry = start_geo, .alpha = 1.0 },
        .end = { .geometry = geo, .alpha = 1.0 },
        .animations = { .geometry = animation_manager_get(ANIMATION_TYPE_EASE_OUT) },
    };
    node_add_transform_effect(grab->snap_node, &options);

    ky_scene_node_set_position(grab->snap_node, geo.x, geo.y);
    ky_scene_rect_set_size(grab->snap_rect, geo.width, geo.height);
}

static void snap_box_enable_filter(struct interactive_grab *grab, bool enabled)
{
    if (grab->filter_enabled == enabled) {
        return;
    }
    grab->filter_enabled = enabled;
    wl_event_source_timer_update(grab->filter, enabled ? SNAP_BOX_FILTER : 0);
}

static int handle_snap_box(void *data)
{
    struct interactive_grab *grab = data;
    grab->output = input_current_output(grab->seat);
    enum kywc_tile mode = get_kywc_tile_mode(grab);

    grab->filter_enabled = false;
    snap_box_update(grab, mode);
    return 0;
}

static void interactive_move_show_snap_box(struct interactive_grab *grab, int cur_x, int cur_y)
{
    struct view *view = grab->view;
    if (!KYWC_VIEW_IS_RESIZABLE(&view->base)) {
        return;
    }

    struct kywc_box *usable = &grab->output->usable_area;

    /* left */
    if (cur_x - usable->x < VIEW_EDGE_GAP) {
        /* trigger timer to show snap box if not the leftmost output */
        if (!output_at_layout_edge(grab->output, LAYOUT_EDGE_LEFT) &&
            grab->snap_mode != KYWC_TILE_TOP_LEFT && grab->snap_mode != KYWC_TILE_LEFT &&
            grab->snap_mode != KYWC_TILE_BOTTOM_LEFT) {
            snap_box_update(grab, KYWC_TILE_NONE);
            snap_box_enable_filter(grab, true);
            return;
        }
        /* right */
    } else if (usable->x + usable->width - cur_x < VIEW_EDGE_GAP) {
        /* trigger timer to show snap box if not the rightmost output */
        if (!output_at_layout_edge(grab->output, LAYOUT_EDGE_RIGHT) &&
            grab->snap_mode != KYWC_TILE_RIGHT && grab->snap_mode != KYWC_TILE_TOP_RIGHT &&
            grab->snap_mode != KYWC_TILE_BOTTOM_RIGHT) {
            snap_box_update(grab, KYWC_TILE_NONE);
            snap_box_enable_filter(grab, true);
            return;
        }
        /* all */
    } else if (cur_y - usable->y < VIEW_TOP_GAP) {
        /* trigger timer to show snap box if not the topmost output */
        if (KYWC_VIEW_IS_MAXIMIZABLE(&view->base) &&
            !output_at_layout_edge(grab->output, LAYOUT_EDGE_TOP) &&
            grab->snap_mode != KYWC_TILE_ALL) {
            snap_box_update(grab, KYWC_TILE_NONE);
            snap_box_enable_filter(grab, true);
            return;
        }
    }

    enum kywc_tile mode = get_kywc_tile_mode(grab);
    snap_box_enable_filter(grab, false);
    snap_box_update(grab, mode);
}

static void interactive_grab_destroy(struct interactive_grab *grab)
{
    grab->view->interactive_moving = false;
    grab->view->current_resize_edges = KYWC_EDGE_NONE;

    wl_list_remove(&grab->view_unmap.link);
    if (grab->proxy) {
        wl_list_remove(&grab->proxy_destroy.link);
        move_proxy_destroy(grab->proxy);
    }
    if (grab->has_linkage) {
        tile_linkage_resize_done(grab->view, false);
        grab->has_linkage = false;
    }

    cursor_set_image(grab->seat->cursor, CURSOR_DEFAULT);
    seat_end_pointer_grab(grab->seat, &grab->pointer_grab);
    seat_end_keyboard_grab(grab->seat, &grab->keyboard_grab);
    seat_end_touch_grab(grab->seat, &grab->touch_grab);
    ky_scene_node_destroy(grab->snap_node);

    /* sync the position to client */
    int lx = grab->view->base.geometry.x;
    int ly = grab->view->base.geometry.y;
    kywc_view_move(&grab->view->base, lx, ly);

    wl_event_source_remove(grab->filter);
    free(grab);
}

static void interactivate_done_move(struct interactive_grab *grab)
{
    grab->output = input_current_output(grab->seat);
    snap_box_update(grab, KYWC_TILE_NONE);
    enum kywc_tile mode = get_kywc_tile_mode(grab);
    if (mode == KYWC_TILE_NONE) {
        return;
    }

    if (mode == KYWC_TILE_ALL) {
        /* current cursor focused output, not the view most at output */
        kywc_view_set_maximized(&grab->view->base, true, &grab->output->base);
    } else {
        kywc_view_set_tiled(&grab->view->base, mode, &grab->output->base);
    }

    if (grab->update_saved_geo) {
        grab->view->saved.geometry = grab->geo;
        if (!kywc_box_equal(&grab->saved_usable_area, &grab->output->usable_area)) {
            view_fix_geometry(grab->view, &grab->view->saved.geometry, &grab->saved_usable_area,
                              &grab->output->usable_area);
        }
    }
}

static void interactive_done(struct interactive_grab *grab)
{
    bool need_assist = false;

    /* for snap to edge */
    if (grab->view->base.mapped && grab->ongoing && grab->mode == INTERACTIVE_MODE_MOVE) {
        interactivate_done_move(grab);
        if (grab->view->base.tiled != KYWC_TILE_NONE && grab->view->base.tiled != KYWC_TILE_ALL) {
            need_assist = true;
        }
    }

    if (grab->mode == INTERACTIVE_MODE_TILE || grab->mode == INTERACTIVE_MODE_TILE_HALF_SCREEN) {
        if (!grab->view->base.minimized && !grab->view->base.maximized && grab->view->base.tiled) {
            need_assist = true;
        }
    }

    struct view *view = grab->view;
    struct output *output = grab->output;
    struct seat *seat = grab->seat;
    interactive_grab_destroy(grab);

    if (need_assist) {
        view_manager_show_tile_assist(view, seat, &output->base);
    }
}

static void window_adsorption_top_or_bottom(struct kywc_box *s_box, const struct kywc_box *l_box,
                                            int *offset, enum interactive_mode mode)
{
    int sx1 = s_box->x, sy1 = s_box->y, sx2 = s_box->x + s_box->width,
        sy2 = s_box->y + s_box->height;
    int lx1 = l_box->x, ly1 = l_box->y, lx2 = l_box->x + l_box->width,
        ly2 = l_box->y + l_box->height;

    if (sx1 > lx2 || sx2 < lx1) {
        return;
    }

    /* top adsorb bottom */
    int temp = abs(ly2 - sy1);
    if (temp < *offset) {
        *offset = temp;
        s_box->y = ly2;
        if (mode == INTERACTIVE_MODE_RESIZE) {
            s_box->height = sy2 - ly2;
        }
        return;
    }

    /* bottom adsorb top */
    temp = abs(ly1 - sy2);
    if (temp < *offset) {
        *offset = temp;
        if (mode == INTERACTIVE_MODE_MOVE) {
            s_box->y = ly1 - (sy2 - sy1);
        } else if (mode == INTERACTIVE_MODE_RESIZE) {
            s_box->height += temp;
        }
    }
}

static void window_adsorption_left_or_right(struct kywc_box *s_box, const struct kywc_box *l_box,
                                            int *offset, enum interactive_mode mode)
{
    int sx1 = s_box->x, sy1 = s_box->y, sx2 = s_box->x + s_box->width,
        sy2 = s_box->y + s_box->height;
    int lx1 = l_box->x, ly1 = l_box->y, lx2 = l_box->x + l_box->width,
        ly2 = l_box->y + l_box->height;

    if (sy1 > ly2 || sy2 < ly1) {
        return;
    }

    /* left adsorb right */
    int temp = abs(lx2 - sx1);
    if (temp < *offset) {
        *offset = temp;
        s_box->x = lx2;
        if (mode == INTERACTIVE_MODE_RESIZE) {
            s_box->width = sx2 - lx2;
        }
        return;
    }

    /* right adsorb left */
    temp = abs(lx1 - sx2);
    if (temp < *offset) {
        *offset = temp;
        if (mode == INTERACTIVE_MODE_MOVE) {
            s_box->x = lx1 - (sx2 - sx1);
        } else if (mode == INTERACTIVE_MODE_RESIZE) {
            s_box->width += temp;
        }
    }
}

static void window_adsorb_window_constraints(struct kywc_view *kywc_view, struct kywc_box *pending,
                                             int *gap_x, int *gap_y, uint32_t edges,
                                             enum interactive_mode mode)
{
    if ((view_manager_get_adsorption() & VIEW_ADSORPTION_WINDOW_EDGES) == 0) {
        return;
    }

    /* actual window box */
    struct kywc_box s_box = {
        .x = pending->x - kywc_view->margin.off_x,
        .y = pending->y - kywc_view->margin.off_y,
        .width = pending->width + kywc_view->margin.off_width,
        .height = pending->height + kywc_view->margin.off_height,
    };

    struct view_proxy *view_proxy;
    struct workspace *workspace = workspace_manager_get_current();
    wl_list_for_each(view_proxy, &workspace->view_proxies, workspace_link) {
        if (!view_proxy->view->base.mapped || view_proxy->view == view_from_kywc_view(kywc_view) ||
            view_proxy->view->base.minimized || view_proxy->view->base.maximized ||
            view_proxy->view->base.fullscreen || !view_proxy->view->tree->node.enabled) {
            continue;
        }

        /* be adsorbed window box */
        struct kywc_box l_box = {
            .x = view_proxy->view->base.geometry.x - view_proxy->view->base.margin.off_x,
            .y = view_proxy->view->base.geometry.y - view_proxy->view->base.margin.off_y,
            .width =
                view_proxy->view->base.geometry.width + view_proxy->view->base.margin.off_width,
            .height =
                view_proxy->view->base.geometry.height + view_proxy->view->base.margin.off_height,
        };

        if (edges & KYWC_EDGE_LEFT || edges & KYWC_EDGE_RIGHT) {
            window_adsorption_left_or_right(&s_box, &l_box, gap_x, mode);
        }
        if (edges & KYWC_EDGE_TOP || edges & KYWC_EDGE_BOTTOM) {
            window_adsorption_top_or_bottom(&s_box, &l_box, gap_y, mode);
        }
    }

    pending->x = s_box.x + kywc_view->margin.off_x;
    pending->y = s_box.y + kywc_view->margin.off_y;
    if (mode == INTERACTIVE_MODE_RESIZE) {
        pending->width = s_box.width - kywc_view->margin.off_width;
        pending->height = s_box.height - kywc_view->margin.off_height;
    }
}

static void window_adsorb_edges_constraints(struct kywc_view *kywc_view, struct kywc_box *pending,
                                            struct output *output, int *gap_x, int *gap_y)
{
    if ((view_manager_get_adsorption() & VIEW_ADSORPTION_SCREEN_EDGES) == 0) {
        return;
    }

    /* actual window box */
    struct kywc_box s_box = {
        .x = pending->x - kywc_view->margin.off_x,
        .y = pending->y - kywc_view->margin.off_y,
        .width = pending->width + kywc_view->margin.off_width,
        .height = pending->height + kywc_view->margin.off_height,
    };
    /* actual view coord */
    int x1 = s_box.x;
    int y1 = s_box.y;
    int x2 = s_box.x + s_box.width;
    int y2 = s_box.y + s_box.height;
    /* usable coord */
    struct kywc_box *usable = &output->usable_area;
    int ux1 = usable->x;
    int uy1 = usable->y;
    int ux2 = usable->x + usable->width;
    int uy2 = usable->y + usable->height;

    /* window edge adsorption in left, right */
    if (abs(ux1 - x1) < *gap_x) {
        *gap_x = abs(ux1 - x1);
        pending->x = ux1 + kywc_view->margin.off_x;
    } else if (abs(x2 - ux2) < *gap_x) {
        *gap_x = abs(x2 - ux2);
        pending->x = ux2 - pending->width - kywc_view->margin.off_width + kywc_view->margin.off_x;
    }
    /* top and bottom */
    if (abs(uy1 - y1) < *gap_y) {
        *gap_y = abs(uy1 - y1);
        pending->y = uy1 + kywc_view->margin.off_y;
    } else if (abs(y2 - uy2) < *gap_y) {
        pending->y = uy2 - pending->height - kywc_view->margin.off_height + kywc_view->margin.off_y;
    }
}

void window_move_constraints(struct kywc_view *kywc_view, struct output *output, int *x, int *y,
                             int width, int height)
{
    if (kywc_view->unconstrained) {
        return;
    }

    struct kywc_box pending = { *x, *y, width, height };
    int gap_x = EDGE_OFFSET, gap_y = EDGE_OFFSET;
    uint32_t edges = KYWC_EDGE_NONE;
    /* window edge adsorption in left, right */
    edges |= *x != kywc_view->geometry.x ? KYWC_EDGE_LEFT | KYWC_EDGE_RIGHT : KYWC_EDGE_NONE;
    /* top and bottom */
    edges |= *y != kywc_view->geometry.y ? KYWC_EDGE_TOP | KYWC_EDGE_BOTTOM : KYWC_EDGE_NONE;
    window_adsorb_window_constraints(kywc_view, &pending, &gap_x, &gap_y, edges,
                                     INTERACTIVE_MODE_MOVE);
    gap_x = gap_x < EDGE_OFFSET ? gap_x : VIEW_EDGE_GAP;
    gap_y = gap_y < EDGE_OFFSET ? gap_y : VIEW_EDGE_GAP;
    window_adsorb_edges_constraints(kywc_view, &pending, output, &gap_x, &gap_y);
    *x = pending.x;
    *y = pending.y;

    /* actual view coord */
    int x1 = *x - kywc_view->margin.off_x;
    int y1 = *y - kywc_view->margin.off_y;
    int x2 = x1 + width + kywc_view->margin.off_width;
    int y2 = y1 + height + kywc_view->margin.off_height;

    /* get current seat constraints output */
    struct kywc_box *usable = &output->usable_area;
    int ux1 = usable->x, uy1 = usable->y;
    int ux2 = usable->x + usable->width, uy2 = usable->y + usable->height;
    struct kywc_box *geo = &output->geometry;
    int top = uy1 - geo->y, bottom = geo->y + geo->height - uy2;
    int left = ux1 - geo->x, right = geo->x + geo->width - ux2;

    int bottom_gap = MIN(VIEW_BOTTOM_GAP, height);
    int left_gap = MIN(VIEW_LEFT_GAP, width);
    int right_gap = MIN(VIEW_RIGHT_GAP, width);

    /* constraints when moving to top and bottom */
    if (output_at_layout_edge(output, LAYOUT_EDGE_TOP) && height > top && y1 < uy1) {
        *y = uy1 + kywc_view->margin.off_y;
    } else if (output_at_layout_edge(output, LAYOUT_EDGE_BOTTOM) && height > bottom && y2 > uy2 &&
               uy2 - y1 < bottom_gap) {
        *y = uy2 - bottom_gap + kywc_view->margin.off_y;
    }

    /* constraints when moving to left and right */
    if (output_at_layout_edge(output, LAYOUT_EDGE_LEFT) && width > left && x1 < ux1 &&
        x2 - ux1 < left_gap) {
        *x = left_gap - width - (kywc_view->margin.off_width - kywc_view->margin.off_x);
    } else if (output_at_layout_edge(output, LAYOUT_EDGE_RIGHT) && width > right && x2 > ux2 &&
               ux2 - x1 < right_gap) {
        *x = ux2 - right_gap + kywc_view->margin.off_y;
    }
}

static void interactive_process_move(struct interactive_grab *grab, double x, double y)
{
    struct kywc_view *kywc_view = &grab->view->base;
    struct kywc_box *geometry = &kywc_view->geometry;

    if (kywc_view->maximized || kywc_view->tiled) {
        struct kywc_box *saved = &grab->view->saved.geometry;
        double frac = (x - geometry->x) / geometry->width;
        int position_x = x - frac * saved->width;
        if (position_x < geometry->x) {
            position_x = geometry->x;
        }
        grab->geo.x = position_x;

        if (kywc_view->maximized) {
            kywc_view_set_maximized(kywc_view, false, NULL);
        } else {
            kywc_view_set_tiled(kywc_view, KYWC_TILE_NONE, NULL);
        }

        if (grab->proxy) {
            int width = kywc_view->margin.off_width + saved->width;
            int height = kywc_view->margin.off_height + saved->height;
            move_proxy_resize(grab->proxy, width, height);
        }
    }

    int nx = grab->geo.x + x - grab->cursor_x;
    int ny = grab->geo.y + y - grab->cursor_y;
    window_move_constraints(&grab->view->base, grab->output, &nx, &ny, geometry->width,
                            geometry->height);

    if (grab->proxy) {
        move_proxy_move(grab->proxy, nx, ny);
    } else {
        kywc_view_move(kywc_view, nx, ny);
    }

    interactive_move_show_snap_box(grab, x, y);
}

void interactive_resize_constraints(struct view *view, struct output *output, struct kywc_box *box,
                                    uint32_t edges)
{
    struct kywc_view *kywc_view = &view->base;
    if (kywc_view->unconstrained) {
        return;
    }

    /* get current seat constraints output */
    struct kywc_box *usable = &output->usable_area;
    struct kywc_box *current = &kywc_view->geometry;

    /* pending view coord */
    int x1 = box->x - kywc_view->margin.off_x;
    int y1 = box->y - kywc_view->margin.off_y;
    int x2 = x1 + box->width + kywc_view->margin.off_width;
    int y2 = y1 + box->height + kywc_view->margin.off_height;

    int gap_x = EDGE_OFFSET, gap_y = EDGE_OFFSET;
    window_adsorb_window_constraints(kywc_view, box, &gap_x, &gap_y, edges,
                                     INTERACTIVE_MODE_RESIZE);

    if (x1 > usable->x + usable->width || x2 < usable->x || y1 > usable->y + usable->height ||
        y2 < usable->y) {
        usable = &output_from_kywc_output(view->output)->usable_area;
    }

    int ux2 = usable->x + usable->width;
    int uy2 = usable->y + usable->height;

    /* constraints when resize to top and bottom */
    if (edges & KYWC_EDGE_TOP) {
        /* top */
        if (output_at_layout_edge(output, LAYOUT_EDGE_TOP) && y1 < usable->y) {
            box->y = usable->y + kywc_view->margin.off_y;
            box->height = current->height + current->y - box->y;
            /* bottom */
        } else if (output_at_layout_edge(output, LAYOUT_EDGE_BOTTOM) &&
                   y1 > uy2 - kywc_view->margin.off_y - VIEW_EDGE_GAP) {
            box->y = uy2 - VIEW_EDGE_GAP;
            box->height = y2 - box->y - (kywc_view->margin.off_height - kywc_view->margin.off_y);
        }
    } else if (edges & KYWC_EDGE_BOTTOM) {
        /* top */
        if (output_at_layout_edge(output, LAYOUT_EDGE_TOP) && y2 < usable->y + VIEW_EDGE_GAP) {
            box->height = usable->y + VIEW_EDGE_GAP - y1 - kywc_view->margin.off_height;
            /* bottom */
        } else if (output_at_layout_edge(output, LAYOUT_EDGE_BOTTOM) && y2 > uy2) {
            box->height = uy2 - y1 - kywc_view->margin.off_height;
        }
    }

    /* constraints when resize to left and right */
    if (edges & KYWC_EDGE_LEFT) {
        /* left */
        if (output_at_layout_edge(output, LAYOUT_EDGE_LEFT) && x1 < usable->x) {
            box->x = usable->x + kywc_view->margin.off_x;
            box->width = current->width + current->x - box->x;
            /* right */
        } else if (output_at_layout_edge(output, LAYOUT_EDGE_RIGHT) && x1 > ux2 - VIEW_EDGE_GAP) {
            box->x = ux2 - VIEW_EDGE_GAP + kywc_view->margin.off_x;
            box->width = x2 - box->x - (kywc_view->margin.off_width - kywc_view->margin.off_x);
        }
    } else if (edges & KYWC_EDGE_RIGHT) {
        /* left */
        if (output_at_layout_edge(output, LAYOUT_EDGE_LEFT) && x2 < usable->x + VIEW_EDGE_GAP) {
            box->width = usable->x + VIEW_EDGE_GAP - x1 - kywc_view->margin.off_width;
            /* right */
        } else if (output_at_layout_edge(output, LAYOUT_EDGE_RIGHT) && x2 > ux2) {
            box->width = ux2 - x1 - kywc_view->margin.off_width;
        }
    }
}

static void interactive_process_resize(struct interactive_grab *grab, double x, double y)
{
    struct kywc_view *kywc_view = &grab->view->base;

    int min_width = kywc_view->min_width;
    int min_height = kywc_view->min_height;
    if (!kywc_view->unconstrained) {
        min_width = MAX(kywc_view->min_width, VIEW_MIN_WIDTH);
        min_height = MAX(kywc_view->min_height, VIEW_MIN_HEIGHT);
    }

    int max_width = kywc_view->max_width;
    int max_height = kywc_view->max_height;
    int width = 0, height = 0;
    output_layout_get_size(&width, &height);
    if (max_width <= 0 || max_width > width - kywc_view->margin.off_width) {
        max_width = width - kywc_view->margin.off_width;
    }
    if (max_height <= 0 || max_height > height - kywc_view->margin.off_height) {
        max_height = height - kywc_view->margin.off_height;
    }

    struct kywc_box pending = kywc_view->geometry;
    double dx = x - grab->cursor_x;
    double dy = y - grab->cursor_y;

    if (grab->resize_edges & KYWC_EDGE_TOP) {
        pending.height = grab->geo.height - dy;
    } else if (grab->resize_edges & KYWC_EDGE_BOTTOM) {
        pending.height = grab->geo.height + dy;
    }
    pending.height = CLAMP(pending.height, min_height, max_height);

    if (grab->resize_edges & KYWC_EDGE_LEFT) {
        pending.width = grab->geo.width - dx;
    } else if (grab->resize_edges & KYWC_EDGE_RIGHT) {
        pending.width = grab->geo.width + dx;
    }
    pending.width = CLAMP(pending.width, min_width, max_width);

    if (grab->resize_edges & KYWC_EDGE_TOP) {
        /* anchor bottom edge */
        pending.y = grab->geo.y + grab->geo.height - pending.height;
    }
    if (grab->resize_edges & KYWC_EDGE_LEFT) {
        /* anchor right edge */
        pending.x = grab->geo.x + grab->geo.width - pending.width;
    }

    interactive_resize_constraints(grab->view, grab->output, &pending, grab->resize_edges);
    kywc_view_resize(kywc_view, &pending);
}

static void interactive_tile_output_update(struct interactive_grab *grab, int key,
                                           enum tile_state current)
{
    grab->output = output_from_kywc_output(grab->view->output);

    enum layout_edge layout_edge = LAYOUT_EDGE_TOP;
    if (key == KEY_LEFT) {
        if (current == TILE_LEFT || current == TILE_TOP_LEFT || current == TILE_BOTTOM_LEFT) {
            layout_edge = LAYOUT_EDGE_LEFT;
        }
    } else if (key == KEY_RIGHT) {
        if (current == TILE_RIGHT || current == TILE_TOP_RIGHT || current == TILE_BOTTOM_RIGHT) {
            layout_edge = LAYOUT_EDGE_RIGHT;
        }
    }

    if (layout_edge == LAYOUT_EDGE_TOP) {
        return;
    }

    if (!output_state_is_mirror_mode()) {
        struct output *output = output_find_specified_output(grab->output, layout_edge);
        if (!output) {
            return;
        }

        view_fix_geometry(grab->view, &grab->view->saved.geometry, &grab->output->usable_area,
                          &output->usable_area);
        grab->output = output;
    }
}

static void interactive_tile_update(struct interactive_grab *grab, struct output *output, int state)
{
    if (state == TILE_MINIMIZE) {
        kywc_view_set_minimized(&grab->view->base, true);
        return;
    }

    if (state == TILE_MAXIMIZE) {
        kywc_view_set_maximized(&grab->view->base, true, NULL);
        return;
    }

    if (grab->view->base.maximized && !state) {
        kywc_view_set_maximized(&grab->view->base, false, NULL);
        return;
    }

    if (grab->view->base.minimized) {
        /* kywc_view_activate will call the minimize restore interface. */
        kywc_view_activate(&grab->view->base);
        view_set_focus(grab->view, grab->view->base.focused_seat);
        return;
    }

    kywc_view_set_tiled(&grab->view->base, state, &output->base);
}

static int interactive_get_tile_state(struct view *view)
{
    if (view->base.maximized) {
        return TILE_MAXIMIZE;
    } else if (view->base.minimized) {
        return TILE_MINIMIZE;
    }

    return view->base.tiled < KYWC_TILE_CENTER ? (enum tile_state)view->base.tiled : TILE_NONE;
}

static void interactive_process_tile(struct interactive_grab *grab, int key)
{
    if (key != KEY_UP && key != KEY_DOWN && key != KEY_LEFT && key != KEY_RIGHT) {
        return;
    }

    enum tile_state pending_state = TILE_NONE;
    enum tile_state current = interactive_get_tile_state(grab->view);
    if (key == KEY_UP) {
        pending_state = tile_states[current].key_up;
    } else if (key == KEY_DOWN) {
        pending_state = tile_states[current].key_down;
    } else if (key == KEY_LEFT) {
        pending_state = tile_states[current].key_left;
    } else if (key == KEY_RIGHT) {
        pending_state = tile_states[current].key_right;
    }

    interactive_tile_output_update(grab, key, current);
    interactive_tile_update(grab, grab->output, pending_state);
}

static void interactive_process_tile_half_screen(struct interactive_grab *grab, int key)
{
    if (key != KEY_UP && key != KEY_DOWN && key != KEY_LEFT && key != KEY_RIGHT) {
        return;
    }

    enum tile_state current = interactive_get_tile_state(grab->view);
    if (current == TILE_MINIMIZE) {
        return;
    }

    grab->output = output_from_kywc_output(grab->view->output);
    enum kywc_tile tile = KYWC_TILE_NONE;
    if (key == KEY_UP) {
        tile = KYWC_TILE_TOP;
    } else if (key == KEY_DOWN) {
        tile = KYWC_TILE_BOTTOM;
    } else if (key == KEY_LEFT) {
        tile = KYWC_TILE_LEFT;
    } else if (key == KEY_RIGHT) {
        tile = KYWC_TILE_RIGHT;
    }

    if (grab->view->base.tiled != tile) {
        kywc_view_set_tiled(&grab->view->base, tile, grab->view->output);
        return;
    }

    if (tile == KYWC_TILE_TOP || tile == KYWC_TILE_BOTTOM) {
        return;
    }

    if (!output_state_is_mirror_mode()) {
        enum layout_edge edge = tile == KYWC_TILE_LEFT ? LAYOUT_EDGE_LEFT : LAYOUT_EDGE_RIGHT;
        struct output *output = output_find_specified_output(grab->output, edge);
        if (!output) {
            return;
        }

        view_fix_geometry(grab->view, &grab->view->saved.geometry, &grab->output->usable_area,
                          &output->usable_area);
        grab->output = output;
    }

    tile = tile == KYWC_TILE_LEFT ? KYWC_TILE_RIGHT : KYWC_TILE_LEFT;
    kywc_view_set_tiled(&grab->view->base, tile, &grab->output->base);
}

static bool interactive_move_filter(struct interactive_grab *grab, double x, double y)
{
    if (grab->ongoing) {
        return false;
    }

    struct kywc_view *kywc_view = &grab->view->base;
    if (kywc_view->maximized || kywc_view->tiled) {
        /* add move filter */
        if (fabs(grab->cursor_x - x) < VIEW_MOVE_STEP &&
            fabs(grab->cursor_y - y) < VIEW_MOVE_STEP) {
            return true;
        }
    }

    return false;
}

static bool interactive_resize_filter(struct interactive_grab *grab, uint32_t time)
{
    if (time - grab->last_time < view_manager_get_resize_filter(grab->view)) {
        return true;
    }
    grab->last_time = time;
    return false;
}

static bool pointer_grab_motion(struct seat_pointer_grab *pointer_grab, uint32_t time, double lx,
                                double ly)
{
    struct interactive_grab *grab = pointer_grab->data;
    grab->output = input_current_output(grab->seat);

    if (grab->mode == INTERACTIVE_MODE_MOVE) {
        if (interactive_move_filter(grab, lx, ly)) {
            return true;
        }
        /* set moving cursor image if moving */
        if (!grab->ongoing) {
            cursor_set_image(grab->seat->cursor, CURSOR_MOVE);
            grab->view->interactive_moving = true;
            grab->ongoing = true;
        }
        interactive_process_move(grab, lx, ly);
    } else if (grab->mode == INTERACTIVE_MODE_RESIZE) {
        if (interactive_resize_filter(grab, time)) {
            return true;
        }
        if (!grab->ongoing) {
            grab->view->current_resize_edges = grab->resize_edges;
            grab->ongoing = true;
        }
        if (grab->has_linkage) {
            tile_linkage_view_resize(grab->view, KYWC_EDGE_NONE, lx, ly);
        } else {
            interactive_process_resize(grab, lx, ly);
        }
    }

    return true;
}

static bool pointer_grab_button(struct seat_pointer_grab *pointer_grab, uint32_t time,
                                uint32_t button, bool pressed)
{
    struct interactive_grab *grab = pointer_grab->data;

    if (!pressed && button == BTN_LEFT) {
        bool on_surface = grab->seat->cursor->focus.node &&
                          wlr_surface_try_from_node(grab->seat->cursor->focus.node);
        interactive_done(grab);
        return !on_surface;
    }
    return true;
}

static bool pointer_grab_axis(struct seat_pointer_grab *pointer_grab, uint32_t time, bool vertical,
                              double value)
{
    return true;
}

static void pointer_grab_cancel(struct seat_pointer_grab *pointer_grab)
{
    struct interactive_grab *grab = pointer_grab->data;
    interactive_grab_destroy(grab);
}

static const struct seat_pointer_grab_interface pointer_grab_impl = {
    .motion = pointer_grab_motion,
    .button = pointer_grab_button,
    .axis = pointer_grab_axis,
    .cancel = pointer_grab_cancel,
};

static bool keyboard_grab_key(struct seat_keyboard_grab *keyboard_grab, struct keyboard *keyboard,
                              uint32_t time, uint32_t key, bool pressed, uint32_t modifiers)
{
    struct interactive_grab *grab = keyboard_grab->data;
    if (!pressed) {
        if (key != KEY_LEFTMETA && key != KEY_RIGHTMETA) {
            return true;
        }

        if (grab->mode == INTERACTIVE_MODE_TILE ||
            grab->mode == INTERACTIVE_MODE_TILE_HALF_SCREEN) {
            interactive_done(grab);
            return true;
        }
    }

    if (grab->mode == INTERACTIVE_MODE_TILE || grab->mode == INTERACTIVE_MODE_TILE_HALF_SCREEN) {
        if (!((WLR_MODIFIER_LOGO | WLR_MODIFIER_ALT) ^ modifiers)) {
            interactive_process_tile_half_screen(grab, key);
        } else if (!(WLR_MODIFIER_LOGO ^ modifiers)) {
            interactive_process_tile(grab, key);
        }
        return true;
    }

    int step = grab->mode == INTERACTIVE_MODE_MOVE ? VIEW_MOVE_STEP : VIEW_RESIZE_STEP;
    int dx = key == KEY_RIGHT ? step : (key == KEY_LEFT ? -step : 0);
    int dy = key == KEY_DOWN ? step : (key == KEY_UP ? -step : 0);

    /* restore to the orig geometry */
    if (key == KEY_ESC) {
        struct kywc_view *kywc_view = &grab->view->base;
        struct kywc_box geo = grab->geo;
        if (grab->has_linkage) {
            tile_linkage_resize_done(grab->view, true);
            grab->has_linkage = false;
        }
        interactive_done(grab);
        kywc_view_resize(kywc_view, &geo);
        return false;
    }
    if (key == KEY_ENTER) {
        interactive_done(grab);
        return false;
    }

    struct cursor *cursor = grab->seat->cursor;
    cursor_move(cursor, NULL, dx, dy, true, false);
    pointer_grab_motion(&grab->pointer_grab, time, cursor->lx, cursor->ly);
    return true;
}

static void keyboard_grab_cancel(struct seat_keyboard_grab *keyboard_grab)
{
    struct interactive_grab *grab = keyboard_grab->data;
    interactive_grab_destroy(grab);
}

static const struct seat_keyboard_grab_interface keyboard_grab_impl = {
    .key = keyboard_grab_key,
    .cancel = keyboard_grab_cancel,
};

static bool touch_grab_touch(struct seat_touch_grab *touch_grab, uint32_t time, bool down)
{
    if (down) {
        return true;
    }

    struct interactive_grab *grab = touch_grab->data;
    /**
     * When touching on a surface, the pointer event is not simulated and the focus is not updated.
     * It will call the motion of the pointer and update the hover.
     * Use hover to determine whether it is on the surface.
     * */
    bool on_surface =
        grab->seat->cursor->hover.node && wlr_surface_try_from_node(grab->seat->cursor->hover.node);
    interactive_done(grab);
    return !on_surface;
}

static bool touch_grab_motion(struct seat_touch_grab *touch_grab, uint32_t time, double lx,
                              double ly)
{
    struct interactive_grab *grab = touch_grab->data;
    bool first = !grab->ongoing;
    pointer_grab_motion(&grab->pointer_grab, time, lx, ly);

    if (first && grab->ongoing) {
        seat_reset_input_gesture(grab->seat);
    }

    return true;
}

static void touch_grab_cancel(struct seat_touch_grab *touch_grab)
{
    struct interactive_grab *grab = touch_grab->data;
    interactive_grab_destroy(grab);
}

static const struct seat_touch_grab_interface touch_grab_impl = {
    .touch = touch_grab_touch,
    .motion = touch_grab_motion,
    .cancel = touch_grab_cancel,
};

static void handle_view_unmap(struct wl_listener *listener, void *data)
{
    struct interactive_grab *grab = wl_container_of(listener, grab, view_unmap);
    interactive_done(grab);
}

static void handle_move_proxy_destroy(struct wl_listener *listener, void *data)
{
    struct interactive_grab *grab = wl_container_of(listener, grab, proxy_destroy);
    wl_list_remove(&grab->proxy_destroy.link);
    grab->proxy = NULL;
}

static int interactive_tiled_key_by_action(int action)
{
    int key = 0;
    if (action == WINDOW_ACTION_TILE_TOP || action == WINDOW_ACTION_TILE_TOP_HALF_SCREEN) {
        key = KEY_UP;
    } else if (action == WINDOW_ACTION_TILE_BOTTOM ||
               action == WINDOW_ACTION_TILE_BOTTOM_HALF_SCREEN) {
        key = KEY_DOWN;
    } else if (action == WINDOW_ACTION_TILE_LEFT || action == WINDOW_ACTION_TILE_LEFT_HALF_SCREEN) {
        key = KEY_LEFT;
    } else if (action == WINDOW_ACTION_TILE_RIGHT ||
               action == WINDOW_ACTION_TILE_RIGHT_HALF_SCREEN) {
        key = KEY_RIGHT;
    }

    return key;
}

static void interactive_grab_add(struct view *view, enum interactive_mode mode, uint32_t edges,
                                 struct seat *seat)
{
    struct interactive_grab *grab = calloc(1, sizeof(*grab));
    if (!grab) {
        return;
    }

    grab->pointer_grab = (struct seat_pointer_grab){ &pointer_grab_impl, seat, grab };
    seat_start_pointer_grab(seat, &grab->pointer_grab);
    grab->keyboard_grab = (struct seat_keyboard_grab){ &keyboard_grab_impl, seat, grab };
    seat_start_keyboard_grab(seat, &grab->keyboard_grab);
    grab->touch_grab = (struct seat_touch_grab){ &touch_grab_impl, seat, grab };
    seat_start_touch_grab(seat, &grab->touch_grab);

    grab->seat = seat;
    grab->mode = mode;
    grab->cursor_x = seat->cursor->lx;
    grab->cursor_y = seat->cursor->ly;
    grab->geo = view_action_change_size(view->pending.configure_action)
                    ? view->pending.configure_geometry
                    : view->base.geometry;
    grab->resize_edges = edges;
    grab->ongoing = false;

    grab->view = view;
    grab->view_unmap.notify = handle_view_unmap;
    wl_signal_add(&view->base.events.unmap, &grab->view_unmap);
    grab->proxy_destroy.notify = handle_move_proxy_destroy;
    wl_list_init(&grab->proxy_destroy.link);

    /* set the default cursor */
    if (mode == INTERACTIVE_MODE_MOVE) {
        grab->saved_usable_area = output_from_kywc_output(view->output)->usable_area;
        grab->update_saved_geo = !view->base.tiled && !view->base.maximized;
        // In order not to rebase after the configure_action
        if (view_action_change_size(view->pending.configure_action)) {
            grab->view->interactive_moving = true;
            // recalculate the coordinates of the window under the cursor
            grab->geo.x = grab->cursor_x -
                          ((grab->cursor_x - view->base.geometry.x) / view->base.geometry.width) *
                              grab->geo.width;
            grab->geo.y = grab->cursor_y -
                          ((grab->cursor_y - view->base.geometry.y) / view->base.geometry.height) *
                              grab->geo.height;
        }
        cursor_set_image(seat->cursor, CURSOR_DEFAULT);
        /* create a move proxy for move effect */
        int width = view->base.geometry.width + view->base.margin.off_width;
        int height = view->base.geometry.height + view->base.margin.off_height;
        grab->proxy = move_proxy_create(view, width, height);
        if (grab->proxy) {
            move_proxy_add_destroy_listener(grab->proxy, &grab->proxy_destroy);
        }
    } else if (mode == INTERACTIVE_MODE_RESIZE) {
        grab->has_linkage =
            tile_linkage_view_resize(view, edges, seat->cursor->lx, seat->cursor->ly);
        cursor_set_resize_image(seat->cursor, edges);
    } else if (mode == INTERACTIVE_MODE_TILE) {
        int key = interactive_tiled_key_by_action(edges);
        interactive_process_tile(grab, key);
    } else if (mode == INTERACTIVE_MODE_TILE_HALF_SCREEN) {
        int key = interactive_tiled_key_by_action(edges);
        interactive_process_tile_half_screen(grab, key);
    }

    struct wl_event_loop *loop = wl_display_get_event_loop(seat->wlr_seat->display);
    grab->filter = wl_event_loop_add_timer(loop, handle_snap_box, grab);
}

void window_begin_move(struct view *view, struct seat *seat)
{
    if (!KYWC_VIEW_IS_MOVABLE(&view->base)) {
        return;
    }
    interactive_grab_add(view, INTERACTIVE_MODE_MOVE, 0, seat);
}

void window_begin_resize(struct view *view, uint32_t edges, struct seat *seat)
{
    if (view->base.maximized || !KYWC_VIEW_IS_RESIZABLE(&view->base)) {
        return;
    }
    interactive_grab_add(view, INTERACTIVE_MODE_RESIZE, edges, seat);
}

void window_begin_tile(struct view *view, uint32_t key, struct seat *seat)
{
    if (view->base.fullscreen || view_manager_get_highlight()) {
        return;
    }

    interactive_grab_add(view, INTERACTIVE_MODE_TILE, key, seat);
}

void window_begin_tile_half_screen(struct view *view, uint32_t key, struct seat *seat)
{
    if (view->base.fullscreen || view_manager_get_highlight()) {
        return;
    }

    interactive_grab_add(view, INTERACTIVE_MODE_TILE_HALF_SCREEN, key, seat);
}
