/*
 * Copyright © 2020 Drew Devault
 * Copyright © 2021 Xaver Hugl
 *
 * Permission to use, copy, modify, distribute, and sell this software
 * and its documentation for any purpose is hereby granted without
 * fee, provided that the above copyright notice appear in all copies
 * and that both that copyright notice and this permission notice
 * appear in supporting documentation, and that the name of the
 * copyright holders not be used in advertising or publicity
 * pertaining to distribution of the software without specific,
 * written prior permission.  The copyright holders make no
 * representations about the suitability of this software for any
 * purpose.  It is provided "as is" without express or implied
 * warranty.
 *
 * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
 * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 */

#include <xwayland-config.h>

#ifdef WITH_LIBDRM
#include <xf86drm.h>
#include <xf86drmMode.h>
#endif

#include "xwayland-drm-lease.h"
#include "xwayland-screen.h"
#include "xwayland-output.h"

static void
drm_lease_handle_lease_fd(void *data,
                          struct wp_drm_lease_v1 *wp_drm_lease_v1,
                          int32_t lease_fd)
{
    struct xwl_drm_lease *lease = (struct xwl_drm_lease *)data;

    lease->fd = lease_fd;
    AttendClient(lease->client);
}

static void
drm_lease_handle_finished(void *data,
                          struct wp_drm_lease_v1 *wp_drm_lease_v1)
{
    struct xwl_drm_lease *lease = (struct xwl_drm_lease *)data;
    struct xwl_output *output;
    int i;

    if (lease->fd >= 0) {
        RRTerminateLease(lease->rrLease);
    } else {
        AttendClient(lease->client);
        for (i = 0; i < lease->rrLease->numOutputs; ++i) {
            output = lease->rrLease->outputs[i]->devPrivate;
            output->lease = NULL;
        }
    }
}

static struct wp_drm_lease_v1_listener drm_lease_listener = {
    .lease_fd = drm_lease_handle_lease_fd,
    .finished = drm_lease_handle_finished,
};

void
xwl_randr_get_lease(ClientPtr client, ScreenPtr screen, RRLeasePtr *rrLease, int *fd)
{
    struct xwl_screen *xwl_screen;
    struct xwl_drm_lease *lease;
    xwl_screen = xwl_screen_get(screen);

    xorg_list_for_each_entry(lease, &xwl_screen->drm_leases, link) {
        if (lease->client == client) {
            *rrLease = lease->rrLease;
            *fd = lease->fd;
            if (lease->fd < 0)
                xorg_list_del(&lease->link);
            return;
        }
    }
    *rrLease = NULL;
    *fd = -1;
}

int
xwl_randr_request_lease(ClientPtr client, ScreenPtr screen, RRLeasePtr rrLease)
{
    struct xwl_screen *xwl_screen;
    struct wp_drm_lease_request_v1 *req;
    struct xwl_drm_lease *lease_private;
    struct xwl_drm_lease_device *lease_device = NULL;
    struct xwl_drm_lease_device *device_data;
    struct xwl_output *output;
    int i;

    xwl_screen = xwl_screen_get(screen);

    if (xorg_list_is_empty(&xwl_screen->drm_lease_devices)) {
        ErrorF("Attempted to create DRM lease without wp_drm_lease_device_v1\n");
        return BadMatch;
    }

    xorg_list_for_each_entry(device_data, &xwl_screen->drm_lease_devices, link) {
        Bool connectors_of_device = FALSE;
        for (i = 0; i < rrLease->numOutputs; ++i) {
            output = rrLease->outputs[i]->devPrivate;
            if (output->lease_device == device_data) {
                connectors_of_device = TRUE;
                break;
            }
        }
        if (connectors_of_device) {
            if (lease_device != NULL) {
                ErrorF("Attempted to create DRM lease from multiple devices\n");
                return BadValue;
            }
            lease_device = device_data;
        }
    }

    for (i = 0; i < rrLease->numOutputs; ++i) {
        output = rrLease->outputs[i]->devPrivate;
        if (!output || !output->lease_connector || output->lease) {
            return BadValue;
        }
    }

    req = wp_drm_lease_device_v1_create_lease_request(
            lease_device->drm_lease_device);
    lease_private = calloc(1, sizeof(struct xwl_drm_lease));
    for (i = 0; i < rrLease->numOutputs; ++i) {
        output = rrLease->outputs[i]->devPrivate;
        output->lease = lease_private;
        wp_drm_lease_request_v1_request_connector(req, output->lease_connector);
    }
    lease_private->fd = -1;
    lease_private->lease = wp_drm_lease_request_v1_submit(req);
    lease_private->rrLease = rrLease;
    lease_private->client = client;
    rrLease->devPrivate = lease_private;

    wp_drm_lease_v1_add_listener(lease_private->lease,
                                  &drm_lease_listener, lease_private);
    xorg_list_add(&lease_private->link, &xwl_screen->drm_leases);

    ResetCurrentRequest(client);
    client->sequence--;
    IgnoreClient(client);

    return Success;
}

void
xwl_randr_terminate_lease(ScreenPtr screen, RRLeasePtr lease)
{
    struct xwl_drm_lease *lease_private = lease->devPrivate;

    if (lease_private) {
        xorg_list_del(&lease_private->link);
        if (lease_private->fd >= 0)
            close(lease_private->fd);
        wp_drm_lease_v1_destroy(lease_private->lease);
        free(lease_private);
        lease->devPrivate = NULL;
    }

    RRLeaseTerminated(lease);
}

static void
lease_connector_handle_name(void *data,
                            struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
                            const char *name)
{
    /* This space is deliberately left blank */
}

static void
lease_connector_handle_description(void *data,
                                   struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
                                   const char *description)
{
    /* This space is deliberately left blank */
}

static RRModePtr *
xwl_get_rrmodes_from_connector_id(int drm, int32_t connector_id, int *nmode, int *npref)
{
#ifdef WITH_LIBDRM
    drmModeConnectorPtr conn;
    drmModeModeInfoPtr kmode;
    RRModePtr *rrmodes;
    int pref, i;

    *nmode = *npref = 0;

    conn = drmModeGetConnectorCurrent(drm, connector_id);
    if (!conn) {
        ErrorF("drmModeGetConnector for connector %d failed\n", connector_id);
        return NULL;
    }
    rrmodes = xallocarray(conn->count_modes, sizeof(RRModePtr));
    if (!rrmodes) {
        ErrorF("Failed to allocate connector modes\n");
        drmModeFreeConnector(conn);
        return NULL;
    }

    /* This spaghetti brought to you courtesey of xf86RandrR12.c
     * It adds preferred modes first, then non-preferred modes */
    for (pref = 1; pref >= 0; pref--) {
        for (i = 0; i < conn->count_modes; ++i) {
            kmode = &conn->modes[i];
            if ((pref != 0) == ((kmode->type & DRM_MODE_TYPE_PREFERRED) != 0)) {
                xRRModeInfo modeInfo;
                RRModePtr rrmode;

                modeInfo.nameLength = strlen(kmode->name);

                modeInfo.width = kmode->hdisplay;
                modeInfo.dotClock = kmode->clock * 1000;
                modeInfo.hSyncStart = kmode->hsync_start;
                modeInfo.hSyncEnd = kmode->hsync_end;
                modeInfo.hTotal = kmode->htotal;
                modeInfo.hSkew = kmode->hskew;

                modeInfo.height = kmode->vdisplay;
                modeInfo.vSyncStart = kmode->vsync_start;
                modeInfo.vSyncEnd = kmode->vsync_end;
                modeInfo.vTotal = kmode->vtotal;
                modeInfo.modeFlags = kmode->flags;

                rrmode = RRModeGet(&modeInfo, kmode->name);
                if (rrmode) {
                    rrmodes[*nmode] = rrmode;
                    *nmode = *nmode + 1;
                    *npref = *npref + pref;
                }
            }
        }
    }
    /* workaround: there could be no preferred mode that got added */
    if (*nmode > 0 && *npref == 0)
        *npref = 1;

    drmModeFreeConnector(conn);
    return rrmodes;
#else
    *nmode = *npref = 0;
    return NULL;
#endif
}

static void
lease_connector_handle_connector_id(void *data,
                                    struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1,
                                    uint32_t connector_id)
{
    struct xwl_output *output;
    Atom name;
    INT32 value;
    int err;
    int nmode, npref;
    RRModePtr *rrmodes;

    value = connector_id;
    output = (struct xwl_output *)data;
    name = MakeAtom("CONNECTOR_ID", 12, TRUE);

    if (name != BAD_RESOURCE) {
        err = RRConfigureOutputProperty(output->randr_output, name,
                                        FALSE, FALSE, TRUE,
                                        1, &value);
        if (err != 0) {
            ErrorF("RRConfigureOutputProperty error, %d\n", err);
            return;
        }
        err = RRChangeOutputProperty(output->randr_output, name,
                                     XA_INTEGER, 32, PropModeReplace, 1,
                                     &value, FALSE, FALSE);
        if (err != 0) {
            ErrorF("RRChangeOutputProperty error, %d\n", err);
            return;
        }
    }
    rrmodes = xwl_get_rrmodes_from_connector_id(output->lease_device->drm_read_only_fd,
                                                connector_id, &nmode, &npref);

    if (rrmodes != NULL)
        RROutputSetModes(output->randr_output, rrmodes, nmode, npref);

    free(rrmodes);
}

static void
lease_connector_handle_done(void *data,
                            struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1)
{
    /* This space is deliberately left blank */
}

static void
lease_connector_handle_withdrawn(void *data,
                                 struct wp_drm_lease_connector_v1 *wp_drm_lease_connector_v1)
{
    xwl_output_remove(data);
}

static const struct wp_drm_lease_connector_v1_listener lease_connector_listener = {
    .name = lease_connector_handle_name,
    .description = lease_connector_handle_description,
    .connector_id = lease_connector_handle_connector_id,
    .withdrawn = lease_connector_handle_withdrawn,
    .done = lease_connector_handle_done,
};

static void
drm_lease_device_handle_drm_fd(void *data,
                               struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1,
                               int fd)
{
    ((struct xwl_drm_lease_device *)data)->drm_read_only_fd = fd;
}

static void
drm_lease_device_handle_connector(void *data,
                                  struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1,
                                  struct wp_drm_lease_connector_v1 *connector)
{
    struct xwl_drm_lease_device *lease_device = data;
    struct xwl_output *xwl_output;
    char name[256];

    xwl_output = calloc(1, sizeof *xwl_output);
    if (xwl_output == NULL) {
        ErrorF("%s ENOMEM\n", __func__);
        return;
    }

    snprintf(name, sizeof name, "XWAYLAND%d", xwl_get_next_output_serial());

    xwl_output->lease_device = lease_device;
    xwl_output->xwl_screen = lease_device->xwl_screen;
    xwl_output->lease_connector = connector;
    xwl_output->randr_crtc = RRCrtcCreate(lease_device->xwl_screen->screen, xwl_output);
    if (!xwl_output->randr_crtc) {
        ErrorF("Failed creating RandR CRTC\n");
        goto err;
    }
    RRCrtcSetRotations(xwl_output->randr_crtc, ALL_ROTATIONS);
    xwl_output->randr_output = RROutputCreate(lease_device->xwl_screen->screen,
                                              name, strlen(name), xwl_output);
    if (!xwl_output->randr_output) {
        ErrorF("Failed creating RandR Output\n");
        goto err;
    }

    RRCrtcGammaSetSize(xwl_output->randr_crtc, 256);
    RROutputSetCrtcs(xwl_output->randr_output, &xwl_output->randr_crtc, 1);
    RROutputSetConnection(xwl_output->randr_output, RR_Connected);
    RROutputSetNonDesktop(xwl_output->randr_output, TRUE);
    xwl_output->randr_output->devPrivate = xwl_output;

    wp_drm_lease_connector_v1_add_listener(connector,
                                            &lease_connector_listener,
                                            xwl_output);

    xorg_list_append(&xwl_output->link, &lease_device->xwl_screen->output_list);
    return;

err:
    if (xwl_output->randr_crtc)
        RRCrtcDestroy(xwl_output->randr_crtc);
    free(xwl_output);
}

static void
drm_lease_device_handle_released(void *data,
                                 struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
    struct xwl_drm_lease_device *lease_device = data;
    xwl_screen_destroy_drm_lease_device(lease_device->xwl_screen, wp_drm_lease_device_v1);
}

static void
drm_lease_device_handle_done(void *data,
                             struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
    /* This space is deliberately left blank */
}

static const struct wp_drm_lease_device_v1_listener drm_lease_device_listener = {
    .drm_fd = drm_lease_device_handle_drm_fd,
    .connector = drm_lease_device_handle_connector,
    .released = drm_lease_device_handle_released,
    .done = drm_lease_device_handle_done,
};

void
xwl_screen_add_drm_lease_device(struct xwl_screen *xwl_screen, uint32_t id)
{
    struct wp_drm_lease_device_v1 *lease_device = wl_registry_bind(
        xwl_screen->registry, id, &wp_drm_lease_device_v1_interface, 1);
    struct xwl_drm_lease_device *device_data = malloc(sizeof(struct xwl_drm_lease_device));

    device_data->drm_lease_device = lease_device;
    device_data->xwl_screen = xwl_screen;
    device_data->drm_read_only_fd = -1;
    device_data->id = id;
    xorg_list_add(&device_data->link, &xwl_screen->drm_lease_devices);
    wp_drm_lease_device_v1_add_listener(lease_device,
                                         &drm_lease_device_listener,
                                         device_data);
}

void
xwl_screen_destroy_drm_lease_device(struct xwl_screen *xwl_screen,
                                    struct wp_drm_lease_device_v1 *wp_drm_lease_device_v1)
{
    struct xwl_drm_lease_device *device_data;

    xorg_list_for_each_entry(device_data, &xwl_screen->drm_lease_devices, link) {
        if (device_data->drm_lease_device == wp_drm_lease_device_v1) {
            wp_drm_lease_device_v1_destroy(wp_drm_lease_device_v1);
            xorg_list_del(&device_data->link);
            if (device_data->drm_read_only_fd >= 0)
                close(device_data->drm_read_only_fd);
            free(device_data);
            return;
        }
    }
}
