#include <kernel/kernel.h>
#include <kernel/proc.h>
#include <kernel/ramdisk.h>
#include <kernel/vmm.h>
#include <kernel/handle.h>
#include <kernel/thread.h>
#include <kernel/fs.h>
#include <os/pe.h>
#include <wchar.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

module_t* peLoad(process_t* proc, const wchar_t* file, dword base)
{
	file_t *fd;
	module_t* mod;
	
	for (mod = proc->mod_first; mod; mod = mod->next)
		if (wcsicmp(mod->name, file) == 0)
			return mod;

	fd = fsOpen(file);
	if (!fd)
		return NULL;
	
	return peLoadMemory(proc, file, fd, base);
}

module_t* peLoadMemory(process_t* proc, const wchar_t* file, 
					   struct file_t *fd, dword base)
{
	module_t *mod;
	IMAGE_DOS_HEADER dos;
	IMAGE_PE_HEADERS pe;
	vm_area_t *area;
	addr_t new_base;
	size_t size;
	
	fsSeek(fd, 0);
	size = fsRead(fd, &dos, sizeof(dos));
	if (size < sizeof(dos))
	{
		wprintf(L"%s: only %d bytes read (%d)\n", file, size, errno);
		return NULL;
	}
	
	if (dos.e_magic != IMAGE_DOS_SIGNATURE)
	{
		wprintf(L"%s: not an executable (%S)\n", file, &dos);
		return NULL;
	}

	fsSeek(fd, dos.e_lfanew);
	fsRead(fd, &pe, sizeof(pe));

	if (pe.Signature != IMAGE_NT_SIGNATURE)
	{
		char *c = (char*) &pe.Signature;
		wprintf(L"%s: not PE format (%c%c%c%c)\n", file, 
			c[0], c[1], c[2], c[3]);
		return NULL;
	}

	mod = hndAlloc(sizeof(module_t), proc);
	mod->name = wcsdup(file);

	if (base == NULL)
		base = pe.OptionalHeader.ImageBase;

	new_base = base;
	while ((area = vmmArea(proc, (const void*) new_base)))
	{
		new_base = (addr_t) area->start + area->pages * PAGE_SIZE;
		wprintf(L"%s: clashed with %08x, adjusting base to %08x\n", 
			file, area->start, new_base);
	}

	assert(new_base == base);

	mod->base = base;
	mod->length = pe.OptionalHeader.SizeOfImage;
	mod->prev = proc->mod_last;
	mod->next = NULL;
	mod->entry = base + pe.OptionalHeader.AddressOfEntryPoint;
	mod->file = fd;
	mod->sizeof_headers = pe.OptionalHeader.SizeOfHeaders;
	mod->imported = false;
	//mod->raw_data = vmmMapFile(proc, NULL, (size_t) -1, fd, MEM_READ | MEM_WRITE);

	/*wprintf(L"%s: %x..%x (pbase = %x)\n",
		file, mod->base, mod->base + mod->length, 
		pe->OptionalHeader.ImageBase);*/

	if (proc->mod_last)
		proc->mod_last->next = mod;
	if (proc->mod_first == NULL)
		proc->mod_first = mod;
	proc->mod_last = mod;
	
	/*area = vmmAlloc(proc, mod->length / PAGE_SIZE, base, 0);
	if (area == NULL ||
		(addr_t) area->start != base)
	{
		vmmFree(proc, area);
		return NULL;
	}*/

	return mod;
}

static IMAGE_PE_HEADERS* peGetHeaders(addr_t base)
{
	IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER*) base;
	return (IMAGE_PE_HEADERS*) (base + dos->e_lfanew);
}

static bool peDoImports(process_t* proc,
						module_t* mod, 
						const IMAGE_DATA_DIRECTORY *directories)
{
	IMAGE_IMPORT_DESCRIPTOR * imp;
	IMAGE_THUNK_DATA *import_entry;
	IMAGE_THUNK_DATA *mapped_entry;
	const char* name;
	enum { bound_none, bound_old, bound_new } bound;
	wchar_t temp[256];
	module_t* newmod;
	dword args[3];
	
	if (directories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
		return true;

	imp = (IMAGE_IMPORT_DESCRIPTOR*) (mod->base + 
		directories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

	mod->imported = true;
	for (; imp->Name; imp++)
	{
		if (imp->Name >= mod->length)
		{
			//wprintf(L"%s: name %d is invalid (%x)\n",
				//mod->name, imp - (IMAGE_IMPORT_DESCRIPTOR*) (mod->base + 
					//directories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress),
				//imp->Name);
			continue;
		}

		name = (const char*) (mod->base + (addr_t) imp->Name);
		
		if (imp->TimeDateStamp == ~0UL)
			bound = bound_new;
		else if (imp->TimeDateStamp)
			bound = bound_old;
		else
			bound = bound_none;
		
		if (imp->u.OriginalFirstThunk)
		{
			import_entry = (IMAGE_THUNK_DATA*) (mod->base + (addr_t) imp->u.OriginalFirstThunk);
			mapped_entry = (IMAGE_THUNK_DATA*) (mod->base + (addr_t) imp->FirstThunk);
		}
		else
		{
			_cputws(L"\t(hint table missing, probably Borland bug)");
			import_entry = (IMAGE_THUNK_DATA*) (mod->base + (addr_t) imp->FirstThunk);
			mapped_entry = 0;
			bound = bound_none;
		}
		
		//wprintf(L"base = %x, name = %x\n", mod->base, imp->Name);
		mbstowcs(temp, name, countof(temp));
		//wprintf(L"Loading library %s...", temp);
		newmod = peLoad(proc, temp, 0);

		//if (newmod)
			//wprintf(L"done\n");
		if (!newmod)
		{
			wprintf(L"Failed to load %s\n", temp);
			mod->imported = false;
			exception(current, NULL, EXCEPTION_MISSING_DLL, (dword) name);
			return true;
			//return E_FAIL;
		}
		
		args[0] = newmod->base;
		args[1] = 0;
		args[2] = 0;
		thrCall(current, (void*) newmod->entry, args, sizeof(args));

		{
			int count, nextforwarder = bound==bound_old ? imp->ForwarderChain : -1;
			for (count = 0; import_entry->u1.Ordinal; count++, import_entry++, 
				/*bound ?*/ mapped_entry++/* : 0*/) // xxx - what is this?
			{
				if (IMAGE_SNAP_BY_ORDINAL(import_entry->u1.Ordinal))
					wprintf(L"\t%6lu %20S", IMAGE_ORDINAL(import_entry->u1.Ordinal),"<ordinal>");
				else
				{
					IMAGE_IMPORT_BY_NAME *name_import = (IMAGE_IMPORT_BY_NAME*) 
						(mod->base + (addr_t) import_entry->u1.AddressOfData);
					//wprintf(L"\t%6u %20S\n", name_import->Hint, name_import->Name);

					mapped_entry->u1.Function = 
						(dword*) peGetExport(newmod, name_import->Name);

					if (mapped_entry->u1.Function == NULL)
					{
						wprintf(L"Failed to access %S from %s\n", name_import->Name, temp);
						mod->imported = false;
						exception(current, NULL, EXCEPTION_MISSING_IMPORT, (dword) name_import->Name);
						return true;
						//return E_FAIL;
					}

					//wprintf(L"GetProcAddress returns %p\n", mapped_entry->u1.Function);
				}

				if (bound)
				{
					if (count != nextforwarder)
						wprintf(L"\t0x%12lx\n", (unsigned long)mapped_entry->u1.Function);
					else
					{
						wprintf(L"\t%12S\n", L"    --> forward");
						nextforwarder = (int)mapped_entry->u1.ForwarderString;
					}
                    //else
                        //_cputws(L"\n");
				}
			}
		}
	}

	return true;
}

bool pePageFault(process_t* proc, module_t* mod, addr_t addr)
{
	IMAGE_PE_HEADERS *pe;
	size_t size, raw_size;
	void *scn_base;
	dword priv;
	addr_t raw_offset;
	
	//if (wcsicmp(mod->name, L"kdebug.dll") == 0)
		//wprintf(L"%s: base = %08x length = %08x\n", mod->name, mod->base, mod->length);

	if (addr < mod->base || addr >= mod->base + mod->length)
		return false;

	//wprintf(L"pePageFault: %s at %p\n", mod->name, addr);
	
	if (addr >= mod->base && addr < mod->base + mod->sizeof_headers)
	{
		//wprintf(L"%x: headers: %d bytes\n", addr, mod->sizeof_headers);

		/* Map headers as a special case */

		size = (mod->sizeof_headers + PAGE_SIZE - 1) & -PAGE_SIZE;
		raw_size = mod->sizeof_headers;
		priv = proc->level | MEM_READ | MEM_COMMIT | MEM_ZERO;
		scn_base = (void*) mod->base;
		//raw_data = mod->raw_data;
		raw_offset = 0;
		pe = NULL;
	}
	else
	{
		IMAGE_SECTION_HEADER *first_scn, *scn;
		int i;
		
		pe = peGetHeaders(mod->base);
		first_scn = IMAGE_FIRST_SECTION(pe);
		
		scn = NULL;
		for (i = 0; i < pe->FileHeader.NumberOfSections; i++)
		{
			if ((first_scn[i].Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) &&
				scn == NULL)
				scn = first_scn + i;
			else if (addr >= mod->base + first_scn[i].VirtualAddress &&
				addr < mod->base + first_scn[i].VirtualAddress + first_scn[i].Misc.VirtualSize)
			{
				scn = first_scn + i;
				break;
			}
		}

		if (scn == NULL)
		{
			wprintf(L"%x: section not found\n", addr);
			return false;
		}

		scn_base = (void*) (mod->base + scn->VirtualAddress);
		size = (scn->Misc.VirtualSize + PAGE_SIZE - 1) & -PAGE_SIZE;
		//wprintf(L"%x: section %d: %8S %d bytes\n", addr, i, scn->Name, size);

		priv = proc->level | MEM_COMMIT | MEM_ZERO;
		if (scn->Characteristics & IMAGE_SCN_MEM_READ)
			priv |= MEM_READ;
		if (scn->Characteristics & IMAGE_SCN_MEM_WRITE)
			priv |= MEM_WRITE;
		
		if ((scn->Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) == 0)
			//raw_data = (byte*) mod->raw_data + scn->PointerToRawData;
			raw_offset = scn->PointerToRawData;
		else
			//raw_data = NULL;
			raw_offset = -1;

		raw_size = scn->SizeOfRawData;
	}

	if (vmmArea(proc, scn_base))
	{
		wprintf(L"Section already mapped\n");
		return false;
	}

	//wprintf(L"%s: 0x%x bytes at 0x%x\n", mod->name, size, scn_base);
	if (!vmmAlloc(proc, size / PAGE_SIZE, (addr_t) scn_base, priv))
	{
		wprintf(L"%s: failed to allocate section\n", mod->name);
		return false;
	}

	if (raw_offset != (addr_t) -1)
	{
		//wprintf(L"Copying %d bytes from %x\n", raw_size, raw_data);
		//memcpy(scn_base, raw_data, raw_size);
		fsSeek(mod->file, raw_offset);
		fsRead(mod->file, scn_base, raw_size);
	}
	//else
		//wprintf(L"Uninitialized data\n");

	if (!pe)
		pe = peGetHeaders(mod->base);

	if (!mod->imported &&
		!peDoImports(proc, mod, pe->OptionalHeader.DataDirectory))
	{
		wprintf(L"%s: imports failed\n", mod->name);
		return false;
	}

	return true;
}

addr_t peGetExport(module_t* mod, const char* name)
{
	IMAGE_PE_HEADERS* header;
	IMAGE_DATA_DIRECTORY* directories;
	IMAGE_EXPORT_DIRECTORY* exp;
	word *ordinal_table;
    dword *function_table;
    dword *name_table;
    size_t i;
    unsigned unused_slots = 0;  /* functions with an RVA of 0 seem to be unused */
	
	header = peGetHeaders(mod->base);
	directories = header->OptionalHeader.DataDirectory;

	if (directories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0)
		return NULL;

	exp = (IMAGE_EXPORT_DIRECTORY*) 
		(mod->base + directories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

	ordinal_table = (word*) (mod->base + (addr_t) exp->AddressOfNameOrdinals);
	function_table = (dword*) (mod->base + (addr_t) exp->AddressOfFunctions);
	name_table = exp->AddressOfNames ? (dword*) (mod->base + (addr_t) exp->AddressOfNames) : 0;

	for (i = 0; i < exp->NumberOfFunctions; i++)
	{
		dword addr = function_table[i];
		if (!addr)        /* skip unused slots */
		{
			++unused_slots;
			continue;
		}
		
		/* if names provided, list all names of this
		* export ordinal
		*/
		if (name_table)
		{
			size_t n;
			int found = 0;
			for (n = 0; n < exp->NumberOfNames; n++)
			{
				if (ordinal_table[n] == i)
				{
					if (stricmp((const char*) (mod->base + name_table[n]), name) == 0)
						return addr + mod->base;

					++found;
				}
			}
		}
		
		/* entry point */
		//if (addr >= section_base_virtual && addr < section_base_virtual + section_length)
			/* forwarder */
			//wprintf(L"%S", adr(addr));
		//else
			/* normal export */
	}
	
    if (unused_slots)
        wprintf(L"\t-- there are %u unused slots --\n", unused_slots);
    
	return NULL;
}

void peUnload(process_t* proc, module_t* mod)
{
	//vm_area_t *area;

	if (mod->next)
		mod->next->prev = mod->prev;
	if (mod->prev)
		mod->prev->next = mod->next;

	if (proc->mod_last == mod)
		proc->mod_last = mod->prev;
	if (proc->mod_first == mod)
		proc->mod_first = mod->next;

	/* xxx - free all areas allocated */
	//area = vmmArea(proc, (const void*) mod->base);
	//vmmFree(proc, area);

	free(mod->name);
	hndFree(mod);
}
