/*
 * \brief   Virtual-memory space
 * \author  Norman Feske
 * \date    2015-05-04
 */

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

#ifndef _CORE__INCLUDE__VM_SPACE_H_
#define _CORE__INCLUDE__VM_SPACE_H_

/* Genode includes */
#include <util/bit_allocator.h>
#include <util/reconstructible.h>
#include <base/log.h>
#include <base/thread.h>
#include <base/session_label.h>

/* core includes */
#include <base/internal/capability_space_sel4.h>
#include <base/internal/stack_area.h>
#include <page_table_registry.h>
#include <cnode.h>
#include <cap_sel_alloc.h>
#include <core_cspace.h>

namespace Genode { class Vm_space; }


class Genode::Vm_space
{
	private:

		Session_label _pd_label;

		Cap_sel_alloc &_cap_sel_alloc;

		Page_table_registry &_page_table_registry;

		unsigned const _id;
		Cap_sel  const _pd_sel;

		Range_allocator &_phys_alloc;

		enum {
			/**
			 * Number of entries of 3rd-level VM CNode ('_vm_3rd_cnode')
			 */
			VM_3RD_CNODE_SIZE_LOG2 = (CONFIG_WORD_SIZE == 32) ? 8 : 7,

			/**
			 * Number of entries of each leaf CNodes
			 */
			LEAF_CNODE_SIZE_LOG2 = (CONFIG_WORD_SIZE == 32) ? 8 : 7,
			LEAF_CNODE_SIZE      = 1UL << LEAF_CNODE_SIZE_LOG2,

			/**
			 * Number of leaf CNodes
			 */
			NUM_LEAF_CNODES_LOG2 = (CONFIG_WORD_SIZE == 32) ? 6 : 5,
			NUM_LEAF_CNODES      = 1UL << NUM_LEAF_CNODES_LOG2,

			/**
			 * Maximum number of page tables and page frames for the VM space
			 */
			NUM_VM_SEL_LOG2 = LEAF_CNODE_SIZE_LOG2 + NUM_LEAF_CNODES_LOG2,

			/**
			 * Number of entries of the VM padding CNode ('_vm_pad_cnode')
			 */
			VM_PAD_CNODE_SIZE_LOG2 = 32 - Core_cspace::NUM_TOP_SEL_LOG2
			                       - VM_3RD_CNODE_SIZE_LOG2 - LEAF_CNODE_SIZE_LOG2,
		};

		Cnode &_top_level_cnode;
		Cnode &_phys_cnode;

		/**
		 * 2nd-level CNode for aligning '_vm_cnode' with the LSB of the CSpace
		 */
		Cnode _vm_pad_cnode;

		/**
		 * 3rd-level CNode that hosts the leaf CNodes
		 */
		Cnode _vm_3rd_cnode;

		/***
		 * 4th-level CNode for storing page-table and page-frame capabilities
		 */
		class Leaf_cnode
		{
			private:

				/*
				 * We leave the CNode unconstructed at the time of its
				 * instantiation to be able to create an array of 'Leaf_cnode'
				 * objects (where we cannot pass any arguments to the
				 * constructors of the individual objects).
				 */
				Constructible<Cnode> _cnode { };

			public:

				void construct(Cap_sel_alloc   &cap_sel_alloc,
				               Cap_sel          core_cnode_sel,
				               Range_allocator &phys_alloc)
				{
					_cnode.construct(core_cnode_sel, cap_sel_alloc.alloc(),
					                 LEAF_CNODE_SIZE_LOG2, phys_alloc);
				}

				void destruct(Cap_sel_alloc &cap_sel_alloc,
				               Range_allocator &phys_alloc)
				{
					_cnode->destruct(phys_alloc);
					cap_sel_alloc.free(_cnode->sel());
				}

				Cnode &cnode() { return *_cnode; }
		};

		Leaf_cnode _vm_cnodes[NUM_LEAF_CNODES];

	public:

		/**
		 * Allocator for the selectors within '_vm_cnodes'
		 */
		using Selector_allocator = Bit_allocator<1UL << NUM_VM_SEL_LOG2>;

		class Alloc_page_table_failed : Exception { };

	private:

		Selector_allocator _sel_alloc { };

		/**
		 * Return leaf CNode that contains an index allocated from '_sel_alloc'
		 */
		Cnode &_leaf_cnode(unsigned idx)
		{
			return _vm_cnodes[idx >> LEAF_CNODE_SIZE_LOG2].cnode();
		}

		/**
		 * Return entry within leaf cnode
		 */
		Cnode_index _leaf_cnode_entry(unsigned idx) const
		{
			return Cnode_index(idx & (LEAF_CNODE_SIZE - 1));
		}

		Lock _lock { };

		/**
		 * Return selector for a capability slot within '_vm_cnodes'
		 */
		addr_t _idx_to_sel(addr_t idx) const { return (_id << 20) | idx; }


		template <typename FN>
		void _flush(bool const flush_support, FN const &fn)
		{
			if (!flush_support) {
				warning("mapping cache full, but can't flush");
				throw;
			}

			warning("flush page table entries - mapping cache full - PD: ",
			        _pd_label.string());

			_page_table_registry.flush_pages(fn);

		}


		template <typename FN>
		bool _map_frame(addr_t const from_phys, addr_t const to_dest,
		                Cache_attribute const cacheability,
		                bool const writable, bool const executable,
		                bool const flush_support, bool guest, FN const &fn)
		{
			if (_page_table_registry.page_frame_at(to_dest)) {
				/*
				 * Valid behaviour if multiple threads concurrently
				 * causing the same page-fault. For the first thread the
				 * fault will be resolved, and the next thread will/would do
				 * it again. We just skip this attempt in order to avoid
				 * wasting of resources (idx selectors, creating kernel
				 * capabilities, causing kernel warning ...).
				 */
				return true;
			}
			/* allocate page-table-entry selector */
			addr_t pte_idx;
			try { pte_idx = _sel_alloc.alloc(); }
			catch (Selector_allocator::Out_of_indices) {

				/* free all page-table-entry selectors and retry once */
				_flush(flush_support, fn);
				pte_idx = _sel_alloc.alloc();
			}

			/*
			 * Copy page-frame selector to pte_sel
			 *
			 * This is needed because each page-frame selector can be
			 * inserted into only a single page table.
			 */
			_leaf_cnode(pte_idx).copy(_phys_cnode,
			                          Cnode_index(from_phys >> get_page_size_log2()),
			                          Cnode_index(_leaf_cnode_entry(pte_idx)));

			/* remember relationship between pte_sel and the virtual address */
			try { _page_table_registry.insert_page_frame(to_dest, Cap_sel(pte_idx)); }
			catch (Page_table_registry::Mapping_cache_full) {

				/* free all entries of mapping cache and re-try once */
				_flush(flush_support, fn);
				_page_table_registry.insert_page_frame(to_dest, Cap_sel(pte_idx));
			}

			/*
			 * Insert copy of page-frame selector into page table
			 */
			long ret = _map_page(Cap_sel(pte_idx), to_dest, cacheability,
			                     writable, executable, guest);
			if (ret != seL4_NoError) {
				error("seL4_*_Page_Map ", Hex(from_phys), "->",
				      Hex(to_dest), " returned ", ret);
				return false;
			}
			return true;
		}

		/**
		 * Platform specific map/unmap of a page frame
		 */
		long _map_page(Genode::Cap_sel const &idx, Genode::addr_t const virt,
		               Cache_attribute const cacheability, bool const write,
		               bool const writable, bool guest);
		long _unmap_page(Genode::Cap_sel const &idx);
		long _invalidate_page(Genode::Cap_sel const &, seL4_Word const,
		                      seL4_Word const);

		/**
		 * Allocate and install page structures for the protection domain.
		 *
		 * \throw Alloc_page_table_failed
		 */
		template <typename KOBJ>
		Cap_sel _alloc_and_map(addr_t const virt,
			                   long (&map_fn)(Cap_sel, Cap_sel, addr_t),
			                   addr_t &phys)
		{
			/* allocate page-* selector */
			addr_t const idx = _sel_alloc.alloc();

			try {
				phys = Untyped_memory::alloc_page(_phys_alloc);
				seL4_Untyped const service = Untyped_memory::untyped_sel(phys).value();
				create<KOBJ>(service, _leaf_cnode(idx).sel(),
				             _leaf_cnode_entry(idx));
			} catch (...) {
				/* XXX free idx, revert untyped memory, phys_addr, */
				 throw Alloc_page_table_failed();
			}

			Cap_sel const pt_sel(_idx_to_sel(idx));

			long const result = map_fn(pt_sel, _pd_sel, virt);
			if (result != seL4_NoError)
				error("seL4_*_Page*_Map(,", Hex(virt), ") returned ",
				      result);

			return Cap_sel(idx);
		}

		void _unmap_and_free(Cap_sel const idx, addr_t const paddr)
		{
			_leaf_cnode(idx.value()).remove(idx);

			_sel_alloc.free(idx.value());

			Untyped_memory::free_page(_phys_alloc, paddr);
		}

	public:

		/**
		 * Constructor
		 *
		 * \param pd_sel              selector for page directory
		 * \param cap_sel_alloc       capability-selector allocator used for
		 *                            VM CNodes
		 * \param phys_alloc          backing store for the CNodes
		 * \param top_level_cnode     top-level CNode to insert 'vm_pad_cnode_sel'
		 * \param id                  ID used as index in 'top_level_cnode'
		 * \param page_table_registry association of VM CNode selectors with
		 *                            with virtual addresses
		 */
		Vm_space(Cap_sel              pd_sel,
		         Cap_sel_alloc       &cap_sel_alloc,
		         Range_allocator     &phys_alloc,
		         Cnode               &top_level_cnode,
		         Cnode               &core_cnode,
		         Cnode               &phys_cnode,
		         unsigned             id,
		         Page_table_registry &page_table_registry,
		         const char *         label)
		:
			_pd_label(label),
			_cap_sel_alloc(cap_sel_alloc),
			_page_table_registry(page_table_registry),
			_id(id), _pd_sel(pd_sel),
			_phys_alloc(phys_alloc),
			_top_level_cnode(top_level_cnode),
			_phys_cnode(phys_cnode),
			_vm_pad_cnode(core_cnode.sel(),
			              Cnode_index(_cap_sel_alloc.alloc().value()),
			              VM_PAD_CNODE_SIZE_LOG2, phys_alloc),
			_vm_3rd_cnode(core_cnode.sel(),
			              Cnode_index(_cap_sel_alloc.alloc().value()),
			              VM_3RD_CNODE_SIZE_LOG2, phys_alloc)
		{
			Cnode_base const cspace(Cap_sel(seL4_CapInitThreadCNode), 32);

			for (unsigned i = 0; i < NUM_LEAF_CNODES; i++) {

				/* init leaf VM CNode */
				_vm_cnodes[i].construct(_cap_sel_alloc, core_cnode.sel(), phys_alloc);

				/* insert leaf VM CNode into 3nd-level VM CNode */
				_vm_3rd_cnode.copy(cspace, _vm_cnodes[i].cnode().sel(), Cnode_index(i));
			}

			/* insert 3rd-level VM CNode into 2nd-level VM-pad CNode */
			_vm_pad_cnode.copy(cspace, _vm_3rd_cnode.sel(), Cnode_index(0));

			/* insert 2nd-level VM-pad CNode into 1st-level CNode */
			_top_level_cnode.copy(cspace, _vm_pad_cnode.sel(), Cnode_index(_id));
		}

		~Vm_space()
		{
			/* delete copy of the mapping's page-frame selectors */
			_page_table_registry.flush_all([&] (Cap_sel const &idx, addr_t const virt) {

				long err = _unmap_page(idx);
				if (err != seL4_NoError)
					error("unmap ", Hex(virt), " failed, ", idx, " res=", err);

				_leaf_cnode(idx.value()).remove(_leaf_cnode_entry(idx.value()));

				_sel_alloc.free(idx.value());

				return true;
			}, [&] (Cap_sel const &idx, addr_t const paddr) {
				_unmap_and_free(idx, paddr);
			});

			for (unsigned i = 0; i < NUM_LEAF_CNODES; i++) {
				_vm_3rd_cnode.remove(Cnode_index(i));
				_vm_cnodes[i].destruct(_cap_sel_alloc, _phys_alloc);
			}
			_vm_pad_cnode.remove(Cnode_index(0));
			_top_level_cnode.remove(Cnode_index(_id));

			_vm_3rd_cnode.destruct(_phys_alloc);
			_vm_pad_cnode.destruct(_phys_alloc);

			_cap_sel_alloc.free(_vm_3rd_cnode.sel());
			_cap_sel_alloc.free(_vm_pad_cnode.sel());
		}

		bool map(addr_t const from_phys, addr_t const to_virt,
		         size_t const num_pages, Cache_attribute const cacheability,
		         bool const writable, bool const executable, bool flush_support)
		{
			auto fn_unmap = [&] (Cap_sel const &idx, addr_t const v_addr)
			{
				/* XXX - INITIAL_IPC_BUFFER can't be re-mapped currently */
				if (v_addr == 0x1000)
					return false;
				/* XXX - UTCB can't be re-mapped currently */
				if (stack_area_virtual_base() <= v_addr
				    && (v_addr < stack_area_virtual_base() +
				                 stack_area_virtual_size())
				    && !((v_addr + 0x1000) & (stack_virtual_size() - 1)))
						return false;

				long err = _unmap_page(idx);
				if (err != seL4_NoError)
					error("unmap failed, idx=", idx, " res=", err);

				_leaf_cnode(idx.value()).remove(_leaf_cnode_entry(idx.value()));

				_sel_alloc.free(idx.value());

				return true;
			};

			Lock::Guard guard(_lock);

			bool ok = true;

			for (size_t i = 0; i < num_pages; i++) {
				off_t const offset = i << get_page_size_log2();

				if (_map_frame(from_phys + offset, to_virt + offset,
				               cacheability, writable, executable,
				               flush_support, false /* host page table */,
				               fn_unmap))
					continue;

				ok = false;

				warning("mapping failed ", Hex(from_phys + offset),
				        " -> ", Hex(to_virt + offset), " ",
				        !flush_support ? "core" : "");
			}

			return ok;
		}

		void map_guest(addr_t const from_phys, addr_t const guest_phys,
		               size_t const num_pages, Cache_attribute const cacheability,
		               bool const writable, bool const executable, bool flush_support)
		{
			auto fn_unmap = [&] (Cap_sel const &idx, addr_t const) {
				long err = _unmap_page(idx);
				if (err != seL4_NoError)
					error("unmap failed, idx=", idx, " res=", err);

				_leaf_cnode(idx.value()).remove(_leaf_cnode_entry(idx.value()));

				_sel_alloc.free(idx.value());

				return true;
			};

			Lock::Guard guard(_lock);

			for (size_t i = 0; i < num_pages; i++) {
				off_t const offset = i << get_page_size_log2();

				_map_frame(from_phys + offset, guest_phys + offset,
				           cacheability, writable, executable,
				           flush_support, true /* guest page table */,
				           fn_unmap);
			}
		}

		bool unmap(addr_t const virt, size_t const num_pages,
		           bool const invalidate = false)
		{
			bool unmap_success = true;

			Lock::Guard guard(_lock);

			for (size_t i = 0; unmap_success && i < num_pages; i++) {
				off_t const offset = i << get_page_size_log2();

				_page_table_registry.flush_page(virt + offset, [&] (Cap_sel const &idx, addr_t) {

					if (invalidate) {
						long result = _invalidate_page(idx, virt + offset,
						                               virt + offset + 4096);
						if (result != seL4_NoError)
							error("invalidating ", Hex(virt + offset),
							      " failed, idx=", idx, " result=", result);
					}

					long result = _unmap_page(idx);
					if (result != seL4_NoError) {
						error("unmap ", Hex(virt + offset), " failed, idx=",
						      idx, " result=", result);
						unmap_success = false;
					}

					_leaf_cnode(idx.value()).remove(_leaf_cnode_entry(idx.value()));

					_sel_alloc.free(idx.value());
				});
			}
			return unmap_success;
		}

		void unsynchronized_alloc_page_tables(addr_t const start,
		                                      addr_t const size);

		void unsynchronized_alloc_guest_page_tables(addr_t, addr_t);

		void alloc_page_tables(addr_t const start, addr_t const size)
		{
			Lock::Guard guard(_lock);
			unsynchronized_alloc_page_tables(start, size);
		}

		void alloc_guest_page_tables(addr_t const start, addr_t const size)
		{
			Lock::Guard guard(_lock);
			unsynchronized_alloc_guest_page_tables(start, size);
		}

		Session_label const & pd_label() const { return _pd_label; }
};

#endif /* _CORE__INCLUDE__VM_SPACE_H_ */
