/*
 * Copyright © 2016 Keith Packard
 *
 * 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.
 */

#ifdef HAVE_DIX_CONFIG_H
#include <dix-config.h>
#endif

#include <X11/X.h>
#include <X11/Xproto.h>
#include <stdlib.h>
#include <unistd.h>
#include "misc.h"               /* for typedef of pointer */
#include "ospoll.h"
#include "list.h"

#if !HAVE_OSPOLL && HAVE_EPOLL_CREATE1
#include <sys/epoll.h>
#define EPOLL           1
#define HAVE_OSPOLL     1
#endif

#if !HAVE_OSPOLL
#include "xserver_poll.h"
#define POLL            1
#define HAVE_OSPOLL     1
#endif

#if EPOLL
#include <sys/epoll.h>

/* epoll-based implementation */
struct ospollfd {
    int                 fd;
    int                 xevents;
    enum ospoll_trigger trigger;
    void                (*callback)(int fd, int xevents, void *data);
    void                *data;
    struct xorg_list    deleted;
};

struct ospoll {
    int                 epoll_fd;
    struct ospollfd     **fds;
    int                 num;
    int                 size;
    struct xorg_list    deleted;
};

#endif

#if POLL

/* poll-based implementation */
struct ospollfd {
    short               revents;
    enum ospoll_trigger trigger;
    void                (*callback)(int fd, int revents, void *data);
    void                *data;
};

struct ospoll {
    struct pollfd       *fds;
    struct ospollfd     *osfds;
    int                 num;
    int                 size;
    Bool                changed;
};

#endif

/* Binary search for the specified file descriptor
 *
 * Returns position if found
 * Returns -position - 1 if not found
 */

static int
ospoll_find(struct ospoll *ospoll, int fd)
{
    int lo = 0;
    int hi = ospoll->num - 1;

    while (lo <= hi) {
        int m = (lo + hi) >> 1;
#if EPOLL
        int t = ospoll->fds[m]->fd;
#endif
#if POLL
        int t = ospoll->fds[m].fd;
#endif

        if (t < fd)
            lo = m + 1;
        else if (t > fd)
            hi = m - 1;
        else
            return m;
    }
    return -(lo + 1);
}

#if EPOLL
static void
ospoll_clean_deleted(struct ospoll *ospoll)
{
    struct ospollfd     *osfd, *tmp;

    xorg_list_for_each_entry_safe(osfd, tmp, &ospoll->deleted, deleted) {
        xorg_list_del(&osfd->deleted);
        free(osfd);
    }
}
#endif

/* Insert an element into an array
 *
 * base: base address of array
 * num:  number of elements in the array before the insert
 * size: size of each element
 * pos:  position to insert at
 */
static inline void
array_insert(void *base, size_t num, size_t size, size_t pos)
{
    char *b = base;

    memmove(b + (pos+1) * size,
            b + pos * size,
            (num - pos) * size);
}

/* Delete an element from an array
 *
 * base: base address of array
 * num:  number of elements in the array before the delete
 * size: size of each element
 * pos:  position to delete from
 */
static inline void
array_delete(void *base, size_t num, size_t size, size_t pos)
{
    char *b = base;

    memmove(b + pos * size, b + (pos + 1) * size,
            (num - pos - 1) * size);
}


struct ospoll *
ospoll_create(void)
{
#if EPOLL
    struct ospoll       *ospoll = calloc(1, sizeof (struct ospoll));

    ospoll->epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    if (ospoll->epoll_fd < 0) {
        free (ospoll);
        return NULL;
    }
    xorg_list_init(&ospoll->deleted);
    return ospoll;
#endif
#if POLL
    return calloc(1, sizeof (struct ospoll));
#endif
}

void
ospoll_destroy(struct ospoll *ospoll)
{
#if EPOLL
    if (ospoll) {
        assert (ospoll->num == 0);
        close(ospoll->epoll_fd);
        ospoll_clean_deleted(ospoll);
        free(ospoll->fds);
        free(ospoll);
    }
#endif
#if POLL
    if (ospoll) {
        assert (ospoll->num == 0);
        free (ospoll->fds);
        free (ospoll->osfds);
        free (ospoll);
    }
#endif
}

Bool
ospoll_add(struct ospoll *ospoll, int fd,
           enum ospoll_trigger trigger,
           void (*callback)(int fd, int xevents, void *data),
           void *data)
{
    int pos = ospoll_find(ospoll, fd);
#if EPOLL
    struct ospollfd *osfd;

    if (pos < 0) {

        struct epoll_event ev;

        osfd = calloc(1, sizeof (struct ospollfd));
        if (!osfd)
            return FALSE;

        if (ospoll->num >= ospoll->size) {
            struct ospollfd **new_fds;
            int new_size = ospoll->size ? ospoll->size * 2 : MAXCLIENTS * 2;

            new_fds = reallocarray(ospoll->fds, new_size, sizeof (ospoll->fds[0]));
            if (!new_fds) {
                free (osfd);
                return FALSE;
            }
            ospoll->fds = new_fds;
            ospoll->size = new_size;
        }

        ev.events = 0;
        ev.data.ptr = osfd;
        if (trigger == ospoll_trigger_edge)
            ev.events |= EPOLLET;
        if (epoll_ctl(ospoll->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
            free(osfd);
            return FALSE;
        }
        osfd->fd = fd;
        osfd->xevents = 0;

        pos = -pos - 1;
        array_insert(ospoll->fds, ospoll->num, sizeof (ospoll->fds[0]), pos);
        ospoll->fds[pos] = osfd;
        ospoll->num++;
    } else {
        osfd = ospoll->fds[pos];
    }
    osfd->data = data;
    osfd->callback = callback;
    osfd->trigger = trigger;
#endif
#if POLL
    if (pos < 0) {
        if (ospoll->num == ospoll->size) {
            struct pollfd   *new_fds;
            struct ospollfd *new_osfds;
            int             new_size = ospoll->size ? ospoll->size * 2 : MAXCLIENTS * 2;

            new_fds = reallocarray(ospoll->fds, new_size, sizeof (ospoll->fds[0]));
            if (!new_fds)
                return FALSE;
            ospoll->fds = new_fds;
            new_osfds = reallocarray(ospoll->osfds, new_size, sizeof (ospoll->osfds[0]));
            if (!new_osfds)
                return FALSE;
            ospoll->osfds = new_osfds;
            ospoll->size = new_size;
        }
        pos = -pos - 1;
        array_insert(ospoll->fds, ospoll->num, sizeof (ospoll->fds[0]), pos);
        array_insert(ospoll->osfds, ospoll->num, sizeof (ospoll->osfds[0]), pos);
        ospoll->num++;
        ospoll->changed = TRUE;

        ospoll->fds[pos].fd = fd;
        ospoll->fds[pos].events = 0;
        ospoll->fds[pos].revents = 0;
        ospoll->osfds[pos].revents = 0;
    }
    ospoll->osfds[pos].trigger = trigger;
    ospoll->osfds[pos].callback = callback;
    ospoll->osfds[pos].data = data;
#endif
    return TRUE;
}

void
ospoll_remove(struct ospoll *ospoll, int fd)
{
    int pos = ospoll_find(ospoll, fd);

    pos = ospoll_find(ospoll, fd);
    if (pos >= 0) {
#if EPOLL
        struct ospollfd *osfd = ospoll->fds[pos];
        struct epoll_event ev;
        ev.events = 0;
        ev.data.ptr = osfd;
        (void) epoll_ctl(ospoll->epoll_fd, EPOLL_CTL_DEL, fd, &ev);

        array_delete(ospoll->fds, ospoll->num, sizeof (ospoll->fds[0]), pos);
        ospoll->num--;
        osfd->callback = NULL;
        osfd->data = NULL;
        xorg_list_add(&osfd->deleted, &ospoll->deleted);
#endif
#if POLL
        array_delete(ospoll->fds, ospoll->num, sizeof (ospoll->fds[0]), pos);
        array_delete(ospoll->osfds, ospoll->num, sizeof (ospoll->osfds[0]), pos);
        ospoll->num--;
        ospoll->changed = TRUE;
#endif
    }
}

#if EPOLL
static void
epoll_mod(struct ospoll *ospoll, struct ospollfd *osfd)
{
    struct epoll_event ev;
    ev.events = 0;
    if (osfd->xevents & X_NOTIFY_READ)
        ev.events |= EPOLLIN;
    if (osfd->xevents & X_NOTIFY_WRITE)
        ev.events |= EPOLLOUT;
    if (osfd->trigger == ospoll_trigger_edge)
        ev.events |= EPOLLET;
    ev.data.ptr = osfd;
    (void) epoll_ctl(ospoll->epoll_fd, EPOLL_CTL_MOD, osfd->fd, &ev);
}
#endif

void
ospoll_listen(struct ospoll *ospoll, int fd, int xevents)
{
    int pos = ospoll_find(ospoll, fd);

    if (pos >= 0) {
#if EPOLL
        struct ospollfd *osfd = ospoll->fds[pos];
        osfd->xevents |= xevents;
        epoll_mod(ospoll, osfd);
#endif
#if POLL
        if (xevents & X_NOTIFY_READ) {
            ospoll->fds[pos].events |= POLLIN;
            ospoll->osfds[pos].revents &= ~POLLIN;
        }
        if (xevents & X_NOTIFY_WRITE) {
            ospoll->fds[pos].events |= POLLOUT;
            ospoll->osfds[pos].revents &= ~POLLOUT;
        }
#endif
    }
}

void
ospoll_mute(struct ospoll *ospoll, int fd, int xevents)
{
    int pos = ospoll_find(ospoll, fd);

    if (pos >= 0) {
#if EPOLL
        struct ospollfd *osfd = ospoll->fds[pos];
        osfd->xevents &= ~xevents;
        epoll_mod(ospoll, osfd);
#endif
#if POLL
        if (xevents & X_NOTIFY_READ)
            ospoll->fds[pos].events &= ~POLLIN;
        if (xevents & X_NOTIFY_WRITE)
            ospoll->fds[pos].events &= ~POLLOUT;
#endif
    }
}


int
ospoll_wait(struct ospoll *ospoll, int timeout)
{
    int nready;
#if EPOLL
#define MAX_EVENTS      256
    struct epoll_event events[MAX_EVENTS];
    int i;

    nready = epoll_wait(ospoll->epoll_fd, events, MAX_EVENTS, timeout);
    for (i = 0; i < nready; i++) {
        struct epoll_event *ev = &events[i];
        struct ospollfd *osfd = ev->data.ptr;
        uint32_t revents = ev->events;
        int xevents = 0;

        if (revents & EPOLLIN)
            xevents |= X_NOTIFY_READ;
        if (revents & EPOLLOUT)
            xevents |= X_NOTIFY_WRITE;
        if (revents & (~(EPOLLIN|EPOLLOUT)))
            xevents |= X_NOTIFY_ERROR;

        if (osfd->callback)
            osfd->callback(osfd->fd, xevents, osfd->data);
    }
    ospoll_clean_deleted(ospoll);
#endif
#if POLL
    nready = xserver_poll(ospoll->fds, ospoll->num, timeout);
    ospoll->changed = FALSE;
    if (nready > 0) {
        int f;
        for (f = 0; f < ospoll->num; f++) {
            short revents = ospoll->fds[f].revents;
            short oldevents = ospoll->osfds[f].revents;

            ospoll->osfds[f].revents = (revents & (POLLIN|POLLOUT));
            if (ospoll->osfds[f].trigger == ospoll_trigger_edge)
                revents &= ~oldevents;
            if (revents) {
                int    xevents = 0;
                if (revents & POLLIN)
                    xevents |= X_NOTIFY_READ;
                if (revents & POLLOUT)
                    xevents |= X_NOTIFY_WRITE;
                if (revents & (~(POLLIN|POLLOUT)))
                    xevents |= X_NOTIFY_ERROR;
                ospoll->osfds[f].callback(ospoll->fds[f].fd, xevents,
                                          ospoll->osfds[f].data);

                /* Check to see if the arrays have changed, and just go back
                 * around again
                 */
                if (ospoll->changed)
                    break;
            }
        }
    }
#endif
    return nready;
}

void
ospoll_reset_events(struct ospoll *ospoll, int fd)
{
#if POLL
    int pos = ospoll_find(ospoll, fd);

    if (pos < 0)
        return;

    ospoll->osfds[pos].revents = 0;
#endif
}

void *
ospoll_data(struct ospoll *ospoll, int fd)
{
    int pos = ospoll_find(ospoll, fd);

    if (pos < 0)
        return NULL;
#if EPOLL
    return ospoll->fds[pos]->data;
#endif
#if POLL
    return ospoll->osfds[pos].data;
#endif
}
