
from __future__ import absolute_import
from __future__ import division

import collections
import functools
import hashlib
import operator
import os
import uuid
import xml.etree.cElementTree as etree

import libvirt
import six

from vdsm.common import conv
from vdsm.common import cpuarch
from vdsm.common import exception
from vdsm.common import hooks
from vdsm.common import libvirtconnection
from vdsm.common import supervdsm
from vdsm.common import validate
from vdsm.common.cache import memoized

_MDEV_PATH = '/sys/class/mdev_bus'
_MDEV_FIELDS = ('name', 'description', 'available_instances', 'device_api')
_OVIRT_MDEV_NAMESPACE = uuid.UUID('8524b17c-f0ca-44a5-9ce4-66fe261e5986')
_MdevDetail = collections.namedtuple('_MdevDetail', _MDEV_FIELDS)


CAPABILITY_TO_XML_ATTR = collections.defaultdict(
    lambda: 'unknown',

    pci='pci',
    scsi='scsi',
    scsi_generic='scsi_generic',
    usb_device='usb',
)

_LIBVIRT_DEVICE_FLAGS = collections.defaultdict(
    lambda: libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SYSTEM,

    system=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SYSTEM,
    pci=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_PCI_DEV,
    usb_device=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_DEV,
    usb=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_USB_INTERFACE,
    net=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_NET,
    scsi_host=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI_HOST,
    scsi_target=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI_TARGET,
    scsi=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI,
    storage=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_STORAGE,
    fc_host=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_FC_HOST,
    vports=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_VPORTS,
    scsi_generic=libvirt.VIR_CONNECT_LIST_NODE_DEVICES_CAP_SCSI_GENERIC,
)

_DATA_PROCESSORS = collections.defaultdict(list)
_last_alldevices_hash = None
_device_tree_cache = {}
_device_address_to_name_cache = {}


class PCIHeaderType:
    ENDPOINT = 0
    BRIDGE = 1
    CARDBUS_BRIDGE = 2
    UNKNOWN = 99


class Vendor:
    NVIDIA = '0x10de'


class MdevPlacement:
    COMPACT = 'compact'
    SEPARATE = 'separate'


class NoIOMMUSupportException(Exception):
    pass


class UnsuitableSCSIDevice(Exception):
    pass


class _DeviceTreeCache(object):

    def __init__(self, devices):
        self._parent_to_device_params = {}
        self.devices = devices
        self._populate(devices)

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self._invalidate()

    def get_by_parent(self, capability, parent_name):
        try:
            return self.devices[
                self._parent_to_device_params[capability][parent_name]]
        except KeyError:
            return None

    def _populate(self, devices):
        self._parent_to_device_params = collections.defaultdict(dict)

        for device_name, device_params in devices.items():
            try:
                parent = device_params['parent']
            except KeyError:
                continue

            self._parent_to_device_params[
                device_params['capability']][parent] = device_name

    def _invalidate(self):
        self._parent_to_device_params = {}


@memoized
def _data_processors_map():
    data_processors_map = collections.defaultdict(
        lambda: _DATA_PROCESSORS['_ANY']
    )

    for capability in _LIBVIRT_DEVICE_FLAGS:
        data_processors_map[capability] = (_DATA_PROCESSORS['_ANY'] +
                                           _DATA_PROCESSORS[capability])
    return data_processors_map


def __device_tree_hash(libvirt_devices):
    """
    The hash generation works iff the order of devices returned from libvirt is
    stable.
    """
    current_hash = hashlib.sha256()
    for _, xml in _each_device_xml(libvirt_devices):
        current_hash.update(xml)

    return current_hash.hexdigest()


def _data_processor(target_bus='_ANY'):
    """
    Register function as a data processor for device processing code.
    """
    def processor(function):
        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            return function(*args, **kwargs)
        _DATA_PROCESSORS[target_bus].append(wrapped)
        return wrapped
    return processor


def is_supported():
    try:
        iommu_groups_exist = bool(len(os.listdir('/sys/kernel/iommu_groups')))
        if cpuarch.is_ppc(cpuarch.real()):
            return iommu_groups_exist

        dmar_exists = bool(len(os.listdir('/sys/class/iommu')))
        return iommu_groups_exist and dmar_exists
    except OSError:
        return False


def _pci_header_type(device_name):
    try:
        with open('/sys/bus/pci/devices/{}/config'.format(
                name_to_pci_path(device_name)), 'rb') as f:
            f.seek(0x0e)
            header_type = ord(f.read(1)) & 0x7f
    except IOError:
        return PCIHeaderType.UNKNOWN

    return int(header_type)


def name_to_pci_path(device_name):
    return device_name[4:].replace('_', '.').replace('.', ':', 2)


def _each_device_xml(libvirt_devices):
    for device in libvirt_devices:
        try:
            yield device.name(), device.XMLDesc(0)
        except libvirt.libvirtError:
            continue


def scsi_address_to_adapter(scsi_address):
    adapter = 'scsi_host{}'.format(scsi_address['host'])

    return ({'unit': scsi_address['lun'],
             'bus': scsi_address['bus'],
             'target': scsi_address['target']},
            adapter)


def pci_address_to_name(domain, bus, slot, function):
    return 'pci_{0}_{1}_{2}_{3}'.format(domain[2:],
                                        bus[2:],
                                        slot[2:],
                                        function[2:])


def _sriov_totalvfs(device_name):
    with open('/sys/bus/pci/devices/{0}/sriov_totalvfs'.format(
            name_to_pci_path(device_name))) as f:
        return int(f.read())


def physical_function_net_name(pf_pci_name):
    devices = list_by_caps()
    libvirt_device_names = [name for name, device in six.iteritems(devices) if
                            device['params'].get('parent') == pf_pci_name and
                            device['params'].get('capability') == 'net']
    if len(libvirt_device_names) > 1:
        raise Exception('could not determine network name for %s. Possible'
                        'devices: %s' % (pf_pci_name, libvirt_device_names))
    if not libvirt_device_names:
        raise Exception('could not determine network name for %s. There are no'
                        'devices with this parent.' % (pf_pci_name,))

    return libvirt_device_names[0].split('_')[1]


def _process_address(device_xml, children):
    params = {}
    for cap in children:
        params[cap] = device_xml.find('./capability/{}'.format(cap)).text

    return {'address': params}


@_data_processor('pci')
def _process_pci_address(device_xml):
    return _process_address(device_xml, ('domain', 'bus', 'slot', 'function'))


@_data_processor('scsi')
def _process_scsi_address(device_xml):
    return _process_address(device_xml, ('host', 'bus', 'target', 'lun'))


@_data_processor('usb_device')
def _process_usb_address(device_xml):
    return _process_address(device_xml, ('bus', 'device'))


@_data_processor('pci')
def _process_mdev_params(device_xml):
    mdev = device_xml.find('./capability/capability[@type="mdev_types"]')
    if mdev is None:
        return {}

    supported_types = collections.defaultdict(dict)

    for mdev_type in mdev.findall('type'):
        name = mdev_type.attrib['id']
        name_elem = mdev_type.find('name')
        supported_types[name]['name'] = \
            name if name_elem is None else name_elem.text
        try:
            supported_types[name]['available_instances'] = \
                mdev_type.find('availableInstances').text
        except AttributeError:
            supported_types[name] = {}
            continue

    supported_types = {k: v for k, v in supported_types.items() if v}
    if supported_types:
        return {'mdev': supported_types}
    else:
        return {}


@_data_processor('pci')
def _process_assignability(device_xml):
    is_assignable = None

    physfn = device_xml.find('./capability/capability')

    if physfn is not None:
        if physfn.attrib['type'] in ('pci-bridge', 'cardbus-bridge'):
            is_assignable = 'false'
    if is_assignable is None:
        name = device_xml.find('name').text
        is_assignable = str(_pci_header_type(name) ==
                            PCIHeaderType.ENDPOINT).lower()

    return {'is_assignable': is_assignable}


@_data_processor('scsi_generic')
def _process_udev_path(device_xml):
    try:
        udev_path = device_xml.find('./capability/char').text
    except AttributeError:
        return {}
    else:
        return {'udev_path': udev_path}


@_data_processor()
def _process_driver(device_xml):
    try:
        driver_name = device_xml.find('./driver/name').text
    except AttributeError:
        return {}
    else:
        return {'driver': driver_name}


@_data_processor('storage')
def _process_storage(device_xml):
    try:
        model = device_xml.find('./capability/model').text
    except AttributeError:
        return {}
    else:
        return {'product': model}


@_data_processor('pci')
def _process_vfs(device_xml):
    name = device_xml.find('name').text

    try:
        return {'totalvfs': _sriov_totalvfs(name)}
    except IOError:
        return {}


@_data_processor('pci')
def _process_iommu(device_xml):
    iommu_group = device_xml.find('./capability/iommuGroup')
    if iommu_group is not None:
        return {'iommu_group': iommu_group.attrib['number']}
    return {}


@_data_processor('pci')
def _process_physfn(device_xml):
    physfn = device_xml.find('./capability/capability')
    if physfn is not None and physfn.attrib['type'] == 'phys_function':
        address = physfn.find('address')
        return {'physfn': pci_address_to_name(**address.attrib)}
    return {}


@_data_processor()
def _process_productinfo(device_xml):
    params = {}

    capabilities = device_xml.findall('./capability/')
    for capability in capabilities:
        if capability.tag in ('vendor', 'product', 'interface'):
            if 'id' in capability.attrib:
                params[capability.tag + '_id'] = capability.attrib['id']
            if capability.text:
                params[capability.tag] = capability.text

    return params


@_data_processor()
def _process_parent(device_xml):
    name = device_xml.find('name').text

    if name != 'computer':
        return {'parent': device_xml.find('parent').text}

    return {}


@_data_processor('pci')
def _process_numa(device_xml):
    numa_node = device_xml.find('./capability/numa')
    if numa_node is not None:
        return {'numa_node': numa_node.attrib['node']}
    return {}


def _process_scsi_device_params(device_name, cache):
    params = {}

    storage_dev_params = cache.get_by_parent('storage', device_name)
    if storage_dev_params:
        for attr in ('vendor', 'product'):
            try:
                res = storage_dev_params[attr]
            except KeyError:
                pass
            else:
                params[attr] = res

    scsi_generic_dev_params = cache.get_by_parent('scsi_generic', device_name)
    if scsi_generic_dev_params:
        params['udev_path'] = scsi_generic_dev_params['udev_path']

    return params


def _process_device_params(device_xml):
    params = {}

    devXML = etree.fromstring(device_xml.decode('ascii', errors='ignore'))

    caps = devXML.find('capability')
    params['capability'] = caps.attrib['type']
    params['is_assignable'] = 'true'

    for data_processor in _data_processors_map()[params['capability']]:
        params.update(data_processor(devXML))

    return params


def _get_device_ref_and_params(device_name):
    libvirt_device = libvirtconnection.get().\
        nodeDeviceLookupByName(device_name)
    params = _process_device_params(libvirt_device.XMLDesc(0))

    if params['capability'] != 'scsi':
        return libvirt_device, params

    devices, _ = _get_devices_from_libvirt()
    with _DeviceTreeCache(devices) as cache:
        params.update(_process_scsi_device_params(device_name, cache))

    return libvirt_device, params


def _process_all_devices(libvirt_devices):
    devices = {}
    for name, xml in _each_device_xml(libvirt_devices):
        params = _process_device_params(xml)
        devices[name] = params

    return devices


def _get_devices_from_libvirt(flags=0):
    libvirt_devices = libvirtconnection.get().listAllDevices(flags)
    global _last_alldevices_hash
    global _device_tree_cache
    global _device_address_to_name_cache

    if (flags == 0 and
            __device_tree_hash(libvirt_devices) == _last_alldevices_hash):
        return _device_tree_cache, _device_address_to_name_cache

    devices = _process_all_devices(libvirt_devices)
    address_to_name = {}

    with _DeviceTreeCache(devices) as cache:
        for device_name, device_params in devices.items():
            if device_params['capability'] == 'scsi':
                device_params.update(
                    _process_scsi_device_params(device_name, cache))

            _update_address_to_name_map(
                address_to_name, device_name, device_params
            )

    if flags == 0:
        _device_tree_cache = devices
        _device_address_to_name_cache = address_to_name
        _last_alldevices_hash = __device_tree_hash(libvirt_devices)
    return devices, address_to_name


def _update_address_to_name_map(address_to_name, device_name, device_params):
    address_type = CAPABILITY_TO_XML_ATTR[device_params['capability']]
    if 'address' in device_params:
        device_address = _format_address(
            address_type, device_params['address']
        )
        address_to_name[device_address] = device_name


def _each_mdev_device():
    for pci_device in sorted(os.listdir(_MDEV_PATH)):
        yield pci_device


def _each_supported_mdev_type(pci_device):
    path = os.path.join(_MDEV_PATH, pci_device, 'mdev_supported_types')
    for mdev_type in os.listdir(path):
        yield mdev_type, path


def _mdev_type_details(mdev_type, path):
    kwargs = {}
    for field in _MDEV_FIELDS:
        syspath = os.path.join(path, mdev_type, field)
        try:
            with open(syspath, 'r') as f:
                value = f.read().strip()
        except IOError:
            value = ''
        kwargs[field] = value
    if not kwargs['name']:
        kwargs['name'] = mdev_type
    return _MdevDetail(**kwargs)


def _mdev_device_vendor(device):
    with open(os.path.join(_MDEV_PATH, device, 'vendor'), 'r') as f:
        return f.read().strip()


def _mdev_type_devices(mdev_type, path):
    return os.listdir(os.path.join(path, mdev_type, 'devices'))


def _suitable_device_for_mdev_type(target_mdev_type, mdev_placement, log):
    target_device = None

    for device in _each_mdev_device():
        vendor = _mdev_device_vendor(device)
        for mdev_type, path in _each_supported_mdev_type(device):
            if mdev_type != target_mdev_type:
                if vendor == '0x10de' and \
                   len(_mdev_type_devices(mdev_type, path)) > 0:
                    target_device = None
                    break
                continue
            elif mdev_placement == MdevPlacement.SEPARATE:
                if len(_mdev_type_devices(mdev_type, path)) > 0:
                    target_device = None
                    break
            if int(
                    _mdev_type_details(mdev_type, path).available_instances
            ) < 1:
                continue

            target_device = device

        if target_device is not None:
            return target_device

    if target_device is None and mdev_placement == MdevPlacement.SEPARATE:
        log.info("Separate mdev placement failed, trying compact placement.")
        return _suitable_device_for_mdev_type(
            target_mdev_type, MdevPlacement.COMPACT, log
        )
    return target_device


def device_name_from_address(address_type, device_address):
    _, address_to_name = _get_devices_from_libvirt()
    return address_to_name[
        _format_address(address_type, device_address)
    ]


def list_by_caps(caps=None):
    devices = {}
    flags = sum([_LIBVIRT_DEVICE_FLAGS[cap] for cap in caps or []])
    libvirt_devices, _ = _get_devices_from_libvirt(flags)

    for devName, params in libvirt_devices.items():
        devices[devName] = {'params': params}

    devices = hooks.after_hostdev_list_by_caps(devices)
    return devices


def get_device_params(device_name):
    _, device_params = _get_device_ref_and_params(device_name)
    return device_params


def detach_detachable(device_name):
    libvirt_device, device_params = _get_device_ref_and_params(device_name)
    capability = CAPABILITY_TO_XML_ATTR[device_params['capability']]

    if capability == 'pci' and conv.tobool(
            device_params['is_assignable']):
        libvirt_device.detachFlags(None)
    elif capability == 'scsi':
        if 'udev_path' not in device_params:
            raise UnsuitableSCSIDevice

    return device_params


def reattach_detachable(device_name, pci_reattach=True):
    libvirt_device, device_params = _get_device_ref_and_params(device_name)
    capability = CAPABILITY_TO_XML_ATTR[device_params['capability']]

    if capability == 'pci' and conv.tobool(
            device_params['is_assignable']):
        if pci_reattach:
            libvirt_device.reAttach()
    elif capability == 'scsi':
        if 'udev_path' not in device_params:
            raise UnsuitableSCSIDevice


def change_numvfs(device_name, numvfs):
    net_name = physical_function_net_name(device_name)
    supervdsm.getProxy().change_numvfs(name_to_pci_path(device_name), numvfs,
                                       net_name)


def spawn_mdev(mdev_type, mdev_uuid, mdev_placement, log):
    device = _suitable_device_for_mdev_type(mdev_type, mdev_placement, log)
    if device is None:
        message = 'vgpu: No device with type {} is available'.format(mdev_type)
        log.error(message)
        raise exception.ResourceUnavailable(message)
    try:
        supervdsm.getProxy().mdev_create(device, mdev_type, mdev_uuid)
    except IOError:
        message = 'vgpu: Failed to create mdev type {}'.format(mdev_type)
        log.error(message)
        raise exception.ResourceUnavailable(message)


def despawn_mdev(mdev_uuid):
    device = None
    for dev in _each_mdev_device():
        if mdev_uuid in os.listdir(os.path.join(_MDEV_PATH, dev)):
            device = dev
            break
    if device is None or mdev_uuid is None:
        raise exception.ResourceUnavailable('vgpu: No mdev found')
    try:
        supervdsm.getProxy().mdev_delete(device, mdev_uuid)
    except IOError:
        pass


def get_mdev_uuid(vm_id):
    return str(uuid.uuid3(_OVIRT_MDEV_NAMESPACE, vm_id))


def _format_address(dev_type, address):
    if dev_type == 'pci':
        address = validate.normalize_pci_address(**address)
    ret = '_'.join(
        '{}{}'.format(key, value) for key, value in sorted(
            address.items(),
            key=operator.itemgetter(0)
        )
    )
    return '{}_{}'.format(dev_type, ret)
