/*
 * \brief  PCI device component implementation
 * \author Alexander Boettcher
 */

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

#include "pci_session_component.h"
#include "pci_device_component.h"

Genode::Io_port_session_capability Platform::Device_component::io_port(Genode::uint8_t v_id)
{
	Genode::uint8_t max = sizeof(_io_port_conn) / sizeof(_io_port_conn[0]);
	Genode::uint8_t i = 0, r_id = 0;

	for (Resource res = resource(0); i < max; i++, res = resource(i))
	{
		if (res.type() != Resource::IO)
			continue;

		if (v_id != r_id) {
			r_id ++;
			continue;
		}

		if (_io_port_conn[v_id] != nullptr)
			return _io_port_conn[v_id]->cap();

		try {
			_io_port_conn[v_id] = new (_slab_ioport) Genode::Io_port_connection(res.base(), res.size());
			return _io_port_conn[v_id]->cap();
		} catch (...) {
			return Genode::Io_port_session_capability();
		}
	}

	return Genode::Io_port_session_capability();
}

Genode::Io_mem_session_capability Platform::Device_component::io_mem(Genode::uint8_t const v_id,
                                                                     Genode::Cache_attribute const caching,
                                                                     Genode::addr_t const offset,
                                                                     Genode::size_t const size)
{
	Genode::uint8_t max = sizeof(_io_mem) / sizeof(_io_mem[0]);
	Genode::uint8_t i = 0, r_id = 0;

	for (Resource res = resource(0); i < max; i++, res = resource(i))
	{
		if (res.type() != Resource::MEMORY)
			continue;

		if (v_id != r_id) {
			r_id ++;
			continue;
		}

		/* limit IO_MEM session size to resource size */
		Genode::size_t const res_size = Genode::min(size, res.size());

		if (offset >= res.size() || offset > res.size() - res_size)
			return Genode::Io_mem_session_capability();

		try {
			bool const wc = caching == Genode::Cache_attribute::WRITE_COMBINED;
			Io_mem * io_mem = new (_slab_iomem) Io_mem(res.base() + offset,
			                                           res_size, wc);
			_io_mem[i].insert(io_mem);
			return io_mem->cap();
		} catch (Genode::Allocator::Out_of_memory) {
			Genode::error("Quota_exceeded in Device_component::io_mem");
			throw Quota_exceeded();
		} catch (...) {
			return Genode::Io_mem_session_capability();
		}
	}

	return Genode::Io_mem_session_capability();
}

void Platform::Device_component::config_write(unsigned char address,
                                              unsigned value,
                                              Access_size size)
{
	/* white list of ports which we permit to write */
	switch (address) {
		case 0x40 ... 0xff:
			/* allow access to device-specific registers if not used by us */
			if (!_device_config.reg_in_use(&_config_access, address, size))
				break;

			Genode::error(_device_config, " write access to "
			              "address=", Genode::Hex(address), " "
			              "value=",   Genode::Hex(value),   " "
			              "size=",    Genode::Hex(size),    " "
			              "denied - it is used by the platform driver.");
			return;
		case PCI_CMD_REG: /* COMMAND register - first byte */
			if (size == Access_size::ACCESS_16BIT)
				break;
		case PCI_CMD_REG + 1: /* COMMAND register - second byte */
		case 0xd: /* Latency timer */
			if (size == Access_size::ACCESS_8BIT)
				break;
		default:
			Genode::warning(_device_config, " write access to "
			                "address=", Genode::Hex(address), " "
			                "value=",   Genode::Hex(value),   " "
			                "size=",    Genode::Hex(size),    " "
			                "got dropped");
			return;
	}

	/* assign device to device_pd */
	if (address == PCI_CMD_REG && value & PCI_CMD_DMA) {
		try {
			_session.assign_device(this);
		} catch (Platform::Session::Out_of_metadata) {
			throw Quota_exceeded();
		} catch (...) {
			Genode::error("assignment to device failed");
		}
	}

	_device_config.write(&_config_access, address, value, size,
	                     _device_config.DONT_TRACK_ACCESS);
}

Genode::Irq_session_capability Platform::Device_component::irq(Genode::uint8_t id)
{
	if (id != 0)
		return Genode::Irq_session_capability();

	if (_irq_session)
		return _irq_session->cap();

	using Genode::construct_at;

	if (!_device_config.valid()) {
		/* Non PCI devices */
		_irq_session = construct_at<Irq_session_component>(_mem_irq_component,
		                                                   _irq_line, ~0UL);

		_ep.manage(_irq_session);
		return _irq_session->cap();
	}

	_irq_session = construct_at<Irq_session_component>(_mem_irq_component,
	                                                   _configure_irq(_irq_line),
	                                                   (!_session.msi_usage() || !_msi_cap()) ? ~0UL : _config_space);
	_ep.manage(_irq_session);

	Genode::uint16_t msi_cap = _msi_cap();

	if (_irq_session->msi()) {

		Genode::addr_t msi_address = _irq_session->msi_address();
		Genode::uint32_t msi_value = _irq_session->msi_data();

		Genode::uint16_t msi = _device_config.read(&_config_access,
		                                           msi_cap + 2,
		                                           Platform::Device::ACCESS_16BIT);

		_device_config.write(&_config_access, msi_cap + 0x4, msi_address,
		                     Platform::Device::ACCESS_32BIT);

		if (msi & CAP_MSI_64) {
			Genode::uint32_t upper_address = sizeof(msi_address) > 4
			                               ? (Genode::uint64_t)msi_address >> 32
			                               : 0UL;

			_device_config.write(&_config_access, msi_cap + 0x8,
			                     upper_address,
			                     Platform::Device::ACCESS_32BIT);
			_device_config.write(&_config_access, msi_cap + 0xc,
			                     msi_value,
			                     Platform::Device::ACCESS_16BIT);
		}
		else
			_device_config.write(&_config_access, msi_cap + 0x8, msi_value,
			                     Platform::Device::ACCESS_16BIT);

		/* enable MSI */
		_device_config.write(&_config_access, msi_cap + 2,
		                     msi ^ MSI_ENABLED,
		                     Platform::Device::ACCESS_8BIT);
	}

	bool msi_64   = false;
	bool msi_mask = false;
	if (msi_cap) {
		Genode::uint16_t msi = _device_config.read(&_config_access,
		                                           msi_cap + 2,
		                                           Platform::Device::ACCESS_16BIT);
		msi_64   = msi & CAP_MSI_64;
		msi_mask = msi & CAP_MASK;
	}

	if (_irq_session->msi())
		Genode::log(_device_config, " uses ",
		            "MSI ", (msi_64 ? "64bit" : "32bit"), ", "
		            "vector ", Genode::Hex(_irq_session->msi_data()), ", "
		            "address ", Genode::Hex(_irq_session->msi_address()), ", ",
		            (msi_mask ? "maskable" : "non-maskable"));
	else
		Genode::log(_device_config, " uses IRQ, vector ",
		            Genode::Hex(_irq_line),
		            (msi_cap ? (msi_64 ? ", MSI 64bit capable"
		                               : ", MSI 32bit capable")
		                     : ""),
		            (msi_mask ? ", maskable" : ", non-maskable"));

	return _irq_session->cap();
}
