/*
 * \brief  Region map for l4lx support library.
 * \author Stefan Kalkowski
 * \date   2011-04-11
 */

/*
 * Copyright (C) 2011-2013 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU General Public License version 2.
 */

/* Genode includes */
#include <base/printf.h>
#include <rm_session/connection.h>
#include <base/capability.h>
#include <util/misc_math.h>

/* L4lx includes */
#include <env.h>

namespace Fiasco {
#include <l4/sys/types.h>
#include <l4/sys/task.h>
#include <l4/util/util.h>
}

using namespace L4lx;

static const bool DEBUG_SEARCH = false; /* print information about the search
                                           for alternative address ranges */

Region* Region_manager::find_region(Genode::addr_t *addr, Genode::size_t *size)
{
	Genode::Allocator_avl_base::Block *b = _find_by_address(*addr);
	if (!b)
		return 0;

	*addr = b->addr();
	*size = b->size();
	return b->used() ? metadata((void*)*addr) : 0;
}


void* Region_manager::attach(Genode::Dataspace_capability cap, const char* name)
{
	/* Put it in the dataspace tree */
	L4lx::Dataspace *ds =
		L4lx::Env::env()->dataspaces()->insert(name, cap);

	return attach(ds);
}


void* Region_manager::attach(Dataspace *ds)
{
	void* addr = Genode::env()->rm_session()->attach(ds->cap());
	alloc_addr(ds->size(), (Genode::addr_t)addr);
	metadata(addr, Region((Genode::addr_t)addr, ds->size(), ds));
	return addr;
}


bool Region_manager::attach_at(Dataspace *ds, Genode::size_t size,
                               Genode::size_t offset, void* addr)
{
	Genode::Allocator_avl_base::Block *b = _find_by_address((Genode::addr_t)addr);

	/* If the region is already known, it should have been reserved before */
	if (b && b->used()) {
		Region *r = metadata(addr);

		/* Sanity check */
		if (!r || r->addr() != (Genode::addr_t)addr ||
		    r->size() != ds->size() || r->ds()) {
			return false;
		}

		/* We have to detach the dataspace placeholder */
		Genode::env()->rm_session()->detach(addr);
	} else
		/* We have to reserve the area in our region map */
		alloc_addr(ds->size(), (Genode::addr_t)addr);

	/* Now call Genode's region map to really attach the dataspace */
	try {
		Genode::env()->rm_session()->attach(ds->cap(), size, offset,
		                                    true, (Genode::addr_t) addr);
	} catch(...) {
		return false;
	}
	metadata(addr, Region((Genode::addr_t)addr, ds->size(), ds));
	return true;
}


Region* Region_manager::reserve_range(Genode::size_t size, int align,
                                      Genode::addr_t start)
{
	using namespace Genode;
	void* addr = 0;
	addr_t original_start = start;

	while (true) {

		Rm_connection *rmc = 0;

		try {
			/*
			 * We attach a managed-dataspace as a placeholder to
			 * Genode's region-map
			 */
			rmc = new (env()->heap()) Rm_connection(0, size);
			addr = start ? env()->rm_session()->attach_at(rmc->dataspace(), start)
			             : env()->rm_session()->attach(rmc->dataspace());
			//PDBG("attach done addr=%p!", addr);
			break;
		} catch(Rm_session::Attach_failed e) {
			destroy(env()->heap(), rmc);
			 /* attach with pre-defined address failed, so search one */
			if (start) {
				/* the original start address might have a different alignment */
				addr_t aligned_start = align_addr(start, align);
				if (aligned_start != start) {
					if (DEBUG_SEARCH)
						PDBG("attach failed: start=%lx, trying %lx instead",
							 start, aligned_start);
					start = aligned_start;
				} else {
					if (start <= ((addr_t)~0 - 2*(1 << align) + 1)) {
						if (DEBUG_SEARCH)
							PDBG("attach failed: start=%lx, trying %lx instead",
								 start, start + (1 << align));
						start += (1 << align);
					} else {
						PWRN("attach failed: start=%lx, size=0x%zx, align=%d", original_start, size, align);
						return 0;
					}
				}
			} else {
				PWRN("attach failed: start=0, size=0x%zx, align=%d", size, align);
				return 0;
			}
		}
	}

	/*
	 * Mark the region reserved, in our region-map by setting the
	 * dataspace reference to zero.
	 */
	alloc_addr(size, (addr_t)addr);
	Region reg((addr_t)addr, size, 0);
	metadata(addr, reg);
	return metadata(addr);
}


void Region_manager::reserve_range(Genode::addr_t addr, Genode::size_t size,
                                   const char *name)
{
	L4lx::Dataspace *ds = new (Genode::env()->heap())
		L4lx::Dataspace(name, size, Genode::reinterpret_cap_cast<Genode::Dataspace>(Genode::Native_capability()));
	L4lx::Env::env()->dataspaces()->insert(ds);
	alloc_addr(size, (Genode::addr_t)addr);
	metadata((void*)addr, Region(addr, size, ds));
}


void Region_manager::dump()
{
	Genode::addr_t addr = 0;
	Genode::printf("Region map:\n");
	while (true) {
		Allocator_avl_base::Block *b = _find_by_address(addr);
		Region *r = metadata((void*)addr);
		if (!b)
			return;
		Genode::printf("     0x%08lx - 0x%08lx ",
		               b->addr(), b->addr() + b->size());
		if (b->used())
			Genode::printf("[%s]\n", (r && r->ds()) ? r->ds()->name() : "reserved");
		else
			Genode::printf("[unused]\n");
		addr = b->addr() + b->size();
	}
};


Mapping* Region_manager::_virt_to_phys(void *virt) {
	return _virt_tree.first() ? _virt_tree.first()->find_by_virt(virt) : 0; }


Phys_mapping* Region_manager::_phys_to_virt(void *phys) {
	return _phys_tree.first() ? _phys_tree.first()->find_by_phys(phys) : 0; }


void Region_manager::add_mapping(void *phys, void *virt, bool rw)
{
	Mapping *m = _virt_to_phys(virt);
	if (!m) {
		m = new (Genode::env()->heap()) Mapping(virt, phys, rw);
		_virt_tree.insert(m);
		Phys_mapping *p = _phys_to_virt(phys);
		if (!p) {
			p = new (Genode::env()->heap()) Phys_mapping(phys);
			_phys_tree.insert(p);
		}
		p->mappings()->insert(m);
	}
}


void Region_manager::remove_mapping(void *virt)
{
	using namespace Fiasco;

	l4_fpage_t fpage = l4_fpage((l4_addr_t)virt, L4_LOG2_PAGESIZE, L4_FPAGE_RW);
	l4_msgtag_t tag  = l4_task_unmap(L4_BASE_TASK_CAP, fpage, L4_FP_ALL_SPACES);
	if (l4_error(tag))
		PWRN("unmapping %p failed with error %ld!", virt, l4_error(tag));

	Mapping *m = _virt_to_phys(virt);
	if (m) {
		_virt_tree.remove(m);
		Phys_mapping *p = _phys_to_virt(m->phys());
		if (p) {
			p->mappings()->remove(m);
			if (!p->mappings()->first()) {
				_phys_tree.remove(p);
				Genode::destroy(Genode::env()->heap(), p);
			}
		}
		Genode::destroy(Genode::env()->heap(), m);
	}
}


void Region_manager::map(void *phys)
{
	using namespace Fiasco;

	Phys_mapping *p = _phys_to_virt(phys);
	if (p) {
		Mapping *m = p->mappings()->first();
		while (m) {
			if (!m->writeable())
				l4_touch_ro(phys, L4_PAGESIZE);
			else
				l4_touch_rw(phys, L4_PAGESIZE);

			l4_fpage_t snd_fpage = m->writeable()
				? l4_fpage((l4_addr_t)phys, L4_LOG2_PAGESIZE, L4_FPAGE_RW)
				: l4_fpage((l4_addr_t)phys, L4_LOG2_PAGESIZE, L4_FPAGE_RO);
			l4_msgtag_t tag = l4_task_map(L4_BASE_TASK_CAP, L4_BASE_TASK_CAP,
			                              snd_fpage, (l4_addr_t)m->virt());
			if (l4_error(tag)) {
				PERR("mapping from %p to %p failed with error %ld!",
					 phys, m->virt(), l4_error(tag));
			}
			m = m->next();
		}
	}
}


void* Region_manager::phys(void *virt)
{
	Mapping *m = _virt_to_phys(virt);
	return m ? m->phys() : 0;
}

