/*
 * Copyright (C) 2006 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 <stddef.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <byteswap.h>
#include <gpxe/list.h>
#include <gpxe/if_ether.h>
#include <gpxe/ethernet.h>
#include <gpxe/iobuf.h>
#include <gpxe/uaccess.h>
#include <gpxe/ata.h>
#include <gpxe/netdevice.h>
#include <gpxe/process.h>
#include <gpxe/features.h>
#include <gpxe/aoe.h>

/** @file
 *
 * AoE protocol
 *
 */

FEATURE ( FEATURE_PROTOCOL, "AoE", DHCP_EB_FEATURE_AOE, 1 );

struct net_protocol aoe_protocol;

/** List of all AoE sessions */
static LIST_HEAD ( aoe_sessions );

static void aoe_free ( struct refcnt *refcnt ) {
	struct aoe_session *aoe =
		container_of ( refcnt, struct aoe_session, refcnt );

	netdev_put ( aoe->netdev );
	free ( aoe );
}

/**
 * Mark current AoE command complete
 *
 * @v aoe		AoE session
 * @v rc		Return status code
 */
static void aoe_done ( struct aoe_session *aoe, int rc ) {

	/* Record overall command status */
	if ( aoe->command ) {
		aoe->command->cb.cmd_stat = aoe->status;
		aoe->command->rc = rc;
		aoe->command = NULL;
	}

	/* Stop retransmission timer */
	stop_timer ( &aoe->timer );

	/* Mark operation as complete */
	aoe->rc = rc;
}

/**
 * Send AoE command
 *
 * @v aoe		AoE session
 * @ret rc		Return status code
 *
 * This transmits an AoE command packet.  It does not wait for a
 * response.
 */
static int aoe_send_command ( struct aoe_session *aoe ) {
	struct ata_command *command = aoe->command;
	struct io_buffer *iobuf;
	struct aoehdr *aoehdr;
	union aoecmd *aoecmd;
	struct aoeata *aoeata;
	unsigned int count;
	unsigned int data_out_len;
	unsigned int aoecmdlen;

	/* Fail immediately if we have no netdev to send on */
	if ( ! aoe->netdev ) {
		aoe_done ( aoe, -ENETUNREACH );
		return -ENETUNREACH;
	}

	/* If we are transmitting anything that requires a response,
         * start the retransmission timer.  Do this before attempting
         * to allocate the I/O buffer, in case allocation itself
         * fails.
         */
	start_timer ( &aoe->timer );

	/* Calculate count and data_out_len for this subcommand */
	switch ( aoe->aoe_cmd_type ) {
	case AOE_CMD_ATA:
		count = command->cb.count.native;
		if ( count > AOE_MAX_COUNT )
			count = AOE_MAX_COUNT;
		data_out_len = ( command->data_out ?
				 ( count * ATA_SECTOR_SIZE ) : 0 );
		aoecmdlen = sizeof ( aoecmd->ata );
		break;
	case AOE_CMD_CONFIG:
		count = 0;
		data_out_len = 0;
		aoecmdlen = sizeof ( aoecmd->cfg );
		break;
	default:
		return -ENOTSUP;
	}

	/* Create outgoing I/O buffer */
	iobuf = alloc_iob ( ETH_HLEN + sizeof ( *aoehdr ) +
			    aoecmdlen + data_out_len );

	if ( ! iobuf )
		return -ENOMEM;
	iob_reserve ( iobuf, ETH_HLEN );
	aoehdr = iob_put ( iobuf, sizeof ( *aoehdr ) );
	aoecmd = iob_put ( iobuf, aoecmdlen );
	memset ( aoehdr, 0, ( sizeof ( *aoehdr ) + aoecmdlen ) );

	/* Fill AoE header */
	aoehdr->ver_flags = AOE_VERSION;
	aoehdr->major = htons ( aoe->major );
	aoehdr->minor = aoe->minor;
	aoehdr->command = aoe->aoe_cmd_type;
	aoehdr->tag = htonl ( ++aoe->tag );

	/* Fill AoE payload */
	switch ( aoe->aoe_cmd_type ) {
	case AOE_CMD_ATA:
		/* Fill AoE command */
		aoeata = &aoecmd->ata;
		linker_assert ( AOE_FL_DEV_HEAD	== ATA_DEV_SLAVE,
				__fix_ata_h__ );
		aoeata->aflags = ( ( command->cb.lba48 ? AOE_FL_EXTENDED : 0 )|
				   ( command->cb.device & ATA_DEV_SLAVE ) |
				   ( data_out_len ? AOE_FL_WRITE : 0 ) );
		aoeata->err_feat = command->cb.err_feat.bytes.cur;
		aoeata->count = count;
		aoeata->cmd_stat = command->cb.cmd_stat;
		aoeata->lba.u64 = cpu_to_le64 ( command->cb.lba.native );
		if ( ! command->cb.lba48 )
			aoeata->lba.bytes[3] |=
				( command->cb.device & ATA_DEV_MASK );

		/* Fill data payload */
		copy_from_user ( iob_put ( iobuf, data_out_len ),
				 command->data_out, aoe->command_offset,
				 data_out_len );
		break;
	case AOE_CMD_CONFIG:
		/* Nothing to do */
		break;
	default:
		assert ( 0 );
	}

	/* Send packet */
	return net_tx ( iobuf, aoe->netdev, &aoe_protocol, aoe->target );
}

/**
 * Handle AoE retry timer expiry
 *
 * @v timer		AoE retry timer
 * @v fail		Failure indicator
 */
static void aoe_timer_expired ( struct retry_timer *timer, int fail ) {
	struct aoe_session *aoe =
		container_of ( timer, struct aoe_session, timer );

	if ( fail ) {
		aoe_done ( aoe, -ETIMEDOUT );
	} else {
		aoe_send_command ( aoe );
	}
}

/**
 * Handle AoE configuration command response
 *
 * @v aoe		AoE session
 * @v ll_source		Link-layer source address
 * @ret rc		Return status code
 */
static int aoe_rx_cfg ( struct aoe_session *aoe, const void *ll_source ) {

	/* Record target MAC address */
	memcpy ( aoe->target, ll_source, sizeof ( aoe->target ) );
	DBGC ( aoe, "AoE %p target MAC address %s\n",
	       aoe, eth_ntoa ( aoe->target ) );

	/* Mark config request as complete */
	aoe_done ( aoe, 0 );

	return 0;
}

/**
 * Handle AoE ATA command response
 *
 * @v aoe		AoE session
 * @v aoeata		AoE ATA command
 * @v len		Length of AoE ATA command
 * @ret rc		Return status code
 */
static int aoe_rx_ata ( struct aoe_session *aoe, struct aoeata *aoeata,
			size_t len ) {
	struct ata_command *command = aoe->command;
	unsigned int rx_data_len;
	unsigned int count;
	unsigned int data_len;

	/* Sanity check */
	if ( len < sizeof ( *aoeata ) ) {
		/* Ignore packet; allow timer to trigger retransmit */
		return -EINVAL;
	}
	rx_data_len = ( len - sizeof ( *aoeata ) );

	/* Calculate count and data_len for this subcommand */
	count = command->cb.count.native;
	if ( count > AOE_MAX_COUNT )
		count = AOE_MAX_COUNT;
	data_len = count * ATA_SECTOR_SIZE;

	/* Merge into overall ATA status */
	aoe->status |= aoeata->cmd_stat;

	/* Copy data payload */
	if ( command->data_in ) {
		if ( rx_data_len > data_len )
			rx_data_len = data_len;
		copy_to_user ( command->data_in, aoe->command_offset,
			       aoeata->data, rx_data_len );
	}

	/* Update ATA command and offset */
	aoe->command_offset += data_len;
	command->cb.lba.native += count;
	command->cb.count.native -= count;

	/* Check for operation complete */
	if ( ! command->cb.count.native ) {
		aoe_done ( aoe, 0 );
		return 0;
	}

	/* Transmit next portion of request */
	stop_timer ( &aoe->timer );
	aoe_send_command ( aoe );

	return 0;
}

/**
 * Process incoming AoE packets
 *
 * @v iobuf		I/O buffer
 * @v netdev		Network device
 * @v ll_source		Link-layer source address
 * @ret rc		Return status code
 *
 */
static int aoe_rx ( struct io_buffer *iobuf,
		    struct net_device *netdev __unused,
		    const void *ll_source ) {
	struct aoehdr *aoehdr = iobuf->data;
	struct aoe_session *aoe;
	int rc = 0;

	/* Sanity checks */
	if ( iob_len ( iobuf ) < sizeof ( *aoehdr ) ) {
		rc = -EINVAL;
		goto done;
	}
	if ( ( aoehdr->ver_flags & AOE_VERSION_MASK ) != AOE_VERSION ) {
		rc = -EPROTONOSUPPORT;
		goto done;
	}
	if ( ! ( aoehdr->ver_flags & AOE_FL_RESPONSE ) ) {
		/* Ignore AoE requests that we happen to see */
		goto done;
	}
	iob_pull ( iobuf, sizeof ( *aoehdr ) );

	/* Demultiplex amongst active AoE sessions */
	list_for_each_entry ( aoe, &aoe_sessions, list ) {
		if ( ntohs ( aoehdr->major ) != aoe->major )
			continue;
		if ( aoehdr->minor != aoe->minor )
			continue;
		if ( ntohl ( aoehdr->tag ) != aoe->tag )
			continue;
		if ( aoehdr->ver_flags & AOE_FL_ERROR ) {
			aoe_done ( aoe, -EIO );
			break;
		}
		switch ( aoehdr->command ) {
		case AOE_CMD_ATA:
			rc = aoe_rx_ata ( aoe, iobuf->data, iob_len ( iobuf ));
			break;
		case AOE_CMD_CONFIG:
			rc = aoe_rx_cfg ( aoe, ll_source );
			break;
		default:
			DBGC ( aoe, "AoE %p ignoring command %02x\n",
			       aoe, aoehdr->command );
			break;
		}
		break;
	}

 done:
	free_iob ( iobuf );
	return rc;
}

/** AoE protocol */
struct net_protocol aoe_protocol __net_protocol = {
	.name = "AoE",
	.net_proto = htons ( ETH_P_AOE ),
	.rx = aoe_rx,
};

/**
 * Issue ATA command via an open AoE session
 *
 * @v ata		ATA device
 * @v command		ATA command
 * @ret rc		Return status code
 */
static int aoe_command ( struct ata_device *ata,
			 struct ata_command *command ) {
	struct aoe_session *aoe =
		container_of ( ata->backend, struct aoe_session, refcnt );

	aoe->command = command;
	aoe->status = 0;
	aoe->command_offset = 0;
	aoe->aoe_cmd_type = AOE_CMD_ATA;

	aoe_send_command ( aoe );

	return 0;
}

/**
 * Issue AoE config query for AoE target discovery
 *
 * @v aoe		AoE session
 * @ret rc		Return status code
 */
static int aoe_discover ( struct aoe_session *aoe ) {
	int rc;

	aoe->status = 0;
	aoe->aoe_cmd_type = AOE_CMD_CONFIG;
	aoe->command = NULL;

	aoe_send_command ( aoe );

	aoe->rc = -EINPROGRESS;
	while ( aoe->rc == -EINPROGRESS )
		step();
	rc = aoe->rc;

	return rc;
}

static int aoe_detached_command ( struct ata_device *ata __unused,
				  struct ata_command *command __unused ) {
	return -ENODEV;
}

void aoe_detach ( struct ata_device *ata ) {
	struct aoe_session *aoe =
		container_of ( ata->backend, struct aoe_session, refcnt );

	stop_timer ( &aoe->timer );
	ata->command = aoe_detached_command;
	list_del ( &aoe->list );
	ref_put ( ata->backend );
	ata->backend = NULL;
}

static int aoe_parse_root_path ( struct aoe_session *aoe,
				 const char *root_path ) {
	char *ptr;

	if ( strncmp ( root_path, "aoe:", 4 ) != 0 )
		return -EINVAL;
	ptr = ( ( char * ) root_path + 4 );

	if ( *ptr++ != 'e' )
		return -EINVAL;

	aoe->major = strtoul ( ptr, &ptr, 10 );
	if ( *ptr++ != '.' )
		return -EINVAL;

	aoe->minor = strtoul ( ptr, &ptr, 10 );
	if ( *ptr )
		return -EINVAL;

	return 0;
}

int aoe_attach ( struct ata_device *ata, struct net_device *netdev,
		 const char *root_path ) {
	struct aoe_session *aoe;
	int rc;

	/* Allocate and initialise structure */
	aoe = zalloc ( sizeof ( *aoe ) );
	if ( ! aoe )
		return -ENOMEM;
	aoe->refcnt.free = aoe_free;
	aoe->netdev = netdev_get ( netdev );
	memcpy ( aoe->target, netdev->ll_broadcast, sizeof ( aoe->target ) );
	aoe->tag = AOE_TAG_MAGIC;
	aoe->timer.expired = aoe_timer_expired;

	/* Parse root path */
	if ( ( rc = aoe_parse_root_path ( aoe, root_path ) ) != 0 )
		goto err;

	/* Attach parent interface, transfer reference to connection
	 * list, and return
	 */
	ata->backend = ref_get ( &aoe->refcnt );
	ata->command = aoe_command;
	list_add ( &aoe->list, &aoe_sessions );

	/* Send discovery packet to find the target MAC address.
	 * Ideally, this ought to be done asynchronously, but the
	 * block device interface does not yet support asynchronous
	 * operation.
	 */
	if ( ( rc = aoe_discover( aoe ) ) != 0 )
	       goto err;

	return 0;

 err:
	ref_put ( &aoe->refcnt );
	return rc;
}