/*
 * \brief  Platform driver - handling of IOMMUs controlled by the kernel
 * \author Alexander Boettcher
 * \author Stefan Kalkowski
 * \author Johannes Schlatow
 * \date   2013-02-10
 */

/*
 * Copyright (C) 2013-2023 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.
 */

/* Genode includes */
#include <base/attached_io_mem_dataspace.h>
#include <base/log.h>
#include <dataspace/client.h>
#include <region_map/client.h>
#include <pd_session/client.h>
#include <util/retry.h>

/* local includes */
#include <device.h>
#include <kernel_io_mmu.h>

using namespace Driver;


Kernel_io_mmu::Device_pd::Region_map_client::Attach_result
Kernel_io_mmu::Device_pd::Region_map_client::attach(Dataspace_capability ds,
                                                    Attr const &attr)
{
	for (;;) {
		Attach_result const result = Genode::Region_map_client::attach(ds, attr);
		if (result == Attach_error::OUT_OF_RAM) {
			if (!upgrade_ram())
				return result;
			continue;
		}
		if (result == Attach_error::OUT_OF_CAPS) {
			if (!upgrade_caps())
				return result;
			continue;
		}
		return result;
	}
}


bool Kernel_io_mmu::Device_pd::Region_map_client::upgrade_ram()
{
	Ram_quota const ram { 4096 };
	if (!_ram_guard.try_withdraw(ram))
		return false;

	_env.pd().transfer_quota(_pd.rpc_cap(), ram);
	return true;
}


bool Kernel_io_mmu::Device_pd::Region_map_client::upgrade_caps()
{
	Cap_quota const caps { 2 };
	if (!_cap_guard.try_withdraw(caps))
		return false;

	_env.pd().transfer_quota(_pd.rpc_cap(), caps);
	return true;
}


Kernel_io_mmu::Device_pd::Result
Kernel_io_mmu::Device_pd::add_range(Io_mmu::Range        const &range,
                                    addr_t               const,
                                    Dataspace_capability const cap)
{
	using namespace Genode;

	if (range.start == 0) return Error::INVALID_RANGE;

	for (;;) {
		Result const result = _pd.attach_dma(cap, range.start).convert<Result>(
			[&] (Ok) -> Result {
				/* trigger eager mapping of memory */
				switch (_pd.map(Pd_session::Virt_range { range.start,
				                                         range.size })) {
				case Pd_session::Map_result::OUT_OF_RAM:  return Error::OUT_OF_RAM;
				case Pd_session::Map_result::OUT_OF_CAPS: return Error::OUT_OF_CAPS;
				case Pd_session::Map_result::OK: ;
				};
				return Ok();
			},
			[&] (Pd_session::Attach_dma_error e) {
				switch (e) {
				case Pd_session::Attach_dma_error::OUT_OF_RAM:
				case Pd_session::Attach_dma_error::OUT_OF_CAPS:
				case Pd_session::Attach_dma_error::DENIED:
					error("Device PD: attach_dma denied!");
				}
				return Error::DENIED;
			}
		);

		if (result == Error::OUT_OF_RAM && _address_space.upgrade_ram())
			continue;

		if (result == Error::OUT_OF_CAPS && _address_space.upgrade_caps())
			continue;

		return result;
	}
}


void Kernel_io_mmu::Device_pd::remove_range(Io_mmu::Range const &range)
{
	_address_space.detach(range.start);
}


Kernel_io_mmu::Device_pd::Device_pd(Env &env, Ram_quota_guard &ram_guard,
                                    Cap_quota_guard &cap_guard,
                                    Allocator &md_alloc)

:
	Io_mmu::Domain(md_alloc),
	_pd(env, Pd_connection::Device_pd()),
	_address_space(env, _pd, ram_guard, cap_guard)
{
	_pd.ref_account(env.pd_session_cap());
}


void Kernel_io_mmu::enregister(Device const &device, Domain &domain)
{
	Device_pd &dpd = static_cast<Device_pd&>(domain);

	device.with_pci_config([&] (Device::Pci_config const &cfg) {
		Attached_io_mem_dataspace io_mem { _env, cfg.addr, 0x1000 };
		Pci::Bdf bdf {cfg.bus_num, cfg.dev_num, cfg.func_num};

		dpd._address_space.attach(io_mem.cap(), {
			.size       = 0x1000,  .offset    = { },
			.use_at     = { },     .at        = { },
			.executable = { },     .writeable = true
		}).with_result(
			[&] (Region_map::Range range) {

				/* trigger eager mapping of memory */
				dpd._pd.map(Pd_session::Virt_range { range.start, range.num_bytes });

				/* try to assign pci device to this protection domain */
				if (!dpd._pd.assign_pci(range.start, Pci::Bdf::rid(bdf)))
					log("Assignment of PCI device ", bdf, " to device PD failed, no IOMMU?!");

				/* we don't need the mapping anymore */
				dpd._address_space.detach(range.start);
			},
			[&] (Region_map::Attach_error) {
				error("failed to attach PCI device to device PD"); }
		);
	});
}


void Kernel_io_mmu::deregister(Device const &, Domain &)
{
	warning("Cannot unassign PCI device from device PD (not implemented by kernel).");
}



Io_mmu::Domain &
Kernel_io_mmu::create_domain(Allocator                  &md_alloc,
                             Ram_allocator              &,
                             Ram_quota_guard            &ram_guard,
                             Cap_quota_guard            &cap_guard)
{
	return *new (md_alloc) Device_pd(_env, ram_guard, cap_guard, md_alloc);
}


void Kernel_io_mmu::destroy_domain(Allocator &md_alloc, Driver::Io_mmu::Domain &domain)
{
	auto device_pd = static_cast<Device_pd *>(&domain);

	if (device_pd)
		destroy(md_alloc, device_pd);
}


Kernel_io_mmu::Kernel_io_mmu(Env                     &env,
                             Io_mmu_devices           &io_mmu_devices,
                             Device_name        const &name)
:
	Io_mmu(io_mmu_devices, name),
	_env(env)
{ };


Kernel_io_mmu::~Kernel_io_mmu() { }
