
from __future__ import absolute_import
from __future__ import division

import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape
from operator import itemgetter

from vdsm.common import conv
from vdsm.common import cpuarch
from vdsm.common import xmlutils
from vdsm.virt import metadata
from vdsm.virt import vmxml
from vdsm.virt import xmlconstants
from vdsm import constants
from vdsm import taskset



_DEFAULT_MACHINES = {
    cpuarch.X86_64: 'pc',
    cpuarch.PPC64: 'pseries',
    cpuarch.PPC64LE: 'pseries',
    cpuarch.S390X: 's390-ccw-virtio',
}


class Domain(object):

    def __init__(self, conf, log, arch):
        self.conf = conf
        self.log = log

        self.arch = arch

        if conv.tobool(self.conf.get('kvmEnable', 'true')):
            domainType = 'kvm'
        else:
            domainType = 'qemu'

        domainAttrs = {'type': domainType}

        self.dom = vmxml.Element('domain', **domainAttrs)

        self.dom.appendChildWithArgs('name', text=self.conf['vmName'])
        self.dom.appendChildWithArgs('uuid', text=self.conf['vmId'])
        if 'numOfIoThreads' in self.conf:
            self.dom.appendChildWithArgs('iothreads',
                                         text=str(self.conf['numOfIoThreads']))
        memSizeKB = str(int(self.conf.get('memSize', '256')) * 1024)
        self.dom.appendChildWithArgs('memory', text=memSizeKB)
        self.dom.appendChildWithArgs('currentMemory', text=memSizeKB)
        if 'maxMemSize' in self.conf:
            maxMemSizeKB = str(int(self.conf['maxMemSize']) * 1024)
            maxMemSlots = str(self.conf.get('maxMemSlots', '16'))
            self.dom.appendChildWithArgs('maxMemory', text=maxMemSizeKB,
                                         slots=maxMemSlots)
        vcpu = self.dom.appendChildWithArgs('vcpu', text=self._getMaxVCpus())
        vcpu.setAttrs(**{'current': self._getSmp()})

        self._devices = vmxml.Element('devices')
        self.dom.appendChild(self._devices)

    def appendClock(self):
        m = vmxml.Element('clock', offset='variable',
                          adjustment=str(self.conf.get('timeOffset', 0)))
        if conv.tobool(self.conf.get('hypervEnable', 'false')):
            m.appendChildWithArgs('timer', name='hypervclock', present='yes')
        m.appendChildWithArgs('timer', name='rtc', tickpolicy='catchup')
        m.appendChildWithArgs('timer', name='pit', tickpolicy='delay')

        if cpuarch.is_x86(self.arch):
            m.appendChildWithArgs('timer', name='hpet', present='no')

        self.dom.appendChild(m)

    def appendMetadata(self):
        metadata_elem = vmxml.Element('metadata')
        vmxml.append_child(
            metadata_elem,
            etree_child=metadata.create(
                xmlconstants.METADATA_VM_TUNE_ELEMENT,
                namespace=xmlconstants.METADATA_VM_TUNE_PREFIX,
                namespace_uri=xmlconstants.METADATA_VM_TUNE_URI
            )
        )
        vmxml.append_child(
            metadata_elem,
            etree_child=metadata.create(
                xmlconstants.METADATA_VM_VDSM_ELEMENT,
                namespace=xmlconstants.METADATA_VM_VDSM_PREFIX,
                namespace_uri=xmlconstants.METADATA_VM_VDSM_URI
            )
        )

        self.dom.appendChild(metadata_elem)

    def appendOs(self, use_serial_console=False):
        oselem = vmxml.Element('os')
        self.dom.appendChild(oselem)

        machine = self.conf.get('emulatedMachine',
                                _DEFAULT_MACHINES[self.arch])

        oselem.appendChildWithArgs('type', text='hvm', arch=self.arch,
                                   machine=machine)

        qemu2libvirtBoot = {'a': 'fd', 'c': 'hd', 'd': 'cdrom', 'n': 'network'}
        for c in self.conf.get('boot', ''):
            oselem.appendChildWithArgs('boot', dev=qemu2libvirtBoot[c])

        if self.conf.get('initrd'):
            oselem.appendChildWithArgs('initrd', text=self.conf['initrd'])

        if self.conf.get('kernel'):
            oselem.appendChildWithArgs('kernel', text=self.conf['kernel'])

        if self.conf.get('kernelArgs'):
            oselem.appendChildWithArgs('cmdline', text=self.conf['kernelArgs'])

        if cpuarch.is_x86(self.arch):
            oselem.appendChildWithArgs('smbios', mode='sysinfo')

        if conv.tobool(self.conf.get('bootMenuEnable', False)):
            oselem.appendChildWithArgs('bootmenu', enable='yes',
                                       timeout=str(_BOOT_MENU_TIMEOUT))

        if use_serial_console and cpuarch.is_x86(self.arch):
            oselem.appendChildWithArgs('bios', useserial='yes')

    def appendSysinfo(self, osname, osversion, serialNumber):
        sysinfoelem = vmxml.Element('sysinfo', type='smbios')
        self.dom.appendChild(sysinfoelem)

        syselem = vmxml.Element('system')
        sysinfoelem.appendChild(syselem)

        def appendEntry(k, v):
            syselem.appendChildWithArgs('entry', text=v, name=k)

        appendEntry('manufacturer', constants.SMBIOS_MANUFACTURER)
        appendEntry('product', osname)
        appendEntry('version', osversion)
        appendEntry('serial', serialNumber)
        appendEntry('uuid', self.conf['vmId'])

    def appendFeatures(self):
        if (conv.tobool(self.conf.get('acpiEnable', 'true')) or
                conv.tobool(self.conf.get('hypervEnable', 'false'))):
            features = self.dom.appendChildWithArgs('features')

        if conv.tobool(self.conf.get('acpiEnable', 'true')):
            features.appendChildWithArgs('acpi')

        if conv.tobool(self.conf.get('hypervEnable', 'false')):
            hyperv = vmxml.Element('hyperv')
            features.appendChild(hyperv)

            hyperv.appendChildWithArgs('relaxed', state='on')
            hyperv.appendChildWithArgs('vapic', state='on')
            hyperv.appendChildWithArgs(
                'spinlocks', state='on', retries='8191')

    def appendCpu(self, hugepages_shared=False):
        cpu = vmxml.Element('cpu')

        if cpuarch.is_x86(self.arch):
            cpu.setAttrs(match='exact')

            features = self.conf.get('cpuType', 'qemu64').split(',')
            model = features[0]

            if model == 'hostPassthrough':
                cpu.setAttrs(mode='host-passthrough')
            elif model == 'hostModel':
                cpu.setAttrs(mode='host-model')
            else:
                cpu.appendChildWithArgs('model', text=model)

                if model == 'qemu64' and '+svm' not in features:
                    features += ['-svm']

                for feature in features[1:]:
                    if feature[1:6] == 'sse4_':
                        feature = feature[0] + 'sse4.' + feature[6:]

                    featureAttrs = {'name': feature[1:]}
                    if feature[0] == '+':
                        featureAttrs['policy'] = 'require'
                    elif feature[0] == '-':
                        featureAttrs['policy'] = 'disable'
                    cpu.appendChildWithArgs('feature', **featureAttrs)
        elif cpuarch.is_ppc(self.arch):
            features = self.conf.get('cpuType', 'POWER8').split(',')
            model = features[0]
            cpu.appendChildWithArgs('model', text=model)

        if ('smpCoresPerSocket' in self.conf or
                'smpThreadsPerCore' in self.conf):
            maxVCpus = int(self._getMaxVCpus())
            cores = int(self.conf.get('smpCoresPerSocket', '1'))
            threads = int(self.conf.get('smpThreadsPerCore', '1'))
            cpu.appendChildWithArgs('topology',
                                    sockets=str(maxVCpus // cores // threads),
                                    cores=str(cores), threads=str(threads))

        if 'cpuPinning' in self.conf:
            cputune = vmxml.Element('cputune')
            cpuPinning = self.conf.get('cpuPinning')
            for cpuPin in sorted(cpuPinning.keys()):
                cputune.appendChildWithArgs('vcpupin', vcpu=cpuPin,
                                            cpuset=cpuPinning[cpuPin])
            self.dom.appendChild(cputune)

        if 'guestNumaNodes' in self.conf:
            numa = vmxml.Element('numa')
            guestNumaNodes = sorted(
                self.conf.get('guestNumaNodes'), key=itemgetter('nodeIndex'))
            for vmCell in guestNumaNodes:
                nodeMem = int(vmCell['memory']) * 1024
                numa_args = {'cpus': vmCell['cpus'], 'memory': str(nodeMem)}
                if hugepages_shared:
                    numa_args.update({'memAccess': 'shared'})
                numa.appendChildWithArgs('cell', **numa_args)
            cpu.appendChild(numa)

        self.dom.appendChild(cpu)

    def appendNumaTune(self):
        numaTune = self.conf.get('numaTune')

        numatune = vmxml.Element('numatune')
        mode = numaTune.get('mode', 'strict')

        numaTuneExists = False

        if 'nodeset' in numaTune:
            numaTuneExists = True
            numatune.appendChildWithArgs('memory',
                                         mode=mode,
                                         nodeset=numaTune['nodeset'])

        for memnode in numaTune.get('memnodes', []):
            numaTuneExists = True
            numatune.appendChildWithArgs('memnode',
                                         mode=mode,
                                         cellid=memnode['vmNodeIndex'],
                                         nodeset=memnode['nodeset'])

        if numaTuneExists:
            self.dom.appendChild(numatune)

    def appendHostdevNumaTune(self, devices):
        numatune = vmxml.Element('numatune')
        numa_map = [dev_object.numa_node for dev_object in devices if
                    dev_object.is_hostdevice and dev_object.numa_node]
        if len(set(numa_map)) == 1:
            numatune.appendChildWithArgs('memory', mode='preferred',
                                         nodeset=numa_map[0])
            self.dom.appendChild(numatune)

    def _appendAgentDevice(self, path, name):
        channel = vmxml.Element('channel', type='unix')
        channel.appendChildWithArgs('target', type='virtio', name=name)
        channel.appendChildWithArgs('source', mode='bind', path=path)
        self._devices.appendChild(channel)

    def appendInput(self):
        if conv.tobool(self.conf.get('tabletEnable')):
            inputAttrs = {'type': 'tablet', 'bus': 'usb'}
        elif cpuarch.is_x86(self.arch):
            inputAttrs = {'type': 'mouse', 'bus': 'ps2'}
        else:
            inputAttrs = {'type': 'mouse', 'bus': 'usb'}

        self._devices.appendChildWithArgs('input', **inputAttrs)

    def appendEmulator(self):
        emulatorPath = '/usr/bin/qemu-system-' + self.arch

        emulator = vmxml.Element('emulator', text=emulatorPath)

        self._devices.appendChild(emulator)

    def appendDeviceXML(self, deviceXML):
        self._devices.appendChild(
            etree_element=xmlutils.fromstring(deviceXML))

    def appendMemoryBacking(self, hugepagesz):
        memorybacking = vmxml.Element('memoryBacking',)
        hugepages_element = vmxml.Element('hugepages')

        hugepages_element.appendChildWithArgs(
            'page', size=str(hugepagesz)
        )
        memorybacking.appendChild(hugepages_element)
        self.dom.appendChild(memorybacking)

    def toxml(self):
        return xmlutils.tostring(self.dom, pretty=True)

    def _getSmp(self):
        return self.conf.get('smp', '1')

    def _getMaxVCpus(self):
        return self.conf.get('maxVCpus', self._getSmp())


def parse_domain(dom_xml, arch):
    conf = {'kvmEnable': 'true'}
    dom = xmlutils.fromstring(dom_xml)
    _parse_domain_init(dom, conf)
    _parse_domain_clock(dom, conf)
    _parse_domain_os(dom, conf)
    _parse_domain_cpu(dom, conf, arch)
    _parse_domain_input(dom, conf)
    return conf


def _parse_domain_init(dom, conf):
    iothreads = dom.findtext('./iothreads', default=None)
    if iothreads is not None:
        conf['numOfIoThreads'] = iothreads
    max_mem = dom.find('./maxMemory')
    if max_mem is not None:
        conf['maxMemSize'] = int(max_mem.text) // 1024
        conf['maxMemSlots'] = int(max_mem.attrib.get('slots', 16))
    smp = _parse_vcpu_element(dom.find('./vcpu'))
    if smp is not None:
        conf['smp'] = smp


def _parse_vcpu_element(vcpu):
    smp = None
    if vcpu is not None:
        smp = vcpu.attrib.get('current')
        if smp is None:
            smp = vcpu.text
    return smp


def _parse_domain_clock(dom, conf):
    clock = dom.find('./clock')
    if clock is not None:
        conf['timeOffset'] = clock.attrib.get('adjustment', 0)
    if dom.find("./clock/timer[@name='hypervclock']"):
        conf['hypervEnable'] = 'true'


def _parse_domain_os(dom, conf):
    os = dom.find('./os/type')
    if os is not None:
        conf['emulatedMachine'] = os.attrib['machine']

    for param in ('initrd', 'kernel', 'kernelArgs'):
        value = dom.findtext('./os/%s' % param)
        if value is not None:
            conf[param] = value

    if dom.find("./os/bootmenu[@enable='yes']") is not None:
        conf['bootMenuEnable'] = 'true'
    else:
        conf['bootMenuEnable'] = 'false'


def _parse_domain_cpu(dom, conf, arch):
    cpu_topology = dom.find('./cpu/topology')
    if cpu_topology is not None:
        cores = cpu_topology.attrib['cores']
        threads = cpu_topology.attrib['threads']
        sockets = cpu_topology.attrib['sockets']
        conf['smpCoresPerSocket'] = cores
        conf['smpThreadsPerCore'] = threads
        conf['maxVCpus'] = str(int(sockets) * int(cores) * int(threads))

    cpu_tune = dom.find('./cputune')
    if cpu_tune is not None:
        cpu_pinning = {}
        for cpu_pin in dom.findall('./cputune/vcpupin'):
            cpu_pinning[cpu_pin.attrib['vcpu']] = cpu_pin.attrib['cpuset']
        if cpu_pinning:
            conf['cpuPinning'] = cpu_pinning

    cpu_numa = dom.find('./cpu/numa')
    if cpu_numa is not None:
        guest_numa_nodes = []
        for index, cell in enumerate(dom.findall('./cpu/numa/cell')):
            guest_numa_nodes.append({
                'nodeIndex': index,
                'cpus': ','.join(_expand_list(cell.attrib['cpus'])),
                'memory': str(int(cell.attrib['memory']) // 1024),
            })
        conf['guestNumaNodes'] = guest_numa_nodes

    if cpuarch.is_x86(arch):
        _parse_domain_cpu_x86(dom, conf)
    elif cpuarch.is_ppc(arch):
        _parse_domain_cpu_ppc(dom, conf)


def _parse_domain_cpu_x86(dom, conf):
    cpu = dom.find('./cpu')
    if cpu is None:
        return
    features = []
    cpu_mode = cpu.attrib.get('mode')
    if cpu_mode == 'host-passthrough':
        model = 'hostPassthrough'
    elif cpu_mode == 'host-model':
        model = 'hostModel'
    else:
        model = dom.findtext('./cpu/model')
        if model is None:
            model = 'Unknown or Fake'
        features = [
            _parse_domain_cpu_x86_feature(feature)
            for feature in dom.findall('./cpu/features')
        ]

    conf['cpuType'] = ','.join([model] + features)


def _parse_domain_cpu_x86_feature(feature):
    policy = feature.attrib.get('policy', '')
    flag = ''
    if policy == 'require':
        flag = '+'
    elif policy == 'disable':
        flag = '-'
    name = feature.attrib['name']
    return flag + name.replace('sse4.', 'sse4_')


def _parse_domain_cpu_ppc(dom, conf):
    model = dom.findtext('./cpu/model')
    if model is not None:
        conf['cpuType'] = model


def _expand_list(cpuset):
    cpulist = sorted(taskset.cpulist_parse(cpuset))
    return [str(cpu) for cpu in cpulist]


def _parse_domain_input(dom, conf):
    if dom.find("./devices/input[@type='tablet']") is not None:
        conf['tabletEnable'] = 'true'


def make_minimal_domain(dom):
    dom.appendMetadata()
    dom.appendClock()
    if cpuarch.is_x86(dom.arch):
        dom.appendFeatures()
    return dom


def make_placeholder_domain_xml(vm):
    return '''<domain type='qemu'>
  <name>{name}</name>
  <uuid>{id}</uuid>
  <memory unit='KiB'>{memory}</memory>
  <os>
    <type arch="{arch}" machine="{machine}">hvm</type>
  </os>
</domain>'''.format(name=escape(vm.name), id=vm.id, memory=vm.mem_size_mb(),
                    arch=vm.arch, machine=_DEFAULT_MACHINES[vm.arch])


def update_sysinfo(dom, osname, osversion, hostserial):
    sys_info = vmxml.find_first(dom, 'sysinfo/system', None)
    if sys_info is None:
        return

    replaceables = {
        'product': ('OS-NAME:', osname),
        'version': ('OS-VERSION:', osversion),
        'serial': ('HOST-SERIAL:', hostserial),
    }

    for entry in vmxml.children(sys_info):
        name = entry.attrib.get('name', None)
        if name not in replaceables:
            continue

        placeholder, value = replaceables[name]
        if entry.text.startswith(placeholder):
            entry.text = value


def make_mdev_element(mdev_uuid):
    hostdev = ET.Element('hostdev')
    hostdev.set('mode', 'subsystem')
    hostdev.set('type', 'mdev')
    hostdev.set('model', 'vfio-pci')
    source = ET.SubElement(hostdev, 'source')
    address = ET.SubElement(source, 'address')
    address.set('uuid', mdev_uuid)
    return hostdev
