/*
 * Copyright 2012 <James.Bottomley@HansenPartnership.com>
 *
 * see COPYING file
 */

#include <efi.h>
#include <efilib.h>

#include <console.h>
#include <simple_file.h>
#include <efiauthenticated.h>
#include <execute.h>		/* for generate_path() */

static EFI_GUID IMAGE_PROTOCOL = LOADED_IMAGE_PROTOCOL;
static EFI_GUID SIMPLE_FS_PROTOCOL = SIMPLE_FILE_SYSTEM_PROTOCOL;
static EFI_GUID FILE_INFO = EFI_FILE_INFO_ID;
static EFI_GUID FS_INFO = EFI_FILE_SYSTEM_INFO_ID;

EFI_STATUS
simple_file_open_by_handle(EFI_HANDLE device, CHAR16 *name, EFI_FILE **file, UINT64 mode)
{
	EFI_STATUS efi_status;
	EFI_FILE_IO_INTERFACE *drive;
	EFI_FILE *root;

	efi_status = uefi_call_wrapper(BS->HandleProtocol, 3, device,
				       &SIMPLE_FS_PROTOCOL, (void **)&drive);

	if (efi_status != EFI_SUCCESS) {
		Print(L"Unable to find simple file protocol (%d)\n", efi_status);
		goto error;
	}

	efi_status = uefi_call_wrapper(drive->OpenVolume, 2, drive, &root);

	if (efi_status != EFI_SUCCESS) {
		Print(L"Failed to open drive volume (%d)\n", efi_status);
		goto error;
	}

	efi_status = uefi_call_wrapper(root->Open, 5, root, file, name,
				       mode, 0);

 error:
	return efi_status;
}

EFI_STATUS
simple_file_open(EFI_HANDLE image, CHAR16 *name, EFI_FILE **file, UINT64 mode)
{
	EFI_STATUS efi_status;
	EFI_HANDLE device;
	EFI_LOADED_IMAGE *li;
	EFI_DEVICE_PATH *loadpath = NULL;
	CHAR16 *PathName = NULL;

	efi_status = uefi_call_wrapper(BS->HandleProtocol, 3, image,
				       &IMAGE_PROTOCOL, (void **)&li);

	if (efi_status != EFI_SUCCESS)
		return simple_file_open_by_handle(image, name, file, mode);

	efi_status = generate_path(name, li, &loadpath, &PathName);

	if (efi_status != EFI_SUCCESS) {
		Print(L"Unable to generate load path for %s\n", name);
		return efi_status;
	}

	device = li->DeviceHandle;

	efi_status = simple_file_open_by_handle(device, PathName, file, mode);

	FreePool(PathName);
	FreePool(loadpath);

	return efi_status;
}

EFI_STATUS
simple_dir_read_all_by_handle(EFI_HANDLE image, EFI_FILE *file, CHAR16* name, EFI_FILE_INFO **entries,
		    int *count)
{
	EFI_STATUS status;
	char buf[4096];
	UINTN size = sizeof(buf);
	EFI_FILE_INFO *fi = (void *)buf;
	
	status = uefi_call_wrapper(file->GetInfo, 4, file, &FILE_INFO,
				   &size, fi);
	if (status != EFI_SUCCESS) {
		Print(L"Failed to get file info\n");
		goto out;
	}
	if ((fi->Attribute & EFI_FILE_DIRECTORY) == 0) {
		Print(L"Not a directory %s\n", name);
		status = EFI_INVALID_PARAMETER;
		goto out;
	}
	size = 0;
	*count = 0;
	for (;;) {
		UINTN len = sizeof(buf);
		status = uefi_call_wrapper(file->Read, 3, file, &len, buf);
		if (status != EFI_SUCCESS || len == 0)
			break;
		(*count)++;
		size += len;
	}
	uefi_call_wrapper(file->SetPosition, 2, file, 0);

	char *ptr = AllocatePool(size);
	*entries = (EFI_FILE_INFO *)ptr;
	if (!*entries)
		return EFI_OUT_OF_RESOURCES;
	int i;
	for (i = 0; i < *count; i++) {
		UINTN len = size;
		uefi_call_wrapper(file->Read, 3, file, &len, ptr);
		ptr += len;
		size -= len;
	}
	status = EFI_SUCCESS;
 out:
	simple_file_close(file);
	if (status != EFI_SUCCESS && *entries) {
		FreePool(*entries);
		*entries = NULL;
	}
	return status;
}

EFI_STATUS
simple_dir_read_all(EFI_HANDLE image, CHAR16 *name, EFI_FILE_INFO **entries,
		    int *count)
{
	EFI_FILE *file;
	EFI_STATUS status;

	status = simple_file_open(image, name, &file, EFI_FILE_MODE_READ);
	if (status != EFI_SUCCESS) {
		Print(L"failed to open file %s: %d\n", name, status);
		return status;
	}

	return simple_dir_read_all_by_handle(image, file, name, entries, count);
}

EFI_STATUS
simple_file_read_all(EFI_FILE *file, UINTN *size, void **buffer)
{
	EFI_STATUS efi_status;
	EFI_FILE_INFO *fi;
	char buf[1024];

	*size = sizeof(buf);
	fi = (void *)buf;
	

	efi_status = uefi_call_wrapper(file->GetInfo, 4, file, &FILE_INFO,
				       size, fi);
	if (efi_status != EFI_SUCCESS) {
		Print(L"Failed to get file info\n");
		return efi_status;
	}

	*size = fi->FileSize;

	*buffer = AllocatePool(*size);
	if (!*buffer) {
		Print(L"Failed to allocate buffer of size %d\n", *size);
		return EFI_OUT_OF_RESOURCES;
	}
	efi_status = uefi_call_wrapper(file->Read, 3, file, size, *buffer);

	return efi_status;
}


EFI_STATUS
simple_file_write_all(EFI_FILE *file, UINTN size, void *buffer)
{
	EFI_STATUS efi_status;

	efi_status = uefi_call_wrapper(file->Write, 3, file, &size, buffer);

	return efi_status;
}

void
simple_file_close(EFI_FILE *file)
{
	uefi_call_wrapper(file->Close, 1, file);
}

EFI_STATUS
simple_volume_selector(CHAR16 **title, CHAR16 **selected, EFI_HANDLE *h)
{
	UINTN count, i;
	EFI_HANDLE *vol_handles = NULL;
	EFI_STATUS status;
	CHAR16 **entries;
	int val;

	uefi_call_wrapper(BS->LocateHandleBuffer, 5, ByProtocol,
			  &SIMPLE_FS_PROTOCOL, NULL, &count, &vol_handles);

	if (!count || !vol_handles)
		return EFI_NOT_FOUND;

	entries = AllocatePool(sizeof(CHAR16 *) * (count+1));
	if (!entries)
		return EFI_OUT_OF_RESOURCES;

	for (i = 0; i < count; i++) {
		char buf[4096];
		UINTN size = sizeof(buf);
		EFI_FILE_SYSTEM_INFO *fi = (void *)buf;
		EFI_FILE *root;
		CHAR16 *name;
		EFI_FILE_IO_INTERFACE *drive;

		status = uefi_call_wrapper(BS->HandleProtocol, 3,
					   vol_handles[i],
					   &SIMPLE_FS_PROTOCOL,
					   (void **)&drive);
		if (status != EFI_SUCCESS || !drive)
			continue;

		status = uefi_call_wrapper(drive->OpenVolume, 2, drive, &root);
		if (status != EFI_SUCCESS)
			continue;

		status = uefi_call_wrapper(root->GetInfo, 4, root, &FS_INFO,
					   &size, fi);
		if (status != EFI_SUCCESS)
			continue;

		name = fi->VolumeLabel;
		
		if (!name || StrLen(name) == 0 || StrCmp(name, L" ") == 0) 
			name = DevicePathToStr(DevicePathFromHandle(vol_handles[i]));

		entries[i] = AllocatePool((StrLen(name) + 2) * sizeof(CHAR16));
		if (!entries[i])
			break;
		StrCpy(entries[i], name);
	}
	entries[i] = NULL;

	val = console_select(title, entries, 0);

	if (val >= 0) {
		*selected = AllocatePool((StrLen(entries[val]) + 1) * sizeof(CHAR16));
		if (*selected) {
			StrCpy(*selected , entries[val]);
		}
		*h = vol_handles[val];
	} else {
		*selected = NULL;
		*h = 0;
	}

	for (i = 0; i < count; i++) {
		if (entries[i])
			FreePool(entries[i]);
	}
	FreePool(entries);
	FreePool(vol_handles);

	
	return EFI_SUCCESS;
}

EFI_STATUS
simple_dir_filter(EFI_HANDLE image, CHAR16 *name, CHAR16 *filter,
		  CHAR16 ***result, int *count, EFI_FILE_INFO **entries)
{
	EFI_STATUS status;
	int tot, offs = StrLen(filter), i, c, filtercount = 1;
	EFI_FILE_INFO *next;
	void *ptr;
	CHAR16 *newfilter = AllocatePool((StrLen(filter) + 1) * sizeof(CHAR16)),
		**filterarr;

	if (!newfilter)
		return EFI_OUT_OF_RESOURCES;

	/* just in case efi ever stops writeable strings */
	StrCpy(newfilter, filter);

	for (i = 0; i < offs; i++) {
		if (filter[i] == '|')
			filtercount++;
	}
	filterarr = AllocatePool(filtercount * sizeof(void *));
	if (!filterarr)
		return EFI_OUT_OF_RESOURCES;
	c = 0;
	filterarr[c++] = newfilter;
	for (i = 0; i < offs; i++) {
		if (filter[i] == '|') {
			newfilter[i] = '\0';
			filterarr[c++] = &newfilter[i+1];
		}
	}

	*count = 0;

	status = simple_dir_read_all(image, name, entries, &tot);
		
	if (status != EFI_SUCCESS)
		goto out;
	ptr = next = *entries;

	for (i = 0; i < tot; i++) {
		int len = StrLen(next->FileName);

		for (c = 0; c < filtercount; c++) {
			offs = StrLen(filterarr[c]);

			if (StrCmp(&next->FileName[len - offs], filterarr[c]) == 0
			    || (next->Attribute & EFI_FILE_DIRECTORY)) {
				(*count)++;
				break;
			}
		}
		ptr += OFFSET_OF(EFI_FILE_INFO, FileName) + (len + 1)*sizeof(CHAR16);
		next = ptr;
	}
	if (*count)
		*result = AllocatePool(((*count) + 1) * sizeof(void *));
	else
		*result = AllocatePool(2 * sizeof(void *));

	*count = 0;
	ptr = next = *entries;

	for (i = 0; i < tot; i++) {
		int len = StrLen(next->FileName);

		if (StrCmp(next->FileName, L".") == 0)
			/* ignore . directory */
			goto next;

		if (next->Attribute & EFI_FILE_DIRECTORY) {
				(*result)[(*count)] = PoolPrint(L"%s/", next->FileName);
				if (!(*result)[(*count)]) {
					Print(L"Failed to allocate buffer");
					return EFI_OUT_OF_RESOURCES;
				}
				(*count)++;
				goto next;
		}

		for (c = 0; c < filtercount; c++) {
			offs = StrLen(filterarr[c]);

			if (StrCmp(&next->FileName[len - offs], filterarr[c]) == 0) {
				(*result)[(*count)] = StrDuplicate(next->FileName);
				if (!(*result)[(*count)]) {
					Print(L"Failed to allocate buffer");
					return EFI_OUT_OF_RESOURCES;
				}
				(*count)++;
			} else {
				continue;
			}
			break;
		}

	next:		
		if (StrCmp(next->FileName, L"..") == 0) {
			/* place .. directory first */
			CHAR16 *tmp = (*result)[(*count) - 1];

			(*result)[(*count) - 1] = (*result)[0];
			(*result)[0] = tmp;
		}

		ptr += OFFSET_OF(EFI_FILE_INFO, FileName) + (len + 1)*sizeof(CHAR16);
		next = ptr;
	}
	if (*count == 0) {
		/* no entries at all ... can happen because top level dir has no . or .. */
		(*result)[(*count)++] = L"./";
	}
	(*result)[*count] = NULL;
	status = EFI_SUCCESS;

 out:
	if (status != EFI_SUCCESS) {
		if (*entries)
			FreePool(*entries);
		*entries = NULL;
		if (*result)
			FreePool(*result);
		*result = NULL;
	}
	return status;
}

static void
free_entries(CHAR16 **entries, int count)
{
	int i;

	for (i = 0; i<count; i++)
		FreePool(entries[i]);
}

void
simple_file_selector(EFI_HANDLE *im, CHAR16 **title, CHAR16 *name,
		     CHAR16 *filter, CHAR16 **result)
{
	EFI_STATUS status;
	CHAR16 **entries = NULL;
	EFI_FILE_INFO *dmp;
	int count, select, len;
	CHAR16 *newname, *selected;

	*result = NULL;
	if (!name)
		name = L"\\";
	if (!filter)
		filter = L"";
	if (!*im) {
		EFI_HANDLE h;
		CHAR16 *volname;

		simple_volume_selector(title, &volname, &h);
		if (!volname)
			return;
		FreePool(volname);
		*im = h;
	}

	newname = AllocatePool((StrLen(name) + 1)*sizeof(CHAR16));
	if (!newname)
		return;

	StrCpy(newname, name);
	name = newname;

 redo:
	status = simple_dir_filter(*im, name, filter, &entries, &count, &dmp);

	if (status != EFI_SUCCESS)
		goto out_free_name;

	select = console_select(title, entries, 0);
	if (select < 0)
		/* ESC key */
		goto out_free;
	selected = entries[select];
	/* note that memory used by selected is valid until dmp is freed */
	len = StrLen(selected);
	if (selected[len - 1] == '/') {
		CHAR16 *newname;

		/* stay where we are */
		if (StrCmp(selected, L"./") == 0) {
			free_entries(entries, count);
			FreePool(entries);
			entries = NULL;
			FreePool(dmp);
			goto redo;
		} else if (StrCmp(selected, L"../") == 0) {
			int i;

			i = StrLen(name) - 1;


			for (i = StrLen(name); i > 0; --i) {
				if (name[i] == '\\')
					break;
			}
			if (i == 0)
				i = 1;

			if (StrCmp(name, L"\\") != 0
			    && StrCmp(&name[i], L"..") != 0) {
				name[i] = '\0';
				free_entries(entries, count);
				FreePool(entries);
				entries = NULL;
				FreePool(dmp);
				goto redo;
			}
		}
		newname = AllocatePool((StrLen(name) + len + 2)*sizeof(CHAR16));
		if (!newname)
			goto out_free;
		StrCpy(newname, name);
			
		if (name[StrLen(name) - 1] != '\\')
			StrCat(newname, L"\\");
		StrCat(newname, selected);
		/* remove trailing / */
		newname[StrLen(newname) - 1] = '\0';

		free_entries(entries, count);
		FreePool(entries);
		entries = NULL;
		FreePool(dmp);
		FreePool(name);
		name = newname;

		goto redo;
	}
	*result = AllocatePool((StrLen(name) + len + 2)*sizeof(CHAR16));
	if (*result) {
		StrCpy(*result, name);
		if (name[StrLen(name) - 1] != '\\')
			StrCat(*result, L"\\");
		StrCat(*result, selected);
	}

 out_free:
	FreePool(dmp);
	if (entries) {
		free_entries(entries, count);
		FreePool(entries);
	}
 out_free_name:
	FreePool(name);
}
