/*
 * Copyright (C) 2020 Uniontech Technology Co., Ltd.
 *
 * Author:     xinbo wang <wangxinbo@uniontech.com>
 *
 * Maintainer: xinbo wang <wangxinbo@uniontech.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "dtk_wmjack_wayland.h"
#include "wayland_client_management.h"

#include "dtk_utils.h"
#include "log.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/time.h>

#include <wayland-client.h>
#include "wayland-client-management-client-protocol.h"

void handle_window_states(void *data,
                          struct com_deepin_client_management *com_deepin_client_management,
                          uint32_t count,
                          struct wl_array *states)
{
    WaylandClientBackendPtr backend = (WaylandClientBackendPtr)data;
    if (!backend) {
        log_error("backend is null");
        return;
    }

    ClientManagement *cm = backend->client_management;
    if (!cm) {
        log_error("client management get pointer failed");
        return;
    }

    if (0 < states->size && (0 == (states->size % sizeof(WindowState)))) {
        if (cm->clients == NULL) {
            cm->clients = malloc(sizeof(struct dtk_array));
        } else {
            dtk_array_release(cm->clients);
            free(cm->clients);

            cm->clients = malloc(sizeof(struct dtk_array));
        }

        dtk_array_init(cm->clients);
        dtk_array_copy(cm->clients, (void *)states);
    } else {
        log_error("receive wayland window states event error");
    }
}

void handle_window_from_point(void *data,
                              struct com_deepin_client_management *com_deepin_client_management,
                              uint32_t wid)
{
    WaylandClientBackendPtr backend = (WaylandClientBackendPtr)data;
    if (!backend) {
        log_error("backend is null");
        return;
    }

    ClientManagement *client_management = backend->client_management;
    if (!client_management) {
        log_error("client management get pointer failed");
        return;
    }

    pthread_mutex_lock(&backend->cond_lock);
    client_management->window_from_point = wid;
    pthread_cond_signal(&client_management->window_from_point_done);
    pthread_mutex_unlock(&backend->cond_lock);
}

void handle_all_window_id(void *data,
                          struct com_deepin_client_management *com_deepin_client_management,
                          struct wl_array *id_array)
{
    WaylandClientBackendPtr backend = (WaylandClientBackendPtr)data;
    if (!backend) {
        log_error("backend is null");
        return;
    }

    ClientManagement *client_management = backend->client_management;
    if (!client_management) {
        log_error("client management get pointer failed");
        return;
    }

    pthread_mutex_lock(&backend->cond_lock);
    if (client_management->all_window_id) {
        dtk_array_release(client_management->all_window_id);
        free(client_management->all_window_id);
        client_management->all_window_id = NULL;
    }

    if (id_array->size > 0 && ((id_array->size % sizeof(uint32_t) == 0))) {
        client_management->all_window_id = malloc(sizeof(struct dtk_array));
        dtk_array_init(client_management->all_window_id);
        dtk_array_copy(client_management->all_window_id, (void *)id_array);
    } else {
        log_error("receive all window id event error");
    }

    pthread_cond_signal(&client_management->all_window_id_done);
    pthread_mutex_unlock(&backend->cond_lock);
}

void handle_specific_window_state(void *data,
                                  struct com_deepin_client_management *com_deepin_client_management,
                                  int32_t pid,
                                  uint32_t window_id,
                                  const char *resource_name,
                                  int32_t x,
                                  int32_t y,
                                  int32_t width,
                                  int32_t height,
                                  int32_t is_minimized,
                                  int32_t is_fullscreen,
                                  int32_t is_active,
                                  int32_t splitable,
                                  const char *uuid)
{
    WaylandClientBackendPtr backend = (WaylandClientBackendPtr)data;
    if (!backend) {
        log_error("backend is null");
        return;
    }

    ClientManagement *client_management = backend->client_management;
    if (!client_management) {
        log_error("client management get pointer failed");
        return;
    }

    pthread_mutex_lock(&backend->cond_lock);
    memset(&client_management->window_state, 0, sizeof(WindowState));
    client_management->window_state.pid = pid;
    client_management->window_state.windowId = window_id;
    client_management->window_state.geometry.x = x;
    client_management->window_state.geometry.y = y;
    client_management->window_state.geometry.width = width;
    client_management->window_state.geometry.height = height;
    client_management->window_state.isMinimized = is_minimized;
    client_management->window_state.isFullScreen = is_fullscreen;
    client_management->window_state.isActive = is_active;
    client_management->window_state.splitable = splitable;
#define MIN(a, b) ((a) < (b) ? (a) : (b))
    strncpy(client_management->window_state.resourceName, resource_name,
            MIN(sizeof(client_management->window_state.resourceName) - 1, strlen(resource_name)));
    strncpy(client_management->window_state.uuid, uuid,
            MIN(sizeof(client_management->window_state.uuid) - 1, strlen(uuid)));
    pthread_cond_signal(&client_management->specific_window_state_done);
    pthread_mutex_unlock(&backend->cond_lock);
}

ClientManagement *init_client_management()
{
    ClientManagement *client_management = (ClientManagement *)malloc(sizeof(ClientManagement));
    memset(client_management, 0, sizeof(ClientManagement));

    client_management->clients = NULL;
    client_management->window_from_point = 0;
    client_management->all_window_id = NULL;
    return client_management;
}

const struct com_deepin_client_management_listener client_management_listener = {
    .window_states = handle_window_states,
    .window_from_point = handle_window_from_point,
    .all_window_id = handle_all_window_id,
    .specific_window_state = handle_specific_window_state
};

void wmjack_handle_client_management(void *data, void *wl_registry, int name, const char *interface, int version)
{
    if (strcmp(interface, com_deepin_client_management_interface.name) == 0) {
        WaylandClientBackendPtr backend = (WaylandClientBackendPtr)data;
        if (backend && backend->client_management == NULL)
            backend->client_management = init_client_management();

        if (backend->client_management) {
            ClientManagement *client_manage = ((ClientManagement *)backend->client_management);

            if (client_manage && client_manage->clientManagement == NULL) {
                client_manage->clientManagement = wl_registry_bind(wl_registry, name, &com_deepin_client_management_interface, 1);
                com_deepin_client_management_add_listener(client_manage->clientManagement, &client_management_listener, backend);

                com_deepin_client_management_get_window_states(client_manage->clientManagement);
                wl_display_roundtrip(backend->display);
            }
        }
    }
}

void destory_client_management(ClientManagement *client_manage)
{
    if (!client_manage)
        return;

    if (client_manage->clients) {
        dtk_array_release(client_manage->clients);
        free(client_manage->clients);
        client_manage->clients = NULL;
    }

    if (client_manage->all_window_id) {
        dtk_array_release(client_manage->all_window_id);
        free(client_manage->all_window_id);
        client_manage->all_window_id = NULL;
    }

    pthread_cond_destroy(&client_manage->window_from_point_done);
    pthread_cond_destroy(&client_manage->all_window_id_done);
    pthread_cond_destroy(&client_manage->specific_window_state_done);

    com_deepin_client_management_destroy(client_manage->clientManagement);
    free(client_manage);
    client_manage = NULL;
}

struct dtk_array *get_client_all_window_states()
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return NULL;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return NULL;

    return client_manage->clients;
}

WindowState *get_client_window_state(int32_t windowId)
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return NULL;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return NULL;

    struct dtk_array *arrays = client_manage->clients;
    if (arrays && arrays->size <= 0)
        return NULL;

    void *p;
    dtk_array_for_each(p, arrays, sizeof(WindowState))
    {
        WindowState *pWindow = (WindowState *)p;
        if (pWindow && pWindow->windowId == windowId)
            return pWindow;
    }

    return NULL;
}

int32_t get_client_window_pid(int32_t windowId)
{
    WindowState state = get_specific_window_state(windowId);
    return state.windowId == 0 ? -1 : state.pid;
}

char *get_client_window_title(int32_t windowId)
{
    char *name = NULL;

    WindowState state = get_specific_window_state(windowId);
    if (state.windowId != 0 && state.resourceName) {
        const int length = strlen(state.resourceName);
        if (length > 0) {
            name = (char *)malloc(length + 1);
            strcpy(name, state.resourceName);
        }
    }

    return name;
}

Size get_client_window_size(int32_t windowId)
{
    Size size;
    size.w = -1;
    size.h = -1;

    WindowState state = get_specific_window_state(windowId);
    if (state.windowId != 0) {
        size.w = state.geometry.width;
        size.h = state.geometry.height;
    }

    return size;
}

Position get_client_window_position(int32_t windowId)
{
    Position position;
    position.x = -1;
    position.y = -1;

    WindowState state = get_specific_window_state(windowId);
    if (state.windowId != 0) {
        position.x = state.geometry.x;
        position.y = state.geometry.y;
    }

    return position;
}

uint32_t get_window_from_point()
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return 0;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return 0;

    pthread_mutex_lock(&pWayland->cond_lock);
    client_manage->window_from_point = 0;
    com_deepin_client_management_get_window_from_point(client_manage->clientManagement);
    wl_display_flush(pWayland->display);

    struct timeval now;
    struct timespec timeout;

    gettimeofday(&now, NULL);
    timeout.tv_sec = now.tv_sec + 1;
    timeout.tv_nsec = now.tv_usec;
    int ret = pthread_cond_timedwait(&client_manage->window_from_point_done, &pWayland->cond_lock, &timeout);
    int window_id = ret == 0 ? client_manage->window_from_point : 0;
    pthread_mutex_unlock(&pWayland->cond_lock);

    return window_id;
}


void show_split_menu(int x, int y, int width, int height, uint32_t wid)
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return;

    com_deepin_client_management_show_split_menu(client_manage->clientManagement, x, y, width, height, wid);
    wl_display_flush(pWayland->display);
}

void hide_split_menu(bool delay)
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return;

    com_deepin_client_management_hide_split_menu(client_manage->clientManagement, delay);
    wl_display_flush(pWayland->display);
}

struct dtk_array *get_all_window_id()
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return NULL;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return NULL;

    pthread_mutex_lock(&pWayland->cond_lock);
    com_deepin_client_management_get_all_window_id(client_manage->clientManagement);
    wl_display_flush(pWayland->display);

    struct timeval now;
    struct timespec timeout;

    gettimeofday(&now, NULL);
    timeout.tv_sec = now.tv_sec + 1;
    timeout.tv_nsec = now.tv_usec;
    int ret = pthread_cond_timedwait(&client_manage->all_window_id_done, &pWayland->cond_lock, &timeout);

    struct dtk_array *result = NULL;
    if (ret == 0 && client_manage->all_window_id) {
        result = (struct dtk_array *)malloc(sizeof(struct dtk_array));
        dtk_array_init(result);
        dtk_array_copy(result, client_manage->all_window_id);
    }
    pthread_mutex_unlock(&pWayland->cond_lock);

    return result;
}

WindowState get_specific_window_state(uint32_t wid)
{
    WindowState state;
    memset(&state, 0, sizeof(WindowState));
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return state;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);
    ClientManagement *client_manage = (ClientManagement *)pWayland->client_management;
    if (!client_manage)
        return state;

    pthread_mutex_lock(&pWayland->cond_lock);
    com_deepin_client_management_get_specific_window_state(client_manage->clientManagement, wid);
    wl_display_flush(pWayland->display);

    struct timeval now;
    struct timespec timeout;

    gettimeofday(&now, NULL);
    timeout.tv_sec = now.tv_sec + 1;
    timeout.tv_nsec = now.tv_usec;
    int ret = pthread_cond_timedwait(&client_manage->specific_window_state_done, &pWayland->cond_lock, &timeout);
    if (ret == 0)
        state = client_manage->window_state;
    pthread_mutex_unlock(&pWayland->cond_lock);

    return state;
}

int get_all_window_states_list(WindowState **states)
{
    if (!states)
        return 0;
    *states = NULL;

    struct dtk_array *id_list = get_all_window_id();
    if (!id_list || id_list->size == 0) {
        log_error("failed to get all window id\n");
        return 0;
    }

    const size_t list_size = id_list->size / sizeof(uint32_t);
    *states = (WindowState *)malloc(list_size * sizeof(WindowState));
    if (!(*states)) {
        log_error("failed to malloc for states list\n");
        return 0;
    }

    void *p;
    size_t actual_size = 0;
    dtk_array_for_each(p, id_list, sizeof(uint32_t)) {
        WindowState state = get_specific_window_state(*(uint32_t *)p);
        if (state.pid != 0 && state.windowId != 0) {
            (*states)[actual_size++] = state;
        }
    }

    dtk_array_release(id_list);
    free(id_list);

    return actual_size;
}

static void handle_global(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version)
{
    wmjack_handle_client_management(data, wl_registry, name, interface, version);
}

static void handle_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name)
{
    // Who cares?
}

static const struct wl_registry_listener registry_listener = {
    .global = handle_global,
    .global_remove = handle_global_remove,
};

static void *wayland_dispatch(void *arg)
{
    WaylandClientBackendPtr pWayland = (WaylandClientBackendPtr *)arg;
    if (!pWayland)
        return NULL;

    if (!pWayland->display)
        return NULL;

    while (pWayland->lock) {
        wl_display_dispatch(pWayland->display);
    }

    return NULL;
}

int init_wayland_client()
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return -1;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);

    pWayland->lock = true;

    pWayland->display = wl_display_connect(NULL);
    if (pWayland->display == NULL) {
        log_error("failed to create display");
        return -1;
    }

    struct wl_registry *registry = wl_display_get_registry(pWayland->display);
    wl_registry_add_listener(registry, &registry_listener, pWayland);
    wl_display_roundtrip(pWayland->display);

    pthread_mutex_init(&pWayland->cond_lock, NULL);
    pthread_create(&pWayland->dispatch, NULL, wayland_dispatch, pWayland);

    return 0;
}

void destory_wayland_client()
{
    if (!pWmJack->backend) {
        log_error("wayland backend has been destroyed \n");
        return;
    }

    WaylandClientBackendPtr pWayland = ((WaylandClientBackend *)pWmJack->backend);

    pthread_mutex_lock(&pWayland->cond_lock);
    pWayland->lock = false;
    pthread_mutex_unlock(&pWayland->cond_lock);

    // cancel pthread
    struct wl_callback *callback = wl_display_sync(pWayland->display);
    wl_display_flush(pWayland->display);
    if (callback)
        wl_callback_destroy(callback);
    pthread_join(pWayland->dispatch, NULL);

    // client manager destory
    destory_client_management(pWayland->client_management);

    // display disconnect
    wl_display_disconnect(pWayland->display);

    free(pWayland);
    pWayland = NULL;
}