/*
 * Copyright (C) 2012 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.
 */

FILE_LICENCE ( GPL2_OR_LATER );

/**
 * @file
 *
 * EFI SNP HII protocol
 *
 * The HII protocols are some of the less-well designed parts of the
 * entire EFI specification.  This is a significant accomplishment.
 *
 * The face-slappingly ludicrous query string syntax seems to be
 * motivated by the desire to allow a caller to query multiple drivers
 * simultaneously via the single-instance HII_CONFIG_ROUTING_PROTOCOL,
 * which is supposed to pass relevant subsets of the query string to
 * the relevant drivers.
 *
 * Nobody uses the HII_CONFIG_ROUTING_PROTOCOL.  Not even the EFI
 * setup browser uses the HII_CONFIG_ROUTING_PROTOCOL.  To the best of
 * my knowledge, there has only ever been one implementation of the
 * HII_CONFIG_ROUTING_PROTOCOL (as part of EDK2), and it just doesn't
 * work.  It's so badly broken that I can't even figure out what the
 * code is _trying_ to do.
 *
 * Fundamentally, the problem seems to be that Javascript programmers
 * should not be allowed to design APIs for C code.
 */

#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <stdio.h>
#include <wchar.h>
#include <errno.h>
#include <ipxe/settings.h>
#include <ipxe/nvo.h>
#include <ipxe/device.h>
#include <ipxe/netdevice.h>
#include <ipxe/version.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_hii.h>
#include <ipxe/efi/efi_snp.h>
#include <ipxe/efi/efi_strings.h>
#include <config/general.h>

/** EFI configuration access protocol GUID */
static EFI_GUID efi_hii_config_access_protocol_guid
	= EFI_HII_CONFIG_ACCESS_PROTOCOL_GUID;

/** EFI platform setup formset GUID */
static EFI_GUID efi_hii_platform_setup_formset_guid
	= EFI_HII_PLATFORM_SETUP_FORMSET_GUID;

/** EFI IBM UCM compliant formset GUID */
static EFI_GUID efi_hii_ibm_ucm_compliant_formset_guid
	= EFI_HII_IBM_UCM_COMPLIANT_FORMSET_GUID;

/** EFI HII database protocol */
static EFI_HII_DATABASE_PROTOCOL *efihii;
EFI_REQUEST_PROTOCOL ( EFI_HII_DATABASE_PROTOCOL, &efihii );

/**
 * Identify settings to be exposed via HII
 *
 * @v snpdev		SNP device
 * @ret settings	Settings, or NULL
 */
static struct settings * efi_snp_hii_settings ( struct efi_snp_device *snpdev ){

	return find_child_settings ( netdev_settings ( snpdev->netdev ),
				     NVO_SETTINGS_NAME );
}

/**
 * Check whether or not setting is applicable
 *
 * @v snpdev		SNP device
 * @v setting		Setting
 * @ret applies		Setting applies
 */
static int efi_snp_hii_setting_applies ( struct efi_snp_device *snpdev,
					 struct setting *setting ) {

	return nvo_applies ( efi_snp_hii_settings ( snpdev ), setting );
}

/**
 * Generate a random GUID
 *
 * @v guid		GUID to fill in
 */
static void efi_snp_hii_random_guid ( EFI_GUID *guid ) {
	uint8_t *byte = ( ( uint8_t * ) guid );
	unsigned int i;

	for ( i = 0 ; i < sizeof ( *guid ) ; i++ )
		*(byte++) = random();
}

/**
 * Generate EFI SNP questions
 *
 * @v snpdev		SNP device
 * @v ifr		IFR builder
 * @v varstore_id	Variable store identifier
 */
static void efi_snp_hii_questions ( struct efi_snp_device *snpdev,
				    struct efi_ifr_builder *ifr,
				    unsigned int varstore_id ) {
	struct setting *setting;
	struct setting *previous = NULL;
	unsigned int name_id;
	unsigned int prompt_id;
	unsigned int help_id;
	unsigned int question_id;

	/* Add all applicable settings */
	for_each_table_entry ( setting, SETTINGS ) {
		if ( ! efi_snp_hii_setting_applies ( snpdev, setting ) )
			continue;
		if ( previous && ( setting_cmp ( setting, previous ) == 0 ) )
			continue;
		previous = setting;
		name_id = efi_ifr_string ( ifr, "%s", setting->name );
		prompt_id = efi_ifr_string ( ifr, "%s", setting->description );
		help_id = efi_ifr_string ( ifr, "http://ipxe.org/cfg/%s",
					   setting->name );
		question_id = setting->tag;
		efi_ifr_string_op ( ifr, prompt_id, help_id,
				    question_id, varstore_id, name_id,
				    0, 0x00, 0xff, 0 );
	}
}

/**
 * Build HII package list for SNP device
 *
 * @v snpdev		SNP device
 * @ret package		Package list, or NULL on error
 */
static EFI_HII_PACKAGE_LIST_HEADER *
efi_snp_hii_package_list ( struct efi_snp_device *snpdev ) {
	struct net_device *netdev = snpdev->netdev;
	struct device *dev = netdev->dev;
	struct efi_ifr_builder ifr;
	EFI_HII_PACKAGE_LIST_HEADER *package;
	const char *product_name;
	EFI_GUID package_guid;
	EFI_GUID formset_guid;
	EFI_GUID varstore_guid;
	unsigned int title_id;
	unsigned int varstore_id;

	/* Initialise IFR builder */
	efi_ifr_init ( &ifr );

	/* Determine product name */
	product_name = ( PRODUCT_NAME[0] ? PRODUCT_NAME : PRODUCT_SHORT_NAME );

	/* Generate GUIDs */
	efi_snp_hii_random_guid ( &package_guid );
	efi_snp_hii_random_guid ( &formset_guid );
	efi_snp_hii_random_guid ( &varstore_guid );

	/* Generate title string (used more than once) */
	title_id = efi_ifr_string ( &ifr, "%s (%s)", product_name,
				    netdev_addr ( netdev ) );

	/* Generate opcodes */
	efi_ifr_form_set_op ( &ifr, &formset_guid, title_id,
			      efi_ifr_string ( &ifr,
					       "Configure " PRODUCT_SHORT_NAME),
			      &efi_hii_platform_setup_formset_guid,
			      &efi_hii_ibm_ucm_compliant_formset_guid, NULL );
	efi_ifr_guid_class_op ( &ifr, EFI_NETWORK_DEVICE_CLASS );
	efi_ifr_guid_subclass_op ( &ifr, 0x03 );
	varstore_id = efi_ifr_varstore_name_value_op ( &ifr, &varstore_guid );
	efi_ifr_form_op ( &ifr, title_id );
	efi_ifr_text_op ( &ifr,
			  efi_ifr_string ( &ifr, "Name" ),
			  efi_ifr_string ( &ifr, "Firmware product name" ),
			  efi_ifr_string ( &ifr, "%s", product_name ) );
	efi_ifr_text_op ( &ifr,
			  efi_ifr_string ( &ifr, "Version" ),
			  efi_ifr_string ( &ifr, "Firmware version" ),
			  efi_ifr_string ( &ifr, "%s", product_version ) );
	efi_ifr_text_op ( &ifr,
			  efi_ifr_string ( &ifr, "Driver" ),
			  efi_ifr_string ( &ifr, "Firmware driver" ),
			  efi_ifr_string ( &ifr, "%s", dev->driver_name ) );
	efi_ifr_text_op ( &ifr,
			  efi_ifr_string ( &ifr, "Device" ),
			  efi_ifr_string ( &ifr, "Hardware device" ),
			  efi_ifr_string ( &ifr, "%s", dev->name ) );
	efi_snp_hii_questions ( snpdev, &ifr, varstore_id );
	efi_ifr_end_op ( &ifr );
	efi_ifr_end_op ( &ifr );

	/* Build package */
	package = efi_ifr_package ( &ifr, &package_guid, "en-us",
				    efi_ifr_string ( &ifr, "English" ) );
	if ( ! package ) {
		DBGC ( snpdev, "SNPDEV %p could not build IFR package\n",
		       snpdev );
		efi_ifr_free ( &ifr );
		return NULL;
	}

	/* Free temporary storage */
	efi_ifr_free ( &ifr );
	return package;
}

/**
 * Append response to result string
 *
 * @v snpdev		SNP device
 * @v key		Key
 * @v value		Value
 * @v results		Result string
 * @ret rc		Return status code
 *
 * The result string is allocated dynamically using
 * BootServices::AllocatePool(), and the caller is responsible for
 * eventually calling BootServices::FreePool().
 */
static int efi_snp_hii_append ( struct efi_snp_device *snpdev __unused,
				const char *key, const char *value,
				wchar_t **results ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	size_t len;
	void *new;

	/* Allocate new string */
	len = ( ( *results ? ( wcslen ( *results ) + 1 /* "&" */ ) : 0 ) +
		strlen ( key ) + 1 /* "=" */ + strlen ( value ) + 1 /* NUL */ );
	bs->AllocatePool ( EfiBootServicesData, ( len * sizeof ( wchar_t ) ),
			   &new );
	if ( ! new )
		return -ENOMEM;

	/* Populate string */
	efi_snprintf ( new, len, "%ls%s%s=%s", ( *results ? *results : L"" ),
		       ( *results ? L"&" : L"" ), key, value );
	bs->FreePool ( *results );
	*results = new;

	return 0;
}

/**
 * Fetch HII setting
 *
 * @v snpdev		SNP device
 * @v key		Key
 * @v value		Value
 * @v results		Result string
 * @v have_setting	Flag indicating detection of a setting
 * @ret rc		Return status code
 */
static int efi_snp_hii_fetch ( struct efi_snp_device *snpdev,
			       const char *key, const char *value,
			       wchar_t **results, int *have_setting ) {
	struct settings *settings = efi_snp_hii_settings ( snpdev );
	struct settings *origin;
	struct setting *setting;
	struct setting fetched;
	int len;
	char *buf;
	char *encoded;
	int i;
	int rc;

	/* Handle ConfigHdr components */
	if ( ( strcasecmp ( key, "GUID" ) == 0 ) ||
	     ( strcasecmp ( key, "NAME" ) == 0 ) ||
	     ( strcasecmp ( key, "PATH" ) == 0 ) ) {
		return efi_snp_hii_append ( snpdev, key, value, results );
	}
	if ( have_setting )
		*have_setting = 1;

	/* Do nothing more unless we have a settings block */
	if ( ! settings ) {
		rc = -ENOTSUP;
		goto err_no_settings;
	}

	/* Identify setting */
	setting = find_setting ( key );
	if ( ! setting ) {
		DBGC ( snpdev, "SNPDEV %p no such setting \"%s\"\n",
		       snpdev, key );
		rc = -ENODEV;
		goto err_find_setting;
	}

	/* Encode value */
	if ( setting_exists ( settings, setting ) ) {

		/* Calculate formatted length */
		len = fetchf_setting ( settings, setting, &origin, &fetched,
				       NULL, 0 );
		if ( len < 0 ) {
			rc = len;
			DBGC ( snpdev, "SNPDEV %p could not fetch %s: %s\n",
			       snpdev, setting->name, strerror ( rc ) );
			goto err_fetchf_len;
		}

		/* Allocate buffer for formatted value and HII-encoded value */
		buf = zalloc ( len + 1 /* NUL */ + ( len * 4 ) + 1 /* NUL */ );
		if ( ! buf ) {
			rc = -ENOMEM;
			goto err_alloc;
		}
		encoded = ( buf + len + 1 /* NUL */ );

		/* Format value */
		fetchf_setting ( origin, &fetched, NULL, NULL, buf,
				 ( len + 1 /* NUL */ ) );
		for ( i = 0 ; i < len ; i++ ) {
			sprintf ( ( encoded + ( 4 * i ) ), "%04x",
				  *( ( uint8_t * ) buf + i ) );
		}

	} else {

		/* Non-existent or inapplicable setting */
		buf = NULL;
		encoded = "";
	}

	/* Append results */
	if ( ( rc = efi_snp_hii_append ( snpdev, key, encoded,
					 results ) ) != 0 ) {
		goto err_append;
	}

	/* Success */
	rc = 0;

 err_append:
	free ( buf );
 err_alloc:
 err_fetchf_len:
 err_find_setting:
 err_no_settings:
	return rc;
}

/**
 * Fetch HII setting
 *
 * @v snpdev		SNP device
 * @v key		Key
 * @v value		Value
 * @v results		Result string (unused)
 * @v have_setting	Flag indicating detection of a setting (unused)
 * @ret rc		Return status code
 */
static int efi_snp_hii_store ( struct efi_snp_device *snpdev,
			       const char *key, const char *value,
			       wchar_t **results __unused,
			       int *have_setting __unused ) {
	struct settings *settings = efi_snp_hii_settings ( snpdev );
	struct setting *setting;
	char *buf;
	char tmp[5];
	char *endp;
	int len;
	int i;
	int rc;

	/* Handle ConfigHdr components */
	if ( ( strcasecmp ( key, "GUID" ) == 0 ) ||
	     ( strcasecmp ( key, "NAME" ) == 0 ) ||
	     ( strcasecmp ( key, "PATH" ) == 0 ) ) {
		/* Nothing to do */
		return 0;
	}

	/* Do nothing more unless we have a settings block */
	if ( ! settings ) {
		rc = -ENOTSUP;
		goto err_no_settings;
	}

	/* Identify setting */
	setting = find_setting ( key );
	if ( ! setting ) {
		DBGC ( snpdev, "SNPDEV %p no such setting \"%s\"\n",
		       snpdev, key );
		rc = -ENODEV;
		goto err_find_setting;
	}

	/* Allocate buffer */
	len = ( strlen ( value ) / 4 );
	buf = zalloc ( len + 1 /* NUL */ );
	if ( ! buf ) {
		rc = -ENOMEM;
		goto err_alloc;
	}

	/* Decode value */
	tmp[4] = '\0';
	for ( i = 0 ; i < len ; i++ ) {
		memcpy ( tmp, ( value + ( i * 4 ) ), 4 );
		buf[i] = strtoul ( tmp, &endp, 16 );
		if ( endp != &tmp[4] ) {
			DBGC ( snpdev, "SNPDEV %p invalid character %s\n",
			       snpdev, tmp );
			rc = -EINVAL;
			goto err_inval;
		}
	}

	/* Store value */
	if ( ( rc = storef_setting ( settings, setting, buf ) ) != 0 ) {
		DBGC ( snpdev, "SNPDEV %p could not store \"%s\" into %s: %s\n",
		       snpdev, buf, setting->name, strerror ( rc ) );
		goto err_storef;
	}

	/* Success */
	rc = 0;

 err_storef:
 err_inval:
	free ( buf );
 err_alloc:
 err_find_setting:
 err_no_settings:
	return rc;
}

/**
 * Process portion of HII configuration string
 *
 * @v snpdev		SNP device
 * @v string		HII configuration string
 * @v progress		Progress through HII configuration string
 * @v results		Results string
 * @v have_setting	Flag indicating detection of a setting (unused)
 * @v process		Function used to process key=value pairs
 * @ret rc		Return status code
 */
static int efi_snp_hii_process ( struct efi_snp_device *snpdev,
				 wchar_t *string, wchar_t **progress,
				 wchar_t **results, int *have_setting,
				 int ( * process ) ( struct efi_snp_device *,
						     const char *key,
						     const char *value,
						     wchar_t **results,
						     int *have_setting ) ) {
	wchar_t *wkey = string;
	wchar_t *wend = string;
	wchar_t *wvalue = NULL;
	size_t key_len;
	size_t value_len;
	void *temp;
	char *key;
	char *value;
	int rc;

	/* Locate key, value (if any), and end */
	while ( *wend ) {
		if ( *wend == L'&' )
			break;
		if ( *(wend++) == L'=' )
			wvalue = wend;
	}

	/* Allocate memory for key and value */
	key_len = ( ( wvalue ? ( wvalue - 1 ) : wend ) - wkey );
	value_len = ( wvalue ? ( wend - wvalue ) : 0 );
	temp = zalloc ( key_len + 1 /* NUL */ + value_len + 1 /* NUL */ );
	if ( ! temp )
		return -ENOMEM;
	key = temp;
	value = ( temp + key_len + 1 /* NUL */ );

	/* Copy key and value */
	while ( key_len-- )
		key[key_len] = wkey[key_len];
	while ( value_len-- )
		value[value_len] = wvalue[value_len];

	/* Process key and value */
	if ( ( rc = process ( snpdev, key, value, results,
			      have_setting ) ) != 0 ) {
		goto err;
	}

	/* Update progress marker */
	*progress = wend;

 err:
	/* Free temporary storage */
	free ( temp );

	return rc;
}

/**
 * Fetch configuration
 *
 * @v hii		HII configuration access protocol
 * @v request		Configuration to fetch
 * @ret progress	Progress made through configuration to fetch
 * @ret results		Query results
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_snp_hii_extract_config ( const EFI_HII_CONFIG_ACCESS_PROTOCOL *hii,
			     EFI_STRING request, EFI_STRING *progress,
			     EFI_STRING *results ) {
	struct efi_snp_device *snpdev =
		container_of ( hii, struct efi_snp_device, hii );
	int have_setting = 0;
	wchar_t *pos;
	int rc;

	DBGC ( snpdev, "SNPDEV %p ExtractConfig request \"%ls\"\n",
	       snpdev, request );

	/* Initialise results */
	*results = NULL;

	/* Process all request fragments */
	for ( pos = *progress = request ; *progress && **progress ;
	      pos = *progress + 1 ) {
		if ( ( rc = efi_snp_hii_process ( snpdev, pos, progress,
						  results, &have_setting,
						  efi_snp_hii_fetch ) ) != 0 ) {
			return EFIRC ( rc );
		}
	}

	/* If we have no explicit request, return all settings */
	if ( ! have_setting ) {
		struct setting *setting;

		for_each_table_entry ( setting, SETTINGS ) {
			if ( ! efi_snp_hii_setting_applies ( snpdev, setting ) )
				continue;
			if ( ( rc = efi_snp_hii_fetch ( snpdev, setting->name,
							NULL, results,
							NULL ) ) != 0 ) {
				return EFIRC ( rc );
			}
		}
	}

	DBGC ( snpdev, "SNPDEV %p ExtractConfig results \"%ls\"\n",
	       snpdev, *results );
	return 0;
}

/**
 * Store configuration
 *
 * @v hii		HII configuration access protocol
 * @v config		Configuration to store
 * @ret progress	Progress made through configuration to store
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_snp_hii_route_config ( const EFI_HII_CONFIG_ACCESS_PROTOCOL *hii,
			   EFI_STRING config, EFI_STRING *progress ) {
	struct efi_snp_device *snpdev =
		container_of ( hii, struct efi_snp_device, hii );
	wchar_t *pos;
	int rc;

	DBGC ( snpdev, "SNPDEV %p RouteConfig \"%ls\"\n", snpdev, config );

	/* Process all request fragments */
	for ( pos = *progress = config ; *progress && **progress ;
	      pos = *progress + 1 ) {
		if ( ( rc = efi_snp_hii_process ( snpdev, pos, progress,
						  NULL, NULL,
						  efi_snp_hii_store ) ) != 0 ) {
			return EFIRC ( rc );
		}
	}

	return 0;
}

/**
 * Handle form actions
 *
 * @v hii		HII configuration access protocol
 * @v action		Form browser action
 * @v question_id	Question ID
 * @v type		Type of value
 * @v value		Value
 * @ret action_request	Action requested by driver
 * @ret efirc		EFI status code
 */
static EFI_STATUS EFIAPI
efi_snp_hii_callback ( const EFI_HII_CONFIG_ACCESS_PROTOCOL *hii,
		       EFI_BROWSER_ACTION action __unused,
		       EFI_QUESTION_ID question_id __unused,
		       UINT8 type __unused, EFI_IFR_TYPE_VALUE *value __unused,
		       EFI_BROWSER_ACTION_REQUEST *action_request __unused ) {
	struct efi_snp_device *snpdev =
		container_of ( hii, struct efi_snp_device, hii );

	DBGC ( snpdev, "SNPDEV %p Callback\n", snpdev );
	return EFI_UNSUPPORTED;
}

/** HII configuration access protocol */
static EFI_HII_CONFIG_ACCESS_PROTOCOL efi_snp_device_hii = {
	.ExtractConfig	= efi_snp_hii_extract_config,
	.RouteConfig	= efi_snp_hii_route_config,
	.Callback	= efi_snp_hii_callback,
};

/**
 * Install HII protocol and packages for SNP device
 *
 * @v snpdev		SNP device
 * @ret rc		Return status code
 */
int efi_snp_hii_install ( struct efi_snp_device *snpdev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	int efirc;
	int rc;

	/* Do nothing if HII database protocol is not supported */
	if ( ! efihii )
		return 0;

	/* Initialise HII protocol */
	memcpy ( &snpdev->hii, &efi_snp_device_hii, sizeof ( snpdev->hii ) );

	/* Create HII package list */
	snpdev->package_list = efi_snp_hii_package_list ( snpdev );
	if ( ! snpdev->package_list ) {
		DBGC ( snpdev, "SNPDEV %p could not create HII package list\n",
		       snpdev );
		rc = -ENOMEM;
		goto err_build_package_list;
	}

	/* Add HII packages */
	if ( ( efirc = efihii->NewPackageList ( efihii, snpdev->package_list,
						snpdev->handle,
						&snpdev->hii_handle ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snpdev, "SNPDEV %p could not add HII packages: %s\n",
		       snpdev, strerror ( rc ) );
		goto err_new_package_list;
	}

	/* Install HII protocol */
	if ( ( efirc = bs->InstallMultipleProtocolInterfaces (
			 &snpdev->handle,
			 &efi_hii_config_access_protocol_guid, &snpdev->hii,
			 NULL ) ) != 0 ) {
		rc = -EEFI ( efirc );
		DBGC ( snpdev, "SNPDEV %p could not install HII protocol: %s\n",
		       snpdev, strerror ( rc ) );
		goto err_install_protocol;
	}

	return 0;

	bs->UninstallMultipleProtocolInterfaces (
			snpdev->handle,
			&efi_hii_config_access_protocol_guid, &snpdev->hii,
			NULL );
 err_install_protocol:
	efihii->RemovePackageList ( efihii, snpdev->hii_handle );
 err_new_package_list:
	free ( snpdev->package_list );
	snpdev->package_list = NULL;
 err_build_package_list:
	return rc;
}

/**
 * Uninstall HII protocol and package for SNP device
 *
 * @v snpdev		SNP device
 */
void efi_snp_hii_uninstall ( struct efi_snp_device *snpdev ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;

	/* Do nothing if HII database protocol is not supported */
	if ( ! efihii )
		return;

	/* Uninstall protocols and remove package list */
	bs->UninstallMultipleProtocolInterfaces (
			snpdev->handle,
			&efi_hii_config_access_protocol_guid, &snpdev->hii,
			NULL );
	efihii->RemovePackageList ( efihii, snpdev->hii_handle );
	free ( snpdev->package_list );
	snpdev->package_list = NULL;
}
