/*
 * 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.
 *
 * 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 <stdlib.h>
#include <stddef.h>
#include <stdarg.h>
#include <string.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_strings.h>
#include <ipxe/efi/efi_hii.h>

/** Tiano GUID */
static const EFI_GUID tiano_guid = EFI_IFR_TIANO_GUID;

/**
 * Add string to IFR builder
 *
 * @v ifr		IFR builder
 * @v fmt		Format string
 * @v ...		Arguments
 * @ret string_id	String identifier, or zero on failure
 */
unsigned int efi_ifr_string ( struct efi_ifr_builder *ifr, const char *fmt,
			      ... ) {
	EFI_HII_STRING_BLOCK *new_strings;
	EFI_HII_SIBT_STRING_UCS2_BLOCK *ucs2;
	size_t new_strings_len;
	va_list args;
	size_t len;
	unsigned int string_id;

	/* Do nothing if a previous allocation has failed */
	if ( ifr->failed )
		return 0;

	/* Calculate string length */
	va_start ( args, fmt );
	len = ( efi_vsnprintf ( NULL, 0, fmt, args ) + 1 /* wNUL */ );
	va_end ( args );

	/* Reallocate strings */
	new_strings_len = ( ifr->strings_len +
			    offsetof ( typeof ( *ucs2 ), StringText ) +
			    ( len * sizeof ( ucs2->StringText[0] ) ) );
	new_strings = realloc ( ifr->strings, new_strings_len );
	if ( ! new_strings ) {
		ifr->failed = 1;
		return 0;
	}
	ucs2 = ( ( ( void * ) new_strings ) + ifr->strings_len );
	ifr->strings = new_strings;
	ifr->strings_len = new_strings_len;

	/* Fill in string */
	ucs2->Header.BlockType = EFI_HII_SIBT_STRING_UCS2;
	va_start ( args, fmt );
	efi_vsnprintf ( ucs2->StringText, len, fmt, args );
	va_end ( args );

	/* Allocate string ID */
	string_id = ++(ifr->string_id);

	DBGC ( ifr, "IFR %p string %#04x is \"%ls\"\n",
	       ifr, string_id, ucs2->StringText );
	return string_id;
}

/**
 * Add IFR opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v opcode		Opcode
 * @v len		Opcode length
 * @ret op		Opcode, or NULL
 */
static void * efi_ifr_op ( struct efi_ifr_builder *ifr, unsigned int opcode,
			   size_t len ) {
	EFI_IFR_OP_HEADER *new_ops;
	EFI_IFR_OP_HEADER *op;
	size_t new_ops_len;

	/* Do nothing if a previous allocation has failed */
	if ( ifr->failed )
		return NULL;

	/* Reallocate opcodes */
	new_ops_len = ( ifr->ops_len + len );
	new_ops = realloc ( ifr->ops, new_ops_len );
	if ( ! new_ops ) {
		ifr->failed = 1;
		return NULL;
	}
	op = ( ( ( void * ) new_ops ) + ifr->ops_len );
	ifr->ops = new_ops;
	ifr->ops_len = new_ops_len;

	/* Fill in opcode header */
	memset ( op, 0, len );
	op->OpCode = opcode;
	op->Length = len;

	return op;
}

/**
 * Add end opcode to IFR builder
 *
 * @v ifr		IFR builder
 */
void efi_ifr_end_op ( struct efi_ifr_builder *ifr ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_END *end;

	/* Add opcode */
	end = efi_ifr_op ( ifr, EFI_IFR_END_OP, sizeof ( *end ) );

	DBGC ( ifr, "IFR %p end\n", ifr );
	DBGC2_HDA ( ifr, dispaddr, end, sizeof ( *end ) );
}

/**
 * Add false opcode to IFR builder
 *
 * @v ifr		IFR builder
 */
void efi_ifr_false_op ( struct efi_ifr_builder *ifr ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_FALSE *false;

	/* Add opcode */
	false = efi_ifr_op ( ifr, EFI_IFR_FALSE_OP, sizeof ( *false ) );

	DBGC ( ifr, "IFR %p false\n", ifr );
	DBGC2_HDA ( ifr, dispaddr, false, sizeof ( *false ) );
}

/**
 * Add form opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v title_id		Title string identifier
 * @ret form_id		Form identifier
 */
unsigned int efi_ifr_form_op ( struct efi_ifr_builder *ifr,
			       unsigned int title_id ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_FORM *form;

	/* Add opcode */
	form = efi_ifr_op ( ifr, EFI_IFR_FORM_OP, sizeof ( *form ) );
	if ( ! form )
		return 0;
	form->Header.Scope = 1;
	form->FormId = ++(ifr->form_id);
	form->FormTitle = title_id;

	DBGC ( ifr, "IFR %p name/value store %#04x title %#04x\n",
	       ifr, form->FormId, title_id );
	DBGC2_HDA ( ifr, dispaddr, form, sizeof ( *form ) );
	return form->FormId;
}

/**
 * Add formset opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v guid		GUID
 * @v title_id		Title string identifier
 * @v help_id		Help string identifier
 * @v ...		Class GUIDs (terminated by NULL)
 */
void efi_ifr_form_set_op ( struct efi_ifr_builder *ifr, const EFI_GUID *guid,
			   unsigned int title_id, unsigned int help_id, ... ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_FORM_SET *formset;
	EFI_GUID *class_guid;
	unsigned int num_class_guids = 0;
	size_t len;
	va_list args;

	/* Count number of class GUIDs */
	va_start ( args, help_id );
	while ( va_arg ( args, const EFI_GUID * ) != NULL )
		num_class_guids++;
	va_end ( args );

	/* Add opcode */
	len = ( sizeof ( *formset ) +
		( num_class_guids * sizeof ( *class_guid ) ) );
	formset = efi_ifr_op ( ifr, EFI_IFR_FORM_SET_OP, len );
	if ( ! formset )
		return;
	formset->Header.Scope = 1;
	memcpy ( &formset->Guid, guid, sizeof ( formset->Guid ) );
	formset->FormSetTitle = title_id;
	formset->Help = help_id;
	formset->Flags = num_class_guids;

	/* Add class GUIDs */
	class_guid = ( ( ( void * ) formset ) + sizeof ( *formset ) );
	va_start ( args, help_id );
	while ( num_class_guids-- ) {
		memcpy ( class_guid++, va_arg ( args, const EFI_GUID * ),
			 sizeof ( *class_guid ) );
	}
	va_end ( args );

	DBGC ( ifr, "IFR %p formset title %#04x help %#04x\n",
	       ifr, title_id, help_id );
	DBGC2_HDA ( ifr, dispaddr, formset, len );
}

/**
 * Add get opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v varstore_id	Variable store identifier
 * @v varstore_info	Variable string identifier or offset
 * @v varstore_type	Variable type
 */
void efi_ifr_get_op ( struct efi_ifr_builder *ifr, unsigned int varstore_id,
		      unsigned int varstore_info, unsigned int varstore_type ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_GET *get;

	/* Add opcode */
	get = efi_ifr_op ( ifr, EFI_IFR_GET_OP, sizeof ( *get ) );
	get->VarStoreId = varstore_id;
	get->VarStoreInfo.VarName = varstore_info;
	get->VarStoreType = varstore_type;

	DBGC ( ifr, "IFR %p get varstore %#04x:%#04x type %#02x\n",
	       ifr, varstore_id, varstore_info, varstore_type );
	DBGC2_HDA ( ifr, dispaddr, get, sizeof ( *get ) );
}

/**
 * Add GUID class opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v class		Class
 */
void efi_ifr_guid_class_op ( struct efi_ifr_builder *ifr, unsigned int class ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_GUID_CLASS *guid_class;

	/* Add opcode */
	guid_class = efi_ifr_op ( ifr, EFI_IFR_GUID_OP,
				  sizeof ( *guid_class ) );
	if ( ! guid_class )
		return;
	memcpy ( &guid_class->Guid, &tiano_guid, sizeof ( guid_class->Guid ) );
	guid_class->ExtendOpCode = EFI_IFR_EXTEND_OP_CLASS;
	guid_class->Class = class;

	DBGC ( ifr, "IFR %p GUID class %#02x\n", ifr, class );
	DBGC2_HDA ( ifr, dispaddr, guid_class, sizeof ( *guid_class ) );
}

/**
 * Add GUID subclass opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v subclass		Subclass
 */
void efi_ifr_guid_subclass_op ( struct efi_ifr_builder *ifr,
				unsigned int subclass ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_GUID_SUBCLASS *guid_subclass;

	/* Add opcode */
	guid_subclass = efi_ifr_op ( ifr, EFI_IFR_GUID_OP,
				     sizeof ( *guid_subclass ) );
	if ( ! guid_subclass )
		return;
	memcpy ( &guid_subclass->Guid, &tiano_guid,
		 sizeof ( guid_subclass->Guid ) );
	guid_subclass->ExtendOpCode = EFI_IFR_EXTEND_OP_SUBCLASS;
	guid_subclass->SubClass = subclass;

	DBGC ( ifr, "IFR %p GUID subclass %#02x\n", ifr, subclass );
	DBGC2_HDA ( ifr, dispaddr, guid_subclass, sizeof ( *guid_subclass ) );
}

/**
 * Add numeric opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v prompt_id		Prompt string identifier
 * @v help_id		Help string identifier
 * @v question_id	Question identifier
 * @v varstore_id	Variable store identifier
 * @v varstore_info	Variable string identifier or offset
 * @v vflags		Variable flags
 * @v min_value		Minimum value
 * @v max_value		Maximum value
 * @v step		Step
 * @v flags		Flags
 */
void efi_ifr_numeric_op ( struct efi_ifr_builder *ifr, unsigned int prompt_id,
			  unsigned int help_id, unsigned int question_id,
			  unsigned int varstore_id, unsigned int varstore_info,
			  unsigned int vflags, unsigned long min_value,
			  unsigned long max_value, unsigned int step,
			  unsigned int flags ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_NUMERIC *numeric;
	unsigned int size;

	/* Add opcode */
	numeric = efi_ifr_op ( ifr, EFI_IFR_NUMERIC_OP, sizeof ( *numeric ) );
	if ( ! numeric )
		return;
	numeric->Question.Header.Prompt = prompt_id;
	numeric->Question.Header.Help = help_id;
	numeric->Question.QuestionId = question_id;
	numeric->Question.VarStoreId = varstore_id;
	numeric->Question.VarStoreInfo.VarName = varstore_info;
	numeric->Question.Flags = vflags;
	size = ( flags & EFI_IFR_NUMERIC_SIZE );
	switch ( size ) {
	case EFI_IFR_NUMERIC_SIZE_1 :
		numeric->data.u8.MinValue = min_value;
		numeric->data.u8.MaxValue = max_value;
		numeric->data.u8.Step = step;
		break;
	case EFI_IFR_NUMERIC_SIZE_2 :
		numeric->data.u16.MinValue = min_value;
		numeric->data.u16.MaxValue = max_value;
		numeric->data.u16.Step = step;
		break;
	case EFI_IFR_NUMERIC_SIZE_4 :
		numeric->data.u32.MinValue = min_value;
		numeric->data.u32.MaxValue = max_value;
		numeric->data.u32.Step = step;
		break;
	case EFI_IFR_NUMERIC_SIZE_8 :
		numeric->data.u64.MinValue = min_value;
		numeric->data.u64.MaxValue = max_value;
		numeric->data.u64.Step = step;
		break;
	}

	DBGC ( ifr, "IFR %p numeric prompt %#04x help %#04x question %#04x "
	       "varstore %#04x:%#04x\n", ifr, prompt_id, help_id, question_id,
	       varstore_id, varstore_info );
	DBGC2_HDA ( ifr, dispaddr, numeric, sizeof ( *numeric ) );
}

/**
 * Add string opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v prompt_id		Prompt string identifier
 * @v help_id		Help string identifier
 * @v question_id	Question identifier
 * @v varstore_id	Variable store identifier
 * @v varstore_info	Variable string identifier or offset
 * @v vflags		Variable flags
 * @v min_size		Minimum size
 * @v max_size		Maximum size
 * @v flags		Flags
 */
void efi_ifr_string_op ( struct efi_ifr_builder *ifr, unsigned int prompt_id,
			 unsigned int help_id, unsigned int question_id,
			 unsigned int varstore_id, unsigned int varstore_info,
			 unsigned int vflags, unsigned int min_size,
			 unsigned int max_size, unsigned int flags ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_STRING *string;

	/* Add opcode */
	string = efi_ifr_op ( ifr, EFI_IFR_STRING_OP, sizeof ( *string ) );
	if ( ! string )
		return;
	string->Question.Header.Prompt = prompt_id;
	string->Question.Header.Help = help_id;
	string->Question.QuestionId = question_id;
	string->Question.VarStoreId = varstore_id;
	string->Question.VarStoreInfo.VarName = varstore_info;
	string->Question.Flags = vflags;
	string->MinSize = min_size;
	string->MaxSize = max_size;
	string->Flags = flags;

	DBGC ( ifr, "IFR %p string prompt %#04x help %#04x question %#04x "
	       "varstore %#04x:%#04x\n", ifr, prompt_id, help_id, question_id,
	       varstore_id, varstore_info );
	DBGC2_HDA ( ifr, dispaddr, string, sizeof ( *string ) );
}

/**
 * Add suppress-if opcode to IFR builder
 *
 * @v ifr		IFR builder
 */
void efi_ifr_suppress_if_op ( struct efi_ifr_builder *ifr ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_SUPPRESS_IF *suppress_if;

	/* Add opcode */
	suppress_if = efi_ifr_op ( ifr, EFI_IFR_SUPPRESS_IF_OP,
				   sizeof ( *suppress_if ) );
	suppress_if->Header.Scope = 1;

	DBGC ( ifr, "IFR %p suppress-if\n", ifr );
	DBGC2_HDA ( ifr, dispaddr, suppress_if, sizeof ( *suppress_if ) );
}

/**
 * Add text opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v prompt_id		Prompt string identifier
 * @v help_id		Help string identifier
 * @v text_id		Text string identifier
 */
void efi_ifr_text_op ( struct efi_ifr_builder *ifr, unsigned int prompt_id,
		       unsigned int help_id, unsigned int text_id ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_TEXT *text;

	/* Add opcode */
	text = efi_ifr_op ( ifr, EFI_IFR_TEXT_OP, sizeof ( *text ) );
	if ( ! text )
		return;
	text->Statement.Prompt = prompt_id;
	text->Statement.Help = help_id;
	text->TextTwo = text_id;

	DBGC ( ifr, "IFR %p text prompt %#04x help %#04x text %#04x\n",
	       ifr, prompt_id, help_id, text_id );
	DBGC2_HDA ( ifr, dispaddr, text, sizeof ( *text ) );
}

/**
 * Add true opcode to IFR builder
 *
 * @v ifr		IFR builder
 */
void efi_ifr_true_op ( struct efi_ifr_builder *ifr ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_TRUE *true;

	/* Add opcode */
	true = efi_ifr_op ( ifr, EFI_IFR_TRUE_OP, sizeof ( *true ) );

	DBGC ( ifr, "IFR %p true\n", ifr );
	DBGC2_HDA ( ifr, dispaddr, true, sizeof ( *true ) );
}

/**
 * Add name/value store opcode to IFR builder
 *
 * @v ifr		IFR builder
 * @v guid		GUID
 * @ret varstore_id	Variable store identifier, or 0 on failure
 */
unsigned int efi_ifr_varstore_name_value_op ( struct efi_ifr_builder *ifr,
					      const EFI_GUID *guid ) {
	size_t dispaddr = ifr->ops_len;
	EFI_IFR_VARSTORE_NAME_VALUE *varstore;

	/* Add opcode */
	varstore = efi_ifr_op ( ifr, EFI_IFR_VARSTORE_NAME_VALUE_OP,
				sizeof ( *varstore ) );
	if ( ! varstore )
		return 0;
	varstore->VarStoreId = ++(ifr->varstore_id);
	memcpy ( &varstore->Guid, guid, sizeof ( varstore->Guid ) );

	DBGC ( ifr, "IFR %p name/value store %#04x\n",
	       ifr, varstore->VarStoreId );
	DBGC2_HDA ( ifr, dispaddr, varstore, sizeof ( *varstore ) );
	return varstore->VarStoreId;
}

/**
 * Free memory used by IFR builder
 *
 * @v ifr		IFR builder
 */
void efi_ifr_free ( struct efi_ifr_builder *ifr ) {

	free ( ifr->ops );
	free ( ifr->strings );
	memset ( ifr, 0, sizeof ( *ifr ) );
}

/**
 * Construct package list from IFR builder
 *
 * @v ifr		IFR builder
 * @v guid		Package GUID
 * @v language		Language
 * @v language_id	Language string ID
 * @ret package		Package list, or NULL
 *
 * The package list is allocated using malloc(), and must eventually
 * be freed by the caller.  (The caller must also call efi_ifr_free()
 * to free the temporary storage used during construction.)
 */
EFI_HII_PACKAGE_LIST_HEADER * efi_ifr_package ( struct efi_ifr_builder *ifr,
						const EFI_GUID *guid,
						const char *language,
						unsigned int language_id ) {
	struct {
		EFI_HII_PACKAGE_LIST_HEADER header;
		struct {
			EFI_HII_PACKAGE_HEADER header;
			uint8_t data[ifr->ops_len];
		} __attribute__ (( packed )) ops;
		struct {
			union {
				EFI_HII_STRING_PACKAGE_HDR header;
				uint8_t pad[offsetof(EFI_HII_STRING_PACKAGE_HDR,
						     Language) +
					    strlen ( language ) + 1 /* NUL */ ];
			} __attribute__ (( packed )) header;
			uint8_t data[ifr->strings_len];
			EFI_HII_STRING_BLOCK end;
		} __attribute__ (( packed )) strings;
		EFI_HII_PACKAGE_HEADER end;
	} __attribute__ (( packed )) *package;

	/* Fail if any previous allocation failed */
	if ( ifr->failed )
		return NULL;

	/* Allocate package list */
	package = zalloc ( sizeof ( *package ) );
	if ( ! package )
		return NULL;

	/* Populate package list */
	package->header.PackageLength = sizeof ( *package );
	memcpy ( &package->header.PackageListGuid, guid,
		 sizeof ( package->header.PackageListGuid ) );
	package->ops.header.Length = sizeof ( package->ops );
	package->ops.header.Type = EFI_HII_PACKAGE_FORMS;
	memcpy ( package->ops.data, ifr->ops, sizeof ( package->ops.data ) );
	package->strings.header.header.Header.Length =
		sizeof ( package->strings );
	package->strings.header.header.Header.Type =
		EFI_HII_PACKAGE_STRINGS;
	package->strings.header.header.HdrSize =
		sizeof ( package->strings.header );
	package->strings.header.header.StringInfoOffset =
		sizeof ( package->strings.header );
	package->strings.header.header.LanguageName = language_id;
	strcpy ( package->strings.header.header.Language, language );
	memcpy ( package->strings.data, ifr->strings,
		 sizeof ( package->strings.data ) );
	package->strings.end.BlockType = EFI_HII_SIBT_END;
	package->end.Type = EFI_HII_PACKAGE_END;
	package->end.Length = sizeof ( package->end );

	return &package->header;
}

