/*
 * Copyright (C) 2010 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <byteswap.h>
#include <ipxe/interface.h>
#include <ipxe/xfer.h>
#include <ipxe/iobuf.h>
#include <ipxe/process.h>
#include <ipxe/fc.h>
#include <ipxe/fcels.h>

/** @file
 *
 * Fibre Channel Extended Link Services
 *
 */

/** Fibre Channel ELS transaction debug message format */
#define FCELS_FMT "FCELS %s %s %s %s"

/** Fibre Channel ELS transaction debug message arguments */
#define FCELS_ARGS( els )						\
	(els)->port->name,						\
	( (els)->handler ? (els)->handler->name : "unknown ELS" ),	\
	( fc_els_is_request ( els ) ? "to" : "from" ),			\
	fc_id_ntoa ( &(els)->peer_port_id )

struct fc_els_handler fc_els_unknown_handler __fc_els_handler;

/**
 * Free Fibre Channel ELS transaction
 *
 * @v refcnt		Reference count
 */
static void fc_els_free ( struct refcnt *refcnt ) {
	struct fc_els *els = container_of ( refcnt, struct fc_els, refcnt );

	assert ( ! process_running ( &els->process ) );
	fc_port_put ( els->port );
	free ( els );
}

/**
 * Close Fibre Channel ELS transaction
 *
 * @v els		Fibre Channel ELS transaction
 * @v rc		Reason for close
 */
static void fc_els_close ( struct fc_els *els, int rc ) {

	if ( rc != 0 ) {
		DBGC ( els, FCELS_FMT " complete (%s)\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
	}

	/* Stop process */
	process_del ( &els->process );

	/* Shut down interfaces */
	intf_shutdown ( &els->xchg, rc );
	intf_shutdown ( &els->job, rc );
}

/**
 * Detect Fibre Channel ELS frame handler
 *
 * @v els		Fibre Channel ELS transaction
 * @v command		ELS command code
 * @ret handler		ELS handler, or NULL
 */
static struct fc_els_handler * fc_els_detect ( struct fc_els *els,
					       const void *data,
					       size_t len ) {
	const struct fc_els_frame_common *frame = data;
	struct fc_els_handler *handler;
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *frame ) )
		return NULL;

	/* Try each handler in turn */
	for_each_table_entry ( handler, FC_ELS_HANDLERS ) {
		if ( ( rc = handler->detect ( els, data, len ) ) == 0 )
			return handler;
	}

	return NULL;
}

/**
 * Transmit Fibre Channel ELS frame
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		Data to transmit
 * @v len		Length of data
 * @ret rc		Return status code
 */
int fc_els_tx ( struct fc_els *els, const void *data, size_t len ) {
	struct xfer_metadata meta;
	struct sockaddr_fc dest;
	int rc;

	DBGC2 ( els, FCELS_FMT " transmitting:\n", FCELS_ARGS ( els ) );
	DBGC2_HDA ( els, 0, data, len );

	/* Construct metadata */
	memset ( &meta, 0, sizeof ( meta ) );
	meta.flags = ( fc_els_is_request ( els ) ?
		       XFER_FL_OVER : ( XFER_FL_RESPONSE | XFER_FL_OUT ) );
	meta.dest = fc_fill_sockaddr ( &dest, &els->peer_port_id );

	/* Transmit frame */
	if ( ( rc = xfer_deliver_raw_meta ( &els->xchg, data, len,
					    &meta ) ) != 0 ) {
		DBGC ( els, FCELS_FMT " could not deliver frame: %s\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
		return rc;
	}

	return 0;
}

/**
 * Receive Fibre Channel ELS frame
 *
 * @v els		Fibre Channel ELS transaction
 * @v iobuf		I/O buffer
 * @v meta		Data transfer metadata
 * @ret rc		Return status code
 */
static int fc_els_rx ( struct fc_els *els,
		       struct io_buffer *iobuf,
		       struct xfer_metadata *meta ) {
	struct fc_els_frame_common *frame = iobuf->data;
	struct sockaddr_fc *src = ( ( struct sockaddr_fc * ) meta->src );
	struct sockaddr_fc *dest = ( ( struct sockaddr_fc * ) meta->dest );
	size_t len = iob_len ( iobuf );
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *frame ) ) {
		DBGC ( els, FCELS_FMT " received underlength frame:\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, frame, len );
		rc = -EINVAL;
		goto done;
	}
	if ( ! src ) {
		DBGC ( els, FCELS_FMT " received frame missing source "
		       "address:\n", FCELS_ARGS ( els ) );
		rc = -EINVAL;
		goto done;
	}
	if ( ! dest ) {
		DBGC ( els, FCELS_FMT " received frame missing destination "
		       "address:\n", FCELS_ARGS ( els ) );
		rc = -EINVAL;
		goto done;
	}

	/* Check for rejection responses */
	if ( fc_els_is_request ( els ) &&
	     ( frame->command != FC_ELS_LS_ACC ) ) {
		DBGC ( els, FCELS_FMT " rejected:\n", FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, frame, len );
		rc = -EACCES;
		goto done;
	}

	/* Update port IDs */
	memcpy ( &els->port_id, &dest->sfc_port_id, sizeof ( els->port_id ) );
	memcpy ( &els->peer_port_id, &src->sfc_port_id,
		 sizeof ( els->peer_port_id ) );

	/* Determine handler, if necessary */
	if ( ! els->handler )
		els->handler = fc_els_detect ( els, frame, len );
	if ( ! els->handler )
		els->handler = &fc_els_unknown_handler;

	DBGC2 ( els, FCELS_FMT " received:\n", FCELS_ARGS ( els ) );
	DBGC2_HDA ( els, 0, frame, len );

	/* Handle received frame */
	if ( ( rc = els->handler->rx ( els, frame, len ) ) != 0 ) {
		DBGC ( els, FCELS_FMT " could not handle received frame: "
		       "%s\n", FCELS_ARGS ( els ), strerror ( rc ) );
		DBGC_HDA ( els, 0, frame, len );
		goto done;
	}

 done:
	/* Free I/O buffer */
	free_iob ( iobuf );

	/* Close transaction */
	fc_els_close ( els, rc );

	return rc;
}

/** Fibre Channel ELS exchange interface operations */
static struct interface_operation fc_els_xchg_op[] = {
	INTF_OP ( xfer_deliver, struct fc_els *, fc_els_rx ),
	INTF_OP ( intf_close, struct fc_els *, fc_els_close ),
};

/** Fibre Channel ELS exchange interface descriptor */
static struct interface_descriptor fc_els_xchg_desc =
	INTF_DESC ( struct fc_els, xchg, fc_els_xchg_op );

/** Fibre Channel ELS job control interface operations */
static struct interface_operation fc_els_job_op[] = {
	INTF_OP ( intf_close, struct fc_els *, fc_els_close ),
};

/** Fibre Channel ELS job control interface descriptor */
static struct interface_descriptor fc_els_job_desc =
	INTF_DESC ( struct fc_els, job, fc_els_job_op );

/**
 * Fibre Channel ELS process
 *
 * @v els		Fibre Channel ELS transaction
 */
static void fc_els_step ( struct fc_els *els ) {
	int xchg_id;
	int rc;

	/* Sanity check */
	assert ( fc_els_is_request ( els ) );

	/* Create exchange */
	if ( ( xchg_id = fc_xchg_originate ( &els->xchg, els->port,
					     &els->peer_port_id,
					     FC_TYPE_ELS ) ) < 0 ) {
		rc = xchg_id;
		DBGC ( els, FCELS_FMT " could not create exchange: %s\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
		fc_els_close ( els, rc );
		return;
	}

	/* Transmit request */
	if ( ( rc = els->handler->tx ( els ) ) != 0 ) {
		DBGC ( els, FCELS_FMT " could not transmit request: %s\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
		fc_els_close ( els, rc );
		return;
	}
}

/** Fibre Channel ELS process descriptor */
static struct process_descriptor fc_els_process_desc =
	PROC_DESC_ONCE ( struct fc_els, process, fc_els_step );

/**
 * Create ELS transaction
 *
 * @v port		Fibre Channel port
 * @v port_id		Local port ID
 * @v peer_port_id	Peer port ID
 * @ret els		Fibre Channel ELS transaction, or NULL
 */
static struct fc_els * fc_els_create ( struct fc_port *port,
				       struct fc_port_id *port_id,
				       struct fc_port_id *peer_port_id ) {
	struct fc_els *els;

	/* Allocate and initialise structure */
	els = zalloc ( sizeof ( *els ) );
	if ( ! els )
		return NULL;
	ref_init ( &els->refcnt, fc_els_free );
	intf_init ( &els->job, &fc_els_job_desc, &els->refcnt );
	intf_init ( &els->xchg, &fc_els_xchg_desc, &els->refcnt );
	process_init_stopped ( &els->process, &fc_els_process_desc,
			       &els->refcnt );
	els->port = fc_port_get ( port );
	memcpy ( &els->port_id, port_id, sizeof ( els->port_id ) );
	memcpy ( &els->peer_port_id, peer_port_id,
		 sizeof ( els->peer_port_id ) );
	return els;
}

/**
 * Create ELS request
 *
 * @v job		Parent job-control interface
 * @v port		Fibre Channel port
 * @v peer_port_id	Peer port ID
 * @v handler		ELS handler
 * @ret rc		Return status code
 */
int fc_els_request ( struct interface *job, struct fc_port *port,
		     struct fc_port_id *peer_port_id,
		     struct fc_els_handler *handler ) {
	struct fc_els *els;

	/* Allocate and initialise structure */
	els = fc_els_create ( port, &port->port_id, peer_port_id );
	if ( ! els )
		return -ENOMEM;
	els->handler = handler;
	els->flags = FC_ELS_REQUEST;
	process_add ( &els->process );

	/* Attach to parent job interface, mortalise self, and return */
	intf_plug_plug ( &els->job, job );
	ref_put ( &els->refcnt );
	return 0;
}

/**
 * Create ELS response
 *
 * @v xchg		Exchange interface
 * @v port		Fibre Channel port
 * @v port_id		Local port ID
 * @v peer_port_id	Peer port ID
 * @ret rc		Return status code
 */
static int fc_els_respond ( struct interface *xchg, struct fc_port *port,
			    struct fc_port_id *port_id,
			    struct fc_port_id *peer_port_id ) {
	struct fc_els *els;

	/* Allocate and initialise structure */
	els = fc_els_create ( port, port_id, peer_port_id );
	if ( ! els )
		return -ENOMEM;

	/* Attach to exchange interface, mortalise self, and return */
	intf_plug_plug ( &els->xchg, xchg );
	ref_put ( &els->refcnt );
	return 0;
}

/** Fibre Channel ELS responder */
struct fc_responder fc_els_responder __fc_responder = {
	.type = FC_TYPE_ELS,
	.respond = fc_els_respond,
};

/******************************************************************************
 *
 * Unknown ELS handler
 *
 ******************************************************************************
 */

/**
 * Transmit unknown ELS request
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_unknown_tx ( struct fc_els *els __unused ) {
	return -ENOTSUP;
}

/**
 * Transmit unknown ELS response
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_unknown_tx_response ( struct fc_els *els ) {
	struct fc_ls_rjt_frame ls_rjt;

	/* Construct LS_RJT */
	memset ( &ls_rjt, 0, sizeof ( ls_rjt ) );
	ls_rjt.command = FC_ELS_LS_RJT;
	ls_rjt.reason = FC_ELS_RJT_UNSUPPORTED;

	/* Transmit LS_RJT */
	return fc_els_tx ( els, &ls_rjt, sizeof ( ls_rjt ) );
}

/**
 * Receive unknown ELS
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_unknown_rx ( struct fc_els *els, void *data, size_t len ) {
	int rc;

	DBGC ( els, FCELS_FMT ":\n", FCELS_ARGS ( els ) );
	DBGC_HDA ( els, 0, data, len );

	/* Transmit response, if applicable */
	if ( ! fc_els_is_request ( els ) ) {
		if ( ( rc = fc_els_unknown_tx_response ( els ) ) != 0 )
			return rc;
	}

	return 0;
}

/**
 * Detect unknown ELS
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_unknown_detect ( struct fc_els *els __unused,
				   const void *data __unused,
				   size_t len __unused ) {
	return -ENOTSUP;
}

/** Unknown ELS handler */
struct fc_els_handler fc_els_unknown_handler __fc_els_handler = {
	.name		= "UNKNOWN",
	.tx		= fc_els_unknown_tx,
	.rx		= fc_els_unknown_rx,
	.detect		= fc_els_unknown_detect,
};

/******************************************************************************
 *
 * FLOGI
 *
 ******************************************************************************
 */

/**
 * Transmit FLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_flogi_tx ( struct fc_els *els ) {
	struct fc_login_frame flogi;

	/* Construct FLOGI */
	memset ( &flogi, 0, sizeof ( flogi ) );
	flogi.command = fc_els_tx_command ( els, FC_ELS_FLOGI );
	flogi.common.version = htons ( FC_LOGIN_VERSION );
	flogi.common.credit = htons ( FC_LOGIN_DEFAULT_B2B );
	flogi.common.flags = htons ( FC_LOGIN_CONTINUOUS_OFFSET );
	flogi.common.mtu = htons ( FC_LOGIN_DEFAULT_MTU );
	memcpy ( &flogi.port_wwn, &els->port->port_wwn,
		 sizeof ( flogi.port_wwn ) );
	memcpy ( &flogi.node_wwn, &els->port->node_wwn,
		 sizeof ( flogi.node_wwn ) );
	flogi.class3.flags = htons ( FC_LOGIN_CLASS_VALID |
				     FC_LOGIN_CLASS_SEQUENTIAL );

	/* Transmit FLOGI */
	return fc_els_tx ( els, &flogi, sizeof ( flogi ) );
}

/**
 * Receive FLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_flogi_rx ( struct fc_els *els, void *data, size_t len ) {
	struct fc_login_frame *flogi = data;
	int has_fabric;
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *flogi ) ) {
		DBGC ( els, FCELS_FMT " received underlength frame:\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, data, len );
		return -EINVAL;
	}

	/* Extract parameters */
	has_fabric = ( flogi->common.flags & htons ( FC_LOGIN_F_PORT ) );
	DBGC ( els, FCELS_FMT " has node %s\n", FCELS_ARGS ( els ),
	       fc_ntoa ( &flogi->node_wwn ) );
	DBGC ( els, FCELS_FMT " has port %s\n", FCELS_ARGS ( els ),
	       fc_ntoa ( &flogi->port_wwn ) );
	if ( has_fabric ) {
		DBGC ( els, FCELS_FMT " has fabric with", FCELS_ARGS ( els ) );
		DBGC ( els, " local ID %s\n", fc_id_ntoa ( &els->port_id ) );
	} else {
		DBGC ( els, FCELS_FMT " has point-to-point link\n",
		       FCELS_ARGS ( els ) );
	}

	/* Log in port */
	if ( ( rc = fc_port_login ( els->port, &els->port_id, &flogi->node_wwn,
				    &flogi->port_wwn, has_fabric ) ) != 0 ) {
		DBGC ( els, FCELS_FMT " could not log in port: %s\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
		return rc;
	}

	/* Send any responses to the newly-assigned peer port ID, if
	 * applicable.
	 */
	if ( ! has_fabric ) {
		memcpy ( &els->peer_port_id, &els->port->ptp_link_port_id,
			 sizeof ( els->peer_port_id ) );
	}

	/* Transmit response, if applicable */
	if ( ! fc_els_is_request ( els ) ) {
		if ( ( rc = fc_els_flogi_tx ( els ) ) != 0 )
			return rc;
	}

	return 0;
}

/**
 * Detect FLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_flogi_detect ( struct fc_els *els __unused, const void *data,
				 size_t len __unused ) {
	const struct fc_login_frame *flogi = data;

	/* Check for FLOGI */
	if ( flogi->command != FC_ELS_FLOGI )
		return -EINVAL;

	return 0;
}

/** FLOGI ELS handler */
struct fc_els_handler fc_els_flogi_handler __fc_els_handler = {
	.name		= "FLOGI",
	.tx		= fc_els_flogi_tx,
	.rx		= fc_els_flogi_rx,
	.detect		= fc_els_flogi_detect,
};

/**
 * Create FLOGI request
 *
 * @v parent		Parent interface
 * @v port		Fibre Channel port
 * @ret rc		Return status code
 */
int fc_els_flogi ( struct interface *parent, struct fc_port *port ) {

	return fc_els_request ( parent, port, &fc_f_port_id,
				&fc_els_flogi_handler );
}

/******************************************************************************
 *
 * PLOGI
 *
 ******************************************************************************
 */

/**
 * Transmit PLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_plogi_tx ( struct fc_els *els ) {
	struct fc_login_frame plogi;

	/* Construct PLOGI */
	memset ( &plogi, 0, sizeof ( plogi ) );
	plogi.command = fc_els_tx_command ( els, FC_ELS_PLOGI );
	plogi.common.version = htons ( FC_LOGIN_VERSION );
	plogi.common.credit = htons ( FC_LOGIN_DEFAULT_B2B );
	plogi.common.flags = htons ( FC_LOGIN_CONTINUOUS_OFFSET );
	plogi.common.mtu = htons ( FC_LOGIN_DEFAULT_MTU );
	plogi.common.u.plogi.max_seq = htons ( FC_LOGIN_DEFAULT_MAX_SEQ );
	plogi.common.u.plogi.rel_offs = htons ( FC_LOGIN_DEFAULT_REL_OFFS );
	plogi.common.e_d_tov = htonl ( FC_LOGIN_DEFAULT_E_D_TOV );
	memcpy ( &plogi.port_wwn, &els->port->port_wwn,
		 sizeof ( plogi.port_wwn ) );
	memcpy ( &plogi.node_wwn, &els->port->node_wwn,
		 sizeof ( plogi.node_wwn ) );
	plogi.class3.flags = htons ( FC_LOGIN_CLASS_VALID |
				     FC_LOGIN_CLASS_SEQUENTIAL );
	plogi.class3.mtu = htons ( FC_LOGIN_DEFAULT_MTU );
	plogi.class3.max_seq = htons ( FC_LOGIN_DEFAULT_MAX_SEQ );
	plogi.class3.max_seq_per_xchg = 1;

	/* Transmit PLOGI */
	return fc_els_tx ( els, &plogi, sizeof ( plogi ) );
}

/**
 * Receive PLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_plogi_rx ( struct fc_els *els, void *data, size_t len ) {
	struct fc_login_frame *plogi = data;
	struct fc_peer *peer;
	int rc;

	/* Sanity checks */
	if ( len < sizeof ( *plogi ) ) {
		DBGC ( els, FCELS_FMT " received underlength frame:\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, data, len );
		rc = -EINVAL;
		goto err_sanity;
	}
	if ( ! fc_link_ok ( &els->port->link ) ) {
		DBGC ( els, FCELS_FMT " received while port link is down\n",
		       FCELS_ARGS ( els ) );
		rc = -EINVAL;
		goto err_sanity;
	}

	/* Extract parameters */
	DBGC ( els, FCELS_FMT " has node %s\n", FCELS_ARGS ( els ),
	       fc_ntoa ( &plogi->node_wwn ) );
	DBGC ( els, FCELS_FMT " has port %s as %s\n",
	       FCELS_ARGS ( els ), fc_ntoa ( &plogi->port_wwn ),
	       fc_id_ntoa ( &els->peer_port_id ) );

	/* Get peer */
	peer = fc_peer_get_wwn ( &plogi->port_wwn );
	if ( ! peer ) {
		DBGC ( els, FCELS_FMT " could not create peer\n",
		       FCELS_ARGS ( els ) );
		rc = -ENOMEM;
		goto err_peer_get_wwn;
	}

	/* Record login */
	if ( ( rc = fc_peer_login ( peer, els->port,
				    &els->peer_port_id ) ) != 0 ) {
		DBGC ( els, FCELS_FMT " could not log in peer: %s\n",
		       FCELS_ARGS ( els ), strerror ( rc ) );
		goto err_login;
	}

	/* Transmit response, if applicable */
	if ( ! fc_els_is_request ( els ) ) {
		if ( ( rc = fc_els_plogi_tx ( els ) ) != 0 )
			goto err_plogi_tx;
	}

	/* Drop temporary reference to peer */
	fc_peer_put ( peer );

	return 0;

 err_plogi_tx:
 err_login:
	fc_peer_put ( peer );
 err_peer_get_wwn:
 err_sanity:
	return rc;
}

/**
 * Detect PLOGI
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_plogi_detect ( struct fc_els *els __unused, const void *data,
				 size_t len __unused ) {
	const struct fc_login_frame *plogi = data;

	/* Check for PLOGI */
	if ( plogi->command != FC_ELS_PLOGI )
		return -EINVAL;

	return 0;
}

/** PLOGI ELS handler */
struct fc_els_handler fc_els_plogi_handler __fc_els_handler = {
	.name		= "PLOGI",
	.tx		= fc_els_plogi_tx,
	.rx		= fc_els_plogi_rx,
	.detect		= fc_els_plogi_detect,
};

/**
 * Create PLOGI request
 *
 * @v parent		Parent interface
 * @v port		Fibre Channel port
 * @v peer_port_id	Peer port ID
 * @ret rc		Return status code
 */
int fc_els_plogi ( struct interface *parent, struct fc_port *port,
		   struct fc_port_id *peer_port_id ) {

	return fc_els_request ( parent, port, peer_port_id,
				&fc_els_plogi_handler );
}

/******************************************************************************
 *
 * LOGO
 *
 ******************************************************************************
 */

/**
 * Transmit LOGO request
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_logo_tx ( struct fc_els *els ) {
	struct fc_logout_request_frame logo;

	/* Construct LOGO */
	memset ( &logo, 0, sizeof ( logo ) );
	logo.command = FC_ELS_LOGO;
	memcpy ( &logo.port_id, &els->port->port_id, sizeof ( logo.port_id ) );
	memcpy ( &logo.port_wwn, &els->port->port_wwn,
		 sizeof ( logo.port_wwn ) );

	/* Transmit LOGO */
	return fc_els_tx ( els, &logo, sizeof ( logo ) );
}

/**
 * Transmit LOGO response
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_logo_tx_response ( struct fc_els *els ) {
	struct fc_logout_response_frame logo;

	/* Construct LOGO */
	memset ( &logo, 0, sizeof ( logo ) );
	logo.command = FC_ELS_LS_ACC;

	/* Transmit LOGO */
	return fc_els_tx ( els, &logo, sizeof ( logo ) );
}

/**
 * Log out individual peer or whole port as applicable
 *
 * @v els		Fibre Channel ELS transaction
 * @v port_id		Peer port ID
 */
static void fc_els_logo_logout ( struct fc_els *els,
				 struct fc_port_id *peer_port_id ) {
	struct fc_peer *peer;

	if ( ( memcmp ( peer_port_id, &fc_f_port_id,
			sizeof ( *peer_port_id ) ) == 0 ) ||
	     ( memcmp ( peer_port_id, &els->port->port_id,
			sizeof ( *peer_port_id ) ) == 0 ) ) {
		fc_port_logout ( els->port, 0 );
	} else {
		peer = fc_peer_get_port_id ( els->port, peer_port_id );
		if ( peer ) {
			fc_peer_logout ( peer, 0 );
			fc_peer_put ( peer );
		}
	}
}

/**
 * Receive LOGO request
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_logo_rx_request ( struct fc_els *els, void *data,
				    size_t len ) {
	struct fc_logout_request_frame *logo = data;
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *logo ) ) {
		DBGC ( els, FCELS_FMT " received underlength frame:\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, data, len );
		return -EINVAL;
	}

	DBGC ( els, FCELS_FMT " has port %s as %s\n", FCELS_ARGS ( els ),
	       fc_ntoa ( &logo->port_wwn ), fc_id_ntoa ( &logo->port_id ) );

	/* Log out individual peer or whole port as applicable */
	fc_els_logo_logout ( els, &logo->port_id );

	/* Transmit repsonse */
	if ( ( rc = fc_els_logo_tx_response ( els ) ) != 0 )
		return rc;

	return 0;
}

/**
 * Receive LOGO response
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_logo_rx_response ( struct fc_els *els, void *data __unused,
				     size_t len __unused ) {

	/* Log out individual peer or whole port as applicable */
	fc_els_logo_logout ( els, &els->peer_port_id );

	return 0;
}

/**
 * Receive LOGO
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_logo_rx ( struct fc_els *els, void *data, size_t len ) {

	if ( fc_els_is_request ( els ) ) {
		return fc_els_logo_rx_response ( els, data, len );
	} else {
		return fc_els_logo_rx_request ( els, data, len );
	}
}

/**
 * Detect LOGO
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_logo_detect ( struct fc_els *els __unused, const void *data,
				size_t len __unused ) {
	const struct fc_logout_request_frame *logo = data;

	/* Check for LOGO */
	if ( logo->command != FC_ELS_LOGO )
		return -EINVAL;

	return 0;
}

/** LOGO ELS handler */
struct fc_els_handler fc_els_logo_handler __fc_els_handler = {
	.name		= "LOGO",
	.tx		= fc_els_logo_tx,
	.rx		= fc_els_logo_rx,
	.detect		= fc_els_logo_detect,
};

/**
 * Create LOGO request
 *
 * @v parent		Parent interface
 * @v port		Fibre Channel port
 * @v peer_port_id	Peer port ID
 * @ret rc		Return status code
 */
int fc_els_logo ( struct interface *parent, struct fc_port *port,
		  struct fc_port_id *peer_port_id ) {

	return fc_els_request ( parent, port, peer_port_id,
				&fc_els_logo_handler );
}

/******************************************************************************
 *
 * PRLI
 *
 ******************************************************************************
 */

/**
 * Find PRLI descriptor
 *
 * @v type		Upper-layer protocol type
 * @ret descriptor	PRLI descriptor, or NULL
 */
static struct fc_els_prli_descriptor *
fc_els_prli_descriptor ( unsigned int type ) {
	struct fc_els_prli_descriptor *descriptor;

	for_each_table_entry ( descriptor, FC_ELS_PRLI_DESCRIPTORS ) {
		if ( descriptor->type == type )
			return descriptor;
	}
	return NULL;
}

/**
 * Transmit PRLI
 *
 * @v els		Fibre Channel ELS transaction
 * @v descriptor	ELS PRLI descriptor
 * @v param		Service parameters
 * @ret rc		Return status code
 */
int fc_els_prli_tx ( struct fc_els *els,
		     struct fc_els_prli_descriptor *descriptor, void *param ) {
	struct {
		struct fc_prli_frame frame;
		uint8_t param[descriptor->param_len];
	} __attribute__ (( packed )) prli;
	struct fc_ulp *ulp;
	int rc;

	/* Get ULP */
	ulp = fc_ulp_get_port_id_type ( els->port, &els->peer_port_id,
					descriptor->type );
	if ( ! ulp ) {
		rc = -ENOMEM;
		goto err_get_port_id_type;
	}

	/* Build frame for transmission */
	memset ( &prli, 0, sizeof ( prli ) );
	prli.frame.command = fc_els_tx_command ( els, FC_ELS_PRLI );
	prli.frame.page_len =
		( sizeof ( prli.frame.page ) + sizeof ( prli.param ) );
	prli.frame.len = htons ( sizeof ( prli ) );
	prli.frame.page.type = descriptor->type;
	if ( fc_els_is_request ( els ) ) {
		prli.frame.page.flags |= htons ( FC_PRLI_ESTABLISH );
	} else if ( fc_link_ok ( &ulp->link ) ) {
		prli.frame.page.flags |= htons ( FC_PRLI_ESTABLISH |
						    FC_PRLI_RESPONSE_SUCCESS );
	}
	memcpy ( &prli.param, param, sizeof ( prli.param ) );

	/* Transmit frame */
	if ( ( rc = fc_els_tx ( els, &prli, sizeof ( prli ) ) ) != 0 )
		goto err_tx;

	/* Drop temporary reference to ULP */
	fc_ulp_put ( ulp );

	return 0;

 err_tx:
	fc_ulp_put ( ulp );
 err_get_port_id_type:
	return rc;
}

/**
 * Receive PRLI
 *
 * @v els		Fibre Channel ELS transaction
 * @v descriptor	ELS PRLI descriptor
 * @v frame		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
int fc_els_prli_rx ( struct fc_els *els,
		     struct fc_els_prli_descriptor *descriptor,
		     void *data, size_t len ) {
	struct {
		struct fc_prli_frame frame;
		uint8_t param[descriptor->param_len];
	} __attribute__ (( packed )) *prli = data;
	struct fc_ulp *ulp;
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *prli ) ) {
		DBGC ( els, FCELS_FMT " received underlength frame:\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, data, len );
		rc = -EINVAL;
		goto err_sanity;
	}

	DBGC ( els, FCELS_FMT " has parameters:\n", FCELS_ARGS ( els ) );
	DBGC_HDA ( els, 0, prli->param, sizeof ( prli->param ) );

	/* Get ULP */
	ulp = fc_ulp_get_port_id_type ( els->port, &els->peer_port_id,
					descriptor->type );
	if ( ! ulp ) {
		rc = -ENOMEM;
		goto err_get_port_id_type;
	}

	/* Sanity check */
	if ( ! fc_link_ok ( &ulp->peer->link ) ) {
		DBGC ( els, FCELS_FMT " received while peer link is down\n",
		       FCELS_ARGS ( els ) );
		rc = -EINVAL;
		goto err_link;
	}

	/* Log in ULP, if applicable */
	if ( prli->frame.page.flags & htons ( FC_PRLI_ESTABLISH ) ) {
		if ( ( rc = fc_ulp_login ( ulp, prli->param,
					   sizeof ( prli->param ),
					   fc_els_is_request ( els ) ) ) != 0 ){
			DBGC ( els, FCELS_FMT " could not log in ULP: %s\n",
			       FCELS_ARGS ( els ), strerror ( rc ) );
			goto err_login;
		}
	} else {
		if ( fc_els_is_request ( els ) ) {
			fc_ulp_logout ( ulp, -EACCES );
		} else {
			/* This is just an information-gathering PRLI; do not
			 * log in or out
			 */
		}
	}

	/* Transmit response, if applicable */
	if ( ! fc_els_is_request ( els ) ) {
		if ( ( rc = els->handler->tx ( els ) ) != 0 )
			goto err_tx;
	}

	/* Drop temporary reference to ULP */
	fc_ulp_put ( ulp );

	return 0;

 err_tx:
 err_login:
 err_link:
	fc_ulp_put ( ulp );
 err_get_port_id_type:
 err_sanity:
	return rc;
}

/**
 * Detect PRLI
 *
 * @v els		Fibre Channel ELS transaction
 * @v descriptor	ELS PRLI descriptor
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
int fc_els_prli_detect ( struct fc_els *els __unused,
			 struct fc_els_prli_descriptor *descriptor,
			 const void *data, size_t len ) {
	const struct {
		struct fc_prli_frame frame;
		uint8_t param[descriptor->param_len];
	} __attribute__ (( packed )) *prli = data;

	/* Check for PRLI */
	if ( prli->frame.command != FC_ELS_PRLI )
		return -EINVAL;

	/* Check for sufficient length to contain service parameter page */
	if ( len < sizeof ( *prli ) )
		return -EINVAL;

	/* Check for upper-layer protocol type */
	if ( prli->frame.page.type != descriptor->type )
		return -EINVAL;

	return 0;
}

/**
 * Create PRLI request
 *
 * @v parent		Parent interface
 * @v port		Fibre Channel port
 * @v peer_port_id	Peer port ID
 * @v type		Upper-layer protocol type
 * @ret rc		Return status code
 */
int fc_els_prli ( struct interface *parent, struct fc_port *port,
		  struct fc_port_id *peer_port_id, unsigned int type ) {
	struct fc_els_prli_descriptor *descriptor;

	/* Find a PRLI descriptor */
	descriptor = fc_els_prli_descriptor ( type );
	if ( ! descriptor )
		return -ENOTSUP;

	return fc_els_request ( parent, port, peer_port_id,
				descriptor->handler );
}

/******************************************************************************
 *
 * RTV
 *
 ******************************************************************************
 */

/**
 * Transmit RTV response
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_rtv_tx_response ( struct fc_els *els ) {
	struct fc_rtv_response_frame rtv;

	/* Construct RTV */
	memset ( &rtv, 0, sizeof ( rtv ) );
	rtv.command = FC_ELS_LS_ACC;
	rtv.e_d_tov = htonl ( FC_LOGIN_DEFAULT_E_D_TOV );

	/* Transmit RTV */
	return fc_els_tx ( els, &rtv, sizeof ( rtv ) );
}

/**
 * Receive RTV
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_rtv_rx ( struct fc_els *els, void *data __unused,
			   size_t len __unused ) {
	int rc;

	DBGC ( els, FCELS_FMT "\n", FCELS_ARGS ( els ) );

	/* Transmit response */
	if ( ! fc_els_is_request ( els ) ) {
		if ( ( rc = fc_els_rtv_tx_response ( els ) ) != 0 )
			return rc;
	}

	return 0;
}

/**
 * Detect RTV
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_rtv_detect ( struct fc_els *els __unused, const void *data,
			       size_t len __unused ) {
	const struct fc_rtv_request_frame *rtv = data;

	/* Check for RTV */
	if ( rtv->command != FC_ELS_RTV )
		return -EINVAL;

	return 0;
}

/** RTV ELS handler */
struct fc_els_handler fc_els_rtv_handler __fc_els_handler = {
	.name		= "RTV",
	.tx		= fc_els_unknown_tx,
	.rx		= fc_els_rtv_rx,
	.detect		= fc_els_rtv_detect,
};

/******************************************************************************
 *
 * ECHO
 *
 ******************************************************************************
 */

/** ECHO request data */
struct fc_echo_request_frame {
	/** ECHO frame header */
	struct fc_echo_frame_header echo;
	/** Magic marker */
	uint32_t magic;
} __attribute__ (( packed ));

/** ECHO magic marker */
#define FC_ECHO_MAGIC 0x69505845

/**
 * Transmit ECHO
 *
 * @v els		Fibre Channel ELS transaction
 * @ret rc		Return status code
 */
static int fc_els_echo_tx ( struct fc_els *els ) {
	struct fc_echo_request_frame echo;

	/* Construct ECHO */
	memset ( &echo, 0, sizeof ( echo ) );
	echo.echo.command = FC_ELS_ECHO;
	echo.magic = htonl ( FC_ECHO_MAGIC );

	/* Transmit ECHO */
	return fc_els_tx ( els, &echo, sizeof ( echo ) );
}

/**
 * Receive ECHO request
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_echo_rx_request ( struct fc_els *els, void *data,
				    size_t len ) {
	struct {
		struct fc_echo_frame_header echo;
		char payload[ len - sizeof ( struct fc_echo_frame_header ) ];
	} *echo = data;
	int rc;

	DBGC ( els, FCELS_FMT "\n", FCELS_ARGS ( els ) );

	/* Transmit response */
	echo->echo.command = FC_ELS_LS_ACC;
	if ( ( rc = fc_els_tx ( els, echo, sizeof ( *echo ) ) ) != 0 )
		return rc;

	/* Nothing to do */
	return 0;
}

/**
 * Receive ECHO response
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_echo_rx_response ( struct fc_els *els, void *data,
				     size_t len ) {
	struct fc_echo_request_frame *echo = data;

	DBGC ( els, FCELS_FMT "\n", FCELS_ARGS ( els ) );

	/* Check response is correct */
	if ( ( len != sizeof ( *echo ) ) ||
	     ( echo->magic != htonl ( FC_ECHO_MAGIC ) ) ) {
		DBGC ( els, FCELS_FMT " received bad echo response\n",
		       FCELS_ARGS ( els ) );
		DBGC_HDA ( els, 0, data, len );
		return -EIO;
	}

	return 0;
}

/**
 * Receive ECHO
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_echo_rx ( struct fc_els *els, void *data, size_t len ) {

	if ( fc_els_is_request ( els ) ) {
		return fc_els_echo_rx_response ( els, data, len );
	} else {
		return fc_els_echo_rx_request ( els, data, len );
	}
}

/**
 * Detect ECHO
 *
 * @v els		Fibre Channel ELS transaction
 * @v data		ELS frame
 * @v len		Length of ELS frame
 * @ret rc		Return status code
 */
static int fc_els_echo_detect ( struct fc_els *els __unused, const void *data,
				size_t len __unused ) {
	const struct fc_echo_frame_header *echo = data;

	/* Check for ECHO */
	if ( echo->command != FC_ELS_ECHO )
		return -EINVAL;

	return 0;
}

/** ECHO ELS handler */
struct fc_els_handler fc_els_echo_handler __fc_els_handler = {
	.name		= "ECHO",
	.tx		= fc_els_echo_tx,
	.rx		= fc_els_echo_rx,
	.detect		= fc_els_echo_detect,
};
