#include <uefi/types.h>
#include <uefi/guid.h>
#include <uefi/tables.h>
#include <uefi/protos/simple_text_output.h>

#include <stdalign.h>
#include <stddef.h>
#include <stdint.h>

#include "console.h"
#include "error.h"
#include "fs.h"
#include "hardware.h"
#include "loader.h"
#include "memory.h"
#include "paging.h"
#include "status.h"

#include "kernel_args.h"

namespace kernel {
#include "kernel_memory.h"
}

namespace args = kernel::args;

namespace boot {

constexpr int max_modules = 5; // Max modules to allocate room for
constexpr int max_programs = 5; // Max programs to allocate room for

struct program_desc
{
	const wchar_t *name;
	const wchar_t *path;
};

const program_desc program_list[] = {
	{L"kernel", L"jsix.elf"},
	{L"null driver", L"nulldrv.elf"},
	{L"fb driver", L"fb.elf"},
};

/// Change a pointer to point to the higher-half linear-offset version
/// of the address it points to.
template <typename T>
void change_pointer(T *&pointer)
{
	pointer = offset_ptr<T>(pointer, kernel::memory::page_offset);
}

/// Allocate space for kernel args. Allocates enough space from pool
/// memory for the args header and the module and program headers.
args::header *
allocate_args_structure(
	uefi::boot_services *bs,
	size_t max_modules,
	size_t max_programs)
{
	status_line status {L"Setting up kernel args memory"};

	args::header *args = nullptr;

	size_t args_size =
		sizeof(args::header) + // The header itself
		max_modules * sizeof(args::module) +  // The module structures
		max_programs * sizeof(args::program); // The program structures

	try_or_raise(
		bs->allocate_pool(memory::args_type, args_size,
			reinterpret_cast<void**>(&args)),
		L"Could not allocate argument memory");

	bs->set_mem(args, args_size, 0);

	args->modules =
		reinterpret_cast<args::module*>(args + 1);
	args->num_modules = 0;

	args->programs =
		reinterpret_cast<args::program*>(args->modules + max_modules);
	args->num_programs = 0;

	return args;
}

/// Add a module to the kernel args list
inline void
add_module(args::header *args, args::mod_type type, buffer &data)
{
	args::module &m = args->modules[args->num_modules++];
	m.type = type;
	m.location = data.data;
	m.size = data.size;
}

/// The main procedure for the portion of the loader that runs while
/// UEFI is still in control of the machine. (ie, while the loader still
/// has access to boot services.
args::header *
uefi_preboot(uefi::handle image, uefi::system_table *st)
{
	status_line status {L"Performing UEFI pre-boot"};

	uefi::boot_services *bs = st->boot_services;
	uefi::runtime_services *rs = st->runtime_services;
	memory::init_pointer_fixup(bs, rs);

	args::header *args =
		allocate_args_structure(bs, max_modules, max_programs);

	args->magic = args::magic;
	args->version = args::version;
	args->runtime_services = rs;
	args->acpi_table = hw::find_acpi_table(st);
	paging::allocate_tables(args, bs);

	memory::mark_pointer_fixup(&args->runtime_services);

	fs::file disk = fs::get_boot_volume(image, bs);

	const uefi::memory_type mod_type = memory::module_type;
	buffer symbols = loader::load_file(disk, L"symbol table", L"symbol_table.dat",
				memory::module_type);
	add_module(args, args::mod_type::symbol_table, symbols);

	for (auto &desc : program_list) {
		buffer buf = loader::load_file(disk, desc.name, desc.path);
		args::program &program = args->programs[args->num_programs++];
		loader::load_program(program, desc.name, buf, bs);
	}

	for (unsigned i = 0; i < args->num_modules; ++i) {
		args::module &mod = args->modules[i];
		change_pointer(mod.location);
	}

	return args;
}

memory::efi_mem_map
uefi_exit(args::header *args, uefi::handle image, uefi::boot_services *bs)
{
	status_line status {L"Exiting UEFI"};

	memory::efi_mem_map map =
		memory::build_kernel_mem_map(args, bs);

	try_or_raise(
		bs->exit_boot_services(image, map.key),
		L"Failed to exit boot services");

	return map;
}

} // namespace boot

/// The UEFI entrypoint for the loader.
extern "C" uefi::status
efi_main(uefi::handle image, uefi::system_table *st)
{
	using namespace boot;
	console con(st->boot_services, st->con_out);

	args::header *args = uefi_preboot(image, st);
	memory::efi_mem_map map = uefi_exit(args, image, st->boot_services);

	args->video = con.fb();
	status_bar status {con.fb()}; // Switch to fb status display

	args::program &kernel = args->programs[0];
	paging::map_pages(args, kernel.phys_addr, kernel.virt_addr, kernel.size);
	kernel::entrypoint kentry =
		reinterpret_cast<kernel::entrypoint>(kernel.entrypoint);
	status.next();

	memory::virtualize(args->pml4, map, st->runtime_services);
	status.next();

	change_pointer(args->pml4);
	hw::setup_cr4();
	status.next();

	kentry(args);
	debug_break();
	return uefi::status::unsupported;
}

