/*
 * Copyright (C) 2014 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );

#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ipxe/netdevice.h>
#include <ipxe/ethernet.h>
#include <ipxe/umalloc.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_driver.h>
#include <ipxe/efi/efi_pci.h>
#include <ipxe/efi/efi_utils.h>
#include <ipxe/efi/Protocol/NetworkInterfaceIdentifier.h>
#include <ipxe/efi/IndustryStandard/Acpi10.h>
#include "nii.h"

/** @file
 *
 * NII driver
 *
 */

/* Error numbers generated by NII */
#define EIO_INVALID_CDB __einfo_error ( EINFO_EIO_INVALID_CDB )
#define EINFO_EIO_INVALID_CDB						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_INVALID_CDB,		      \
			  "Invalid CDB" )
#define EIO_INVALID_CPB __einfo_error ( EINFO_EIO_INVALID_CPB )
#define EINFO_EIO_INVALID_CPB						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_INVALID_CPB,		      \
			  "Invalid CPB" )
#define EIO_BUSY __einfo_error ( EINFO_EIO_BUSY )
#define EINFO_EIO_BUSY							      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_BUSY,			      \
			  "Busy" )
#define EIO_QUEUE_FULL __einfo_error ( EINFO_EIO_QUEUE_FULL )
#define EINFO_EIO_QUEUE_FULL						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_QUEUE_FULL,		      \
			  "Queue full" )
#define EIO_ALREADY_STARTED __einfo_error ( EINFO_EIO_ALREADY_STARTED )
#define EINFO_EIO_ALREADY_STARTED					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_ALREADY_STARTED,	      \
			  "Already started" )
#define EIO_NOT_STARTED __einfo_error ( EINFO_EIO_NOT_STARTED )
#define EINFO_EIO_NOT_STARTED						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NOT_STARTED,		      \
			  "Not started" )
#define EIO_NOT_SHUTDOWN __einfo_error ( EINFO_EIO_NOT_SHUTDOWN )
#define EINFO_EIO_NOT_SHUTDOWN						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NOT_SHUTDOWN,		      \
			  "Not shutdown" )
#define EIO_ALREADY_INITIALIZED __einfo_error ( EINFO_EIO_ALREADY_INITIALIZED )
#define EINFO_EIO_ALREADY_INITIALIZED					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_ALREADY_INITIALIZED,	      \
			  "Already initialized" )
#define EIO_NOT_INITIALIZED __einfo_error ( EINFO_EIO_NOT_INITIALIZED )
#define EINFO_EIO_NOT_INITIALIZED					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NOT_INITIALIZED,	      \
			  "Not initialized" )
#define EIO_DEVICE_FAILURE __einfo_error ( EINFO_EIO_DEVICE_FAILURE )
#define EINFO_EIO_DEVICE_FAILURE					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_DEVICE_FAILURE,	      \
			  "Device failure" )
#define EIO_NVDATA_FAILURE __einfo_error ( EINFO_EIO_NVDATA_FAILURE )
#define EINFO_EIO_NVDATA_FAILURE					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NVDATA_FAILURE,	      \
			  "Non-volatile data failure" )
#define EIO_UNSUPPORTED __einfo_error ( EINFO_EIO_UNSUPPORTED )
#define EINFO_EIO_UNSUPPORTED						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_UNSUPPORTED,		      \
			  "Unsupported" )
#define EIO_BUFFER_FULL __einfo_error ( EINFO_EIO_BUFFER_FULL )
#define EINFO_EIO_BUFFER_FULL						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_BUFFER_FULL,		      \
			  "Buffer full" )
#define EIO_INVALID_PARAMETER __einfo_error ( EINFO_EIO_INVALID_PARAMETER )
#define EINFO_EIO_INVALID_PARAMETER					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_INVALID_PARAMETER,	      \
			  "Invalid parameter" )
#define EIO_INVALID_UNDI __einfo_error ( EINFO_EIO_INVALID_UNDI )
#define EINFO_EIO_INVALID_UNDI						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_INVALID_UNDI,		      \
			  "Invalid UNDI" )
#define EIO_IPV4_NOT_SUPPORTED __einfo_error ( EINFO_EIO_IPV4_NOT_SUPPORTED )
#define EINFO_EIO_IPV4_NOT_SUPPORTED					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_IPV4_NOT_SUPPORTED,	      \
			  "IPv4 not supported" )
#define EIO_IPV6_NOT_SUPPORTED __einfo_error ( EINFO_EIO_IPV6_NOT_SUPPORTED )
#define EINFO_EIO_IPV6_NOT_SUPPORTED					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_IPV6_NOT_SUPPORTED,	      \
			  "IPv6 not supported" )
#define EIO_NOT_ENOUGH_MEMORY __einfo_error ( EINFO_EIO_NOT_ENOUGH_MEMORY )
#define EINFO_EIO_NOT_ENOUGH_MEMORY					      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NOT_ENOUGH_MEMORY,	      \
			  "Not enough memory" )
#define EIO_NO_DATA __einfo_error ( EINFO_EIO_NO_DATA )
#define EINFO_EIO_NO_DATA						      \
	__einfo_uniqify ( EINFO_EIO, PXE_STATCODE_NO_DATA,		      \
			  "No data" )
#define EIO_STAT( stat )						      \
	EUNIQ ( EINFO_EIO, -(stat), EIO_INVALID_CDB, EIO_INVALID_CPB,	      \
		EIO_BUSY, EIO_QUEUE_FULL, EIO_ALREADY_STARTED,		      \
		EIO_NOT_STARTED, EIO_NOT_SHUTDOWN, EIO_ALREADY_INITIALIZED,   \
		EIO_NOT_INITIALIZED, EIO_DEVICE_FAILURE, EIO_NVDATA_FAILURE,  \
		EIO_UNSUPPORTED, EIO_BUFFER_FULL, EIO_INVALID_PARAMETER,      \
		EIO_INVALID_UNDI, EIO_IPV4_NOT_SUPPORTED,		      \
		EIO_IPV6_NOT_SUPPORTED, EIO_NOT_ENOUGH_MEMORY, EIO_NO_DATA )

/** Maximum PCI BAR
 *
 * This is defined in <ipxe/efi/IndustryStandard/Pci22.h>, but we
 * can't #include that since it collides with <ipxe/pci.h>.
 */
#define PCI_MAX_BAR 6

/** An NII memory mapping */
struct nii_mapping {
	/** List of mappings */
	struct list_head list;
	/** Mapped address */
	UINT64 addr;
	/** Mapping cookie created by PCI I/O protocol */
	VOID *mapping;
};

/** An NII NIC */
struct nii_nic {
	/** EFI device */
	struct efi_device *efidev;
	/** Network interface identifier protocol */
	EFI_NETWORK_INTERFACE_IDENTIFIER_PROTOCOL *nii;
	/** !PXE structure */
	PXE_SW_UNDI *undi;
	/** Entry point */
	EFIAPI VOID ( * issue ) ( UINT64 cdb );
	/** Generic device */
	struct device dev;

	/** PCI device */
	EFI_HANDLE pci_device;
	/** PCI I/O protocol */
	EFI_PCI_IO_PROTOCOL *pci_io;
	/** Memory BAR */
	unsigned int mem_bar;
	/** I/O BAR */
	unsigned int io_bar;

	/** Broadcast address */
	PXE_MAC_ADDR broadcast;
	/** Maximum packet length */
	size_t mtu;

	/** Hardware transmit/receive buffer */
	userptr_t buffer;
	/** Hardware transmit/receive buffer length */
	size_t buffer_len;

	/** Saved task priority level */
	EFI_TPL saved_tpl;

	/** Media status is supported */
	int media;

	/** Current transmit buffer */
	struct io_buffer *txbuf;
	/** Current receive buffer */
	struct io_buffer *rxbuf;

	/** Mapping list */
	struct list_head mappings;
};

/** Maximum number of received packets per poll */
#define NII_RX_QUOTA 4

/**
 * Open PCI I/O protocol and identify BARs
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_pci_open ( struct nii_nic *nii ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE device = nii->efidev->device;
	EFI_HANDLE pci_device;
	union {
		EFI_PCI_IO_PROTOCOL *pci_io;
		void *interface;
	} pci_io;
	union {
		EFI_ACPI_ADDRESS_SPACE_DESCRIPTOR *acpi;
		void *resource;
	} desc;
	int bar;
	EFI_STATUS efirc;
	int rc;

	/* Locate PCI I/O protocol */
	if ( ( rc = efi_locate_device ( device, &efi_pci_io_protocol_guid,
					&pci_device, 0 ) ) != 0 ) {
		DBGC ( nii, "NII %s could not locate PCI I/O protocol: %s\n",
		       nii->dev.name, strerror ( rc ) );
		goto err_locate;
	}
	nii->pci_device = pci_device;

	/* Open PCI I/O protocol */
	if ( ( efirc = bs->OpenProtocol ( pci_device, &efi_pci_io_protocol_guid,
					  &pci_io.interface, efi_image_handle,
					  device,
					  EFI_OPEN_PROTOCOL_GET_PROTOCOL ))!=0){
		rc = -EEFI ( efirc );
		DBGC ( nii, "NII %s could not open PCI I/O protocol: %s\n",
		       nii->dev.name, strerror ( rc ) );
		goto err_open;
	}
	nii->pci_io = pci_io.pci_io;

	/* Identify memory and I/O BARs */
	nii->mem_bar = PCI_MAX_BAR;
	nii->io_bar = PCI_MAX_BAR;
	for ( bar = ( PCI_MAX_BAR - 1 ) ; bar >= 0 ; bar-- ) {
		efirc = nii->pci_io->GetBarAttributes ( nii->pci_io, bar, NULL,
							&desc.resource );
		if ( efirc == EFI_UNSUPPORTED ) {
			/* BAR not present; ignore */
			continue;
		}
		if ( efirc != 0 ) {
			rc = -EEFI ( efirc );
			DBGC ( nii, "NII %s could not get BAR %d attributes: "
			       "%s\n", nii->dev.name, bar, strerror ( rc ) );
			goto err_get_bar_attributes;
		}
		if ( desc.acpi->ResType == ACPI_ADDRESS_SPACE_TYPE_MEM ) {
			nii->mem_bar = bar;
		} else if ( desc.acpi->ResType == ACPI_ADDRESS_SPACE_TYPE_IO ) {
			nii->io_bar = bar;
		}
		bs->FreePool ( desc.resource );
	}
	DBGC ( nii, "NII %s has ", nii->dev.name );
	if ( nii->mem_bar < PCI_MAX_BAR ) {
		DBGC ( nii, "memory BAR %d and ", nii->mem_bar );
	} else {
		DBGC ( nii, "no memory BAR and " );
	}
	if ( nii->io_bar < PCI_MAX_BAR ) {
		DBGC ( nii, "I/O BAR %d\n", nii->io_bar );
	} else {
		DBGC ( nii, "no I/O BAR\n" );
	}

	return 0;

 err_get_bar_attributes:
	bs->CloseProtocol ( pci_device, &efi_pci_io_protocol_guid,
			    efi_image_handle, device );
 err_open:
 err_locate:
	return rc;
}

/**
 * Close PCI I/O protocol
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static void nii_pci_close ( struct nii_nic *nii ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct nii_mapping *map;
	struct nii_mapping *tmp;

	/* Remove any stale mappings */
	list_for_each_entry_safe ( map, tmp, &nii->mappings, list ) {
		DBGC ( nii, "NII %s removing stale mapping %#llx\n",
		       nii->dev.name, ( ( unsigned long long ) map->addr ) );
		nii->pci_io->Unmap ( nii->pci_io, map->mapping );
		list_del ( &map->list );
		free ( map );
	}

	/* Close protocols */
	bs->CloseProtocol ( nii->pci_device, &efi_pci_io_protocol_guid,
			    efi_image_handle, nii->efidev->device );
}

/**
 * I/O callback
 *
 * @v unique_id		NII NIC
 * @v op		Operations
 * @v len		Length of data
 * @v addr		Address
 * @v data		Data buffer
 */
static EFIAPI VOID nii_io ( UINT64 unique_id, UINT8 op, UINT8 len, UINT64 addr,
			    UINT64 data ) {
	struct nii_nic *nii = ( ( void * ) ( intptr_t ) unique_id );
	EFI_PCI_IO_PROTOCOL_ACCESS *access;
	EFI_PCI_IO_PROTOCOL_IO_MEM io;
	EFI_PCI_IO_PROTOCOL_WIDTH width;
	unsigned int bar;
	EFI_STATUS efirc;
	int rc;

	/* Determine accessor and BAR */
	if ( op & ( PXE_MEM_READ | PXE_MEM_WRITE ) ) {
		access = &nii->pci_io->Mem;
		bar = nii->mem_bar;
	} else {
		access = &nii->pci_io->Io;
		bar = nii->io_bar;
	}

	/* Determine operaton */
	io = ( ( op & ( PXE_IO_WRITE | PXE_MEM_WRITE ) ) ?
	       access->Write : access->Read );

	/* Determine width */
	width = ( fls ( len ) - 1 );

	/* Issue operation */
	if ( ( efirc = io ( nii->pci_io, width, bar, addr, 1,
			    ( ( void * ) ( intptr_t ) data ) ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( nii, "NII %s I/O operation %#x failed: %s\n",
		       nii->dev.name, op, strerror ( rc ) );
		/* No way to report failure */
		return;
	}
}

/**
 * Map callback
 *
 * @v unique_id		NII NIC
 * @v addr		Address of memory to be mapped
 * @v len		Length of memory to be mapped
 * @v dir		Direction of data flow
 * @v mapped		Device mapped address to fill in
 */
static EFIAPI VOID nii_map ( UINT64 unique_id, UINT64 addr, UINT32 len,
			     UINT32 dir, UINT64 mapped ) {
	struct nii_nic *nii = ( ( void * ) ( intptr_t ) unique_id );
	EFI_PHYSICAL_ADDRESS *phys = ( ( void * ) ( intptr_t ) mapped );
	EFI_PCI_IO_PROTOCOL_OPERATION op;
	struct nii_mapping *map;
	UINTN count = len;
	EFI_STATUS efirc;
	int rc;

	/* Return a zero mapped address on failure */
	*phys = 0;

	/* Determine PCI mapping operation */
	switch ( dir ) {
	case TO_AND_FROM_DEVICE:
		op = EfiPciIoOperationBusMasterCommonBuffer;
		break;
	case FROM_DEVICE:
		op = EfiPciIoOperationBusMasterWrite;
		break;
	case TO_DEVICE:
		op = EfiPciIoOperationBusMasterRead;
		break;
	default:
		DBGC ( nii, "NII %s unsupported mapping direction %d\n",
		       nii->dev.name, dir );
		goto err_dir;
	}

	/* Allocate a mapping record */
	map = zalloc ( sizeof ( *map ) );
	if ( ! map )
		goto err_alloc;
	map->addr = addr;

	/* Create map */
	if ( ( efirc = nii->pci_io->Map ( nii->pci_io, op,
					  ( ( void * ) ( intptr_t ) addr ),
					  &count, phys, &map->mapping ) ) != 0){
		rc = -EEFI ( efirc );
		DBGC ( nii, "NII %s map operation failed: %s\n",
		       nii->dev.name, strerror ( rc ) );
		goto err_map;
	}

	/* Add to list of mappings */
	list_add ( &map->list, &nii->mappings );
	DBGC2 ( nii, "NII %s mapped %#llx+%#x->%#llx\n",
		nii->dev.name, ( ( unsigned long long ) addr ),
		len, ( ( unsigned long long ) *phys ) );
	return;

	list_del ( &map->list );
 err_map:
	free ( map );
 err_alloc:
 err_dir:
	return;
}

/**
 * Unmap callback
 *
 * @v unique_id		NII NIC
 * @v addr		Address of mapped memory
 * @v len		Length of mapped memory
 * @v dir		Direction of data flow
 * @v mapped		Device mapped address
 */
static EFIAPI VOID nii_unmap ( UINT64 unique_id, UINT64 addr, UINT32 len,
			       UINT32 dir __unused, UINT64 mapped ) {
	struct nii_nic *nii = ( ( void * ) ( intptr_t ) unique_id );
	struct nii_mapping *map;

	/* Locate mapping record */
	list_for_each_entry ( map, &nii->mappings, list ) {
		if ( map->addr == addr ) {
			nii->pci_io->Unmap ( nii->pci_io, map->mapping );
			list_del ( &map->list );
			free ( map );
			DBGC2 ( nii, "NII %s unmapped %#llx+%#x->%#llx\n",
				nii->dev.name, ( ( unsigned long long ) addr ),
				len, ( ( unsigned long long ) mapped ) );
			return;
		}
	}

	DBGC ( nii, "NII %s non-existent mapping %#llx+%#x->%#llx\n",
	       nii->dev.name, ( ( unsigned long long ) addr ),
	       len, ( ( unsigned long long ) mapped ) );
}

/**
 * Sync callback
 *
 * @v unique_id		NII NIC
 * @v addr		Address of mapped memory
 * @v len		Length of mapped memory
 * @v dir		Direction of data flow
 * @v mapped		Device mapped address
 */
static EFIAPI VOID nii_sync ( UINT64 unique_id __unused, UINT64 addr,
			      UINT32 len, UINT32 dir, UINT64 mapped ) {
	const void *src;
	void *dst;

	/* Do nothing if this is an identity mapping */
	if ( addr == mapped )
		return;

	/* Determine direction */
	if ( dir == FROM_DEVICE ) {
		src = ( ( void * ) ( intptr_t ) mapped );
		dst = ( ( void * ) ( intptr_t ) addr );
	} else {
		src = ( ( void * ) ( intptr_t ) addr );
		dst = ( ( void * ) ( intptr_t ) mapped );
	}

	/* Copy data */
	memcpy ( dst, src, len );
}

/**
 * Delay callback
 *
 * @v unique_id		NII NIC
 * @v microseconds	Delay in microseconds
 */
static EFIAPI VOID nii_delay ( UINT64 unique_id __unused, UINTN microseconds ) {

	udelay ( microseconds );
}

/**
 * Block callback
 *
 * @v unique_id		NII NIC
 * @v acquire		Acquire lock
 */
static EFIAPI VOID nii_block ( UINT64 unique_id, UINT32 acquire ) {
	struct nii_nic *nii = ( ( void * ) ( intptr_t ) unique_id );
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;

	/* This functionality (which is copied verbatim from the
	 * SnpDxe implementation of this function) appears to be
	 * totally brain-dead, since it produces no actual blocking
	 * behaviour.
	 */
	if ( acquire ) {
		nii->saved_tpl = bs->RaiseTPL ( TPL_NOTIFY );
	} else {
		bs->RestoreTPL ( nii->saved_tpl );
	}
}

/**
 * Construct operation from opcode and flags
 *
 * @v opcode		Opcode
 * @v opflags		Flags
 * @ret op		Operation
 */
#define NII_OP( opcode, opflags ) ( (opcode) | ( (opflags) << 16 ) )

/**
 * Extract opcode from operation
 *
 * @v op		Operation
 * @ret opcode		Opcode
 */
#define NII_OPCODE( op ) ( (op) & 0xffff )

/**
 * Extract flags from operation
 *
 * @v op		Operation
 * @ret opflags		Flags
 */
#define NII_OPFLAGS( op ) ( (op) >> 16 )

/**
 * Issue command with parameter block and data block
 *
 * @v nii		NII NIC
 * @v op		Operation
 * @v cpb		Command parameter block, or NULL
 * @v cpb_len		Command parameter block length
 * @v db		Data block, or NULL
 * @v db_len		Data block length
 * @ret stat		Status flags, or negative status code
 */
static int nii_issue_cpb_db ( struct nii_nic *nii, unsigned int op, void *cpb,
			      size_t cpb_len, void *db, size_t db_len ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	PXE_CDB cdb;
	UINTN tpl;

	/* Prepare command descriptor block */
	memset ( &cdb, 0, sizeof ( cdb ) );
	cdb.OpCode = NII_OPCODE ( op );
	cdb.OpFlags = NII_OPFLAGS ( op );
	cdb.CPBaddr = ( ( intptr_t ) cpb );
	cdb.CPBsize = cpb_len;
	cdb.DBaddr = ( ( intptr_t ) db );
	cdb.DBsize = db_len;
	cdb.IFnum = nii->nii->IfNum;

	/* Raise task priority level */
	tpl = bs->RaiseTPL ( efi_internal_tpl );

	/* Issue command */
	DBGC2 ( nii, "NII %s issuing %02x:%04x ifnum %d%s%s\n",
		nii->dev.name, cdb.OpCode, cdb.OpFlags, cdb.IFnum,
		( cpb ? " cpb" : "" ), ( db ? " db" : "" ) );
	if ( cpb )
		DBGC2_HD ( nii, cpb, cpb_len );
	if ( db )
		DBGC2_HD ( nii, db, db_len );
	nii->issue ( ( intptr_t ) &cdb );

	/* Restore task priority level */
	bs->RestoreTPL ( tpl );

	/* Check completion status */
	if ( cdb.StatCode != PXE_STATCODE_SUCCESS )
		return -cdb.StatCode;

	/* Return command-specific status flags */
	return ( cdb.StatFlags & ~PXE_STATFLAGS_STATUS_MASK );
}

/**
 * Issue command with parameter block
 *
 * @v nii		NII NIC
 * @v op		Operation
 * @v cpb		Command parameter block, or NULL
 * @v cpb_len		Command parameter block length
 * @ret stat		Status flags, or negative status code
 */
static int nii_issue_cpb ( struct nii_nic *nii, unsigned int op, void *cpb,
			   size_t cpb_len ) {

	return nii_issue_cpb_db ( nii, op, cpb, cpb_len, NULL, 0 );
}

/**
 * Issue command with data block
 *
 * @v nii		NII NIC
 * @v op		Operation
 * @v db		Data block, or NULL
 * @v db_len		Data block length
 * @ret stat		Status flags, or negative status code
 */
static int nii_issue_db ( struct nii_nic *nii, unsigned int op, void *db,
			  size_t db_len ) {

	return nii_issue_cpb_db ( nii, op, NULL, 0, db, db_len );
}

/**
 * Issue command
 *
 *
 * @v nii		NII NIC
 * @v op		Operation
 * @ret stat		Status flags, or negative status code
 */
static int nii_issue ( struct nii_nic *nii, unsigned int op ) {

	return nii_issue_cpb_db ( nii, op, NULL, 0, NULL, 0 );
}

/**
 * Start UNDI
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_start_undi ( struct nii_nic *nii ) {
	PXE_CPB_START_31 cpb;
	int stat;
	int rc;

	/* Construct parameter block */
	memset ( &cpb, 0, sizeof ( cpb ) );
	cpb.Delay = ( ( intptr_t ) nii_delay );
	cpb.Block = ( ( intptr_t ) nii_block );
	cpb.Mem_IO = ( ( intptr_t ) nii_io );
	cpb.Map_Mem = ( ( intptr_t ) nii_map );
	cpb.UnMap_Mem = ( ( intptr_t ) nii_unmap );
	cpb.Sync_Mem = ( ( intptr_t ) nii_sync );
	cpb.Unique_ID = ( ( intptr_t ) nii );

	/* Issue command */
	if ( ( stat = nii_issue_cpb ( nii, PXE_OPCODE_START, &cpb,
				      sizeof ( cpb ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not start: %s\n",
		       nii->dev.name, strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Stop UNDI
 *
 * @v nii		NII NIC
 */
static void nii_stop_undi ( struct nii_nic *nii ) {
	int stat;
	int rc;

	/* Issue command */
	if ( ( stat = nii_issue ( nii, PXE_OPCODE_STOP ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not stop: %s\n",
		       nii->dev.name, strerror ( rc ) );
		/* Nothing we can do about it */
		return;
	}
}

/**
 * Get initialisation information
 *
 * @v nii		NII NIC
 * @v netdev		Network device to fill in
 * @ret rc		Return status code
 */
static int nii_get_init_info ( struct nii_nic *nii,
			       struct net_device *netdev ) {
	PXE_DB_GET_INIT_INFO db;
	int stat;
	int rc;

	/* Issue command */
	if ( ( stat = nii_issue_db ( nii, PXE_OPCODE_GET_INIT_INFO, &db,
				     sizeof ( db ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not get initialisation info: %s\n",
		       nii->dev.name, strerror ( rc ) );
		return rc;
	}

	/* Determine link layer protocol */
	switch ( db.IFtype ) {
	case PXE_IFTYPE_ETHERNET :
		netdev->ll_protocol = &ethernet_protocol;
		break;
	default:
		DBGC ( nii, "NII %s unknown interface type %#02x\n",
		       nii->dev.name, db.IFtype );
		return -ENOTSUP;
	}

	/* Sanity checks */
	assert ( db.MediaHeaderLen == netdev->ll_protocol->ll_header_len );
	assert ( db.HWaddrLen == netdev->ll_protocol->hw_addr_len );
	assert ( db.HWaddrLen == netdev->ll_protocol->ll_addr_len );

	/* Extract parameters */
	nii->buffer_len = db.MemoryRequired;
	nii->mtu = ( db.FrameDataLen + db.MediaHeaderLen );
	netdev->max_pkt_len = nii->mtu;
	nii->media = ( stat & PXE_STATFLAGS_GET_STATUS_NO_MEDIA_SUPPORTED );

	return 0;
}

/**
 * Initialise UNDI
 *
 * @v nii		NII NIC
 * @v flags		Flags
 * @ret rc		Return status code
 */
static int nii_initialise_flags ( struct nii_nic *nii, unsigned int flags ) {
	PXE_CPB_INITIALIZE cpb;
	PXE_DB_INITIALIZE db;
	unsigned int op;
	int stat;
	int rc;

	/* Allocate memory buffer */
	nii->buffer = umalloc ( nii->buffer_len );
	if ( ! nii->buffer ) {
		rc = -ENOMEM;
		goto err_alloc;
	}

	/* Construct parameter block */
	memset ( &cpb, 0, sizeof ( cpb ) );
	cpb.MemoryAddr = ( ( intptr_t ) nii->buffer );
	cpb.MemoryLength = nii->buffer_len;

	/* Construct data block */
	memset ( &db, 0, sizeof ( db ) );

	/* Issue command */
	op = NII_OP ( PXE_OPCODE_INITIALIZE, flags );
	if ( ( stat = nii_issue_cpb_db ( nii, op, &cpb, sizeof ( cpb ),
					 &db, sizeof ( db ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not initialise: %s\n",
		       nii->dev.name, strerror ( rc ) );
		goto err_initialize;
	}

	return 0;

 err_initialize:
	ufree ( nii->buffer );
 err_alloc:
	return rc;
}

/**
 * Initialise UNDI with cable detection
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_initialise_cable ( struct nii_nic *nii ) {
	unsigned int flags;

	/* Initialise UNDI */
	flags = PXE_OPFLAGS_INITIALIZE_DETECT_CABLE;
	return nii_initialise_flags ( nii, flags );
}

/**
 * Initialise UNDI
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_initialise ( struct nii_nic *nii ) {
	unsigned int flags;

	/* Initialise UNDI */
	flags = PXE_OPFLAGS_INITIALIZE_DO_NOT_DETECT_CABLE;
	return nii_initialise_flags ( nii, flags );
}

/**
 * Shut down UNDI
 *
 * @v nii		NII NIC
 */
static void nii_shutdown ( struct nii_nic *nii ) {
	int stat;
	int rc;

	/* Issue command */
	if ( ( stat = nii_issue ( nii, PXE_OPCODE_SHUTDOWN ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not shut down: %s\n",
		       nii->dev.name, strerror ( rc ) );
		/* Leak memory to avoid corruption */
		return;
	}

	/* Free buffer */
	ufree ( nii->buffer );
}

/**
 * Get station addresses
 *
 * @v nii		NII NIC
 * @v netdev		Network device to fill in
 * @ret rc		Return status code
 */
static int nii_get_station_address ( struct nii_nic *nii,
				     struct net_device *netdev ) {
	PXE_DB_STATION_ADDRESS db;
	unsigned int op;
	int stat;
	int rc;

	/* Initialise UNDI */
	if ( ( rc = nii_initialise ( nii ) ) != 0 )
		goto err_initialise;

	/* Issue command */
	op = NII_OP ( PXE_OPCODE_STATION_ADDRESS,
		      PXE_OPFLAGS_STATION_ADDRESS_READ );
	if ( ( stat = nii_issue_db ( nii, op, &db, sizeof ( db ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not get station address: %s\n",
		       nii->dev.name, strerror ( rc ) );
		goto err_station_address;
	}

	/* Copy MAC addresses */
	memcpy ( netdev->ll_addr, db.StationAddr,
		 netdev->ll_protocol->ll_addr_len );
	memcpy ( netdev->hw_addr, db.PermanentAddr,
		 netdev->ll_protocol->hw_addr_len );
	memcpy ( nii->broadcast, db.BroadcastAddr,
		 sizeof ( nii->broadcast ) );

 err_station_address:
	nii_shutdown ( nii );
 err_initialise:
	return rc;
}

/**
 * Set station address
 *
 * @v nii		NII NIC
 * @v netdev		Network device
 * @ret rc		Return status code
 */
static int nii_set_station_address ( struct nii_nic *nii,
				     struct net_device *netdev ) {
	uint32_t implementation = nii->undi->Implementation;
	PXE_CPB_STATION_ADDRESS cpb;
	unsigned int op;
	int stat;
	int rc;

	/* Fail if setting station address is unsupported */
	if ( ! ( implementation & PXE_ROMID_IMP_STATION_ADDR_SETTABLE ) )
		return -ENOTSUP;

	/* Construct parameter block */
	memset ( &cpb, 0, sizeof ( cpb ) );
	memcpy ( cpb.StationAddr, netdev->ll_addr,
		 netdev->ll_protocol->ll_addr_len );

	/* Issue command */
	op = NII_OP ( PXE_OPCODE_STATION_ADDRESS,
	              PXE_OPFLAGS_STATION_ADDRESS_WRITE );
	if ( ( stat = nii_issue_cpb ( nii, op, &cpb, sizeof ( cpb ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not set station address: %s\n",
		       nii->dev.name, strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Set receive filters
 *
 * @v nii		NII NIC
 * @v flags		Flags
 * @ret rc		Return status code
 */
static int nii_set_rx_filters ( struct nii_nic *nii, unsigned int flags ) {
	uint32_t implementation = nii->undi->Implementation;
	unsigned int op;
	int stat;
	int rc;

	/* Construct receive filter set */
	flags |= PXE_OPFLAGS_RECEIVE_FILTER_UNICAST;
	if ( implementation & PXE_ROMID_IMP_BROADCAST_RX_SUPPORTED )
		flags |= PXE_OPFLAGS_RECEIVE_FILTER_BROADCAST;
	if ( implementation & PXE_ROMID_IMP_PROMISCUOUS_RX_SUPPORTED )
		flags |= PXE_OPFLAGS_RECEIVE_FILTER_PROMISCUOUS;
	if ( implementation & PXE_ROMID_IMP_PROMISCUOUS_MULTICAST_RX_SUPPORTED )
		flags |= PXE_OPFLAGS_RECEIVE_FILTER_ALL_MULTICAST;

	/* Issue command */
	op = NII_OP ( PXE_OPCODE_RECEIVE_FILTERS, flags );
	if ( ( stat = nii_issue ( nii, op ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not %s%sable receive filters "
		       "%#04x: %s\n", nii->dev.name,
		       ( ( flags & PXE_OPFLAGS_RECEIVE_FILTER_ENABLE ) ?
			 "en" : "" ),
		       ( ( flags & PXE_OPFLAGS_RECEIVE_FILTER_DISABLE ) ?
			 "dis" : "" ), flags, strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Enable receive filters
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_enable_rx_filters ( struct nii_nic *nii ) {

	return nii_set_rx_filters ( nii, PXE_OPFLAGS_RECEIVE_FILTER_ENABLE );
}

/**
 * Disable receive filters
 *
 * @v nii		NII NIC
 * @ret rc		Return status code
 */
static int nii_disable_rx_filters ( struct nii_nic *nii ) {

	return nii_set_rx_filters ( nii, PXE_OPFLAGS_RECEIVE_FILTER_DISABLE );
}

/**
 * Transmit packet
 *
 * @v netdev		Network device
 * @v iobuf		I/O buffer
 * @ret rc		Return status code
 */
static int nii_transmit ( struct net_device *netdev,
			  struct io_buffer *iobuf ) {
	struct nii_nic *nii = netdev->priv;
	PXE_CPB_TRANSMIT cpb;
	unsigned int op;
	int stat;
	int rc;

	/* Defer the packet if there is already a transmission in progress */
	if ( nii->txbuf ) {
		netdev_tx_defer ( netdev, iobuf );
		return 0;
	}

	/* Construct parameter block */
	memset ( &cpb, 0, sizeof ( cpb ) );
	cpb.FrameAddr = ( ( intptr_t ) iobuf->data );
	cpb.DataLen = iob_len ( iobuf );

	/* Transmit packet */
	op = NII_OP ( PXE_OPCODE_TRANSMIT,
		      ( PXE_OPFLAGS_TRANSMIT_WHOLE |
			PXE_OPFLAGS_TRANSMIT_DONT_BLOCK ) );
	if ( ( stat = nii_issue_cpb ( nii, op, &cpb, sizeof ( cpb ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not transmit: %s\n",
		       nii->dev.name, strerror ( rc ) );
		return rc;
	}
	nii->txbuf = iobuf;

	return 0;
}

/**
 * Poll for completed packets
 *
 * @v netdev		Network device
 * @v stat		Status flags
 */
static void nii_poll_tx ( struct net_device *netdev, unsigned int stat ) {
	struct nii_nic *nii = netdev->priv;
	struct io_buffer *iobuf;

	/* Do nothing unless we have a completion */
	if ( stat & PXE_STATFLAGS_GET_STATUS_NO_TXBUFS_WRITTEN )
		return;

	/* Sanity check */
	assert ( nii->txbuf != NULL );

	/* Complete transmission */
	iobuf = nii->txbuf;
	nii->txbuf = NULL;
	netdev_tx_complete ( netdev, iobuf );
}

/**
 * Poll for received packets
 *
 * @v netdev		Network device
 */
static void nii_poll_rx ( struct net_device *netdev ) {
	struct nii_nic *nii = netdev->priv;
	PXE_CPB_RECEIVE cpb;
	PXE_DB_RECEIVE db;
	unsigned int quota;
	int stat;
	int rc;

	/* Retrieve up to NII_RX_QUOTA packets */
	for ( quota = NII_RX_QUOTA ; quota ; quota-- ) {

		/* Allocate buffer, if required */
		if ( ! nii->rxbuf ) {
			nii->rxbuf = alloc_iob ( nii->mtu );
			if ( ! nii->rxbuf ) {
				/* Leave for next poll */
				break;
			}
		}

		/* Construct parameter block */
		memset ( &cpb, 0, sizeof ( cpb ) );
		cpb.BufferAddr = ( ( intptr_t ) nii->rxbuf->data );
		cpb.BufferLen = iob_tailroom ( nii->rxbuf );

		/* Issue command */
		if ( ( stat = nii_issue_cpb_db ( nii, PXE_OPCODE_RECEIVE,
						 &cpb, sizeof ( cpb ),
						 &db, sizeof ( db ) ) ) < 0 ) {

			/* PXE_STATCODE_NO_DATA is just the usual "no packet"
			 * status indicator; ignore it.
			 */
			if ( stat == -PXE_STATCODE_NO_DATA )
				break;

			/* Anything else is an error */
			rc = -EIO_STAT ( stat );
			DBGC ( nii, "NII %s could not receive: %s\n",
			       nii->dev.name, strerror ( rc ) );
			netdev_rx_err ( netdev, NULL, rc );
			break;
		}

		/* Hand off to network stack */
		iob_put ( nii->rxbuf, db.FrameLen );
		netdev_rx ( netdev, nii->rxbuf );
		nii->rxbuf = NULL;
	}
}

/**
 * Check for link state changes
 *
 * @v netdev		Network device
 * @v stat		Status flags
 */
static void nii_poll_link ( struct net_device *netdev, unsigned int stat ) {
	int no_media = ( stat & PXE_STATFLAGS_GET_STATUS_NO_MEDIA );

	if ( no_media && netdev_link_ok ( netdev ) ) {
		netdev_link_down ( netdev );
	} else if ( ( ! no_media ) && ( ! netdev_link_ok ( netdev ) ) ) {
		netdev_link_up ( netdev );
	}
}

/**
 * Poll for completed packets
 *
 * @v netdev		Network device
 */
static void nii_poll ( struct net_device *netdev ) {
	struct nii_nic *nii = netdev->priv;
	PXE_DB_GET_STATUS db;
	unsigned int op;
	int stat;
	int rc;

	/* Construct data block */
	memset ( &db, 0, sizeof ( db ) );

	/* Get status */
	op = NII_OP ( PXE_OPCODE_GET_STATUS,
		      ( PXE_OPFLAGS_GET_INTERRUPT_STATUS |
			( nii->txbuf ? PXE_OPFLAGS_GET_TRANSMITTED_BUFFERS : 0)|
			( nii->media ? PXE_OPFLAGS_GET_MEDIA_STATUS : 0 ) ) );
	if ( ( stat = nii_issue_db ( nii, op, &db, sizeof ( db ) ) ) < 0 ) {
		rc = -EIO_STAT ( stat );
		DBGC ( nii, "NII %s could not get status: %s\n",
		       nii->dev.name, strerror ( rc ) );
		return;
	}

	/* Process any TX completions */
	if ( nii->txbuf )
		nii_poll_tx ( netdev, stat );

	/* Process any RX completions */
	nii_poll_rx ( netdev );

	/* Check for link state changes */
	if ( nii->media )
		nii_poll_link ( netdev, stat );
}

/**
 * Open network device
 *
 * @v netdev		Network device
 * @ret rc		Return status code
 */
static int nii_open ( struct net_device *netdev ) {
	struct nii_nic *nii = netdev->priv;
	int rc;

	/* Initialise NIC
	 *
	 * We don't care about link state here, and would prefer to
	 * have the NIC initialise even if no cable is present, to
	 * match the behaviour of all other iPXE drivers.
	 *
	 * Some Emulex NII drivers have a bug which prevents packets
	 * from being sent or received unless we specifically ask it
	 * to detect cable presence during initialisation.
	 *
	 * Unfortunately, some other NII drivers (e.g. Mellanox) may
	 * time out and report failure if asked to detect cable
	 * presence during initialisation on links that are physically
	 * slow to reach link-up.
	 *
	 * Attempt to work around both of these problems by first
	 * attempting to initialise with cable presence detection,
	 * then falling back to initialising without cable presence
	 * detection.
	 */
	if ( ( rc = nii_initialise_cable ( nii ) ) != 0 ) {
		DBGC ( nii, "NII %s could not initialise with cable "
		       "detection: %s\n", nii->dev.name, strerror ( rc ) );
		if ( ( rc = nii_initialise ( nii ) ) != 0 ) {
			DBGC ( nii, "NII %s could not initialise without "
			       "cable detection: %s\n",
			       nii->dev.name, strerror ( rc ) );
			goto err_initialise;
		}
	}

	/* Attempt to set station address */
	if ( ( rc = nii_set_station_address ( nii, netdev ) ) != 0 ) {
		DBGC ( nii, "NII %s could not set station address: %s\n",
		       nii->dev.name, strerror ( rc ) );
		/* Treat as non-fatal */
	}

	/* Disable receive filters
	 *
	 * We have no reason to disable receive filters here (or
	 * anywhere), but some NII drivers have a bug which prevents
	 * packets from being received unless we attempt to disable
	 * the receive filters.
	 *
	 * Ignore any failures, since we genuinely don't care if the
	 * NII driver cannot disable the filters.
	 */
	nii_disable_rx_filters ( nii );

	/* Enable receive filters */
	if ( ( rc = nii_enable_rx_filters ( nii ) ) != 0 )
		goto err_enable_rx_filters;

	return 0;

 err_enable_rx_filters:
	nii_shutdown ( nii );
 err_initialise:
	return rc;
}

/**
 * Close network device
 *
 * @v netdev		Network device
 */
static void nii_close ( struct net_device *netdev ) {
	struct nii_nic *nii = netdev->priv;

	/* Shut down NIC */
	nii_shutdown ( nii );

	/* Discard transmit buffer, if applicable */
	if ( nii->txbuf ) {
		netdev_tx_complete_err ( netdev, nii->txbuf, -ECANCELED );
		nii->txbuf = NULL;
	}

	/* Discard receive buffer, if applicable */
	if ( nii->rxbuf ) {
		free_iob ( nii->rxbuf );
		nii->rxbuf = NULL;
	}
}

/** NII network device operations */
static struct net_device_operations nii_operations = {
	.open = nii_open,
	.close = nii_close,
	.transmit = nii_transmit,
	.poll = nii_poll,
};

/**
 * Attach driver to device
 *
 * @v efidev		EFI device
 * @ret rc		Return status code
 */
int nii_start ( struct efi_device *efidev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE device = efidev->device;
	struct net_device *netdev;
	struct nii_nic *nii;
	void *interface;
	EFI_STATUS efirc;
	int rc;

	/* Allocate and initialise structure */
	netdev = alloc_netdev ( sizeof ( *nii ) );
	if ( ! netdev ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	netdev_init ( netdev, &nii_operations );
	nii = netdev->priv;
	nii->efidev = efidev;
	INIT_LIST_HEAD ( &nii->mappings );
	netdev->ll_broadcast = nii->broadcast;
	efidev_set_drvdata ( efidev, netdev );

	/* Populate underlying device information */
	efi_device_info ( device, "NII", &nii->dev );
	nii->dev.driver_name = "NII";
	nii->dev.parent = &efidev->dev;
	list_add ( &nii->dev.siblings, &efidev->dev.children );
	INIT_LIST_HEAD ( &nii->dev.children );
	netdev->dev = &nii->dev;

	/* Open NII protocol */
	if ( ( efirc = bs->OpenProtocol ( device, &efi_nii31_protocol_guid,
					  &interface, efi_image_handle, device,
					  ( EFI_OPEN_PROTOCOL_BY_DRIVER |
					    EFI_OPEN_PROTOCOL_EXCLUSIVE )))!=0){
		rc = -EEFI ( efirc );
		DBGC ( nii, "NII %s cannot open NII protocol: %s\n",
		       nii->dev.name, strerror ( rc ) );
		DBGC_EFI_OPENERS ( device, device, &efi_nii31_protocol_guid );
		goto err_open_protocol;
	}
	nii->nii = interface;

	/* Locate UNDI and entry point */
	nii->undi = ( ( void * ) ( intptr_t ) nii->nii->Id );
	if ( ! nii->undi ) {
		DBGC ( nii, "NII %s has no UNDI\n", nii->dev.name );
		rc = -ENODEV;
		goto err_no_undi;
	}
	if ( nii->undi->Implementation & PXE_ROMID_IMP_HW_UNDI ) {
		DBGC ( nii, "NII %s is a mythical hardware UNDI\n",
		       nii->dev.name );
		rc = -ENOTSUP;
		goto err_hw_undi;
	}
	if ( nii->undi->Implementation & PXE_ROMID_IMP_SW_VIRT_ADDR ) {
		nii->issue = ( ( void * ) ( intptr_t ) nii->undi->EntryPoint );
	} else {
		nii->issue = ( ( ( void * ) nii->undi ) +
			       nii->undi->EntryPoint );
	}
	DBGC ( nii, "NII %s using UNDI v%x.%x at %p entry %p impl %#08x\n",
	       nii->dev.name, nii->nii->MajorVer, nii->nii->MinorVer,
	       nii->undi, nii->issue, nii->undi->Implementation );

	/* Open PCI I/O protocols and locate BARs */
	if ( ( rc = nii_pci_open ( nii ) ) != 0 )
		goto err_pci_open;

	/* Start UNDI */
	if ( ( rc = nii_start_undi ( nii ) ) != 0 )
		goto err_start_undi;

	/* Get initialisation information */
	if ( ( rc = nii_get_init_info ( nii, netdev ) ) != 0 )
		goto err_get_init_info;

	/* Get MAC addresses */
	if ( ( rc = nii_get_station_address ( nii, netdev ) ) != 0 )
		goto err_get_station_address;

	/* Register network device */
	if ( ( rc = register_netdev ( netdev ) ) != 0 )
		goto err_register_netdev;
	DBGC ( nii, "NII %s registered as %s for %s\n", nii->dev.name,
	       netdev->name, efi_handle_name ( device ) );

	/* Set initial link state (if media detection is not supported) */
	if ( ! nii->media )
		netdev_link_up ( netdev );

	return 0;

	unregister_netdev ( netdev );
 err_register_netdev:
 err_get_station_address:
 err_get_init_info:
	nii_stop_undi ( nii );
 err_start_undi:
	nii_pci_close ( nii );
 err_pci_open:
 err_hw_undi:
 err_no_undi:
	bs->CloseProtocol ( device, &efi_nii31_protocol_guid,
			    efi_image_handle, device );
 err_open_protocol:
	list_del ( &nii->dev.siblings );
	netdev_nullify ( netdev );
	netdev_put ( netdev );
 err_alloc:
	return rc;
}

/**
 * Detach driver from device
 *
 * @v efidev		EFI device
 */
void nii_stop ( struct efi_device *efidev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	struct net_device *netdev = efidev_get_drvdata ( efidev );
	struct nii_nic *nii = netdev->priv;
	EFI_HANDLE device = efidev->device;

	/* Unregister network device */
	unregister_netdev ( netdev );

	/* Stop UNDI */
	nii_stop_undi ( nii );

	/* Close PCI I/O protocols */
	nii_pci_close ( nii );

	/* Close NII protocol */
	bs->CloseProtocol ( device, &efi_nii31_protocol_guid,
			    efi_image_handle, device );

	/* Free network device */
	list_del ( &nii->dev.siblings );
	netdev_nullify ( netdev );
	netdev_put ( netdev );
}
