from . import hotplug
from .decorators import *
import tuned.consts as consts
import tuned.logs

import errno
import os

log = tuned.logs.get()

# the plugin manages each IRQ as a "device" and keeps a IrqInfo object for it
class IrqInfo(object):
	def __init__(self, irq):
		self.irq = irq
		self.device = "irq%s" % irq
		self.unchangeable = False
		self.original_affinity = None

class IrqPlugin(hotplug.Plugin):
	r"""
	`irq`::

	Allows tuning of IRQ affinities, and thus re-implements functionality
	already present in the `scheduler` plugin. However, this plugin offers
	more flexibility, as it allows tuning of individual interrupts with
	different affinities. When using the `irq` plugin, make sure to disable
	IRQ processing in the `scheduler` plugin by setting its option
	[option]`irq_process=false`.
	The plugin handles individual IRQs as `devices`, and multiple plugin
	instances can be defined, each addressing different devices/irqs.
	The `device` names used by the plugin are `irq<n>`, where `<n>` is the
	IRQ number. The special `device` `DEFAULT` controls values written to
	`/proc/irq/default_smp_affinity`, which applies to all non-active IRQs.
	===
	The option [option]`affinity` controls the IRQ affinity to be set. It is
	a string in "cpulist" format (such as 1,3-4). If the configured affinity
	is empty, then the affinity of the respective IRQs is not touched.
	===
	The option [option]`mode` is a string which can either be `set` (default)
	or `intersect`. In `set` mode the [option]`affinity` is always written
	as configured, whereas in `intersect` mode, the new affinity will be
	calculated as the intersection of the current and the configured affinity.
	If that intersection is empty, the configured affinity will be used.
	--
	.Example moving all IRQs to CPU0, except irq16, which is directed to CPU2
	====
	----
	[irq_special]
	type=irq
	devices=irq16
	affinity=2
	[irq]
	affinity=0
	----
	====
	"""

	def __init__(self, monitor_repository, storage_factory, hardware_inventory, device_matcher, device_matcher_udev, plugin_instance_factory, global_cfg, variables):
		super(IrqPlugin, self).__init__(monitor_repository, storage_factory, hardware_inventory, device_matcher, device_matcher_udev, plugin_instance_factory, global_cfg, variables)
		self._irqs = {}

	#
	# plugin-level methods: devices and plugin options
	#
	def _init_devices(self):
		"""Read /proc/irq to collect devices
		"""
		self._devices_supported = True
		self._free_devices = set()
		self._assigned_devices = set()
		for i in os.listdir("/proc/irq"):
			p = os.path.join("/proc/irq", i)
			if os.path.isdir(p) and i.isdigit():
				info = IrqInfo(i)
				self._irqs[i] = info
				self._free_devices.add(info.device)
		# add the virtual device for default_smp_affinity
		default_info = IrqInfo("DEFAULT")
		default_info.device = "DEFAULT"
		self._irqs["DEFAULT"] = default_info
		self._free_devices.add(default_info.device)

	@classmethod
	def _get_config_options(cls):
		return {
			"affinity": "",
			"mode": "set",
		}

	#
	# instance-level methods: implement the Instance interface
	#
	def _instance_init(self, instance):
		instance._has_static_tuning = True
		instance._has_dynamic_tuning = False

		affinity = self._variables.expand(instance.options.get("affinity"))
		affinity_list = self._cmd.cpulist_unpack(affinity)
		if len(affinity.strip()) == 0:
			# empty affinity in profile -> assume it's intentional
			log.info("Instance '%s' configured with empty affinity. Deactivating." % instance.name)
			instance._active = False
		elif len(affinity_list) == 0:
			# non-empty affinity string evaluates to empty list -> assume parse error
			log.error("Instance '%s' with invalid affinity '%s'. Deactivating." % (instance.name, affinity))
			instance._active = False

		mode = self._variables.expand(instance.options.get("mode"))
		if mode not in ["set", "intersect"]:
			log.error("Invalid operating mode '%s' for instance '%s'. Using the default 'set' instead."
					% (mode, instance.name))
			instance.options["mode"] = "set"

	def _instance_cleanup(self, instance):
		pass

	def _instance_apply_static(self, instance):
		log.debug("Applying IRQ affinities (%s)" % instance.name)
		super(IrqPlugin, self)._instance_apply_static(instance)

	def _instance_unapply_static(self, instance, rollback):
		log.debug("Unapplying IRQ affinities (%s)" % instance.name)
		super(IrqPlugin, self)._instance_unapply_static(instance, rollback)

	def _instance_verify_static(self, instance, ignore_missing, devices):
		log.debug("Verifying IRQ affinities (%s)" % instance.name)
		return super(IrqPlugin, self)._instance_verify_static(instance, ignore_missing, devices)

	#
	# "low-level" methods to get/set irq affinities
	#
	def _get_irq_affinity(self, irq):
		"""Get current IRQ affinity from the kernel

		Args:
			irq (str): IRQ number (as string) or "DEFAULT"

		Returns:
			affinity (set): set of all CPUs that belong to the IRQ affinity mask,
				if reading of the affinity fails, an empty set is returned
		"""
		try:
			filename = "/proc/irq/default_smp_affinity" if irq == "DEFAULT" else "/proc/irq/%s/smp_affinity" % irq
			with open(filename, "r") as f:
				affinity_hex = f.readline().strip()
			return set(self._cmd.hex2cpulist(affinity_hex))
		except (OSError, IOError) as e:
			log.debug("Failed to read SMP affinity of IRQ %s: %s" % (irq, e))
			return set()

	def _set_irq_affinity(self, irq, affinity, restoring):
		"""Set IRQ affinity in the kernel

		Args:
			irq (str): IRQ number (as string) or "DEFAULT"
			affinity (set): affinity mask as set of CPUs
			restoring (bool): are we rolling back a previous change?

		Returns:
			status (int):  0 on success, -2 if changing the affinity is not
				supported, -1 if some other error occurs
		"""
		try:
			affinity_hex = self._cmd.cpulist2hex(list(affinity))
			log.debug("Setting SMP affinity of IRQ %s to '%s'" % (irq, affinity_hex))
			filename = "/proc/irq/default_smp_affinity" if irq == "DEFAULT" else "/proc/irq/%s/smp_affinity" % irq
			with open(filename, "w") as f:
				f.write(affinity_hex)
			return 0
		except (OSError, IOError) as e:
			# EIO is returned by
			# kernel/irq/proc.c:write_irq_affinity() if changing
			# the affinity is not supported
			# (at least on kernels 3.10 and 4.18)
			if hasattr(e, "errno") and e.errno == errno.EIO and not restoring:
				log.debug("Setting SMP affinity of IRQ %s is not supported" % irq)
				return -2
			else:
				log.error("Failed to set SMP affinity of IRQ %s to '%s': %s" % (irq, affinity_hex, e))
				return -1

	#
	# "high-level" methods: apply tuning while saving original affinities
	#
	def _apply_irq_affinity(self, irqinfo, affinity, mode):
		"""Apply IRQ affinity tuning

		Args:
			irqinfo (IrqInfo): IRQ that should be tuned
			affinity (set): desired affinity
		"""
		original = self._get_irq_affinity(irqinfo.irq)
		if mode == "intersect":
			# intersection of affinity and original, if that is empty fall back to configured affinity
			affinity = affinity & original or affinity
		if irqinfo.unchangeable or affinity == original:
			return
		res = self._set_irq_affinity(irqinfo.irq, affinity, False)
		if res == 0:
			if irqinfo.original_affinity is None:
				irqinfo.original_affinity = original
		elif res == -2:
			irqinfo.unchangeable = True

	def _restore_irq_affinity(self, irqinfo):
		"""Restore IRQ affinity

		Args:
			irqinfo (IrqInfo): IRQ that should be restored
		"""
		if irqinfo.unchangeable or irqinfo.original_affinity is None:
			return
		self._set_irq_affinity(irqinfo.irq, irqinfo.original_affinity, True)
		irqinfo.original_affinity = None

	def _verify_irq_affinity(self, irqinfo, affinity, mode):
		"""Verify IRQ affinity tuning

		Args:
			irqinfo (IrqInfo): IRQ that should be verified
			affinity (set): desired affinity

		Returns:
			status (bool): True if verification successful, False otherwise
		"""
		if irqinfo.unchangeable:
			return True
		affinity_description = "IRQ %s affinity" % irqinfo.irq
		desired_affinity = affinity
		desired_affinity_string = self._cmd.cpulist2string(self._cmd.cpulist_pack(list(desired_affinity)))
		current_affinity = self._get_irq_affinity(irqinfo.irq)
		current_affinity_string = self._cmd.cpulist2string(self._cmd.cpulist_pack(list(current_affinity)))
		if mode == "intersect":
			# In intersect mode, we don't use a strict comparison; it's sufficient
			# if the current affinity is a subset of the desired one
			desired_affinity_string = "subset of " + desired_affinity_string
		if ((mode == "intersect" and current_affinity <= desired_affinity) or
					(mode == "set" and current_affinity == desired_affinity)):
			log.info(consts.STR_VERIFY_PROFILE_VALUE_OK
					% (affinity_description, desired_affinity_string))
			return True
		else:
			log.error(consts.STR_VERIFY_PROFILE_VALUE_FAIL
					% (affinity_description, current_affinity_string, desired_affinity_string))
			return False

	#
	# command definitions: entry to device-specific tuning
	#
	@command_custom("mode", per_device=False, priority=-10)
	def _mode(self, enabling, value, verify, ignore_missing):
		if (enabling or verify) and value is not None:
			# Store the operating mode of the current instance in the plugin
			# object, from where it is read by the "affinity" command.
			# This works because instances are processed sequentially by the engine.
			self._mode_val = value

	@command_custom("affinity", per_device=True)
	def _affinity(self, enabling, value, device, verify, ignore_missing):
		irq = "DEFAULT" if device == "DEFAULT" else device[len("irq"):]
		if irq not in self._irqs:
			log.error("Unknown device: %s" % device)
			return None
		irqinfo = self._irqs[irq]
		if verify:
			affinity = set(self._cmd.cpulist_unpack(value))
			return self._verify_irq_affinity(irqinfo, affinity, self._mode_val)
		if enabling:
			affinity = set(self._cmd.cpulist_unpack(value))
			return self._apply_irq_affinity(irqinfo, affinity, self._mode_val)
		else:
			return self._restore_irq_affinity(irqinfo)
