/*
 * Copyright (c) 2014 VMware, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "precomp.h"

#include "Datapath.h"
#include "Vport.h"
#include "Event.h"

#ifdef OVS_DBG_MOD
#undef OVS_DBG_MOD
#endif
#define OVS_DBG_MOD OVS_DBG_EVENT
#include "Debug.h"

LIST_ENTRY ovsEventQueueArr[OVS_MCAST_EVENT_TYPES_MAX];
static NDIS_SPIN_LOCK eventQueueLockArr[OVS_MCAST_EVENT_TYPES_MAX];
UINT32 ovsNumEventQueueArr[OVS_MCAST_EVENT_TYPES_MAX];

NTSTATUS
OvsInitEventQueue()
{
    for (int i = 0; i < OVS_MCAST_EVENT_TYPES_MAX; i++) {
        InitializeListHead(&ovsEventQueueArr[i]);
        NdisAllocateSpinLock(&eventQueueLockArr[i]);
    }
    return STATUS_SUCCESS;
}

VOID
OvsCleanupEventQueue()
{
    for (int i = 0; i < OVS_MCAST_EVENT_TYPES_MAX; i++) {
        ASSERT(IsListEmpty(&ovsEventQueueArr[i]));
        ASSERT(ovsNumEventQueueArr[i] == 0);
        NdisFreeSpinLock(&eventQueueLockArr[i]);
    }
}

static __inline VOID
OvsAcquireEventQueueLock(int eventId)
{
    NdisAcquireSpinLock(&eventQueueLockArr[eventId]);
}

static __inline VOID
OvsReleaseEventQueueLock(int eventId)
{
   NdisReleaseSpinLock(&eventQueueLockArr[eventId]);
}

NDIS_STATUS
OvsGetMcastEventId(UINT32 protocol, UINT32 mcastMask, UINT32 *eventId)
{
    switch (protocol) {
    case NETLINK_GENERIC:
        *eventId = OVS_MCAST_VPORT_EVENT;
        return NDIS_STATUS_SUCCESS;
    case NETLINK_NETFILTER:
        if ((mcastMask & OVS_EVENT_CT_NEW)
            || (mcastMask & OVS_EVENT_CT_DELETE)) {
            *eventId =  OVS_MCAST_CT_EVENT;
            return NDIS_STATUS_SUCCESS;
        }
        break;
    default:
        goto error;
    }

error:
    return NDIS_STATUS_INVALID_PARAMETER;
}

/*
 * --------------------------------------------------------------------------
 * Cleanup the event queue of the OpenInstance.
 * --------------------------------------------------------------------------
 */
VOID
OvsCleanupEvent(POVS_OPEN_INSTANCE instance)
{
    POVS_EVENT_QUEUE queue;
    PIRP irp = NULL;
    UINT32 eventId;
    queue = (POVS_EVENT_QUEUE)instance->eventQueue;
    if (queue) {
        POVS_EVENT_QUEUE_ELEM elem;
        PLIST_ENTRY link, next;

        /* Handle the error */
        OvsGetMcastEventId(instance->protocol, instance->mcastMask, &eventId);
        OvsAcquireEventQueueLock(eventId);
        RemoveEntryList(&queue->queueLink);
        ovsNumEventQueueArr[eventId]--;
        if (queue->pendingIrp) {
            PDRIVER_CANCEL cancelRoutine;
            irp = queue->pendingIrp;
            cancelRoutine = IoSetCancelRoutine(irp, NULL);
            queue->pendingIrp = NULL;
            if (cancelRoutine == NULL) {
                irp = NULL;
            }
        }
        instance->eventQueue = NULL;
        OvsReleaseEventQueueLock(eventId);
        if (irp) {
            OvsCompleteIrpRequest(irp, 0, STATUS_SUCCESS);
        }

        LIST_FORALL_SAFE(&queue->elemList, link, next) {
            elem = CONTAINING_RECORD(link, OVS_EVENT_QUEUE_ELEM, link);
            OvsFreeMemoryWithTag(elem, OVS_EVENT_POOL_TAG);
        }
        OvsFreeMemoryWithTag(queue, OVS_EVENT_POOL_TAG);
    }
}

/*
 * --------------------------------------------------------------------------
 * When event is generated, we need to post the event to all
 * the event queues. If there is pending Irp waiting for event
 * complete the Irp to wakeup the user thread.
 *
 * Side effects: User thread may be woken up.
 * --------------------------------------------------------------------------
 */
VOID
OvsPostVportEvent(POVS_VPORT_EVENT_ENTRY event)
{
    POVS_EVENT_QUEUE_ELEM elem;
    POVS_EVENT_QUEUE queue;
    PLIST_ENTRY link;
    LIST_ENTRY list;
    PLIST_ENTRY entry;
    PIRP irp;

    InitializeListHead(&list);

    OVS_LOG_TRACE("Enter: portNo: %#x, status: %#x", event->portNo,
                  event->type);

    OvsAcquireEventQueueLock(OVS_MCAST_VPORT_EVENT);

    LIST_FORALL(&ovsEventQueueArr[OVS_MCAST_VPORT_EVENT], link) {
        queue = CONTAINING_RECORD(link, OVS_EVENT_QUEUE, queueLink);
        if ((event->type & queue->mask) == 0) {
            continue;
        }
        event->type &= queue->mask;

        elem = (POVS_EVENT_QUEUE_ELEM)OvsAllocateMemoryWithTag(
            sizeof(*elem), OVS_EVENT_POOL_TAG);

        if (elem == NULL) {
            OVS_LOG_WARN("Fail to allocate memory for event");
            OvsReleaseEventQueueLock(OVS_MCAST_VPORT_EVENT);
            return;
        }

        RtlCopyMemory(&elem->vportEvent, event, sizeof elem->vportEvent);
        InsertTailList(&queue->elemList, &elem->link);
        queue->numElems++;
        OVS_LOG_INFO("Queue: %p, numElems: %d",
                        queue, queue->numElems);

        if (queue->pendingIrp != NULL) {
            PDRIVER_CANCEL cancelRoutine;
            irp = queue->pendingIrp;
            queue->pendingIrp = NULL;
            cancelRoutine = IoSetCancelRoutine(irp, NULL);
            if (cancelRoutine) {
                InsertTailList(&list, &irp->Tail.Overlay.ListEntry);
            }
        }
    }
    OvsReleaseEventQueueLock(OVS_MCAST_VPORT_EVENT);
    while (!IsListEmpty(&list)) {
        entry = RemoveHeadList(&list);
        irp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry);
        OVS_LOG_INFO("Wakeup thread with IRP: %p", irp);
        OvsCompleteIrpRequest(irp, 0, STATUS_SUCCESS);
    }
}


/*
 * --------------------------------------------------------------------------
 * Subscribe for event notification.
 *
 * Results:
 *     STATUS_SUCCESS for valid request and enough resource.
 *     STATUS_NO_RESOURCES for queue allocation failure
 *     STATUS_INVALID_PARAMETER for invalid request
 *
 * Side effects:
 *     Event queue is created for the current open instance.
 * --------------------------------------------------------------------------
 */
NTSTATUS
OvsSubscribeEventIoctl(PFILE_OBJECT fileObject,
                       PVOID inputBuffer,
                       UINT32 inputLength)
{
    POVS_EVENT_SUBSCRIBE request = (POVS_EVENT_SUBSCRIBE)inputBuffer;
    NTSTATUS status = STATUS_SUCCESS;
    POVS_OPEN_INSTANCE instance;
    POVS_EVENT_QUEUE queue = NULL;
    UINT32 eventId;

    OVS_LOG_TRACE("Enter: fileObject: %p, inputLength: %d", fileObject,
                  inputLength);

    if (request->protocol == NETLINK_GENERIC) {
        if (inputLength < sizeof (OVS_EVENT_SUBSCRIBE) ||
            (request->mask & OVS_EVENT_MASK_ALL) == 0) {
            OVS_LOG_TRACE("Exit: subscribe failed with invalid request.");
            return STATUS_INVALID_PARAMETER;
        }
    }

    status = OvsGetMcastEventId(request->protocol, request->mask, &eventId);
    if (status != NDIS_STATUS_SUCCESS) {
        return STATUS_INVALID_PARAMETER;
    }

    OvsAcquireEventQueueLock(eventId);

    instance = OvsGetOpenInstance(fileObject, request->dpNo);

    if (instance == NULL) {
        status = STATUS_INVALID_PARAMETER;
        OVS_LOG_WARN("can not find open instance");
        goto done_event_subscribe;
    }

    /*
     * XXX for now, we don't allow change mask.
     */
    queue = (POVS_EVENT_QUEUE)instance->eventQueue;
    if (request->subscribe && queue) {
        if (request->protocol == NETLINK_GENERIC
            && queue->mask != request->mask) {
            status = STATUS_INVALID_PARAMETER;
            OVS_LOG_WARN("Can not change mask when the queue is subscribed");
            goto done_event_subscribe;
        }
        if (request->protocol == NETLINK_NETFILTER) {
            if (queue->mask == request->mask) {
                /* Resubscribing to subscribed event */
                status = STATUS_SUCCESS;
                goto done_event_subscribe;
            } else {
                /* Update the instance and queue mask to reflect this */
                queue->mask |= request->mask;
                instance->mcastMask |= request->mask;
            }
            status = STATUS_SUCCESS;
            goto done_event_subscribe;
        }
    } else if (!request->subscribe && queue == NULL) {
        status = STATUS_SUCCESS;
        goto done_event_subscribe;
    }

    if (request->subscribe) {
        queue = (POVS_EVENT_QUEUE)OvsAllocateMemoryWithTag(
            sizeof(OVS_EVENT_QUEUE), OVS_EVENT_POOL_TAG);
        if (queue == NULL) {
            status = STATUS_NO_MEMORY;
            OVS_LOG_WARN("Fail to allocate event queue");
            goto done_event_subscribe;
        }
        InitializeListHead(&queue->elemList);
        queue->mask = request->mask;
        queue->mcastEventId = eventId;
        queue->pendingIrp = NULL;
        queue->numElems = 0;
        InsertHeadList(&ovsEventQueueArr[eventId], &queue->queueLink);
        ovsNumEventQueueArr[eventId]++;
        instance->eventQueue = queue;
        instance->mcastMask = request->mask;
        queue->instance = instance;
    } else {
        queue = (POVS_EVENT_QUEUE)instance->eventQueue;
        queue->mask &= ~(request->mask);
        instance->mcastMask &= ~(request->mask);
        if (!queue->mask) {
            /* No other mcast group exists */
            RemoveEntryList(&queue->queueLink);
            ovsNumEventQueueArr[eventId]--;
            instance->eventQueue = NULL;
            instance->mcastMask = 0;
        }
    }
done_event_subscribe:
    if (!request->subscribe && queue && !queue->mask) {
        POVS_EVENT_QUEUE_ELEM elem;
        PLIST_ENTRY link, next;
        PIRP irp = NULL;
        if (queue->pendingIrp) {
            PDRIVER_CANCEL cancelRoutine;
            irp = queue->pendingIrp;
            queue->pendingIrp = NULL;
            cancelRoutine = IoSetCancelRoutine(irp, NULL);
            if (cancelRoutine == NULL) {
                irp = NULL;
            }
        }
        OvsReleaseEventQueueLock(eventId);
        if (irp) {
            OvsCompleteIrpRequest(queue->pendingIrp, 0, STATUS_SUCCESS);
        }
        LIST_FORALL_SAFE(&queue->elemList, link, next) {
            elem = CONTAINING_RECORD(link, OVS_EVENT_QUEUE_ELEM, link);
            OvsFreeMemoryWithTag(elem, OVS_EVENT_POOL_TAG);
        }
        OvsFreeMemoryWithTag(queue, OVS_EVENT_POOL_TAG);
    } else {
        OvsReleaseEventQueueLock(eventId);
    }
    OVS_LOG_TRACE("Exit: subscribe event with status: %#x.", status);
    return status;
}

/*
 * --------------------------------------------------------------------------
 * Cancel wait IRP for event
 *
 * Please note, when this routine is called, it is always guaranteed that
 * IRP is valid.
 *
 * Side effects: Pending IRP is completed.
 * --------------------------------------------------------------------------
 */
VOID
OvsCancelIrp(PDEVICE_OBJECT deviceObject,
             PIRP irp)
{
    PIO_STACK_LOCATION irpSp;
    PFILE_OBJECT fileObject;
    POVS_EVENT_QUEUE queue;
    POVS_OPEN_INSTANCE instance;
    UINT32 eventId;
    NDIS_STATUS status;

    UNREFERENCED_PARAMETER(deviceObject);

    IoReleaseCancelSpinLock(irp->CancelIrql);

    irpSp = IoGetCurrentIrpStackLocation(irp);
    fileObject = irpSp->FileObject;

    if (fileObject == NULL) {
        goto done;
    }

    instance = (POVS_OPEN_INSTANCE)fileObject->FsContext;
    if (instance == NULL) {
        goto done;
    }

    status = OvsGetMcastEventId(instance->protocol,
                                instance->mcastMask,
                                &eventId);
    if (status != NDIS_STATUS_SUCCESS) {
        goto done;
    }

    OvsAcquireEventQueueLock(eventId);
    if (instance->eventQueue == NULL) {
        OvsReleaseEventQueueLock(eventId);
        goto done;
    }

    queue = instance->eventQueue;
    if (queue->pendingIrp == irp) {
        queue->pendingIrp = NULL;
    }
    OvsReleaseEventQueueLock(eventId);
done:
    OvsCompleteIrpRequest(irp, 0, STATUS_CANCELLED);
}

/*
 * --------------------------------------------------------------------------
 * Wait for event.
 *
 * Results:
 *     STATUS_SUCCESS for valid request
 *     STATUS_DEVICE_BUSY if already in waiting state.
 *     STATUS_INVALID_PARAMETER for invalid request
 *     STATUS_PENDING wait for event
 *
 * Side effects:
 *     May return pending to IO manager.
 * --------------------------------------------------------------------------
 */
NTSTATUS
OvsWaitEventIoctl(PIRP irp,
                  PFILE_OBJECT fileObject,
                  PVOID inputBuffer,
                  UINT32 inputLength)
{
    NTSTATUS status = STATUS_SUCCESS;
    POVS_EVENT_POLL poll;
    POVS_EVENT_QUEUE queue;
    POVS_OPEN_INSTANCE instance;
    BOOLEAN cancelled = FALSE;
    PDRIVER_CANCEL cancelRoutine;
    UINT32 eventId;

    OVS_LOG_TRACE("Enter: inputLength: %u", inputLength);

    if (inputLength < sizeof (OVS_EVENT_POLL)) {
        OVS_LOG_TRACE("Exit: Invalid input buffer length.");
        return STATUS_INVALID_PARAMETER;
    }
    poll = (POVS_EVENT_POLL)inputBuffer;

    instance = OvsGetOpenInstance(fileObject, poll->dpNo);
    if (instance == NULL) {
        OVS_LOG_TRACE("Exit: Can not find open instance, dpNo: %d",
                      poll->dpNo);
        return STATUS_INVALID_PARAMETER;
    }

    status = OvsGetMcastEventId(instance->protocol,
                                instance->mcastMask,
                                &eventId);
    if (status != NDIS_STATUS_SUCCESS) {
        return STATUS_INVALID_PARAMETER;
    }
    OvsAcquireEventQueueLock(eventId);

    queue = (POVS_EVENT_QUEUE)instance->eventQueue;
    if (queue == NULL) {
        OVS_LOG_TRACE("Exit: Event queue does not exist");
        status = STATUS_INVALID_PARAMETER;
        goto unlock;
    }
    if (queue->pendingIrp) {
        OVS_LOG_TRACE("Exit: Event queue already in pending state");
        status = STATUS_DEVICE_BUSY;
        goto unlock;
    }

    IoMarkIrpPending(irp);
    IoSetCancelRoutine(irp, OvsCancelIrp);
    if (irp->Cancel) {
        cancelRoutine = IoSetCancelRoutine(irp, NULL);
        if (cancelRoutine) {
            cancelled = TRUE;
        }
    } else {
        queue->pendingIrp = irp;
        status = STATUS_PENDING;
    }

unlock:
    OvsReleaseEventQueueLock(eventId);
    if (cancelled) {
        OvsCompleteIrpRequest(irp, 0, STATUS_CANCELLED);
        OVS_LOG_INFO("Event IRP cancelled: %p", irp);
    }
    OVS_LOG_TRACE("Exit: return status: %#x", status);
    return status;
}

/*
 *--------------------------------------------------------------------------
 * Poll event queued in the event queue.always synchronous.
 *
 * Results:
 *     STATUS_SUCCESS event was dequeued
 *     STATUS_UNSUCCESSFUL the queue is empty.
 * --------------------------------------------------------------------------
 */
NTSTATUS
OvsRemoveVportEventEntry(POVS_OPEN_INSTANCE instance,
                         POVS_VPORT_EVENT_ENTRY entry)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    POVS_EVENT_QUEUE queue;
    POVS_EVENT_QUEUE_ELEM elem;

    OvsAcquireEventQueueLock(OVS_MCAST_VPORT_EVENT);

    queue = (POVS_EVENT_QUEUE)instance->eventQueue;

    if (queue == NULL) {
        ASSERT(queue);
        goto remove_event_done;
    }

    if (queue->numElems) {
        elem = (POVS_EVENT_QUEUE_ELEM)RemoveHeadList(&queue->elemList);
        *entry = elem->vportEvent;
        OvsFreeMemoryWithTag(elem, OVS_EVENT_POOL_TAG);
        queue->numElems--;
        status = STATUS_SUCCESS;
    }

remove_event_done:
    OvsReleaseEventQueueLock(OVS_MCAST_VPORT_EVENT);
    return status;
}

/*
 * --------------------------------------------------------------------------
 * OvsPostCtEvent - used to post a Conntrack related event
 *
 * Side effects: User thread may be woken up.
 * XXX - Try to consolidate PostEvent for Vport/Ct events
 * --------------------------------------------------------------------------
 */
VOID
OvsPostCtEvent(POVS_CT_EVENT_ENTRY ctEvent)
{
    POVS_EVENT_QUEUE_ELEM elem;
    POVS_EVENT_QUEUE queue;
    PLIST_ENTRY link;
    LIST_ENTRY list;
    PLIST_ENTRY entry;
    PIRP irp;

    InitializeListHead(&list);

    OvsAcquireEventQueueLock(OVS_MCAST_CT_EVENT);

    LIST_FORALL(&ovsEventQueueArr[OVS_MCAST_CT_EVENT], link) {
        queue = CONTAINING_RECORD(link, OVS_EVENT_QUEUE, queueLink);
        if ((ctEvent->type & queue->mask) == 0) {
            continue;
        }
        ctEvent->type &= queue->mask;

        elem = (POVS_EVENT_QUEUE_ELEM)OvsAllocateMemoryWithTag(
            sizeof(*elem), OVS_EVENT_POOL_TAG);

        if (elem == NULL) {
            OvsReleaseEventQueueLock(OVS_MCAST_CT_EVENT);
            return;
        }

        RtlCopyMemory(&elem->ctEvent, ctEvent, sizeof elem->ctEvent);
        InsertTailList(&queue->elemList, &elem->link);
        queue->numElems++;

        if (queue->pendingIrp != NULL) {
            PDRIVER_CANCEL cancelRoutine;
            irp = queue->pendingIrp;
            queue->pendingIrp = NULL;
            cancelRoutine = IoSetCancelRoutine(irp, NULL);
            if (cancelRoutine) {
                InsertTailList(&list, &irp->Tail.Overlay.ListEntry);
            }
        }
    }

    OvsReleaseEventQueueLock(OVS_MCAST_CT_EVENT);
    while (!IsListEmpty(&list)) {
        entry = RemoveHeadList(&list);
        irp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry);
        OvsCompleteIrpRequest(irp, 0, STATUS_SUCCESS);
    }
}

/*
 *--------------------------------------------------------------------------
 * Poll event queued in the event queue.always synchronous.
 *
 * Results:
 *     STATUS_SUCCESS event was dequeued
 *     STATUS_UNSUCCESSFUL the queue is empty.
 * --------------------------------------------------------------------------
 */
NTSTATUS
OvsRemoveCtEventEntry(POVS_OPEN_INSTANCE instance,
                      POVS_CT_EVENT_ENTRY entry)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    POVS_EVENT_QUEUE queue;
    POVS_EVENT_QUEUE_ELEM elem;

    OvsAcquireEventQueueLock(OVS_MCAST_CT_EVENT);

    queue = (POVS_EVENT_QUEUE)instance->eventQueue;

    if (queue == NULL) {
        ASSERT(queue);
        goto remove_event_done;
    }

    if (queue->numElems) {
        elem = (POVS_EVENT_QUEUE_ELEM)RemoveHeadList(&queue->elemList);
        *entry = elem->ctEvent;
        OvsFreeMemoryWithTag(elem, OVS_EVENT_POOL_TAG);
        queue->numElems--;
        status = STATUS_SUCCESS;
    }

remove_event_done:
    OvsReleaseEventQueueLock(OVS_MCAST_CT_EVENT);
    return status;
}
