#include <kernel/kernel.h>
#include <kernel/memory.h>
#include <stdlib.h>
#include <wchar.h>
#include <errno.h>

extern byte scode[], edata[], ebss[];
extern dword kernel_pagedir[];
void main();

page_pool_t pool_all, pool_low;

//! Marks a range of pages with the specified flags.
/*!
 *	\param	start	Page number of the start of the range
 *	\param	end		Page number of the end of the range
 *	\param	flags	Flags to assign to each of the pages
 */
/*void memMark(int start, int end, dword flags)
{
	//if (flags == PAGE_RESERVED)
		//wprintf(L"Reserved: %08x-%08x\n", start * PAGE_SIZE, end * PAGE_SIZE);

	assert(end < num_pages);

	for (; start < end; start++)
		pages[start].flags = flags;
}*/

//! Allocates a contiguous block of physical memory.
/*!
 *	\param	length	The number of pages to allocate
 *	\return	The physical address of the start of the block. If there was
 *		insufficient free memory, or if a contiguous block could not be found,
 *		returns NULL.
 */
/*addr_t memAlloc(int length)
{
	int i, j;

	for (i = 0; i < num_pages; i++)
	{
		for (j = 0; pages[i + j].flags == PAGE_FREE && j < length; j++)
			;

		if (j == length)
		{
			wprintf(L"allocated %d pages at %x\n", length, i * PAGE_SIZE);
			memMark(i, i + length, PAGE_USED);
			return i * PAGE_SIZE;
		}
	}

	wprintf(L"memAlloc: no pages left!\n");
	halt(0);
	errno = ENOMEM;
	return NULL;
}*/

addr_t memAlloc()
{
	addr_t page;

	do
	{
		assert(pool_all.free_pages > 0);
		pool_all.free_pages--;
		page = pool_all.pages[pool_all.free_pages];
	} while (page == (addr_t) -1);

	return page;
}

addr_t memAllocLow()
{
	addr_t page;

	do
	{
		assert(pool_low.free_pages > 0);
		pool_low.free_pages--;
		page = pool_low.pages[pool_low.free_pages];
	} while (page == (addr_t) -1);

	assert(page < NUM_LOW_PAGES * PAGE_SIZE);
	return page;
}

addr_t memAllocLowSpan(size_t pages)
{
	addr_t page, j, start;
	bool found;

	for (page = 0; page < pool_low.free_pages; page++)
	{
		if (pool_low.pages[page] != (addr_t) -1)
		{
			found = true;

			for (j = 0; j < pages; j++)
				if (pool_low.pages[page + j] != 
					pool_low.pages[page] + j * PAGE_SIZE)
				{
					found = false;
					break;
				}

			if (found)
			{
				start = pool_low.pages[page];
				for (j = 0; j < pages; j++)
					pool_low.pages[page + j] = (addr_t) -1;
				return start;
			}
		}
	}

	return NULL;
}

//! Frees a block of memory allocated by memAlloc.
/*!
 *	\param	block	The physical address of the start of the block
 *	\param	length	The number of pages occupied by the block
 */
/*void memFree(addr_t block, int length)
{
	if (block)
		memMark(block / PAGE_SIZE, block / PAGE_SIZE + length, PAGE_FREE);
}*/

void memFree(addr_t block)
{
	pool_all.pages[pool_all.free_pages] = block;
	pool_all.free_pages++;
}

void memFreeLow(addr_t block)
{
	assert(block < NUM_LOW_PAGES * PAGE_SIZE);
	pool_low.pages[pool_low.free_pages] = block;
	pool_low.free_pages++;
}

//! Creates a linear-to-physical address mapping in the specified page directory.
/*!
 *	\param	PageDir	The page directory to modify
 *	\param	Virt	The virtual address of the start of the range
 *	\param	Phys	The physical address of the start of the range
 *	\param	pages	The number of pages to map
 *	\param	Privilge	The IA32 privilege flags to assign to each of the 
 *		pages. In addition, PRIV_PRES is assigned to each page.
 *	\return	true if the pages could be mapped, false if there was insufficient
 *		free memory to allocate another page table.
 */
bool memMap(dword *PageDir, dword Virt, dword Phys, dword pages, byte Privilege)
{
	dword PageTabOff, PageDirEnt, Temp;

	/* the top 20 bits of the page table entries are for address translation,
	   the bottom 12 bits (on the 386) are:
	   x x x 0 0 Dirty Accessed 0 0 User Writable Present
	   let the User and Writable bits be set by Privilege */

	Privilege = (Privilege & PRIV_ALL)/* | PRIV_PRES*/;

	//wprintf(L"map %x => %x: ", Virt, Phys);
	for (; pages > 0; pages--, Virt += PAGE_SIZE, Phys += PAGE_SIZE)
	{
		/* get top-level page offset (i.e. page directory entry on x86)
		   from virtual address */
		PageDirEnt = PAGE_DIRENT(Virt);

		if (PageDir[PageDirEnt] == 0)
		/* the page directory entry is zero, which means there is no corresponding
		   page table. Get another 4K of memory and use it to create a new page table. */
		{
			Temp = memAlloc();
			//wprintf(L"%new page = %p: ", Virt, Temp);
			if (!Temp)
				return false;

			/* zero the new page table */
			i386_lmemset32(Temp, 0, PAGE_SIZE);

			/* point page directory entry to new page table */

			/* xxx - Privilege of PageDir is not necessarily Privilege of PageTable
			   e.g. call this function while creating the code pages, and the
			   page table ends up read-only (so ALL Ring 3 pages will read-only!!!!) */

			PageDir[PageDirEnt] = Temp | PRIV_PRES | PRIV_WR | PRIV_USER;
		}

		/* get bottom-level page offset (i.e. page table entry on x86)
		   from virtual address */
		Temp = PageDir[PageDirEnt] & -PAGE_SIZE;
		PageTabOff = PAGE_TABENT(Virt);

		// point page table entry to physical address, and set privilege bits
		i386_lpoke32(Temp + PageTabOff * 4, PAGE_NUM(Phys) | Privilege);
	}

	//wprintf(L"done\n");
	return true;
}

//! Retrieves the linear-to-physical address mapping for the specified address.
/*!
 *	\param	pPageDir	The page directory to query
 *	\param	pAddress	The address to look up
 *	\return	The page table entry associated with the page at pAddress, 
 *		including the physical address and the privilege flags. Returns NULL
 *		if the page has not been mapped.
 */
dword memTranslate(const dword* pPageDir, const void* pAddress)
{
	dword PageDirEnt, PageTable, PageTabOff;

	PageDirEnt = PAGE_DIRENT((dword) pAddress);
	PageTable = (pPageDir[PageDirEnt]) & -PAGE_SIZE;
	if (!PageTable)
		return NULL;

	PageTabOff = PAGE_TABENT((dword) pAddress);
	asm("movl %%gs:(%1),%0" : "=r" (PageTable): "r" (PageTable + PageTabOff * 4));
	return PageTable;
}

#define PAGETABLE_MAP	0xdfc00000
#define ADDR_TO_PDE(v) (addr_t*)(PAGEDIRECTORY_MAP + \
                                (((addr_t)v / (1024 * 1024))&(~0x3)))
#define ADDR_TO_PTE(v) (addr_t*)(PAGETABLE_MAP + ((((addr_t)v / 1024))&(~0x3)))

addr_t* cur_page_dir = (addr_t*) PAGETABLE_MAP;

void memFreeRange(page_pool_t* pool, addr_t start, addr_t end)
{
	int i;

	//start = (start + PAGE_SIZE) & -PAGE_SIZE;
	start &= -PAGE_SIZE;
	end = (end + PAGE_SIZE / 2) & -PAGE_SIZE;
	wprintf(L"memFreeRange: %x to %x", start, end);

	start >>= PAGE_BITS;
	end >>= PAGE_BITS;
	wprintf(L" (%d pages)\n", end - start);
	for (i = 0; i < end - start; i++)
		pool->pages[i + pool->free_pages] = (i + start) * PAGE_SIZE;

	pool->free_pages += end - start;
}

//! Initialises the physical memory manager and creates the initial kernel page
//!		mappings.
/*!
 *	Called by main() during kernel startup.
 *	\return	true if initialisation succeeded, false otherwise. If this function 
 *		returns false then the system will be unstable.
 */
bool INIT_CODE memInit()
{
	addr_t pagedir_phys;
	int num_pages;
	addr_t* pages;
	
	num_pages = _sysinfo.memory_top / PAGE_SIZE;
	_sysinfo.kernel_data = _sysinfo.kernel_size + ebss - edata + num_pages * sizeof(addr_t);
	pages = (addr_t*) ebss;
	i386_lmemset32((addr_t) pages, 0, num_pages * sizeof(addr_t));
	
	pool_all.num_pages = num_pages - NUM_LOW_PAGES;
	pool_all.pages = pages + NUM_LOW_PAGES;
	pool_all.free_pages = 0;

	pool_low.num_pages = NUM_LOW_PAGES;
	pool_low.pages = pages;
	pool_low.free_pages = 0;

	/* Free memory below the BIOS */
	memFreeRange(&pool_low, 0x5000, 0xA0000);

	/*memFreeRange(&pool_all, 
		0x100000, _sysinfo.kernel_phys);
	memFreeRange(&pool_all, 
		_sysinfo.kernel_phys + _sysinfo.kernel_data, _sysinfo.ramdisk_phys);
	memFreeRange(&pool_all, 
		_sysinfo.ramdisk_phys + _sysinfo.ramdisk_size, _sysinfo.memory_top);*/

	/* Free memory between 1MB and the ramdisk */
	memFreeRange(&pool_all, 0x100000, _sysinfo.ramdisk_phys);
	memFreeRange(&pool_all, 
		_sysinfo.ramdisk_phys + _sysinfo.ramdisk_size, _sysinfo.memory_top);

	/*memMark(0, 5, PAGE_RESERVED);

	memMark(0xA0, 0x100, PAGE_RESERVED);
	
	memMark(_sysinfo.ramdisk_phys / PAGE_SIZE,
		(_sysinfo.ramdisk_phys + _sysinfo.ramdisk_size) / PAGE_SIZE, PAGE_RESERVED);
	memMark(_sysinfo.kernel_phys / PAGE_SIZE,
		(_sysinfo.kernel_phys + _sysinfo.kernel_data) / PAGE_SIZE, PAGE_RESERVED);*/
	
	/* Map physical memory except bottom 4KB */
	memMap(kernel_pagedir, PAGE_SIZE, PAGE_SIZE, 
		_sysinfo.memory_top / PAGE_SIZE - 1, PRIV_KERN | PRIV_WR | PRIV_PRES);
	/* Map graphics memory */
	memMap(kernel_pagedir, 0xa0000, 0xa0000, 16, PRIV_USER | PRIV_WR | PRIV_PRES);
	/* Map text memory */
	memMap(kernel_pagedir, 0xb8000, 0xb8000, 1, PRIV_USER | PRIV_WR | PRIV_PRES);
	/* Map kernel above 0x80000000 */
	memMap(kernel_pagedir, (addr_t) scode, _sysinfo.kernel_phys, 
		_sysinfo.kernel_data / PAGE_SIZE, PRIV_KERN | PRIV_WR | PRIV_PRES);
	//memMap(kernel_pagedir, 0xff000000, 0xff000000, 1048576 / 4096, PRIV_USER | PRIV_WR);

	pagedir_phys = (addr_t) kernel_pagedir - (addr_t) scode + _sysinfo.kernel_phys;
	//kernel_pagedir[PAGE_DIRENT((addr_t) cur_page_dir)] = pagedir_phys | PRIV_KERN | PRIV_WR | PRIV_PRES;
	asm("mov	%0, %%cr3" : : "r" (pagedir_phys));
	
	//wprintf(L"main (%p) maps to %x\n", main, memTranslate(kernel_pagedir, main));
	//wprintf(L"_con_x (%p) maps to %x\n", &_con_x, memTranslate(kernel_pagedir, &_con_x));
	//wprintf(L"text video buffer maps to %x\n", memTranslate(kernel_pagedir, (void*) 0xb8000));
	//wprintf(L"cur_page_dir = %x\n", kernel_pagedir[PAGE_DIRENT((addr_t) cur_page_dir)]);
	//wprintf(L"addr 0x100000 = %p = %x\n", 
		//ADDR_TO_PTE(0x10000), *ADDR_TO_PTE(0x10000));

	asm("mov	$0x20,%%eax ;"
		"mov	%%eax,%%ds ;"
		"mov	%%eax,%%es ;"
		"mov	%%eax,%%fs ;"
		"mov	%%eax,%%gs ;"
		"mov	%%eax,%%ss ;" : : : "eax");
	asm("mov	%%cr0,%%eax ;"
		"or		$0x80000000,%%eax ;"
		"mov	%%eax,%%cr0" : : : "eax");
	asm("ljmp	$0x18,$_paging ;"
		"_paging:");

	return true;
}

