/*
 * Copyright (c) 2007, 2008 University of Tsukuba
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the University of Tsukuba nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
/*
 * Copyright (c) 2010-2012 Yuichi Watanabe
 */

#include <core/assert.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/string.h>
#include "asm.h"
#include "comphappy.h"
#include "entry.h"
#include "mm.h"
#include "panic.h"
#include "thread.h"
#include <core/types.h>

static spinlock_t mm_lock_process_virt_to_phys = SPINLOCK_INITIALIZER;;

static void
process_create_initial_map (void *virt, phys_t phys)
{
	u32 *pde;
	int i;

	pde = virt;
	/* clear PDEs for user area */
	for (i = 0; i < 0x400; i++)
		pde[i] = 0;
}

int
mm_process_alloc (phys_t *phys2)
{
	void *l2table;
	void *l3table;
#ifdef __x86_64__
	void *l4table;
#endif
	phys_t phys;
	vmmerr_t err;

	err = alloc_page(&l2table, &phys);
	if (err) {
		return -1;
	}
	process_create_initial_map (l2table, phys);
	err = alloc_page(&l3table, phys2);
	if (err) {
		free_page(l2table);
		return -1;
	}
	memset(l3table, 0, PAGESIZE);
	/* virt 0x00000000 process area */
	((u64 *)l3table)[0] = phys | PDPE_ATTR;
	/* virt 0x40000000 vmm area */
	((u64 *)l3table)[1] = vmm_main_pdp[1];
	/* virt 0x80000000 mapmem area */
	((u64 *)l3table)[2] = vmm_main_pdp[2];
#ifdef __x86_64__
	phys = *phys2;
	err = alloc_page(&l4table, phys2);
	if (err) {
		free_page(l3table);
		free_page(l2table);
		return -1;
	}
	memcpy (l4table, vmm_main_pml4, PAGESIZE);
	((u64 *)l4table)[0] = phys | PDE_P_BIT | PDE_RW_BIT | PDE_US_BIT;
#endif
	return 0;
}

void
mm_process_free (phys_t phys)
{
#ifdef __x86_64__
	free_page_phys (((u64 *)phys_to_virt (((u64 *)phys_to_virt (phys))[0]
					      & ~PAGESIZE_MASK))[0]);
#endif
	free_page_phys (((u64 *)phys_to_virt (phys))[0]);
	free_page_phys (phys);
}

static vmmerr_t
mm_process_mappage(virt_t virt, u64 pte)
{
	ulong cr3;
	pmap_t m;
	vmmerr_t err;

	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	pmap_setvirt (&m, virt, 1);
	err = pmap_autoalloc (&m);
	if (err) {
		printf("mm_process_mappage: Failed to prepare page tables.");
		goto out;
	}
	ASSERT (!(pmap_read (&m) & PTE_P_BIT));
	pmap_write (&m, pte, 0xFFF);
out:
	pmap_close (&m);
	asm_wrcr3 (cr3);
	return err;
}

static vmmerr_t
mm_process_mapstack (virt_t virt)
{
	ulong cr3;
	pmap_t m;
	u64 pte;
	void *tmp;
	phys_t phys;
	vmmerr_t err;

	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	pmap_setvirt (&m, virt, 1);
	err = pmap_autoalloc (&m);
	if (err) {
		printf("mm_process_mapstack: Failed to prepare page tables.");
		goto out;
	}
	pte = pmap_read (&m);
	if (!(pte & PTE_P_BIT)) {
		err = alloc_page (&tmp, &phys);
		if (err) {
			printf("mm_process_mapstack: Failed to alloc a page.");
			goto out;
		}
		memset (tmp, 0, PAGESIZE);
		pte = phys | PTE_P_BIT | PTE_RW_BIT | PTE_US_BIT;
	} else {
		pte &= ~PTE_AVAILABLE1_BIT;
	}
	pmap_write (&m, pte, 0xFFF);
out:
	pmap_close (&m);
	asm_wrcr3 (cr3);
	return err;
}

int
mm_process_map_alloc (virt_t virt, uint len)
{
	void *tmp;
	phys_t phys;
	uint npages;
	virt_t v;
	vmmerr_t err;

	if (virt >= VMM_START_VIRT)
		return -1;
	if (virt + len >= VMM_START_VIRT)
		return -1;
	if (virt > virt + len)
		return -1;
	len += virt & PAGESIZE_MASK;
	virt -= virt & PAGESIZE_MASK;
	npages = (len + PAGESIZE - 1) >> PAGESIZE_SHIFT;
	mm_process_unmap (virt, len);
	for (v = virt; npages > 0; v += PAGESIZE, npages--) {
		err = alloc_page (&tmp, &phys);
		if (err) {
			printf("Failed to allocate for a process.");
			goto err;
		}
		memset (tmp, 0, PAGESIZE);
		if (mm_process_mappage(v, phys | PTE_P_BIT | PTE_RW_BIT |
				       PTE_US_BIT)) {
			printf("Failed to map to a process.");
			goto err;
		}
	}
	return 0;

err:
	mm_process_unmap(virt, len);
	return -1;
}

static bool
alloc_sharedmem_sub (u32 virt)
{
	pmap_t m;
	ulong cr3;
	u64 pte;

	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	pmap_setvirt (&m, virt, 1);
	pte = pmap_read (&m);
	pmap_close (&m);
	if (!(pte & PTE_P_BIT))
		return true;
	return false;
}

int
mm_process_map_shared_physpage (virt_t virt, phys_t phys, bool rw)
{
	virt &= ~PAGESIZE_MASK;
	if (virt >= VMM_START_VIRT)
		return -1;
	mm_process_unmap (virt, PAGESIZE);
	if (mm_process_mappage (virt, phys | PTE_P_BIT |
				(rw ? PTE_RW_BIT : 0) | PTE_US_BIT |
				PTE_AVAILABLE2_BIT)) {
		return -1;
	}
	return 0;
}

static int
process_virt_to_phys (phys_t procphys, virt_t virt, phys_t *phys)
{
	u64 pte;
	int r = -1;
	pmap_t m;

	spinlock_lock (&mm_lock_process_virt_to_phys);
	pmap_open_vmm (&m, procphys, PMAP_LEVELS);
	pmap_setvirt (&m, virt, 1);
	pte = pmap_read (&m);
	if (pte & PTE_P_BIT) {
		*phys = (pte & PTE_ADDR_MASK) | (virt & ~PTE_ADDR_MASK);
		r = 0;
	}
	pmap_close (&m);
	spinlock_unlock (&mm_lock_process_virt_to_phys);
	return r;
}

void *
mm_process_map_shared (phys_t procphys, void *buf, uint len, bool rw)
{
	virt_t uservirt = 0x30000000;
	virt_t virt, virt_s, virt_e, off;
	phys_t phys;

	VAR_IS_INITIALIZED(phys);

	if (len == 0)
		return NULL;
	virt_s = (virt_t)buf;
	virt_e = (virt_t)buf + len;
retry:
	for (virt = virt_s & ~0xFFF; virt < virt_e; virt += PAGESIZE) {
		uservirt -= PAGESIZE;
		ASSERT (uservirt > 0x20000000);
		if (!alloc_sharedmem_sub (uservirt))
			goto retry;
	}
	for (virt = virt_s & ~0xFFF, off = 0; virt < virt_e;
	     virt += PAGESIZE, off += PAGESIZE) {
		if (process_virt_to_phys (procphys, virt, &phys)) {
			mm_process_unmap ((virt_t)(uservirt + (virt_s &
							       0xFFF)),
					  len);
			return NULL;
		}
		if (mm_process_map_shared_physpage (uservirt + off, phys, rw)) {
			panic("mm_process_map_shared: Fail to map.");
		}
	}
	return (void *)(uservirt + (virt_s & 0xFFF));
}

static bool
alloc_stack_sub (virt_t virt)
{
	pmap_t m;
	ulong cr3;
	u64 pte;

	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	pmap_setvirt (&m, virt, 1);
	pte = pmap_read (&m);
	pmap_close (&m);
	if (!(pte & PTE_P_BIT))
		return true;
	if (pte & PTE_AVAILABLE1_BIT)
		return true;
	return false;
}

virt_t
mm_process_map_stack (uint len)
{
	uint i;
	virt_t virt = 0x3FFFF000;
	uint npages;

	npages = (len + PAGESIZE - 1) >> PAGESIZE_SHIFT;
retry:
	virt -= PAGESIZE;
	for (i = 0; i < npages; i++) {
		ASSERT (virt - i * PAGESIZE > 0x20000000);
		if (!alloc_stack_sub (virt - i * PAGESIZE)) {
			virt = virt - i * PAGESIZE;
			goto retry;
		}
	}
	for (i = 0; i < npages; i++)
		mm_process_mapstack (virt - i * PAGESIZE);
	return virt + PAGESIZE;
}

int
mm_process_unmap (virt_t virt, uint len)
{
	uint npages;
	virt_t v;
	u64 pte;
	ulong cr3;
	pmap_t m;

	if (virt >= VMM_START_VIRT)
		return -1;
	if (virt + len >= VMM_START_VIRT)
		return -1;
	if (virt > virt + len)
		return -1;
	len += virt & PAGESIZE_MASK;
	virt -= virt & PAGESIZE_MASK;
	npages = (len + PAGESIZE - 1) >> PAGESIZE_SHIFT;
	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	for (v = virt; npages > 0; v += PAGESIZE, npages--) {
		pmap_setvirt (&m, v, 1);
		pte = pmap_read (&m);
		if (!(pte & PDE_P_BIT))
			continue;
		if (!(pte & PTE_AVAILABLE2_BIT)) /* if not shared memory */
			free_page_phys (pte);
		pmap_write (&m, 0, 0);
	}
	pmap_close (&m);
	asm_wrcr3 (cr3);
	return 0;
}

int
mm_process_unmapstack (virt_t virt, uint len)
{
	uint npages;
	virt_t v;
	u64 pte;
	ulong cr3;
	pmap_t m;

	if (virt >= VMM_START_VIRT)
		return -1;
	if (virt + len >= VMM_START_VIRT)
		return -1;
	if (virt > virt + len)
		return -1;
	len += virt & PAGESIZE_MASK;
	virt -= virt & PAGESIZE_MASK;
	npages = (len + PAGESIZE - 1) >> PAGESIZE_SHIFT;
	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	for (v = virt; npages > 0; v += PAGESIZE, npages--) {
		pmap_setvirt (&m, v, 1);
		pte = pmap_read (&m);
		if (!(pte & PDE_P_BIT))
			continue;
		if (pte & PTE_AVAILABLE2_BIT)
			continue;
		pmap_write (&m, pte | PTE_AVAILABLE1_BIT, 0xFFF);
	}
	pmap_close (&m);
	asm_wrcr3 (cr3);
	return 0;
}

void
mm_process_unmapall (void)
{
	virt_t v;
	u64 pde;
	pmap_t m;
	ulong cr3;

	ASSERT (!mm_process_unmap (0, VMM_START_VIRT - 1));
	asm_rdcr3 (&cr3);
	pmap_open_vmm (&m, cr3, PMAP_LEVELS);
	for (v = 0; v < VMM_START_VIRT; v += PAGESIZE2M) {
		pmap_setvirt (&m, v, 2);
		pde = pmap_read (&m);
		if (!(pde & PDE_P_BIT))
			continue;
		free_page_phys (pde);
		pmap_write (&m, 0, 0);
	}
	pmap_close (&m);
	asm_wrcr3 (cr3);
}

phys_t
mm_process_switch (phys_t switchto)
{
	ulong oldcr3;

	asm_rdcr3 (&oldcr3);
	asm_wrcr3 ((ulong)switchto);
	thread_set_process_switch (switchto);
	return oldcr3;
}
