#include <linux/kvm.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>

#include <mz.h>

#define MSIZE 0x100000 // size of conventional memory

/* x86 memory map
 0x00000 - 0x003FF	IVT	0x400
 0x00400 - 0x9FFFF	FREE	0x9FC00
 0xA0000 - 0xBFFFF	VGA	0x20000
 0xC0000 - 0xFFFFF	FREE	0x40000

 total freemem - 0xDFC00 - 916480 bytes/895.0K
*/

int main(int argc, char** argv) {

	struct stat s;
	int ret = stat(argv[1], &s);
	if(ret) exit(!!printf("invalid file!\n"));

	int kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);

	assert((ioctl(kvm, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY) ^ ioctl(kvm, KVM_GET_API_VERSION, NULL)) == 13);

	int	vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0),
		vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0),
		codefd = open(argv[1], O_RDONLY); // load boot sector from file

	uint8_t* mem = mmap(NULL, MSIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // allocate conventional memory for vm
	
	mz* mzbuf = malloc(s.st_size);
	read(codefd, mzbuf, s.st_size);
	close(codefd);

	mz_start ms;
	uint32_t mzsz = loadmz(mzbuf, mem, s.st_size, 0x40, &ms);
	free(mzbuf);

	printf("done\n");

	struct kvm_userspace_memory_region memmap[2] = {
		{
			.guest_phys_addr = 0,
			.memory_size = 0xA0000,
			.userspace_addr = (uint64_t)mem,
		},
		/*{ // implied map
			.guest_phys_addr = 0xA0000,
			.memory_size = 0x20000,
			.userspace_addr = (uint64_t)mem,
		},*/
		{
			.guest_phys_addr = 0xC0000,
			.memory_size = 0x40000,
			.userspace_addr = (uint64_t)mem + 0xC0000,
		},
	};

	for(int i = 0; i < 2; i++) {
		memmap[i].slot = i; // id of memory mapping
		ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, memmap + i);
	}

	struct kvm_sregs sregs;
	struct kvm_run* run;
	size_t run_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL); // size of struct is. determined at runtime for some reason?
	run = mmap(NULL, run_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);

	ioctl(vcpufd, KVM_GET_SREGS, &sregs);
	
	sregs.cs.base = ms.cs;
	sregs.ss.base = ms.ss;

	sregs.cr0 = 0;

	sregs.cs.selector = 0; // probably protected mode related?
	sregs.ss.selector = 0;
	
	ioctl(vcpufd, KVM_SET_SREGS, &sregs);

	struct kvm_regs regs = {
		.rip = ms.ip,
		.rsp = ms.sp,
		.rflags = 0x2,
	};

	mem[((uint32_t)ms.cs << 4) + ms.ip] = 0xF4; // hlt

	ioctl(vcpufd, KVM_SET_REGS, &regs);

	printf("begin vm\n");

	while(1) {
		errno = 0;
		ret = ioctl(vcpufd, KVM_RUN, NULL);
		if(ret == -1) exit(!!printf("kvm error: %s\n", strerror(errno)));
		switch (run->exit_reason) {
			case KVM_EXIT_HLT:
				exit(!printf("got instruction `hlt`, exiting\n"));
				break;
			case KVM_EXIT_IO:

				break;
			case KVM_EXIT_INTR:
				break;
			case KVM_EXIT_INTERNAL_ERROR:
				exit(printf("internal error %u\n", run->internal.suberror));
				break;
			default:
				exit(printf("vmexit %u\n", run->exit_reason));
				break;
		}
	}
}
