"""
xenconfig.py: Implementation of the XenConfigManager class for managing
configurable items for XenNodes.

Configuration for a XenNode is available at these three levels:
Global config:         XenConfigManager.configs[0] = (type="xen", values)
   Nodes of this machine type have this config. These are the default values.
   XenConfigManager.default_config comes from defaults + xen.conf
Node type config:      XenConfigManager.configs[0] = (type="mytype", values)
   All nodes of this type have this config.
Node-specific config:  XenConfigManager.configs[nodenumber] = (type, values)
   The node having this specific number has this config.
"""

import ConfigParser
import os
import string

from core import constants
from core import logger
from core.api import coreapi
from core.conf import Configurable
from core.conf import ConfigurableManager
from core.enumerations import ConfigDataTypes
from core.enumerations import ConfigFlags
from core.enumerations import ConfigTlvs
from core.enumerations import RegisterTlvs


class XenConfigManager(ConfigurableManager):
    """
    Xen controller object. Lives in a Session instance and is used for
    building Xen profiles.
    """
    name = "xen"
    config_type = RegisterTlvs.EMULATION_SERVER.value

    def __init__(self, session):
        """
        Creates a XenConfigManager instance.

        :param core.session.Session session: session this manager is tied to
        :return: nothing
        """
        ConfigurableManager.__init__(self)
        self.default_config = XenDefaultConfig(session, object_id=None)
        self.loadconfigfile()

    def setconfig(self, nodenum, conftype, values):
        """
        Add configuration values for a node to a dictionary; values are
        usually received from a Configuration Message, and may refer to a
        node for which no object exists yet

        :param int nodenum: node id to configure
        :param str conftype: configuration type
        :param tuple values: values to configure
        :return: None
        """
        # used for storing the global default config
        if nodenum is None:
            nodenum = 0
        return ConfigurableManager.setconfig(self, nodenum, conftype, values)

    def getconfig(self, nodenum, conftype, defaultvalues):
        """
        Get configuration values for a node; if the values don"t exist in
        our dictionary then return the default values supplied; if conftype
        is None then we return a match on any conftype.

        :param int nodenum: node id to configure
        :param str conftype: configuration type
        :param tuple defaultvalues: default values to return
        :return: configuration for node and config type
        :rtype: tuple
        """
        # used for storing the global default config
        if nodenum is None:
            nodenum = 0
        return ConfigurableManager.getconfig(self, nodenum, conftype, defaultvalues)

    def clearconfig(self, nodenum):
        """
        Remove configuration values for a node

        :param int nodenum: node id to clear config
        :return: nothing
        """
        ConfigurableManager.clearconfig(self, nodenum)
        if 0 in self.configs:
            self.configs.pop(0)

    def configure(self, session, config_data):
        """
        Handle configuration messages for global Xen config.

        :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
        """
        return self.default_config.configure(self, config_data)

    def loadconfigfile(self, filename=None):
        """
        Load defaults from the /etc/core/xen.conf file into dict object.

        :param str filename: file name of configuration to load
        :return: nothing
        """
        if filename is None:
            filename = os.path.join(constants.CORE_CONF_DIR, "xen.conf")
        cfg = ConfigParser.SafeConfigParser()
        if filename not in cfg.read(filename):
            logger.warn("unable to read Xen config file: %s", filename)
            return
        section = "xen"
        if not cfg.has_section(section):
            logger.warn("%s is missing a xen section!", filename)
            return
        self.configfile = dict(cfg.items(section))
        # populate default config items from config file entries
        vals = list(self.default_config.getdefaultvalues())
        names = self.default_config.getnames()
        for i in range(len(names)):
            if names[i] in self.configfile:
                vals[i] = self.configfile[names[i]]
        # this sets XenConfigManager.configs[0] = (type="xen", vals)
        self.setconfig(None, self.default_config.name, vals)

    def getconfigitem(self, name, model=None, node=None, value=None):
        """
        Get a config item of the given name, first looking for node-specific
        configuration, then model specific, and finally global defaults.
        If a value is supplied, it will override any stored config.

        :param str name: name of config item to get
        :param model: model config to get
        :param node: node config to get
        :param value: value to override stored config, if provided
        :return: nothing
        """
        if value is not None:
            return value
        n = None
        if node:
            n = node.objid
        (t, v) = self.getconfig(nodenum=n, conftype=model, defaultvalues=None)
        if n is not None and v is None:
            # get item from default config for the node type
            (t, v) = self.getconfig(nodenum=None, conftype=model, defaultvalues=None)
        if v is None:
            # get item from default config for the machine type
            (t, v) = self.getconfig(nodenum=None, conftype=self.default_config.name, defaultvalues=None)

        confignames = self.default_config.getnames()
        if v and name in confignames:
            i = confignames.index(name)
            return v[i]
        else:
            # name may only exist in config file
            if name in self.configfile:
                return self.configfile[name]
            else:
                # logger.warn("missing config item "%s"" % name)
                return None


class XenConfig(Configurable):
    """
    Manage Xen configuration profiles.
    """

    @classmethod
    def configure(cls, xen, config_data):
        """
        Handle configuration messages for setting up a model.
        Similar to Configurable.configure(), but considers opaque data
        for indicating node types.

        :param xen: xen instance to configure
        :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
        """
        reply = None
        node_id = config_data.node
        object_name = config_data.object
        config_type = config_data.type
        opaque = config_data.opaque
        values_str = config_data.data_values

        nodetype = object_name
        if opaque is not None:
            opaque_items = opaque.split(":")
            if len(opaque_items) != 2:
                logger.warn("xen config: invalid opaque data in conf message")
                return None
            nodetype = opaque_items[1]

        logger.info("received configure message for %s", nodetype)
        if config_type == ConfigFlags.REQUEST.value:
            logger.info("replying to configure request for %s " % nodetype)
            # when object name is "all", the reply to this request may be None
            # if this node has not been configured for this model; otherwise we
            # reply with the defaults for this model
            if object_name == "all":
                typeflags = ConfigFlags.UPDATE.value
            else:
                typeflags = ConfigFlags.NONE.value
            values = xen.getconfig(node_id, nodetype, defaultvalues=None)[1]
            if values is None:
                # get defaults from default "xen" config which includes
                # settings from both cls._confdefaultvalues and  xen.conf
                defaults = cls.getdefaultvalues()
                values = xen.getconfig(node_id, cls.name, defaults)[1]
            if values is None:
                return None
            # reply with config options
            if node_id is None:
                node_id = 0
            reply = cls.config_data(0, node_id, typeflags, nodetype, values)
        elif config_type == ConfigFlags.RESET.value:
            if object_name == "all":
                xen.clearconfig(node_id)
        # elif conftype == coreapi.CONF_TYPE_FLAGS_UPDATE:
        else:
            # store the configuration values for later use, when the XenNode
            # object has been created
            if object_name is None:
                logger.info("no configuration object for node %s" % node_id)
                return None
            if values_str is None:
                # use default or preconfigured values
                defaults = cls.getdefaultvalues()
                values = xen.getconfig(node_id, cls.name, defaults)[1]
            else:
                # use new values supplied from the conf message
                values = values_str.split("|")
            xen.setconfig(node_id, nodetype, values)

        return reply

    @classmethod
    def config_data(cls, flags, node_id, type_flags, nodetype, values):
        """
        Convert this class to a Config API message. Some TLVs are defined
        by the class, but node number, conf type flags, and values must
        be passed in.

        :param int flags: configuration flags
        :param int node_id: node id
        :param int type_flags: type flags
        :param int nodetype: node type
        :param tuple values: values
        :return: configuration message
        """
        values_str = string.join(values, "|")
        tlvdata = ""
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.NODE.value, node_id)
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.OBJECT.value, cls.name)
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.TYPE.value, type_flags)
        datatypes = tuple(map(lambda x: x[1], cls.config_matrix))
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.DATA_TYPES.value, datatypes)
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.VALUES.value, values_str)
        captions = reduce(lambda a, b: a + "|" + b, map(lambda x: x[4], cls.config_matrix))
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.CAPTIONS, captions)
        possiblevals = reduce(lambda a, b: a + "|" + b, map(lambda x: x[3], cls.config_matrix))
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.POSSIBLE_VALUES.value, possiblevals)
        if cls.bitmap is not None:
            tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.BITMAP.value, cls.bitmap)
        if cls.config_groups is not None:
            tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.GROUPS.value, cls.config_groups)
        opaque = "%s:%s" % (cls.name, nodetype)
        tlvdata += coreapi.CoreConfigTlv.pack(ConfigTlvs.OPAQUE.value, opaque)
        msg = coreapi.CoreConfMessage.pack(flags, tlvdata)
        return msg


class XenDefaultConfig(XenConfig):
    """
    Global default Xen configuration options.
    """
    name = "xen"
    # Configuration items:
    #   ("name", "type", "default", "possible-value-list", "caption")
    config_matrix = [
        ("ram_size", ConfigDataTypes.STRING.value, "256", "",
         "ram size (MB)"),
        ("disk_size", ConfigDataTypes.STRING.value, "256M", "",
         "disk size (use K/M/G suffix)"),
        ("iso_file", ConfigDataTypes.STRING.value, "", "",
         "iso file"),
        ("mount_path", ConfigDataTypes.STRING.value, "", "",
         "mount path"),
        ("etc_path", ConfigDataTypes.STRING.value, "", "",
         "etc path"),
        ("persist_tar_iso", ConfigDataTypes.STRING.value, "", "",
         "iso persist tar file"),
        ("persist_tar", ConfigDataTypes.STRING.value, "", "",
         "persist tar file"),
        ("root_password", ConfigDataTypes.STRING.value, "password", "",
         "root password"),
    ]

    config_groups = "domU properties:1-%d" % len(config_matrix)
