#include "../include/CursorTrackerWayland.hpp"
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <wayland-client.h>
#include "xdg-output-unstable-v1-client-protocol.h"

namespace gsr {
    typedef struct {
        int type;
        int count;
    } drm_connector_type_count;

    static const int CONNECTOR_TYPE_COUNTS = 32;

    typedef enum {
        PLANE_PROPERTY_CRTC_X      = 1 << 0,
        PLANE_PROPERTY_CRTC_Y      = 1 << 1,
        PLANE_PROPERTY_CRTC_ID     = 1 << 2,
        PLANE_PROPERTY_TYPE_CURSOR = 1 << 3,
    } plane_property_mask;

    static const uint32_t plane_property_all = 0xF;

    /* Returns plane_property_mask */
    static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id, bool *is_cursor) {
        *crtc_x = 0;
        *crtc_y = 0;
        *crtc_id = 0;
        *is_cursor = false;

        uint32_t property_mask = 0;

        drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE);
        if(!props)
            return property_mask;

        for(uint32_t i = 0; i < props->count_props; ++i) {
            drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
            if(!prop)
                continue;

            // SRC_* values are fixed 16.16 points
            const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE);
            if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) {
                *crtc_x = (int)props->prop_values[i];
                property_mask |= PLANE_PROPERTY_CRTC_X;
            } else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) {
                *crtc_y = (int)props->prop_values[i];
                property_mask |= PLANE_PROPERTY_CRTC_Y;
            } else if((type & DRM_MODE_PROP_OBJECT) && strcmp(prop->name, "CRTC_ID") == 0) {
                *crtc_id = (int)props->prop_values[i];
                property_mask |= PLANE_PROPERTY_CRTC_ID;
            } else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) {
                const uint64_t current_enum_value = props->prop_values[i];
                for(int j = 0; j < prop->count_enums; ++j) {
                    if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) {
                        property_mask |= PLANE_PROPERTY_TYPE_CURSOR;
                        break;
                    }
                }
            }

            drmModeFreeProperty(prop);
        }

        drmModeFreeObjectProperties(props);
        return property_mask;
    }

    static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
        for(int i = 0; i < props->count_props; ++i) {
            drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
            if(!prop)
                continue;

            if(strcmp(name, prop->name) == 0) {
                *result = props->prop_values[i];
                drmModeFreeProperty(prop);
                return true;
            }
            drmModeFreeProperty(prop);
        }
        return false;
    }

    static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
        for(int i = 0; i < *num_type_counts; ++i) {
            if(type_counts[i].type == connector_type)
                return &type_counts[i];
        }

        if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
            return NULL;

        const int index = *num_type_counts;
        type_counts[index].type = connector_type;
        type_counts[index].count = 0;
        ++*num_type_counts;
        return &type_counts[index];
    }

    // Note: this monitor name logic is kept in sync with gpu screen recorder
    static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
        std::string result;
        drmModeResPtr resources = drmModeGetResources(drm_fd);
        if(!resources)
            return result;

        drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
        int num_type_counts = 0;

        for(int i = 0; i < resources->count_connectors; ++i) {
            uint64_t connector_crtc_id = 0;
            drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
            if(!connector)
                continue;

            drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
            const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
            if(connector_type)
                ++connector_type->count;

            if(connector->connection != DRM_MODE_CONNECTED)
                goto next;

            if(connector_type && connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
                result = connection_name;
                result += "-";
                result += std::to_string(connector_type->count);
                drmModeFreeConnector(connector);
                break;
            }

            next:
            drmModeFreeConnector(connector);
        }

        drmModeFreeResources(resources);
        return result;
    }

    // Name is the crtc name. TODO: verify if this works on all wayland compositors
    static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) {
        for(const WaylandOutput &monitor : monitors) {
            if(monitor.name == name)
                return &monitor;
        }
        return nullptr;
    }

    static WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) {
        for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) {
            if(monitor.output == output)
                return &monitor;
        }
        return nullptr;
    }

    static void output_handle_geometry(void *data, struct wl_output *wl_output,
        int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
        int32_t subpixel, const char *make, const char *model,
        int32_t transform) {
        (void)wl_output;
        (void)phys_width;
        (void)phys_height;
        (void)subpixel;
        (void)make;
        (void)model;
        CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
        WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
        if(!monitor)
            return;

        monitor->pos.x = x;
        monitor->pos.y = y;
        monitor->transform = transform;
    }

    static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
        (void)wl_output;
        (void)flags;
        (void)refresh;
        CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
        WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
        if(!monitor)
            return;

        monitor->size.x = width;
        monitor->size.y = height;
    }

    static void output_handle_done(void *data, struct wl_output *wl_output) {
        (void)data;
        (void)wl_output;
    }

    static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
        (void)data;
        (void)wl_output;
        (void)factor;
    }

    static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
        (void)wl_output;
        CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
        WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output);
        if(!monitor)
            return;

        monitor->name = name;
    }

    static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
        (void)data;
        (void)wl_output;
        (void)description;
    }

    static const struct wl_output_listener output_listener = {
        output_handle_geometry,
        output_handle_mode,
        output_handle_done,
        output_handle_scale,
        output_handle_name,
        output_handle_description,
    };

    static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
        (void)version;
        CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
        if(strcmp(interface, wl_output_interface.name) == 0) {
            if(version < 4) {
                fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n");
                return;
            }

            struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
            cursor_tracker_wayland->monitors.push_back(
                WaylandOutput{
                    name,
                    output,
                    nullptr,
                    mgl::vec2i{0, 0},
                    mgl::vec2i{0, 0},
                    0,
                    ""
                });
            wl_output_add_listener(output, &output_listener, cursor_tracker_wayland);
        } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
            if(version < 1) {
                fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n");
                return;
            }

            if(cursor_tracker_wayland->xdg_output_manager) {
                zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager);
                cursor_tracker_wayland->xdg_output_manager = NULL;
            }
            cursor_tracker_wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
        }
    }

    static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
        (void)data;
        (void)registry;
        (void)name;
        // TODO: Remove output
    }

    static struct wl_registry_listener registry_listener = {
        registry_add_object,
        registry_remove_object,
    };

    static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) {
        (void)zxdg_output_v1;
        WaylandOutput *monitor = (WaylandOutput*)data;
        monitor->pos.x = x;
        monitor->pos.y = y;
    }

    static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) {
        (void)data;
        (void)xdg_output;
        (void)width;
        (void)height;
    }

    static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
        (void)data;
        (void)xdg_output;
    }

    static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) {
        (void)data;
        (void)xdg_output;
        (void)name;
    }

    static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) {
        (void)data;
        (void)xdg_output;
        (void)description;
    }

    static const struct zxdg_output_v1_listener xdg_output_listener = {
        xdg_output_logical_position,
        xdg_output_handle_logical_size,
        xdg_output_handle_done,
        xdg_output_handle_name,
        xdg_output_handle_description,
    };

    CursorTrackerWayland::CursorTrackerWayland(const char *card_path) {
        drm_fd = open(card_path, O_RDONLY);
        if(drm_fd <= 0) {
            fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path);
            return;
        }

        drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
        drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1);
    }

    CursorTrackerWayland::~CursorTrackerWayland() {
        if(drm_fd > 0)
            close(drm_fd);
    }

    void CursorTrackerWayland::update() {
        if(drm_fd <= 0)
            return;

        drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd);
        if(!planes)
            return;

        for(uint32_t i = 0; i < planes->count_planes; ++i) {
            int crtc_x = 0;
            int crtc_y = 0;
            int crtc_id = 0;
            bool is_cursor = false;
            const uint32_t property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id, &is_cursor);
            if(property_mask == plane_property_all && crtc_id > 0) {
                latest_cursor_position.x = crtc_x;
                latest_cursor_position.y = crtc_y;
                latest_crtc_id = crtc_id;
                break;
            }
        }

        drmModeFreePlaneResources(planes);
    }

    void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) {
        if(!xdg_output_manager) {
            fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
            return;
        }

        for(WaylandOutput &monitor : monitors) {
            monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output);
            zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
        }

        // Fetch xdg_output
        wl_display_roundtrip(dpy);
    }

    std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
        if(drm_fd <= 0 || latest_crtc_id == -1)
            return std::nullopt;

        std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id);
        if(monitor_name.empty())
            return std::nullopt;

        struct wl_display *dpy = wl_display_connect(nullptr);
        if(!dpy) {
            fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n");
            return std::nullopt;
        }

        monitors.clear();
        struct wl_registry *registry = wl_display_get_registry(dpy);
        wl_registry_add_listener(registry, &registry_listener, this);

        // Fetch globals
        wl_display_roundtrip(dpy);

        // Fetch wl_output
        wl_display_roundtrip(dpy);

        set_monitor_outputs_from_xdg_output(dpy);

        mgl::vec2i cursor_position = latest_cursor_position;
        const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name);
        if(!wayland_monitor)
            return std::nullopt;

        cursor_position = wayland_monitor->pos + latest_cursor_position;
        for(WaylandOutput &monitor : monitors) {
            if(monitor.output) {
                wl_output_destroy(monitor.output);
                monitor.output = nullptr;
            }

            if(monitor.xdg_output) {
                zxdg_output_v1_destroy(monitor.xdg_output);
                monitor.xdg_output = nullptr;
            }
        }
        monitors.clear();

        if(xdg_output_manager) {
            zxdg_output_manager_v1_destroy(xdg_output_manager);
            xdg_output_manager = nullptr;
        }

        wl_registry_destroy(registry);
        wl_display_disconnect(dpy);

        return CursorInfo{ cursor_position, std::move(monitor_name) };
    }
}