
from __future__ import absolute_import
from __future__ import division

from vdsm.common import xmlutils
from vdsm.virt import metadata
from vdsm.virt import vmxml

from . import core
from . import graphics
from . import hostdevice
from . import hwclass
from . import lease
from . import network
from . import storage
from . import storagexml


def _update_unknown_device_info(vm):
    def isKnownDevice(alias):
        for dev in vm.conf['devices']:
            if dev.get('alias') == alias:
                return True
        return False

    for x in vmxml.children(vm.domain.devices):
        if vmxml.find_first(x, 'address', None) is None:
            continue

        alias = core.find_device_alias(x)
        if not isKnownDevice(alias):
            address = vmxml.device_address(x)
            device = vmxml.attr(x, 'type')
            newDev = {'type': vmxml.tag(x),
                      'alias': alias,
                      'device': device,
                      'address': address}
            vm.conf['devices'].append(newDev)


def update_device_info(vm, devices):
    network.Interface.update_device_info(vm, devices[hwclass.NIC])
    storage.Drive.update_device_info(vm, devices[hwclass.DISK])
    core.Sound.update_device_info(vm, devices[hwclass.SOUND])
    graphics.Graphics.update_device_info(vm, devices[hwclass.GRAPHICS])
    core.Video.update_device_info(vm, devices[hwclass.VIDEO])
    core.Controller.update_device_info(vm, devices[hwclass.CONTROLLER])
    core.Balloon.update_device_info(vm, devices[hwclass.BALLOON])
    core.Watchdog.update_device_info(vm, devices[hwclass.WATCHDOG])
    core.Smartcard.update_device_info(vm, devices[hwclass.SMARTCARD])
    core.Rng.update_device_info(vm, devices[hwclass.RNG])
    core.Console.update_device_info(vm, devices[hwclass.CONSOLE])
    hostdevice.HostDevice.update_device_info(vm, devices[hwclass.HOSTDEV])
    core.Memory.update_device_info(vm, devices[hwclass.MEMORY])
    lease.Device.update_device_info(vm, devices[hwclass.LEASE])
    _update_unknown_device_info(vm)


_DEVICE_MAPPING = {
    hwclass.DISK: storage.Drive,
    hwclass.NIC: network.Interface,
    hwclass.SOUND: core.Sound,
    hwclass.VIDEO: core.Video,
    hwclass.GRAPHICS: graphics.Graphics,
    hwclass.CONTROLLER: core.Controller,
    hwclass.GENERAL: core.Generic,
    hwclass.BALLOON: core.Balloon,
    hwclass.WATCHDOG: core.Watchdog,
    hwclass.CONSOLE: core.Console,
    hwclass.REDIR: core.Redir,
    hwclass.RNG: core.Rng,
    hwclass.SMARTCARD: core.Smartcard,
    hwclass.TPM: core.Tpm,
    hwclass.HOSTDEV: hostdevice.HostDevice,
    hwclass.MEMORY: core.Memory,
    hwclass.LEASE: lease.Device,
}


def identify_from_xml_elem(dev_elem):
    dev_name = core.dev_class_from_dev_elem(dev_elem)
    if dev_name not in _DEVICE_MAPPING:
        raise core.SkipDevice()
    return dev_name, _DEVICE_MAPPING[dev_name]


def empty_dev_map():
    return {dev: [] for dev in _DEVICE_MAPPING}


def dev_map_from_dev_spec_map(dev_spec_map, log):
    dev_map = empty_dev_map()

    for dev_type, dev_class in _DEVICE_MAPPING.items():
        for dev in dev_spec_map[dev_type]:
            dev_map[dev_type].append(dev_class(log, **dev))

    return dev_map


def dev_map_from_domain_xml(vmid, dom_desc, md_desc, log, noerror=False):
    log.debug('Initializing device classes from domain XML')
    dev_map = empty_dev_map()
    for dev_type, dev_class, dev_elem in _device_elements(dom_desc, log):
        dev_meta = _get_metadata_from_elem_xml(vmid, md_desc,
                                               dev_class, dev_elem)
        try:
            dev_obj = dev_class.from_xml_tree(log, dev_elem, dev_meta)
        except NotImplementedError:
            log.debug('Cannot initialize %s device: not implemented',
                      dev_type)
        except Exception:
            if noerror:
                log.exception("Device initialization from XML failed")
                dev_elem_xml = xmlutils.tostring(dev_elem, pretty=True)
                log.error("Failed XML: %s", dev_elem_xml)
                log.error("Failed metadata: %s", dev_meta)
            else:
                raise
        else:
            dev_map[dev_type].append(dev_obj)
    log.debug('Initialized %d device classes from domain XML', len(dev_map))
    return dev_map


def dev_elems_from_xml(vm, xml):
    dom = xmlutils.fromstring(xml)
    devices = vmxml.find_first(dom, 'devices')
    dev_elem = next(vmxml.children(devices))
    _dev_type, dev_class = identify_from_xml_elem(dev_elem)
    meta = vmxml.find_first(dom, 'metadata', None)
    if meta is None:
        md_desc = metadata.Descriptor()
    else:
        md_desc = metadata.Descriptor.from_xml(xmlutils.tostring(meta))
    dev_meta = _get_metadata_from_elem_xml(vm.id, md_desc, dev_class, dev_elem)
    return dev_class, dev_elem, dev_meta


def dev_meta_from_elem(dev_elem, vmid, md_desc):
    _dev_type, dev_class = identify_from_xml_elem(dev_elem)
    return _get_metadata_from_elem_xml(vmid, md_desc, dev_class, dev_elem)


def dev_from_xml(vm, xml):
    cls, elem, meta = dev_elems_from_xml(vm, xml)
    return cls.from_xml_tree(vm.log, elem, meta)


def storage_device_params_from_domain_xml(vmid, dom_desc, md_desc, log):
    log.debug('Extracting storage devices params from domain XML')
    params = []
    for dev_type, dev_class, dev_elem in _device_elements(dom_desc, log):
        if dev_type != hwclass.DISK:
            log.debug('skipping non-storage device: %r', dev_elem.tag)
            continue

        dev_meta = _get_metadata_from_elem_xml(vmid, md_desc,
                                               dev_class, dev_elem)
        params.append(storagexml.parse(dev_elem, dev_meta))
    log.debug('Extracted %d storage devices params from domain XML',
              len(params))
    return params


def get_metadata(dev_class, dev_obj):
    if dev_class != hwclass.DISK:
        return dev_obj.get_metadata(dev_class)
    return storagexml.get_metadata(dev_obj)


def save_device_metadata(md_desc, dev_map, log):
    log.debug('Saving the device metadata into domain XML')
    count = 0
    for dev_class, dev_objs in dev_map.items():
        for dev_obj in dev_objs:
            attrs, data = get_metadata(dev_class, dev_obj)
            if not data:
                continue
            elif not attrs:
                log.warning('No metadata attrs for %s', dev_obj)
                continue

            with md_desc.device(**attrs) as dev:
                dev.clear()
                dev.update(data)
                count += 1

    log.debug('Saved %d device metadata', count)


def _device_elements(dom_desc, log):
    for dev_elem in vmxml.children(dom_desc.devices):
        try:
            dev_type, dev_class = identify_from_xml_elem(dev_elem)
        except core.SkipDevice:
            log.debug('skipping unhandled device: %r', dev_elem.tag)
        else:
            yield dev_type, dev_class, dev_elem


def _get_metadata_from_elem_xml(vmid, md_desc, dev_class, dev_elem):
    dev_meta = {'vmid': vmid}
    attrs = dev_class.get_identifying_attrs(dev_elem)
    if attrs:
        with md_desc.device(**attrs) as dev_data:
            dev_meta.update(dev_data)
    return dev_meta


def update_guest_disk_mapping(md_desc, disk_devices, guest_disk_mapping, log):
    for serial, value in guest_disk_mapping:
        for d in disk_devices:
            image_id = storage.image_id(d.path)
            if image_id and image_id[:20] in serial:
                d.guestName = value['name']
                log.debug("Guest name of drive %s: %s",
                          image_id, d.guestName)
                attrs, data = storagexml.get_metadata(d)
                with md_desc.device(**attrs) as dev:
                    dev.update(data)
                break
        else:
            if serial[20:]:
                log.warning("Unidentified guest drive %s: %s",
                            serial, value['name'])
