/*++

Copyright (c) 1999  Microsoft Corporation

Module Name:

    write.c

Abstract: ESC/POS (serial) interface for USB Point-of-Sale devices

Author:

    ervinp

Environment:

    Kernel mode

Revision History:


--*/

#include <WDM.H>

#include <usbdi.h>
#include <usbdlib.h>
#include <usbioctl.h>

#include "escpos.h"
#include "debug.h"




NTSTATUS WriteComPort(POSPDOEXT *pdoExt, PIRP irp)
{
	NTSTATUS status;
	PIO_STACK_LOCATION currentIrpSp;

    /* 
     *  In order to support ODD ENDPOINTs, we check
     *  whether this COM port has a write endpoint or not.
     */
    if(!pdoExt->outputEndpointInfo.pipeHandle) {
        DBGVERBOSE(("This PORT does not have an OUT endpoint - Write request Rejected."));
        return STATUS_NOT_SUPPORTED;
    }

	currentIrpSp = IoGetCurrentIrpStackLocation(irp);

	/*
	 *  Because this pdo's buffering method is METHOD_NEITHER,
	 *  the buffer pointer is irp->UserBuffer, which may be an application address.
	 *  Since we may not be able to send this buffer synchronously on this thread,
	 *  we have to allocate an MDL for it.
	 */
	ASSERT(!irp->MdlAddress);
	ASSERT(currentIrpSp->Parameters.Write.Length);
	irp->MdlAddress = MmCreateMdl(NULL, irp->UserBuffer, currentIrpSp->Parameters.Write.Length);
	if (irp->MdlAddress){
		status = STATUS_SUCCESS;
		__try {
			/*
			 *  We're reading the write data from the buffer, so probe for ReadAccess.
			 */
			MmProbeAndLockPages(irp->MdlAddress, UserMode, IoReadAccess);
		}
		__except(EXCEPTION_EXECUTE_HANDLER) {
			status = GetExceptionCode();
			DBGERR(("MmProbeAndLockPages triggered exception status %xh.", status));
		}
		if (NT_SUCCESS(status)){
			status = TryWrite(pdoExt, irp);
		}
	}
	else {
		DBGERR(("MmCreateMdl failed"));
		status = STATUS_DATA_ERROR;
	}

	return status;
}


NTSTATUS TryWrite(POSPDOEXT *pdoExt, PIRP irp)
{
    NTSTATUS status = STATUS_PENDING;
    BOOLEAN isBusy;
    KIRQL oldIrql;
    BOOLEAN irpWasCancelled = FALSE;


    KeAcquireSpinLock(&pdoExt->devExtSpinLock, &oldIrql);

    if (pdoExt->outputEndpointInfo.endpointIsBusy){
        /*
         *  Another thread is writing to this endpoint now.
         *  Queue the IRP.
         */
        PDRIVER_CANCEL oldCancelRoutine;

        DBGWARN(("WriteComPort:  endpoint is busy so queuing irp"));
        oldCancelRoutine = IoSetCancelRoutine(irp, WriteCancelRoutine);
        ASSERT(!oldCancelRoutine);
        if (irp->Cancel){
            DBGWARN(("WriteComPort: irp %ph was cancelled.", irp));
            irpWasCancelled = TRUE;
            oldCancelRoutine = IoSetCancelRoutine(irp, NULL);
            if (oldCancelRoutine){
                /*
                 *  Cancel routine was not called, so complete the IRP here.
                 */
                ASSERT(oldCancelRoutine == ReadCancelRoutine);
                status = STATUS_CANCELLED;
            }
            else {
                /*
                 *  Cancel routine was called and it will complete the IRP
                 *  as soon as we drop the spinlock.  So don't touch this IRP.
                 *  Return PENDING so dispatch routine won't complete the IRP.
                 */
                status = STATUS_PENDING;
            }
        }
        else {
            InsertTailList(&pdoExt->pendingWriteIrpsList, &irp->Tail.Overlay.ListEntry);
        }
        isBusy = TRUE;
    }
    else {
        pdoExt->outputEndpointInfo.endpointIsBusy = TRUE;
        isBusy = FALSE;
    }

    KeReleaseSpinLock(&pdoExt->devExtSpinLock, oldIrql);


    if (!isBusy){

        PUCHAR mappedUserBuffer;
        ULONG dataLen;
        ULONG dataWritten = 0;
        PIO_STACK_LOCATION currentIrpSp;
        BOOLEAN callWriteWorkItem = FALSE;

        currentIrpSp = IoGetCurrentIrpStackLocation(irp);

        status = STATUS_SUCCESS;  // in case dataLen = 0

        /*
         *  This function is called from the dispatch routine and also
         *  from a workItem callback.  So we may not be on the calling thread.
         *  Therefore, we cannot use irp->UserBuffer because it may not be
         *  mapped in this context.  Use the MDL we created at call time instead.
         */
        mappedUserBuffer = PosMmGetSystemAddressForMdlSafe(irp->MdlAddress);

        if(mappedUserBuffer) {
            dataLen = currentIrpSp->Parameters.Write.Length;

            while (dataLen){
                ULONG len = MIN(dataLen, pdoExt->outputEndpointInfo.pipeLen);

                DBGVERBOSE(("Writing %xh bytes to pipe.", len));
                status = WritePipe( pdoExt->parentFdoExt, 
                                    pdoExt->outputEndpointInfo.pipeHandle, 
                                    mappedUserBuffer, 
                                    len);
                if (NT_SUCCESS(status)){
                    dataLen -= len;
                    dataWritten += len;
                    mappedUserBuffer += len;
                }
                else {
                    DBGERR(("Write failed with status %xh.", status));
                    break;
                }
            }

            /*
             *  Free the MDL we created for the UserBuffer
             */
            ASSERT(irp->MdlAddress);
            MmUnlockPages(irp->MdlAddress);
            FREEPOOL(irp->MdlAddress);
            irp->MdlAddress = NULL;

            irp->IoStatus.Information = dataWritten;

            KeAcquireSpinLock(&pdoExt->devExtSpinLock, &oldIrql);
            pdoExt->outputEndpointInfo.endpointIsBusy = FALSE;
            if (!IsListEmpty(&pdoExt->pendingWriteIrpsList)){
	            callWriteWorkItem = TRUE;;
            }
            KeReleaseSpinLock(&pdoExt->devExtSpinLock, oldIrql);

            /*
             *  If there are some writes waiting, schedule a workItem to process them.
             */
            if (callWriteWorkItem){
                ExQueueWorkItem(&pdoExt->writeWorkItem, DelayedWorkQueue);
            }
        }
        else {
            /*
             *  Return STATUS_UNSUCCESSFUL and free the MDL we created for the UserBuffer.
             */
            DBGERR(("PosMmGetSystemAddressForMdlSafe failed"));
            irp->IoStatus.Information = 0;
            status = STATUS_UNSUCCESSFUL;

            ASSERT(irp->MdlAddress);
            MmUnlockPages(irp->MdlAddress);
            FREEPOOL(irp->MdlAddress);
            irp->MdlAddress = NULL;
        }
    }

    return status;
}


VOID WorkItemCallback_Write(PVOID context)
{
	POSPDOEXT *pdoExt = (POSPDOEXT *)context;
	KIRQL oldIrql;
	PIRP irp = NULL;

	DBGVERBOSE(("WorkItemCallback_Write:  pdoExt=%ph ", pdoExt));

	KeAcquireSpinLock(&pdoExt->devExtSpinLock, &oldIrql);

	if (IsListEmpty(&pdoExt->pendingWriteIrpsList)){
		DBGERR(("WorkItemCallback_Write: list is empty ?!"));
	}
	else {
		while (!irp && !IsListEmpty(&pdoExt->pendingWriteIrpsList)){

			PDRIVER_CANCEL cancelRoutine;
			PLIST_ENTRY listEntry = RemoveHeadList(&pdoExt->pendingWriteIrpsList);

			ASSERT(listEntry);
			irp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry);
			cancelRoutine = IoSetCancelRoutine(irp, NULL);

			if (cancelRoutine){
				ASSERT(cancelRoutine == WriteCancelRoutine);
			}
			else {
				/*
				 *  This IRP was cancelled and the cancel routine was called.
				 *  The cancel routine will complete this IRP as soon as we drop
				 *  the spinlock, so don't touch the IRP.
				 */
				ASSERT(irp->Cancel);
				DBGWARN(("WorkItemCallback_Write: irp was cancelled"));
				irp = NULL;
			}
		}
	}

	KeReleaseSpinLock(&pdoExt->devExtSpinLock, oldIrql);

	if (irp){
		NTSTATUS status = TryWrite(pdoExt, irp);
		if (status != STATUS_PENDING){

			/*
			 *  Set the SL_PENDING in the IRP 
			 *  to indicate that the IRP is completing on a different thread.
			 */
			IoMarkIrpPending(irp);

			irp->IoStatus.Status = status;
			IoCompleteRequest(irp, IO_NO_INCREMENT);
		}
	}
}


VOID WriteCancelRoutine(PDEVICE_OBJECT devObj, PIRP irp)
{
	DEVEXT *devExt;
	POSPDOEXT *pdoExt;
	KIRQL oldIrql;

	DBGWARN(("WriteCancelRoutine: devObj=%ph, irp=%ph.", devObj, irp));

	devExt = devObj->DeviceExtension;
	ASSERT(devExt->signature == DEVICE_EXTENSION_SIGNATURE);
	ASSERT(devExt->isPdo);
	pdoExt = &devExt->pdoExt;

	KeAcquireSpinLock(&pdoExt->devExtSpinLock, &oldIrql);
	RemoveEntryList(&irp->Tail.Overlay.ListEntry);
	KeReleaseSpinLock(&pdoExt->devExtSpinLock, oldIrql);

	IoReleaseCancelSpinLock(irp->CancelIrql);

    irp->IoStatus.Status = STATUS_CANCELLED;
    IoCompleteRequest(irp, IO_NO_INCREMENT);
}