/**
 * @brief Main bootloader logic.
 *
 * Does all the heavy lifting after configuration options have
 * been selected by the user. Loads the kernel and ramdisk,
 * sets up multiboot structures, and jumps to the kernel.
 *
 * @copyright
 * This file is part of ToaruOS and is released under the terms
 * of the NCSA / University of Illinois License - see LICENSE.md
 * Copyright (C) 2018-2021 K. Lange
 */
#include <stdint.h>
#include <stddef.h>
#include "multiboot.h"
#include "text.h"
#include "util.h"
#include "menu.h"
#include "elf.h"
#include "options.h"
#include "iso9660.h"
#include "kbd.h"

extern void draw_logo(int);
char * kernel_load_start = 0;

mboot_mod_t modules_mboot[1] = {
	{0,0,0,1}
};

static struct multiboot multiboot_header = {
	/* flags;             */ MULTIBOOT_FLAG_CMDLINE | MULTIBOOT_FLAG_MODS | MULTIBOOT_FLAG_MEM | MULTIBOOT_FLAG_MMAP | MULTIBOOT_FLAG_LOADER,
	/* mem_lower;         */ 0x100000,
	/* mem_upper;         */ 0x640000,
	/* boot_device;       */ 0,
	/* cmdline;           */ 0,
	/* mods_count;        */ 1,
	/* mods_addr;         */ 0,
	/* num;               */ 0,
	/* size;              */ 0,
	/* addr;              */ 0,
	/* shndx;             */ 0,
	/* mmap_length;       */ 0,
	/* mmap_addr;         */ 0,
	/* drives_length;     */ 0,
	/* drives_addr;       */ 0,
	/* config_table;      */ 0,
	/* boot_loader_name;  */ 0,
	/* apm_table;         */ 0,
	/* vbe_control_info;  */ 0,
	/* vbe_mode_info;     */ 0,
	/* vbe_mode;          */ 0,
	/* vbe_interface_seg; */ 0,
	/* vbe_interface_off; */ 0,
	/* vbe_interface_len; */ 0,
};

static uintptr_t ramdisk_off = 0;
static uintptr_t ramdisk_len = 0;
uintptr_t final_offset = 0;
uintptr_t _xmain = 0;

static int load_aout(uint32_t * hdr) {
	uintptr_t base_offset = (uintptr_t)hdr - (uintptr_t)kernel_load_start;
	uintptr_t hdr_offset  = hdr[3] - base_offset;

	memcpy((void*)(uintptr_t)hdr[4], kernel_load_start + (hdr[4] - hdr_offset), (hdr[5] - hdr[4]));
	memset((void*)(uintptr_t)hdr[5], 0, (hdr[6] - hdr[5]));
	_xmain = (uintptr_t)hdr[7];

	if (hdr[6] > final_offset) final_offset = hdr[6];
	final_offset = (final_offset & ~(0xFFF)) + ((final_offset & 0xFFF) ? 0x1000 : 0);

	return 1;
}

static int load_elf32(Elf32_Header * header) {
	if (header->e_ident[0] != ELFMAG0 ||
	    header->e_ident[1] != ELFMAG1 ||
	    header->e_ident[2] != ELFMAG2 ||
	    header->e_ident[3] != ELFMAG3) {
		print_("Not a valid ELF32.\n");
		return 0;
	}

	uintptr_t entry = (uintptr_t)header->e_entry;
	for (uintptr_t x = 0; x < (uint32_t)header->e_phentsize * header->e_phnum; x += header->e_phentsize) {
		Elf32_Phdr * phdr = (Elf32_Phdr *)(kernel_load_start + header->e_phoff + x);
		if (phdr->p_type == PT_LOAD) {
			memcpy((uint8_t*)(uintptr_t)phdr->p_vaddr, kernel_load_start + phdr->p_offset, phdr->p_filesz);
			uintptr_t r = phdr->p_filesz;
			while (r < phdr->p_memsz) {
				*(char *)(phdr->p_vaddr + r) = 0;
				r++;
			}
			if (phdr->p_vaddr + r > final_offset) final_offset = phdr->p_vaddr + r;
		}
	}

	_xmain = entry;

	/* Round final offset */
	final_offset = (final_offset & ~(0xFFF)) + ((final_offset & 0xFFF) ? 0x1000 : 0);

	print("Loaded with end at 0x"); print_hex(final_offset); print("\n");
	return 1;
}

static int load_kernel(void) {
	clear();

	/* Check for Multiboot header */
	for (uintptr_t x = 0; x < 8192; x += 4) {
		uint32_t * check = (uint32_t *)(kernel_load_start + x);
		if (*check == 0x1BADB002) {
			if (check[1] & (1 << 16)) {
				return load_aout(check);
			} else {
				return load_elf32((void*)kernel_load_start);
			}
		}
	}

	return 0;
}

static void relocate_ramdisk(mboot_mod_t * mboot_mods) {
	char * dest = (char*)final_offset;
	char * src  = (char*)ramdisk_off;
	for (size_t s = 0; s < ramdisk_len; ++s) {
		dest[s] = src[s];
	}

	mboot_mods->mod_start = final_offset;
	mboot_mods->mod_end   = final_offset + ramdisk_len;

	final_offset += ramdisk_len;
	final_offset = (final_offset & ~(0xFFF)) + ((final_offset & 0xFFF) ? 0x1000 : 0);
}

#ifdef EFI_PLATFORM
#include <efi.h>
extern EFI_GRAPHICS_OUTPUT_PROTOCOL * GOP;

/* EFI boot uses simple filesystem driver */
static EFI_GUID efi_simple_file_system_protocol_guid =
	{0x0964e5b22,0x6459,0x11d2,0x8e,0x39,0x00,0xa0,0xc9,0x69,0x72,0x3b};

static EFI_GUID efi_loaded_image_protocol_guid =
	{0x5B1B31A1,0x9562,0x11d2, {0x8E,0x3F,0x00,0xA0,0xC9,0x69,0x72,0x3B}};

extern EFI_SYSTEM_TABLE *ST;
extern EFI_HANDLE ImageHandleIn;

extern char do_the_nasty[];
static void finish_boot(void) {
	/* Set up multiboot header */
	struct multiboot * finalHeader = (void*)(uintptr_t)final_offset;
	memcpy((void*)final_offset, &multiboot_header, sizeof(struct multiboot));
	final_offset += sizeof(struct multiboot);

	finalHeader->flags |= MULTIBOOT_FLAG_FB;
	finalHeader->framebuffer_addr = GOP->Mode->FrameBufferBase;
	finalHeader->framebuffer_pitch = GOP->Mode->Info->PixelsPerScanLine * 4;
	finalHeader->framebuffer_width = GOP->Mode->Info->HorizontalResolution;
	finalHeader->framebuffer_height = GOP->Mode->Info->VerticalResolution;
	finalHeader->framebuffer_bpp  = 32;

	/* Copy in command line */
	memcpy((void*)final_offset, cmdline, strlen(cmdline)+1);
	finalHeader->cmdline = (uintptr_t)final_offset;
	final_offset += strlen(cmdline) + 1;

	/* Copy bootloader name */
	memcpy((void*)final_offset, VERSION_TEXT, strlen(VERSION_TEXT)+1);
	finalHeader->boot_loader_name = (uintptr_t)final_offset;
	final_offset += strlen(VERSION_TEXT) + 1;

	/* Copy module pointers */
	memcpy((void*)final_offset, modules_mboot, sizeof(modules_mboot));
	finalHeader->mods_addr = (uintptr_t)final_offset;
	final_offset += sizeof(modules_mboot);

	/* Realign for memory map */
	final_offset = (final_offset & ~(0xFFF)) + ((final_offset & 0xFFF) ? 0x1000 : 0);

	/* Write memory map */
	mboot_memmap_t * mmap = (void*)final_offset;
	memset((void*)final_offset, 0x00, 1024);
	finalHeader->mmap_addr = (uint32_t)(uintptr_t)mmap;

	{
		EFI_STATUS e;
		UINTN mapSize = 0, mapKey, descriptorSize;
		UINT32 descriptorVersion;
		e = uefi_call_wrapper(ST->BootServices->GetMemoryMap, 5, &mapSize, NULL, &mapKey, &descriptorSize, NULL);

		EFI_MEMORY_DESCRIPTOR * efi_memory = (void*)(final_offset);
		final_offset += mapSize;
		while ((uintptr_t)final_offset & 0x3ff) final_offset++;

		e = uefi_call_wrapper(ST->BootServices->GetMemoryMap, 5, &mapSize, efi_memory, &mapKey, &descriptorSize, NULL);

		if (EFI_ERROR(e)) {
			print_("EFI error.\n");
			while (1) {};
		}

		uint64_t upper_mem = 0;
		int descriptors = mapSize / descriptorSize;
		for (int i = 0; i < descriptors; ++i) {
			EFI_MEMORY_DESCRIPTOR * d = efi_memory;

			mmap->size = sizeof(uint64_t) * 2 + sizeof(uint32_t);
			mmap->base_addr = d->PhysicalStart;
			mmap->length = d->NumberOfPages * 4096;
			switch (d->Type) {
				case EfiConventionalMemory:
				case EfiLoaderCode:
				case EfiLoaderData:
				case EfiBootServicesCode:
				case EfiBootServicesData:
				case EfiRuntimeServicesCode:
				case EfiRuntimeServicesData:
					mmap->type = 1;
					break;
				case EfiReservedMemoryType:
				case EfiUnusableMemory:
				case EfiMemoryMappedIO:
				case EfiMemoryMappedIOPortSpace:
				case EfiPalCode:
				case EfiACPIMemoryNVS:
				case EfiACPIReclaimMemory:
				default:
					mmap->type = 2;
					break;
			}
			if (mmap->type == 1 && mmap->base_addr >= 0x100000) {
				upper_mem += mmap->length;
			}
			mmap = (mboot_memmap_t *) ((uintptr_t)mmap + mmap->size + sizeof(uint32_t));
			efi_memory = (EFI_MEMORY_DESCRIPTOR *)((char *)efi_memory + descriptorSize);
		}

		finalHeader->mmap_length = (uintptr_t)mmap - finalHeader->mmap_addr;

		finalHeader->mem_lower = 1024;
		finalHeader->mem_upper = upper_mem / 1024;
	}

	relocate_ramdisk((void*)(uintptr_t)finalHeader->mods_addr);

	{
		EFI_STATUS e;
		UINTN mapSize = 0, mapKey, descriptorSize;
		UINT32 descriptorVersion;
		uefi_call_wrapper(ST->BootServices->GetMemoryMap, 5, &mapSize, NULL, &mapKey, &descriptorSize, NULL);
		e = uefi_call_wrapper(ST->BootServices->ExitBootServices, 2, ImageHandleIn, mapKey);

		if (e != EFI_SUCCESS) { 
			print_("Exit services failed. \n");
			print_hex_(e);
			while (1) {};
		}
	}

	uint64_t foobar = ((uint32_t)(uintptr_t)&do_the_nasty) | (0x10L << 32L);

	uint32_t * foo = (uint32_t *)0x7c00;

	foo[0] = MULTIBOOT_EAX_MAGIC;
	foo[1] = (uintptr_t)finalHeader;
	foo[2] = _xmain;

	__asm__ __volatile__ (
			"push %0\n"
			"lretl\n"
			 : : "g"(foobar));

	__asm__ (
		"do_the_nasty:\n"
		"cli\n"
		".code32\n"
		"mov %cr0, %eax\n"
		"and $0x7FFeFFFF, %eax\n"
		"mov %eax, %cr0\n"
		"mov $0xc0000080, %ecx\n"
		"rdmsr\n"
		"and $0xfffffeff, %eax\n"
		"wrmsr\n"
		"mov $0x640, %eax\n"
		"mov %eax, %cr4\n"
		"mov 0x7c00, %eax\n"
		"mov 0x7c04, %ebx\n"
		"mov 0x7c08, %ecx\n"
		"jmp *%ecx\n"
		"target: jmp target\n"
		".code64\n"
		);

	__builtin_unreachable();
}

void boot(void) {
	UINTN count;
	EFI_HANDLE * handles;
	EFI_LOADED_IMAGE * loaded_image;
	EFI_FILE_IO_INTERFACE *efi_simple_filesystem;
	EFI_FILE *root;
	EFI_STATUS status;

	uefi_call_wrapper(ST->BootServices->SetWatchdogTimer, 4, 0, 0, 0, NULL);

	clear_();

	draw_logo(0);

	for (unsigned int i = 0; i < ST->NumberOfTableEntries; ++i) {
		if (ST->ConfigurationTable[i].VendorGuid.Data1 == 0xeb9d2d30 &&
			ST->ConfigurationTable[i].VendorGuid.Data2 == 0x2d88 &&
			ST->ConfigurationTable[i].VendorGuid.Data3 == 0x11d3) {
			multiboot_header.config_table = (uintptr_t)ST->ConfigurationTable[i].VendorTable & 0xFFFFffff;
			break;
		}
	}

	status = uefi_call_wrapper(ST->BootServices->HandleProtocol,
			3, ImageHandleIn, &efi_loaded_image_protocol_guid,
			(void **)&loaded_image);

	if (EFI_ERROR(status)) {
		print_("Could not obtain loaded_image_protocol\n");
		while (1) {};
	}

	print("Found loaded image...\n");

	status = uefi_call_wrapper(ST->BootServices->HandleProtocol,
			3, loaded_image->DeviceHandle, &efi_simple_file_system_protocol_guid,
			(void **)&efi_simple_filesystem);

	if (EFI_ERROR(status)) {
		print_("Could not obtain simple_file_system_protocol.\n");
		while (1) {};
	}

	status = uefi_call_wrapper(efi_simple_filesystem->OpenVolume,
			2, efi_simple_filesystem, &root);

	if (EFI_ERROR(status)) {
		print_("Could not open volume.\n");
		while (1) {};
	}

	EFI_FILE * file;

	CHAR16 kernel_name[16] = {0};
	{
		char * c = kernel_path;
		char * ascii = c;
		int i = 0;
		while (*ascii) {
			kernel_name[i] = *ascii;
			i++;
			ascii++;
		}
		if (kernel_name[i-1] == L'.') {
			kernel_name[i-1] = 0;
		}
	}

	/* Load kernel */
	status = uefi_call_wrapper(root->Open,
			5, root, &file, kernel_name, EFI_FILE_MODE_READ, 0);

	if (EFI_ERROR(status)) {
		print_("Error opening kernel.\n");
		while (1) {};
	}

#define KERNEL_LOAD_START 0x4000000ULL
	kernel_load_start = (char*)KERNEL_LOAD_START;

	{
		EFI_PHYSICAL_ADDRESS addr = KERNEL_LOAD_START;
		EFI_ALLOCATE_TYPE type = AllocateAddress;
		EFI_MEMORY_TYPE memtype = EfiLoaderData;
		UINTN pages = 8192;
		EFI_STATUS status = 0;
		status = uefi_call_wrapper(ST->BootServices->AllocatePages, 4, type, memtype, pages, &addr);
		if (EFI_ERROR(status)) {
			print_("Could not allocate space to load boot payloads: ");
			print_hex_(status);
			print_(" ");
			print_hex_(addr);
			while (1) {};
		}
	}

	unsigned int offset = 0;
	UINTN bytes = 134217728;
	status = uefi_call_wrapper(file->Read,
			3, file, &bytes, (void *)KERNEL_LOAD_START);

	if (EFI_ERROR(status)) {
		print_("Error loading kernel.\n");
		while (1) {};
	}

	offset += bytes;
	while (offset % 4096) offset++;

	{
		char * c = ramdisk_path;
		CHAR16 name[16] = {0};
		char * ascii = c;
		int i = 0;
		while (*ascii) {
			name[i] = *ascii;
			i++;
			ascii++;
		}
		if (name[i-1] == L'.') {
			name[i-1] == 0;
		}
		bytes = 134217728;
		status = uefi_call_wrapper(root->Open,
				5, root, &file, name, EFI_FILE_MODE_READ, 0);
		if (!EFI_ERROR(status)) {
			status = uefi_call_wrapper(file->Read,
					3, file, &bytes, (void*)(uintptr_t)(KERNEL_LOAD_START + offset));
			if (!EFI_ERROR(status)) {
				ramdisk_off = KERNEL_LOAD_START + offset;
				ramdisk_len = bytes;
			} else {
				print_("Failed to read ramdisk\n");
			}
		} else {
			print_("Error opening "); print_(c); print_("\n");
		}
	}

	load_kernel();
	finish_boot();
}

#else
struct mmap_entry {
	uint64_t base;
	uint64_t len;
	uint32_t type;
	uint32_t reserved;
};

extern unsigned short mmap_ent;
extern unsigned short lower_mem;

static void finish_boot(void) {
	print("Setting up memory map...\n");
	print_hex(mmap_ent);
	print("\n");
	memset(kernel_load_start, 0x00, 1024);
	mboot_memmap_t * mmap = (void*)final_offset;
	multiboot_header.mmap_addr = (uintptr_t)mmap;
	multiboot_header.mods_addr = (uintptr_t)&modules_mboot;
	multiboot_header.boot_loader_name = (uintptr_t)VERSION_TEXT;

	struct mmap_entry * e820 = (void*)0x5000;

	uint64_t upper_mem = 0;
	for (int i = 0; i < mmap_ent; ++i) {
		print("entry "); print_hex(i);
		print(" "); print_hex((uint32_t)(e820[i].base >> 32ULL)); print_hex((uint32_t)e820[i].base);
		print(" "); print_hex((uint32_t)(e820[i].len >> 32ULL)); print_hex((uint32_t)e820[i].len);
		print(" "); print_hex(e820[i].type); print("\n");

		mmap->size = sizeof(uint64_t) * 2 + sizeof(uintptr_t);
		mmap->base_addr = e820[i].base;
		mmap->length = e820[i].len;
		mmap->type = e820[i].type;
		if (mmap->type == 1 && mmap->base_addr >= 0x100000) {
			upper_mem += mmap->length;
		}
		mmap = (mboot_memmap_t *) ((uintptr_t)mmap + mmap->size + sizeof(uintptr_t));
	}
	multiboot_header.mmap_length = (uintptr_t)mmap - multiboot_header.mmap_addr;

	print("lower "); print_hex(lower_mem); print("KB\n");
	multiboot_header.mem_lower = 1024;
	print("upper ");
	print_hex(upper_mem >> 32);
	print_hex(upper_mem);
	print("\n");
	
	multiboot_header.mem_upper = upper_mem / 1024;

	print("Jumping to kernel...\n");

	uint32_t foo[3];
	foo[0] = MULTIBOOT_EAX_MAGIC;
	foo[1] = (unsigned int)&multiboot_header;;
	foo[2] = _xmain;
	__asm__ __volatile__ (
		"mov %%cr0,%%eax\n"
		/* Disable paging */
		"and $0x7FFeFFFF, %%eax\n"
		"mov %%eax,%%cr0\n"
		/* Ensure PAE is not enabled */
		"mov %%cr4,%%eax\n"
		"and $0xffffffdf, %%eax\n"
		"mov %%eax,%%cr4\n"
		"mov %1,%%eax \n"
		"mov %2,%%ebx \n"
		"jmp *%0" : : "g"(foo[2]), "g"(foo[0]), "g"(foo[1]) : "eax", "ebx"
		);
}

extern void do_bios_call(uint32_t function, uint32_t arg1);
extern int bios_call(char * into, uint32_t sector);

static int spin_x = 0;
static void spin(void) {
	static int  spincnt = 0;
	draw_logo(spincnt+1);
	spincnt = (spincnt + 1) & 0x7;
}

static void clear_spin(void) {
	y = 16;
	//print_banner("");
}

extern uint16_t * vbe_cont_info_mode_off;
extern uint32_t *vbe_info_fbaddr;
extern uint16_t vbe_info_pitch;
extern uint16_t vbe_info_width;
extern uint16_t vbe_info_height;
extern uint8_t vbe_info_bpp;

extern void bios_text_mode(void);

void boot(void) {
	/* Did we ask for VGA text mode and are currently in a video mode? */
	if (boot_mode == 5) {
		bios_text_mode();
	}

	clear_();

	draw_logo(0);

	print("Looking for ISO9660 filesystem... ");
	for (int i = 0x10; i < 0x15; ++i) {
		bios_call((char*)(DATA_LOAD_BASE + ISO_SECTOR_SIZE * i), i);
		root = (void*)(DATA_LOAD_BASE + ISO_SECTOR_SIZE * i);
		switch (root->type) {
			case 1:
				print("found.\n");
				goto done;
		}
	}
	return;
done:

	print("Looking for kernel... ");
	if (!navigate(kernel_path)) {
		print_("Failed to locate kernel.\n");
		return;
	}
	print("found.\n");

	kernel_load_start = (char*)(DATA_LOAD_BASE + dir_entry->extent_start_LSB * ISO_SECTOR_SIZE);

	print("Loading kernel... "); spin_x = x;
	for (int i = 0, j = 0; i < dir_entry->extent_length_LSB; j++) {
		if (!(j & 0x3FF)) spin();
		bios_call(kernel_load_start + i, dir_entry->extent_start_LSB + j);
		i += ISO_SECTOR_SIZE;
	}
	print("\n");

	print("Looking for ramdisk... ");
	if (!navigate(ramdisk_path)) {
		print_("Failed to locate ramdisk.\n");
		return;
	}
	print("found.\n");

	ramdisk_off = DATA_LOAD_BASE + dir_entry->extent_start_LSB * ISO_SECTOR_SIZE;

	print("Loading ramdisk... "); spin_x = x;
	for (int i = 0, j = 0; i < dir_entry->extent_length_LSB; j++) {
		if (!(j & 0x3FF)) spin();
		bios_call((char*)(ramdisk_off + i), dir_entry->extent_start_LSB + j);
		i += ISO_SECTOR_SIZE;
	}
	print("\n");

	ramdisk_len = dir_entry->extent_length_LSB;

	multiboot_header.cmdline = (uintptr_t)cmdline;

	draw_logo(0);

	if (vbe_info_width) {
		multiboot_header.framebuffer_addr   = (uint32_t)vbe_info_fbaddr;
		multiboot_header.framebuffer_pitch  = vbe_info_pitch;
		multiboot_header.framebuffer_width  = vbe_info_width;
		multiboot_header.framebuffer_height = vbe_info_height;
		multiboot_header.framebuffer_bpp    = vbe_info_bpp;
	}

	print("Loading kernel from 0x"); print_hex((uint32_t)kernel_load_start); print("... ");
	if (!load_kernel()) {
		print_("Failed to load kernel.\n");
		return;
	}

	print("Relocating ramdisk from 0x"); print_hex((uint32_t)ramdisk_off); print(":0x"); print_hex(ramdisk_len); print(" to 0x"); print_hex(final_offset); print("... ");
	relocate_ramdisk(modules_mboot);

	finish_boot();
}
#endif
