#include "precomp.h"
#include "vioser.h"
#include "public.h"

#if defined(EVENT_TRACING)
#include "Control.tmh"
#endif

static
VOID
VIOSerialHandleCtrlMsg(
    IN WDFDEVICE Device,
    IN PPORT_BUFFER buf
);

#define     CTRL_MSG_INITIAL_WAIT           (-1000000LL)
#define     CTRL_MSG_NORMAL_WAIT           (-10000000LL)

VOID
VIOSerialSendCtrlMsg(
    IN WDFDEVICE Device,
    IN ULONG id,
    IN USHORT event,
    IN USHORT value,
    IN BOOLEAN LongWaitAllowed
)
{
    struct VirtIOBufferDescriptor sg;
    struct virtqueue *vq;
    UINT len;
    PPORTS_DEVICE pContext = GetPortsDevice(Device);
    VIRTIO_CONSOLE_CONTROL *cpkt;

    if (!pContext->isHostMultiport || !pContext->ControlDmaBlock)
    {
        return;
    }

    vq = pContext->c_ovq;

    cpkt = pContext->ControlDmaBlock->get_slice(pContext->ControlDmaBlock, &sg.physAddr);
    if (!cpkt) {

        TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "<-- %s can't get a slice\n", __FUNCTION__);
        return;
    }
    TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "--> %s vq = %p, &cmd %p\n", __FUNCTION__, vq, cpkt);

    cpkt->id = id;
    cpkt->event = event;
    cpkt->value = value;

    sg.length = sizeof(*cpkt);

    WdfWaitLockAcquire(pContext->COutVqLock, NULL);
    if(0 <= virtqueue_add_buf(vq, &sg, 1, 0, &cpkt, NULL, 0))
    {
        LARGE_INTEGER interval;

        virtqueue_kick(vq);
        interval.QuadPart = CTRL_MSG_INITIAL_WAIT;
        while(!virtqueue_get_buf(vq, &len))
        {
            TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "--> %s waiting for the hypervisor (%lld ms)\n", __FUNCTION__, -interval.QuadPart / 10000);
            KeDelayExecutionThread(KernelMode, FALSE, &interval);
            if (!LongWaitAllowed &&
                interval.QuadPart == CTRL_MSG_NORMAL_WAIT)
            {
                if (!virtqueue_get_buf(vq, &len))
                {
                    TraceEvents(TRACE_LEVEL_WARNING, DBG_PNP, "--> %s log waits not possible, exiting the wait loop\n", __FUNCTION__);
                }

                break;
            }

            interval.QuadPart = CTRL_MSG_NORMAL_WAIT;
        }
    }
    WdfWaitLockRelease(pContext->COutVqLock);

    pContext->ControlDmaBlock->return_slice(pContext->ControlDmaBlock, cpkt);

    TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "<-- %s\n", __FUNCTION__);
}

VOID
VIOSerialCtrlWorkHandler(
    IN WDFDEVICE Device
)
{
    struct virtqueue *vq;
    PPORT_BUFFER buf;
    UINT len;
    NTSTATUS  status = STATUS_SUCCESS;
    PPORTS_DEVICE pContext = GetPortsDevice(Device);

    TraceEvents(TRACE_LEVEL_VERBOSE, DBG_PNP, "--> %s\n", __FUNCTION__);

    vq = pContext->c_ivq;
    ASSERT(vq);

    WdfSpinLockAcquire(pContext->CInVqLock);
    while ((buf = virtqueue_get_buf(vq, &len)))
    {
        WdfSpinLockRelease(pContext->CInVqLock);
        buf->len = len;
        buf->offset = 0;
        VIOSerialHandleCtrlMsg(Device, buf);

        WdfSpinLockAcquire(pContext->CInVqLock);
        status = VIOSerialAddInBuf(vq, buf);
        if (!NT_SUCCESS(status))
        {
           TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "%s::%d Error adding buffer to queue\n", __FUNCTION__, __LINE__);
           VIOSerialFreeBuffer(buf);
        }
    }
    TraceEvents(TRACE_LEVEL_VERBOSE, DBG_PNP, "<-- %s\n", __FUNCTION__);
    WdfSpinLockRelease(pContext->CInVqLock);
}

VOID
VIOSerialHandleCtrlMsg(
    IN WDFDEVICE Device,
    IN PPORT_BUFFER buf
)
{
    PPORTS_DEVICE pContext = GetPortsDevice(Device);
    PVIRTIO_CONSOLE_CONTROL cpkt;
    PVIOSERIAL_PORT port;

    TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "--> %s\n", __FUNCTION__);

    cpkt = (PVIRTIO_CONSOLE_CONTROL)((ULONG_PTR)buf->va_buf + buf->offset);

    port = VIOSerialFindPortById(Device, cpkt->id);

    if (!port && (cpkt->event != VIRTIO_CONSOLE_PORT_ADD))
    {
        TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "Invalid index %u in control packet\n", cpkt->id);
    }

    switch (cpkt->event)
    {
        case VIRTIO_CONSOLE_PORT_ADD:
           if (port)
           {
               TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "VIRTIO_CONSOLE_PORT_ADD id = %d\n", cpkt->id);
               break;
           }
           if (cpkt->id >= pContext->consoleConfig.max_nr_ports)
           {
               TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "Out-of-bound id %u\n", cpkt->id);
               break;
           }
           VIOSerialAddPort(Device, cpkt->id);
        break;

        case VIRTIO_CONSOLE_PORT_REMOVE:
           if (!port)
           {
              TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "VIRTIO_CONSOLE_PORT_REMOVE invalid id = %d\n", cpkt->id);
              break;
           }
           TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "VIRTIO_CONSOLE_PORT_REMOVE id = %d\n", cpkt->id);
           VIOSerialRemovePort(Device, port);
        break;

        case VIRTIO_CONSOLE_CONSOLE_PORT:
           TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP,
                       "VIRTIO_CONSOLE_CONSOLE_PORT id = %d value = %u\n", cpkt->id, cpkt->value);
           if (port && cpkt->value)
           {
              VIOSerialInitPortConsole(Device,port);
           }
        break;

        case VIRTIO_CONSOLE_RESIZE:
           TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "VIRTIO_CONSOLE_RESIZE id = %d\n", cpkt->id);
        break;

        case VIRTIO_CONSOLE_PORT_OPEN:
           if (port)
           {
              BOOLEAN  Connected = (BOOLEAN)cpkt->value;
              TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "VIRTIO_CONSOLE_PORT_OPEN id = %d, HostConnected = %d\n", cpkt->id, Connected);
              if (port->HostConnected != Connected)
              {
                 VIOSerialPortPnpNotify(Device, port, Connected);
              }

              // Someone is listening. Trigger a check to see if we have
              // something waiting to be told.
              if (port->HostConnected)
              {
                  WDF_INTERRUPT_INFO info;
                  WDFINTERRUPT *interrupt;

                  WDF_INTERRUPT_INFO_INIT(&info);
                  WdfInterruptGetInfo(pContext->QueuesInterrupt, &info);

                  // Check if MSI is enabled and notify the right interrupt.
                  interrupt = (info.Vector == 0) ? &pContext->WdfInterrupt :
                      &pContext->QueuesInterrupt;

                  WdfInterruptQueueDpcForIsr(*interrupt);
              }
           }
           else
           {
              TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP, "VIRTIO_CONSOLE_PORT_OPEN invalid id = %d\n", cpkt->id);
           }
        break;

        case VIRTIO_CONSOLE_PORT_NAME:
           if (port)
           {
              VIOSerialPortCreateName(Device, port, buf);
           }
        break;
        default:
           TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "%s UNKNOWN event = %d\n", __FUNCTION__, cpkt->event);
    }
    TraceEvents(TRACE_LEVEL_INFORMATION, DBG_PNP, "<-- %s\n", __FUNCTION__);
}
