#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free  Software Foundation; either version 2 of the License, or
# (at your option)  any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301 USA.
#

import virtconv.formats as formats
import virtconv.vmcfg as vmcfg
import virtconv.diskcfg as diskcfg
import virtconv.netdevcfg as netdevcfg

import sys
import re
import os
import logging
import shlex


_VMX_MAIN_TEMPLATE = """
#!/usr/bin/vmplayer

# Generated by %(progname)s
# http://virt-manager.org/

# This is a Workstation 5 or 5.5 config file and can be used with Player
config.version = "8"
virtualHW.version = "4"
guestOS = "other"
displayName = "%(vm_name)s"
annotation = "%(vm_description)s"
guestinfo.vmware.product.long = "%(vm_name)s"
guestinfo.vmware.product.url = "http://virt-manager.org/"
guestinfo.vmware.product.class = "virtual machine"
numvcpus = "%(vm_nr_vcpus)s"
memsize = "%(vm_memory)d"
MemAllowAutoScaleDown = "FALSE"
MemTrimRate = "-1"
uuid.action = "create"
tools.remindInstall = "TRUE"
hints.hideAll = "TRUE"
tools.syncTime = "TRUE"
serial0.present = "FALSE"
serial1.present = "FALSE"
parallel0.present = "FALSE"
logging = "TRUE"
log.fileName = "%(vm_name)s.log"
log.append = "TRUE"
log.keepOld = "3"
isolation.tools.hgfs.disable = "FALSE"
isolation.tools.dnd.disable = "FALSE"
isolation.tools.copy.enable = "TRUE"
isolation.tools.paste.enabled = "TRUE"
floppy0.present = "FALSE"
"""
_VMX_ETHERNET_TEMPLATE = """
ethernet%(dev)s.present = "TRUE"
ethernet%(dev)s.connectionType = "nat"
ethernet%(dev)s.addressType = "generated"
ethernet%(dev)s.generatedAddressOffset = "0"
ethernet%(dev)s.autoDetect = "TRUE"
"""
_VMX_IDE_TEMPLATE = """
# IDE disk
ide%(dev)s.present = "TRUE"
ide%(dev)s.fileName = "%(disk_filename)s"
ide%(dev)s.mode = "persistent"
ide%(dev)s.startConnected = "TRUE"
ide%(dev)s.writeThrough = "TRUE"
"""


class _VMXLine(object):
    """
    Class tracking an individual line in a VMX/VMDK file
    """
    def __init__(self, content):
        self.content = content

        self.pair = None
        self.is_blank = False
        self.is_comment = False
        self.is_disk = False
        self._parse()

    def _parse(self):
        line = self.content.strip()
        if not line:
            self.is_blank = True
        elif line.startswith("#"):
            self.is_comment = True
        elif line.startswith("RW ") or line.startswith("RDONLY "):
            self.is_disk = True
        else:
            # Expected that this will raise an error for unknown format
            before_eq, after_eq = line.split("=", 1)
            key = before_eq.strip().lower()
            value = after_eq.strip().strip('"')
            self.pair = (key, value)

    def parse_disk_path(self):
        # format:
        # RW 16777216 VMFS "test-flat.vmdk"
        # RDONLY 156296322 V2I "virtual-pc-diskformat.v2i"
        content = self.content.split(" ", 3)[3]
        if not content.startswith("\""):
            raise ValueError("Path was not fourth entry in VMDK storage line")
        return shlex.split(content, " ", 1)[0]


class _VMXFile(object):
    """
    Class tracking a parsed VMX/VMDK format file
    """
    def __init__(self, content):
        self.content = content
        self.lines = []

        self._parse()

    def _parse(self):
        for line in self.content:
            try:
                lineobj = _VMXLine(line)
                self.lines.append(lineobj)
            except Exception, e:
                raise Exception(_("Syntax error at line %d: %s\n%s") %
                    (len(self.lines) + 1, line.strip(), e))

    def pairs(self):
        ret = {}
        for line in self.lines:
            if line.pair:
                ret[line.pair[0]] = line.pair[1]
        return ret


def parse_vmdk(disk, filename):
    """
    Parse a VMDK descriptor file
    Reference: http://sanbarrow.com/vmdk-basics.html
    """
    # Detect if passed file is a descriptor file
    # Assume descriptor isn't larger than 10K
    if not os.path.exists(filename):
        logging.debug("VMDK file '%s' doesn't exist", filename)
        return
    if os.path.getsize(filename) > (10 * 1024):
        logging.debug("VMDK file '%s' too big to be a descriptor", filename)
        return

    f = open(filename, "r")
    content = f.readlines()
    f.close()

    try:
        vmdkfile = _VMXFile(content)
    except:
        logging.exception("%s looked like a vmdk file, but parsing failed",
                          filename)
        return

    disklines = [l for l in vmdkfile.lines if l.is_disk]
    if len(disklines) == 0:
        raise RuntimeError(_("Didn't detect a storage line in the VMDK "
                             "descriptor file"))
    if len(disklines) > 1:
        raise RuntimeError(_("Don't know how to handle multistorage VMDK "
                             "descriptors"))

    diskline = disklines[0]
    newpath = diskline.parse_disk_path()
    logging.debug("VMDK file parsed path %s->%s", disk.path, newpath)
    disk.path = newpath


def parse_netdev_entry(vm, fullkey, value):
    """
    Parse a particular key/value for a network.  Throws ValueError.
    """

    ignore, ignore, inst, key = re.split("^(ethernet)([0-9]+).", fullkey)

    lvalue = value.lower()

    if key == "present" and lvalue == "false":
        return

    if not vm.netdevs.get(inst):
        vm.netdevs[inst] = netdevcfg.netdev(typ=netdevcfg.NETDEV_TYPE_UNKNOWN)

    # "vlance", "vmxnet", "e1000"
    if key == "virtualdev":
        vm.netdevs[inst].driver = lvalue
    if key == "addresstype" and lvalue == "generated":
        vm.netdevs[inst].mac = "auto"
    # we ignore .generatedAddress for auto mode
    if key == "address":
        vm.netdevs[inst].mac = lvalue


def parse_disk_entry(vm, fullkey, value):
    """
    Parse a particular key/value for a disk.  FIXME: this should be a
    lot smarter.
    """

    # skip bus values, e.g. 'scsi0.present = "TRUE"'
    if re.match(r"^(scsi|ide)[0-9]+[^:]", fullkey):
        return

    ignore, bus, bus_nr, inst, key = re.split(r"^(scsi|ide)([0-9]+):([0-9]+)\.",
        fullkey)

    lvalue = value.lower()

    if key == "present" and lvalue == "false":
        return

    # Does anyone else think it's scary that we're still doing things
    # like this?
    if bus == "ide":
        inst = int(bus_nr) * 2 + (int(inst) % 2)
    elif bus == "scsi":
        inst = int(bus_nr) * 16 + (int(inst) % 16)


    devid = (bus, inst)
    if not vm.disks.get(devid):
        vm.disks[devid] = diskcfg.disk(bus=bus,
            typ=diskcfg.DISK_TYPE_DISK)
    disk = vm.disks[devid]

    if key == "devicetype":
        if lvalue == "atapi-cdrom" or lvalue == "cdrom-raw":
            disk.type = diskcfg.DISK_TYPE_CDROM
        elif lvalue == "cdrom-image":
            disk.type = diskcfg.DISK_TYPE_ISO

    if key == "filename":
        disk.path = value
        disk.format = diskcfg.DISK_FORMAT_RAW
        if lvalue.endswith(".vmdk"):
            disk.format = diskcfg.DISK_FORMAT_VMDK
            # See if the filename is actually a VMDK descriptor file
            parse_vmdk(disk, disk.path)



class vmx_parser(formats.parser):
    """
    Support for VMWare .vmx files.  Note that documentation is
    particularly sparse on this format, with pretty much the best
    resource being http://sanbarrow.com/vmx.html
    """

    name = "vmx"
    suffix = ".vmx"
    can_import = True
    can_export = True
    can_identify = True

    @staticmethod
    def identify_file(input_file):
        """
        Return True if the given file is of this format.
        """
        infile = open(input_file, "r")
        content = infile.readlines()
        infile.close()

        for line in content:
            # some .vmx files don't bother with the header
            if (re.match(r'^config.version\s+=', line) or
                re.match(r'^#!\s*/usr/bin/vm(ware|player)', line)):
                return True
        return False

    @staticmethod
    def import_file(input_file):
        """
        Import a configuration file.  Raises if the file couldn't be
        opened, or parsing otherwise failed.
        """

        vm = vmcfg.vm()

        infile = open(input_file, "r")
        contents = infile.readlines()
        infile.close()
        logging.debug("Importing VMX file:\n%s", "".join(contents))

        vmxfile = _VMXFile(contents)
        config = vmxfile.pairs()

        if not config.get("displayname"):
            raise ValueError(_("No displayName defined in '%s'") %
                             input_file)

        vm.name = config.get("displayname")
        vm.memory = config.get("memsize")
        vm.description = config.get("annotation")
        vm.nr_vcpus = config.get("numvcpus")

        for key, value in config.items():
            if key.startswith("scsi") or key.startswith("ide"):
                parse_disk_entry(vm, key, value)
            if key.startswith("ethernet"):
                parse_netdev_entry(vm, key, value)

        for devid, disk in vm.disks.iteritems():
            if disk.type == diskcfg.DISK_TYPE_DISK:
                continue

            # vmx files often have dross left in path for CD entries
            if (disk.path is None
                or disk.path.lower() == "auto detect" or
                not os.path.exists(disk.path)):
                vm.disks[devid].path = None

        vm.validate()
        return vm

    @staticmethod
    def export(vm):
        """
        Export a configuration file as a string.
        @vm vm configuration instance

        Raises ValueError if configuration is not suitable.
        """

        vm.description = vm.description.strip()
        vm.description = vm.description.replace("\n", "|")
        vmx_out_template = []
        vmx_dict = {
            #"now": time.strftime("%Y-%m-%dT%H:%M:%S %Z", time.localtime()),
            "progname": os.path.basename(sys.argv[0]),
            "vm_name": vm.name,
            "vm_description": vm.description or "None",
            "vm_nr_vcpus" : vm.nr_vcpus,
            "vm_memory": long(vm.memory)
       }
        vmx_out = _VMX_MAIN_TEMPLATE % vmx_dict
        vmx_out_template.append(vmx_out)

        disk_out_template = []
        for devid, disk in sorted(vm.disks.items()):
            bus, dev_nr = devid
            if bus.lower() != "ide":
                logging.debug("Disk bus '%s' not yet supported. Skipping.",
                               bus.lower())
                continue

            dev = "%d:%d" % (dev_nr / 2, dev_nr % 2)
            disk_dict = {
                "dev": dev,
                "disk_filename" : disk.path
           }
            disk_out = _VMX_IDE_TEMPLATE % disk_dict
            disk_out_template.append(disk_out)

        eth_out_template = []
        if len(vm.netdevs):
            for devnum in vm.netdevs:
                eth_dict = {
                   "dev" : devnum
               }
                eth_out = _VMX_ETHERNET_TEMPLATE % eth_dict
                eth_out_template.append(eth_out)

        return "".join(vmx_out_template + disk_out_template + eth_out_template)

formats.register_parser(vmx_parser)
