"""
Common support for configurable CORE objects.
"""

import string

from core import logger
from core.data import ConfigData
from core.enumerations import ConfigDataTypes
from core.enumerations import ConfigFlags


class ConfigurableManager(object):
    """
    A generic class for managing Configurables. This class can register
    with a session to receive Config Messages for setting some parameters
    for itself or for the Configurables that it manages.
    """
    # name corresponds to configuration object field
    name = ""

    # type corresponds with register message types
    config_type = None

    def __init__(self):
        """
        Creates a ConfigurableManager instance.
        """
        # configurable key=values, indexed by node number
        self.configs = {}

        # TODO: fix the need for this and isolate to the mobility class that wants it
        self._modelclsmap = {}

    def configure(self, session, config_data):
        """
        Handle configure messages. The configuration message sent to a
        ConfigurableManager usually is used to:
        1. Request a list of Configurables (request flag)
        2. Reset manager and clear configs (reset flag)
        3. Send values that configure the manager or one of its Configurables

        :param core.session.Session session: CORE session object
        :param ConfigData config_data: configuration data for carrying out a configuration
        :return: response messages
        """

        if config_data.type == ConfigFlags.REQUEST.value:
            return self.configure_request(config_data)
        elif config_data.type == ConfigFlags.RESET.value:
            return self.configure_reset(config_data)
        else:
            return self.configure_values(config_data)

    def configure_request(self, config_data):
        """
        Request configuration data.

        :param ConfigData config_data: configuration data for carrying out a configuration
        :return: nothing
        """
        return None

    def configure_reset(self, config_data):
        """
        By default, resets this manager to clear configs.

        :param ConfigData config_data: configuration data for carrying out a configuration
        :return: reset response messages, or None
        """
        return self.reset()

    def configure_values(self, config_data):
        """
        Values have been sent to this manager.

        :param ConfigData config_data: configuration data for carrying out a configuration
        :return: nothing
        """
        return None

    def configure_values_keyvalues(self, config_data, target, keys):
        """
        Helper that can be used for configure_values for parsing in
        'key=value' strings from a values field. The key name must be
        in the keys list, and target.key=value is set.

        :param ConfigData config_data: configuration data for carrying out a configuration
        :param target: target to set attribute values on
        :param keys: list of keys to verify validity
        :return: nothing
        """
        values = config_data.data_values

        if values is None:
            return None

        kvs = values.split('|')
        for kv in kvs:
            try:
                key, value = kv.split('=', 1)
                if value is not None and not value.strip():
                    value = None
            except ValueError:
                # value only
                key = keys[kvs.index(kv)]
                value = kv
            if key not in keys:
                raise ValueError("invalid key: %s" % key)
            if value is not None:
                setattr(target, key, value)

        return None

    def reset(self):
        """
        Reset functionality for the configurable class.

        :return: nothing
        """
        return None

    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
        :param conftype: configuration types
        :param values: configuration values
        :return: nothing
        """
        logger.info("setting config for node(%s): %s - %s", nodenum, conftype, values)
        conflist = []
        if nodenum in self.configs:
            oldlist = self.configs[nodenum]
            found = False
            for t, v in oldlist:
                if t == conftype:
                    # replace existing config
                    found = True
                    conflist.append((conftype, values))
                else:
                    conflist.append((t, v))
            if not found:
                conflist.append((conftype, values))
        else:
            conflist.append((conftype, values))
        self.configs[nodenum] = conflist

    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

        :param int nodenum: node id
        :param conftype: configuration type
        :param defaultvalues: default values
        :return: configuration type and default values
        :type: tuple
        """
        logger.info("getting config for node(%s): %s - default(%s)",
                    nodenum, conftype, defaultvalues)
        if nodenum in self.configs:
            # return configured values
            conflist = self.configs[nodenum]
            for t, v in conflist:
                if conftype is None or t == conftype:
                    return t, v
        # return default values provided (may be None)
        return conftype, defaultvalues

    def getallconfigs(self, use_clsmap=True):
        """
        Return (nodenum, conftype, values) tuples for all stored configs.
        Used when reconnecting to a session.

        :param bool use_clsmap: should a class map be used, default to True
        :return: list of all configurations
        :rtype: list
        """
        r = []
        for nodenum in self.configs:
            for t, v in self.configs[nodenum]:
                if use_clsmap:
                    t = self._modelclsmap[t]
                r.append((nodenum, t, v))
        return r

    def clearconfig(self, nodenum):
        """
        remove configuration values for the specified node;
        when nodenum is None, remove all configuration values

        :param int nodenum: node id
        :return: nothing
        """
        if nodenum is None:
            self.configs = {}
            return
        if nodenum in self.configs:
            self.configs.pop(nodenum)

    def setconfig_keyvalues(self, nodenum, conftype, keyvalues):
        """
        Key values list of tuples for a node.

        :param int nodenum: node id
        :param conftype: configuration type
        :param keyvalues: key valyes
        :return: nothing
        """
        if conftype not in self._modelclsmap:
            logger.warn("Unknown model type '%s'" % conftype)
            return
        model = self._modelclsmap[conftype]
        keys = model.getnames()
        # defaults are merged with supplied values here
        values = list(model.getdefaultvalues())
        for key, value in keyvalues:
            if key not in keys:
                logger.warn("Skipping unknown configuration key for %s: '%s'" % \
                            (conftype, key))
                continue
            i = keys.index(key)
            values[i] = value
        self.setconfig(nodenum, conftype, values)

    def getmodels(self, n):
        """
        Return a list of model classes and values for a net if one has been
        configured. This is invoked when exporting a session to XML.
        This assumes self.configs contains an iterable of (model-names, values)
        and a self._modelclsmapdict exists.

        :param n: network node to get models for
        :return: list of model and values tuples for the network node
        :rtype: list
        """
        r = []
        if n.objid in self.configs:
            v = self.configs[n.objid]
            for model in v:
                cls = self._modelclsmap[model[0]]
                vals = model[1]
                r.append((cls, vals))
        return r


class Configurable(object):
    """
    A generic class for managing configuration parameters.
    Parameters are sent via Configuration Messages, which allow the GUI
    to build dynamic dialogs depending on what is being configured.
    """
    name = ""
    # Configuration items:
    #   ('name', 'type', 'default', 'possible-value-list', 'caption')
    config_matrix = []
    config_groups = None
    bitmap = None

    def __init__(self, session=None, object_id=None):
        """
        Creates a Configurable instance.

        :param core.session.Session session: session for this configurable
        :param object_id:
        """
        self.session = session
        self.object_id = object_id

    def reset(self):
        """
        Reset method.

        :return: nothing
        """
        pass

    def register(self):
        """
        Register method.

        :return: nothing
        """
        pass

    @classmethod
    def getdefaultvalues(cls):
        """
        Retrieve default values from configuration matrix.

        :return: tuple of default values
        :rtype: tuple
        """
        return tuple(map(lambda x: x[2], cls.config_matrix))

    @classmethod
    def getnames(cls):
        """
        Retrieve name values from configuration matrix.

        :return: tuple of name values
        :rtype: tuple
        """
        return tuple(map(lambda x: x[0], cls.config_matrix))

    @classmethod
    def configure(cls, manager, config_data):
        """
        Handle configuration messages for this object.

        :param ConfigurableManager manager: configuration manager
        :param config_data: configuration data
        :return: configuration data object
        :rtype: ConfigData
        """
        reply = None
        node_id = config_data.node
        object_name = config_data.object
        config_type = config_data.type
        interface_id = config_data.interface_number
        values_str = config_data.data_values

        if interface_id is not None:
            node_id = node_id * 1000 + interface_id

        logger.info("received configure message for %s nodenum:%s", cls.name, str(node_id))
        if config_type == ConfigFlags.REQUEST.value:
            logger.info("replying to configure request for %s model", cls.name)
            # 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":
                defaults = None
                typeflags = ConfigFlags.UPDATE.value
            else:
                defaults = cls.getdefaultvalues()
                typeflags = ConfigFlags.NONE.value
            values = manager.getconfig(node_id, cls.name, defaults)[1]
            if values is None:
                logger.warn("no active configuration for node (%s), ignoring request")
                # node has no active config for this model (don't send defaults)
                return None
            # reply with config options
            reply = cls.config_data(0, node_id, typeflags, values)
        elif config_type == ConfigFlags.RESET.value:
            if object_name == "all":
                manager.clearconfig(node_id)
        # elif conftype == coreapi.CONF_TYPE_FLAGS_UPDATE:
        else:
            # store the configuration values for later use, when the node
            # object has been created
            if object_name is None:
                logger.info("no configuration object for node %s", node_id)
                return None
            defaults = cls.getdefaultvalues()
            if values_str is None:
                # use default or preconfigured values
                values = manager.getconfig(node_id, cls.name, defaults)[1]
            else:
                # use new values supplied from the conf message
                values = values_str.split('|')
                # determine new or old style config
                new = cls.haskeyvalues(values)
                if new:
                    new_values = list(defaults)
                    keys = cls.getnames()
                    for v in values:
                        key, value = v.split('=', 1)
                        try:
                            new_values[keys.index(key)] = value
                        except ValueError:
                            logger.info("warning: ignoring invalid key '%s'" % key)
                    values = new_values
            manager.setconfig(node_id, object_name, values)

        return reply

    @classmethod
    def config_data(cls, flags, node_id, type_flags, 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 flags: message flags
        :param int node_id: node id
        :param type_flags: type flags
        :param values: values
        :return: configuration data object
        :rtype: ConfigData
        """
        keys = cls.getnames()
        keyvalues = map(lambda a, b: "%s=%s" % (a, b), keys, values)
        values_str = string.join(keyvalues, '|')
        datatypes = tuple(map(lambda x: x[1], cls.config_matrix))
        captions = reduce(lambda a, b: a + '|' + b, map(lambda x: x[4], cls.config_matrix))
        possible_valuess = reduce(lambda a, b: a + '|' + b, map(lambda x: x[3], cls.config_matrix))

        return ConfigData(
            message_type=flags,
            node=node_id,
            object=cls.name,
            type=type_flags,
            data_types=datatypes,
            data_values=values_str,
            captions=captions,
            possible_values=possible_valuess,
            bitmap=cls.bitmap,
            groups=cls.config_groups
        )

    @staticmethod
    def booltooffon(value):
        """
        Convenience helper turns bool into on (True) or off (False) string.

        :param str value: value to retrieve on/off value for
        :return: on or off string
        :rtype: str
        """
        if value == "1" or value == "true" or value == "on":
            return "on"
        else:
            return "off"

    @staticmethod
    def offontobool(value):
        """
        Convenience helper for converting an on/off string to a integer.

        :param str value: on/off string
        :return: on/off integer value
        :rtype: int
        """
        if type(value) == str:
            if value.lower() == "on":
                return 1
            elif value.lower() == "off":
                return 0
        return value

    @classmethod
    def valueof(cls, name, values):
        """
        Helper to return a value by the name defined in confmatrix.
        Checks if it is boolean

        :param str name: name to get value of
        :param values: values to get value from
        :return: value for name
        """
        i = cls.getnames().index(name)
        if cls.config_matrix[i][1] == ConfigDataTypes.BOOL.value and values[i] != "":
            return cls.booltooffon(values[i])
        else:
            return values[i]

    @staticmethod
    def haskeyvalues(values):
        """
        Helper to check for list of key=value pairs versus a plain old
        list of values. Returns True if all elements are "key=value".

        :param values: items to check for key/value pairs
        :return: True if all values are key/value pairs, False otherwise
        :rtype: bool
        """
        if len(values) == 0:
            return False
        for v in values:
            if "=" not in v:
                return False
        return True

    def getkeyvaluelist(self):
        """
        Helper to return a list of (key, value) tuples. Keys come from
        configuration matrix and values are instance attributes.

        :return: tuples of key value pairs
        :rtype: list
        """
        key_values = []

        for name in self.getnames():
            if hasattr(self, name):
                value = getattr(self, name)
                key_values.append((name, value))

        return key_values