/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Event loop implementation for *BSD.
 *
 * Copyright: Eugene Wissner 2016-2019.
 * License: $(LINK2 https://www.mozilla.org/en-US/MPL/2.0/,
 *                  Mozilla Public License, v. 2.0).
 * Authors: $(LINK2 mailto:info@caraus.de, Eugene Wissner)
 * Source: $(LINK2 https://github.com/caraus-ecms/tanya/blob/master/source/tanya/async/event/kqueue.d,
 *                 tanya/async/event/kqueue.d)
 */
module tanya.async.event.kqueue;

version (D_Ddoc)
{
}
else version (OSX)
{
    version = MacBSD;
}
else version (iOS)
{
    version = MacBSD;
}
else version (TVOS)
{
    version = MacBSD;
}
else version (WatchOS)
{
    version = MacBSD;
}
else version (FreeBSD)
{
    version = MacBSD;
}
else version (OpenBSD)
{
    version = MacBSD;
}
else version (DragonFlyBSD)
{
    version = MacBSD;
}

version (MacBSD):

import core.stdc.errno;
import core.sys.posix.time; // timespec
import core.sys.posix.unistd;
import core.time;
import tanya.algorithm.comparison;
import tanya.async.event.selector;
import tanya.async.loop;
import tanya.async.transport;
import tanya.async.watcher;
import tanya.container.array;
import tanya.memory.allocator;
import tanya.network.socket;

void EV_SET(kevent_t* kevp, typeof(kevent_t.tupleof) args) pure nothrow @nogc
{
    *kevp = kevent_t(args);
}

enum : short
{
    EVFILT_READ     =  -1,
    EVFILT_WRITE    =  -2,
    EVFILT_AIO      =  -3, /* attached to aio requests */
    EVFILT_VNODE    =  -4, /* attached to vnodes */
    EVFILT_PROC     =  -5, /* attached to struct proc */
    EVFILT_SIGNAL   =  -6, /* attached to struct proc */
    EVFILT_TIMER    =  -7, /* timers */
    EVFILT_MACHPORT =  -8, /* Mach portsets */
    EVFILT_FS       =  -9, /* filesystem events */
    EVFILT_USER     = -10, /* User events */
    EVFILT_VM       = -12, /* virtual memory events */
    EVFILT_SYSCOUNT =  11
}

struct kevent_t
{
    uintptr_t ident; // Identifier for this event
    short filter; // Filter for event
    ushort flags;
    uint fflags;
    intptr_t data;
    void* udata; // Opaque user data identifier
}

enum
{
    /* actions */
    EV_ADD      = 0x0001, /* add event to kq (implies enable) */
    EV_DELETE   = 0x0002, /* delete event from kq */
    EV_ENABLE   = 0x0004, /* enable event */
    EV_DISABLE  = 0x0008, /* disable event (not reported) */

    /* flags */
    EV_ONESHOT  = 0x0010, /* only report one occurrence */
    EV_CLEAR    = 0x0020, /* clear event state after reporting */
    EV_RECEIPT  = 0x0040, /* force EV_ERROR on success, data=0 */
    EV_DISPATCH = 0x0080, /* disable event after reporting */

    EV_SYSFLAGS = 0xF000, /* reserved by system */
    EV_FLAG1    = 0x2000, /* filter-specific flag */

    /* returned values */
    EV_EOF      = 0x8000, /* EOF detected */
    EV_ERROR    = 0x4000, /* error, data contains errno */
}

extern(C) int kqueue() nothrow @nogc;
extern(C) int kevent(int kq, const kevent_t *changelist, int nchanges,
                     kevent_t *eventlist, int nevents, const timespec *timeout)
                     nothrow @nogc;

final class KqueueLoop : SelectorLoop
{
    protected int fd;
    private Array!kevent_t events;
    private Array!kevent_t changes;
    private size_t changeCount;

    /**
     * Returns: Maximal event count can be got at a time
     *          (should be supported by the backend).
     */
    override protected @property uint maxEvents()
    const pure nothrow @safe @nogc
    {
        return cast(uint) events.length;
    }

    this() @nogc
    {
        super();

        if ((fd = kqueue()) == -1)
        {
            throw make!BadLoopException(defaultAllocator,
                                        "kqueue initialization failed");
        }
        events = Array!kevent_t(64);
        changes = Array!kevent_t(64);
    }

    /**
     * Frees loop internals.
     */
    ~this() @nogc
    {
        close(fd);
    }

    private void set(SocketType socket, short filter, ushort flags) @nogc
    {
        if (changes.length <= changeCount)
        {
            changes.length = changeCount + maxEvents;
        }
        EV_SET(&changes[changeCount],
               cast(ulong) socket,
               filter,
               flags,
               0U,
               0,
               null);
        ++changeCount;
    }

    /**
     * Should be called if the backend configuration changes.
     *
     * Params:
     * 	watcher   = Watcher.
     * 	oldEvents = The events were already set.
     * 	events    = The events should be set.
     *
     * Returns: $(D_KEYWORD true) if the operation was successful.
     */
    override protected bool reify(SocketWatcher watcher,
                                  EventMask oldEvents,
                                  EventMask events) @nogc
    {
        if (events != oldEvents)
        {
            if (oldEvents & Event.read || oldEvents & Event.accept)
            {
                set(watcher.socket.handle, EVFILT_READ, EV_DELETE);
            }
            if (oldEvents & Event.write)
            {
                set(watcher.socket.handle, EVFILT_WRITE, EV_DELETE);
            }
        }
        if (events & (Event.read | events & Event.accept))
        {
            set(watcher.socket.handle, EVFILT_READ, EV_ADD | EV_ENABLE);
        }
        if (events & Event.write)
        {
            set(watcher.socket.handle, EVFILT_WRITE, EV_ADD | EV_DISPATCH);
        }
        return true;
    }

    /**
     * Does the actual polling.
     */
    protected override void poll() @nogc
    {
        timespec ts;
        blockTime.split!("seconds", "nsecs")(ts.tv_sec, ts.tv_nsec);

        if (changeCount > maxEvents)
        {
            events.length = changes.length;
        }

        auto eventCount = kevent(fd,
                                 changes.get().ptr,
                                 cast(int) changeCount,
                                 events.get().ptr,
                                 maxEvents,
                                 &ts);
        changeCount = 0;

        if (eventCount < 0)
        {
            if (errno != EINTR)
            {
                throw defaultAllocator.make!BadLoopException();
            }
            return;
        }

        for (int i; i < eventCount; ++i)
        {
            assert(connections.length > events[i].ident);

            auto transport = cast(StreamTransport) connections[events[i].ident];
            // If it is a ConnectionWatcher. Accept connections.
            if (transport is null)
            {
                auto connection = cast(ConnectionWatcher) connections[events[i].ident];
                assert(connection !is null);

                acceptConnections(connection);
            }
            else if (events[i].flags & EV_ERROR)
            {
                kill(transport);
            }
            else if (events[i].filter == EVFILT_READ)
            {
                SocketException exception;
                try
                {
                    ptrdiff_t received;
                    do
                    {
                        received = transport.socket.receive(transport.output[]);
                        transport.output += received;
                    }
                    while (received);
                }
                catch (SocketException e)
                {
                    exception = e;
                }
                if (transport.socket.disconnected)
                {
                    kill(transport, exception);
                }
                else if (transport.output.length)
                {
                    pendings.insertBack(transport);
                }
            }
            else if (events[i].filter == EVFILT_WRITE)
            {
                transport.writeReady = true;
                if (transport.input.length)
                {
                    feed(transport);
                }
            }
        }
    }

    /**
     * Returns: The blocking time.
     */
    override protected @property inout(Duration) blockTime()
    inout @nogc @safe pure nothrow
    {
        return min(super.blockTime, 1.dur!"seconds");
    }

    /**
     * If the transport couldn't send the data, the further sending should
     * be handled by the event loop.
     *
     * Params:
     * 	transport = Transport.
     * 	exception = Exception thrown on sending.
     *
     * Returns: $(D_KEYWORD true) if the operation could be successfully
     *          completed or scheduled, $(D_KEYWORD false) otherwise (the
     *          transport will be destroyed then).
     */
    protected override bool feed(StreamTransport transport,
                                 SocketException exception = null) @nogc
    {
        if (!super.feed(transport, exception))
        {
            return false;
        }
        if (!transport.writeReady)
        {
            set(transport.socket.handle, EVFILT_WRITE, EV_DISPATCH);
            return true;
        }
        return false;
    }
}
