# IM - Infrastructure Manager
# Copyright (C) 2011 - GRyCAP - Universitat Politecnica de Valencia
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

import copy
import re
from packaging.version import Version

try:
    unicode
except Exception:
    unicode = bytes


def UnitToValue(unit):
    """Return the value of an unit."""

    if not unit:
        return 1
    unit = unit[0].upper()
    if unit == "K":
        return 1024
    if unit == "M":
        return 1024 * 1024
    if unit == "G":
        return 1024 * 1024 * 1024
    return 1


def is_version(version, _):
    if version.getValue() == "":
        return True
    else:
        return all([num.isdigit() for num in version.getValue().split(".")])


def check_password(password, _):
    passwd = password.value
    # Al least 6 chars
    if len(passwd) <= 6:
        return False
    # At least one Upper leter
    if passwd.lower() == passwd:
        return False
    # At least one digit
    if len([x for x in passwd if x.isdigit()]) == 0:
        return False
    # At least one special char
    if len([x for x in passwd if not x.isalnum()]) == 0:
        return False
    return True


def check_outports_format(outports, _):
    """
    Check the format of the outports string.
    Valid formats:
    8899/tcp-8899/tcp,22/tcp-22/tcp
    8899/tcp-8899,22/tcp-22
    8899-8899,22-22
    8899/tcp,22/udp
    8899,22
    1:10/tcp,9:22/udp
    1:10,9:22
    """
    try:
        outport.parseOutPorts(outports.getValue())
    except Exception:
        return False
    else:
        return True


class RADLParseException(Exception):
    """Error parsing RADL document."""

    def __init__(self, msg="", line=None):
        if line:
            msg = "Line %d: %s" % (line, msg)
        self.line = line
        Exception.__init__(self, msg)


class Feature:
    """
    Every property that can appear in the definitions of a ``network`` and ``system``.

    Args:
    - prop: feature name.
    - operator: ``<=``, ``=``, ``=>`` or ``contains``.
    - value: value associated to the feature.
    - unit: like ``K``, ``M`` and ``G``.
    - line: line number in the RADL document.
    """

    def __init__(self, prop=None, operator=None, value=None, unit='', line=None):
        self.prop = prop
        self.operator = operator
        self.value = value
        self.unit = unit
        self.line = line

    def __str__(self):
        if isinstance(self.value, list):
            return "{0} {1} ['{2}']".format(self.prop, self.operator, "','".join(self.value))
        else:
            return ("{0} {1} ({2})" if self.operator == "contains" else
                    "{0} {1} '{2}'" if isinstance(self.value, str) or isinstance(self.value, unicode) else
                    "{0} {1} {2}{3}").format(self.prop, self.operator, self.value,
                                             self.unit if self.unit else "")

    def clone(self):
        """Return a copy of this Feature."""

        return copy.deepcopy(self)

    def getLogOperator(self):
        """Return the operator of this Feature in python style."""
        if self.operator == "=":
            return "=="
        else:
            return self.operator

    def getValue(self, unit=None):
        """
        Return the value of the feature.

        If the unit is specified and the feature has a unit, the value is converted

        Args:
        - unit(str,optional): A unit to convert the current feature value ('B','K','M','G')
        """

        if unit or self.unit:
            r = float(self.value * UnitToValue(self.unit)) / UnitToValue(unit)
            return int(round(r)) if isinstance(self.value, int) else r
        return self.value

    def _check(self, check, radl):
        """
        Check type, operator and unit in a feature.

        Args:
        - check(tuple):
           - v[0]: expected type of the feature value.
           - v[1]: can be a list of possible values or a function to test the value or None.
           - v[2] (optional): can be a list of possible units; if None or not set the
             unit valid is none.
        - radl: second argument passed when calling v[1].
        """

        # Check type
        # If the value must be float, int is also valid
        if check[0] == float:
            if not isinstance(self.value, int) and not isinstance(self.value, float):
                raise RADLParseException("Invalid type; expected %s" % check[0],
                                         line=self.line)
        elif check[0] == str:
            if not isinstance(self.value, str) and not isinstance(self.value, unicode):
                raise RADLParseException("Invalid type; expected %s" % check[0],
                                         line=self.line)
        else:
            if not isinstance(self.value, check[0]):
                raise RADLParseException("Invalid type; expected %s" % check[0],
                                         line=self.line)
        # Check operator
        if (isinstance(self.value, str) or isinstance(self.value, unicode)) and self.prop.find('version') == -1:
            if self.operator != "=":
                raise RADLParseException("Invalid operator; expected '='",
                                         line=self.line)
        elif isinstance(self.value, int) or isinstance(self.value, float) or self.prop.find('version') >= 0:
            if self.operator not in ["=", "<=", ">=", ">", "<"]:
                raise RADLParseException("Invalid operator; expected '=', '<=', " +
                                         "'>=', '>' or '<'", line=self.line)
        elif isinstance(self.value, Features):
            if self.operator != "contains":
                raise RADLParseException(
                    "Invalid operator; expected 'contains'", line=self.line)
        # Check value
        if isinstance(check[1], list):
            if self.value.upper() not in check[1]:
                raise RADLParseException("Invalid value; expected one of %s" % check[1],
                                         line=self.line)
        elif callable(check[1]):
            if not check[1](self, radl):
                raise RADLParseException("Invalid value in property '%s'" % self.prop, line=self.line)
        # Check unit
        if len(check) < 3 or check[2] is None:
            if self.unit:
                raise RADLParseException("Invalid unit; expected none", line=self.line)
        elif len(check) > 2 and check[2]:
            if self.unit.upper() not in check[2]:
                raise RADLParseException(
                    "Invalid unit; expected one of %s" % check[2], line=self.line)
        return True


class Features(object):
    """
    Collects a group of features.
    """

    def __init__(self, features=None):
        self.props = {}
        if features:
            for f in features:
                self.addFeature(f)

    @property
    def features(self):
        """List of features."""

        r = []
        for _, inter in self.props.items():
            if isinstance(inter, tuple):
                if (inter[0] and inter[1] and inter[0].getValue() == inter[1].getValue() and
                        inter[0].operator == "=" and inter[1].operator == "="):
                    r.append(inter[0])
                else:
                    r.extend([f for f in inter if f])
            elif isinstance(inter, dict):
                r.extend(inter.values())
            elif isinstance(inter, list):
                r.extend(inter)
            else:
                r.append(inter)
        return r

    def __str__(self):
        return " and\n".join([str(f) for f in self.features])

    def __eq__(self, other):
        if other is None:
            return self is None
        return self.props == other.props

    def clone(self):
        """Return a copy of this aspect."""

        return copy.deepcopy(self)

    def addFeature(self, f, conflict="error", missing="other"):
        """
        Add a feature.

        Args:

        - f(Feature): feature to add.
        - conflict(str): if a property hasn't compatible values/constrains, do:
           - ``"error"``: raise exception.
           - ``"ignore"``: go on.
           - ``"me"``: keep the old value.
           - ``"other"``: set the passed value.
        - missing(str): if a property has not been set yet, do:
           - ``"error"``: raise exception.
           - ``"ignore"``: do nothning.
           - ``"me"``: do nothing.
           - ``"other"``: set the passed value.
        """

        OPTIONS = ["error", "ignore", "me", "other"]
        assert missing in OPTIONS, "Invalid value in `missing`."
        assert conflict in OPTIONS, "Invalid value in `missing`."

        if f.prop not in self.props and missing == "error":
            raise Exception("Property has not set.")
        elif f.prop not in self.props and missing in ["ignore", "first"]:
            return

        if isinstance(f.value, (int, float)):
            if f.operator == "=":
                inter1 = (f, f)
            elif f.operator[0] == "<":
                inter1 = (None, f)
            elif f.operator[0] == ">":
                inter1 = (f, None)
            inter0 = self.props.get(f.prop, (None, None))
            try:
                self.props[f.prop] = Features._applyInter(inter0, inter1, conflict)
            except Exception as e:
                raise RADLParseException("%s. Involved features: %s" % (e, [str(f0) for f0 in inter0]),
                                         line=f.line)
        elif isinstance(f, SoftFeatures):
            self.props.setdefault(f.prop, []).append(f)
        elif f.operator == "contains":
            if f.prop in self.props and f.value.getValue("name") in self.props[f.prop]:
                feature = self.props[f.prop][f.value.getValue("name")].clone()
                for f0 in f.value.features:
                    feature.value.addFeature(f0, conflict, missing)
                self.props[f.prop][f.value.getValue("name")] = feature
            else:
                self.props.setdefault(f.prop, {})[f.value.getValue("name")] = f
        else:
            value0 = self.props.get(f.prop, None)
            if not value0 or (conflict == "other"):
                self.props[f.prop] = f
            elif value0.value != f.value and conflict == "error":
                raise RADLParseException("Conflict adding `%s` because `%s` is already set and conflict is"
                                         " %s" % (f, value0, conflict), line=f.line)

    def hasFeature(self, prop, check_softs=False):
        """Return if there is a property with that name."""

        return prop in self.props or (check_softs and
                                      any([fs.hasFeature(prop) for fs in self.props.get(SoftFeatures.SOFT, [])]))

    def getValue(self, prop, default=None):
        """Return the value of feature with that name or ``default``."""

        f = self.props.get(prop, None)
        if not f:
            return default
        if isinstance(f, Feature):
            return f.getValue()
        if isinstance(f, tuple):
            # if f[0].getValue() == f[1].getValue():
            #    return f[0].getValue()
            # Miguel: para poder probar
            if f[0]:
                return f[0].getValue()
            elif f[1]:
                return f[1].getValue()
            raise Exception("Getting value from a property with a constrain")
        return f

    def getFeature(self, prop):
        """Return the feature with that name."""

        f = self.props.get(prop, None)
        if not f:
            return None
        if isinstance(f, Feature):
            return f
        if isinstance(f, tuple):
            if f[0]:
                return f[0]
            elif f[1]:
                return f[1]
            raise Exception("Getting value from a property with a constrain")
        if isinstance(f, list):
            return f
        raise Exception("Getting value from a not simple property.")

    def setValue(self, prop, value, unit=None):
        """Set the value of feature with that name."""

        if isinstance(value, int) or isinstance(value, float):
            if prop in self.props:
                for i, j in [(0, 1), (1, 0)]:
                    if self.props[prop][i] is None:
                        self.props[prop] = (self.props[prop][j], self.props[prop][j])
                for v in self.props[prop]:
                    v.value, v.unit = value, unit
            else:
                f = Feature(prop, "=", value, unit=unit)
                self.props[prop] = (f, f)
        elif prop in self.props:
            self.props[prop].value, self.props[prop].unit = value, unit
        else:
            self.props[prop] = Feature(prop, "=", value, unit=unit)

    def delValue(self, prop):
        """Remove the feature with that name."""

        try:
            del self.props[prop]
        except Exception:
            pass

    @staticmethod
    def _applyInter(finter0, finter1, conflict="ignore"):
        """
        Return the restriction of first interval by the second.

        Args:

        - inter0, inter1 (tuple of Feature): intervals

        Return(tuple of Feature): the resulting interval
        - conflict(str): if a property hasn't compatible values/constrains, do:
           - ``"error"``: raise exception.
           - ``"ignore"``: return None.
           - ``"me"``: return finter0.
           - ``"other"``: return finter1.
        """

        OPTIONS = ["error", "ignore", "me", "other"]
        assert conflict in OPTIONS, "Invalid value in `conflict`."

        # Compute the comparison of the interval extremes
        min_int = -2**63
        # Remember, None <= number and None <= None are True, but number <= None is False.
        inter0 = tuple([f.getValue() if f else min_int for f in finter0])
        inter1 = tuple([f.getValue() if f else min_int for f in finter1])
        le00 = inter0[0] <= inter1[0]                           # finter0[0] <= finter1[0]
        le01 = inter1[1] == min_int or inter0[0] <= inter1[1]   # finter0[0] <= finter1[1]
        le11 = inter1[1] == min_int or (inter0[1] != min_int and inter0[1] <= inter1[1])  # finter0[1] <= finter1[1]
        ge00 = not le00 or inter0[0] == inter1[0]               # finter0[0] >= finter1[0]
        ge10 = inter0[1] == min_int or inter0[1] >= inter1[0]   # finter0[1] >= finter1[0]

        # print "\n".join("%s: %s" % (s, v) for v, s in [
        #    (le00, "finter0[0] <= finter1[0]"),
        #    (le01, "finter0[0] <= finter1[1]"),
        #    (le11, "finter0[1] <= finter1[1]"),
        #    (ge00, "finter0[0] >= finter1[0]"),
        #    (ge10, "finter0[1] >= finter1[0]") ])

        # First interval is (  ), second interval is [  ]
        if le00 and ge10 and le11:                       # ( [ ) ] chain first-second
            return finter1[0], finter0[1]
        elif le00 and ge10 and not le11:                 # ( [ ] )  second is inside first
            return finter1
        elif ge00 and le01 and le11:                     # [ ( ) ] first is inside second
            return finter0
        elif ge00 and le01 and not le11:                 # [ ( ] ) chain second-first
            return finter0[0], finter1[1]
        elif conflict == "me":
            return finter0
        elif conflict == "other":
            return finter1
        elif conflict == "error":
            raise Exception("Disjoint intervals!")
        return None

    def applyFeatures(self, new_features, conflict="error", missing="error"):
        """
        Apply the constrain of the features passed to this instance.

        .. warning::
           Feature instances are only considered, that is, SoftFeatures will be
           not considered.

        Args:

        - new_features(Features): features to apply
        - conflict(str): if a property hasn't compatible values/constrains, do:
           - ``"error"``: raise exception.
           - ``"ignore"``: nothing.
           - ``"me"``: preserve the original value.
           - ``"other"``: set like the passed feature.
        - missing(str): if a property is missing in some side, do:
           - ``"error"``: raise exception.
           - ``"ignore"``: nothing.
           - ``"me"``: preserve the original value.
           - ``"other"``: set like the passed feature.
        """

        OPTIONS = ["error", "ignore", "me", "other"]
        assert missing in OPTIONS, "Invalid value in `missing`."
        assert conflict in OPTIONS, "Invalid value in `missing`."

        self0 = self.clone()
        if isinstance(new_features, Features):
            new_features = new_features.features
        for f in new_features:
            self0.addFeature(f, conflict=conflict, missing=missing)
        self.props = self0.props
        return self

    def check_simple(self, checks, radl):
        """Check types, operators and units in simple features."""

        for f in self.features:
            if not isinstance(f, Feature) or f.prop not in checks:
                continue
            f._check(checks[f.prop], radl)

    def check_num(self, checks, radl):
        """
        Check types, operators and units in features with numbers.

        Args:

        - checks(dict of dict of str:tuples): keys are property name prefixes, and the
          values are dict with keys are property name suffixes and values are iterable
          as in ``_check_feature``.
        - radl: passed to ``_check_feature``.
        """

        prefixes = {}
        for f in self.features:
            if not isinstance(f, Feature):
                continue
            (prefix, sep, tail) = f.prop.partition(".")
            if not sep or prefix not in checks:
                continue
            checks0 = checks[prefix]
            (num, sep, suffix) = tail.partition(".")
            try:
                num = int(num)
            except Exception:
                raise RADLParseException(
                    "Invalid property name; expected an index.", line=f.line)
            if not sep or suffix not in checks0:
                continue
            f._check(checks0[suffix], radl)
            if prefix not in prefixes:
                prefixes[prefix] = set()
            prefixes[prefix].add(num)

        # Check consecutive indices for num properties.
        for prefix, nums in prefixes.items():
            if min(nums) != 0 or max(nums) != len(nums) - 1:
                raise RADLParseException(
                    "Invalid indices values in properties '%s'" % prefix)

        return prefixes


class Aspect:
    """A network, ansible_host, system, deploy, configure or contextualize element in a RADL."""

    def getId(self):
        """Return the id of the aspect."""

        return id(self)

    def clone(self):
        """Return a copy of this aspect."""

        return copy.deepcopy(self)


class contextualize_item:
    """Store a line under ``contextualize`` RADL keyword."""
    def __init__(self, system_id, configure_id, num=0, ctxt_tool=None, line=None):
        self.system = system_id
        """System id."""
        self.configure = configure_id
        """Configure id."""
        self.num = num
        """Num of steps (optional)."""
        self.ctxt_tool = ctxt_tool
        """Name of the Ctxt. tool (optional). Currently supported: 'Ansible' and 'cloud-init'. Default 'Ansible'."""
        self.line = line

    def __str__(self):
        return "system %s configure %s %s %s" % (self.system, self.configure,
                                                 "step " + str(self.num) if self.num else "",
                                                 "with " + self.ctxt_tool if self.ctxt_tool else "")

    def getId(self):
        """Return an unique key for this element."""

        return (self.system, self.configure)

    def get_ctxt_tool(self):
        """Return the name of the Ctxt. tool."""

        return self.ctxt_tool if self.ctxt_tool else "Ansible"

    def check(self, radl):
        """Check a line under a contextualize."""

        if not radl.get_system_by_name(self.system):
            raise RADLParseException("Invalid system id '%s'" % self.system, line=self.line)
        if not radl.get_configure_by_name(self.configure):
            raise RADLParseException("Invalid configure id '%s'" % self.configure, line=self.line)


class contextualize(Aspect, object):
    """Store a ``contextualize`` RADL keyword."""
    def __init__(self, items=None, max_time=0, options=None, line=None):
        self.max_time = max_time
        """Maximum time."""
        self.items = None
        """List of contextualize_item."""
        self.options = None
        """"List of contextualization options"""
        if isinstance(items, list):
            self.items = dict([(c.getId(), c) for c in items])
        elif isinstance(items, dict):
            self.items = items
        elif items is not None:
            raise ValueError("Unexpected type for 'items'.")

        if isinstance(options, list):
            self.options = dict([(o.prop, o) for o in options])
        elif isinstance(options, dict):
            self.options = options
        elif options is not None:
            raise ValueError("Unexpected type for 'options'.")

        self.line = line

    def __str__(self):
        if self.items is None and self.options is None:
            return ""
        elif not self.items and not self.options:
            return "contextualize ()"
        else:
            options = ""
            if self.options:
                options = "\n".join(["option %s" % i for i in self.options.values()]) + "\n"
            items = ""
            if self.items:
                items = "\n".join([str(i) for i in self.items.values()])
            return "contextualize %s (\n%s%s\n)" % (self.max_time if self.max_time else "", options, items)

    def __len__(self):
        if self.items is None:
            return 0
        else:
            return len(self.items)

    def update(self, cont):
        """Update this instance with the contextualize passed."""

        self.max_time = max(self.max_time, cont.max_time)
        if cont.items is not None:
            if self.items is None:
                self.items = cont.items
            else:
                self.items.update(cont.items)
        if cont.options is not None:
            if self.options is None:
                self.options = cont.options
            else:
                self.options.update(cont.options)

    def check(self, radl):
        """Check a contextualize."""

        if not isinstance(self.max_time, int) or self.max_time < 0:
            raise RADLParseException("Invalid 'max time' in 'contextualize'",
                                     line=self.line)
        if self.items is not None:
            for i in self.items.values():
                i.check(radl)

    def get_contextualize_items_by_step(self, default=None):
        """Get a dictionary of the contextualize_items grouped by the step or the default value"""
        if self.items:
            res = {}
            for elem in self.items.values():
                if elem.num in res:
                    res[elem.num].append(elem)
                else:
                    res[elem.num] = [elem]
            return res
        else:
            return default


class configure(Aspect):
    """Store a RADL ``configure``."""

    def __init__(self, name, recipe="", reference=False, line=None):
        self.recipes = None
        if recipe is not None:
            # encode the recipe to enable to set special chars in the recipes
            self.recipes = str(recipe.encode('utf-8', 'ignore').decode())
        """Recipe content."""
        self.name = name
        """Configure id."""
        self.reference = reference
        """True if it is only a reference and it isn't a definition."""
        self.line = line

    def getId(self):
        return self.name

    def __str__(self):
        if self.reference:
            return "configure %s" % self.name
        if self.recipes is None:
            return "configure %s ()" % self.name
        return "configure %s (\n@begin\n%s\n@end\n)" % (self.name, self.recipes)

    def check(self, _):
        """Check this configure."""
        if self.recipes is None:
            return True
        try:
            import yaml
        except Exception:
            return True
        try:
            yaml.safe_load(self.recipes)
        except Exception as e:
            raise RADLParseException("Invalid YAML code: %s." % e, line=self.line)
        return True


class deploy(Aspect):
    """Store a RADL ``deploy``."""

    def __init__(self, deploy_id, vm_number, cloud_id=None, line=None):
        self.id = deploy_id
        """System id."""
        self.vm_number = vm_number
        """Number of virtual machines to deploy."""
        self.cloud_id = cloud_id
        """Cloud provider id."""
        self.line = line

    def __str__(self):
        res = "deploy " + self.id + (" %s" % self.vm_number)
        if self.cloud_id:
            res += " " + self.cloud_id
        return res

    def check(self, radl):
        """Check this deploy."""

        if not radl.get_system_by_name(self.id):
            raise RADLParseException("Invalid system id in the deploy.", line=self.line)

        if self.vm_number < 0:
            raise RADLParseException("Invalid number of virtual machines to deploy.",
                                     line=self.line)


class network(Features, Aspect):
    """Store a RADL ``network``."""

    def __init__(self, name, features=None, reference=False, line=None):
        self.id = name
        """Network id."""
        self.reference = reference
        """True if it is only a reference and it isn't a definition."""
        Features.__init__(self, features)
        self.line = line

    def getId(self):
        return self.id

    def __str__(self):
        return "network %s %s" % (self.id, "" if self.reference else "( %s )" % Features.__str__(self))

    def check(self, radl):
        """Check the features in this network."""

        SIMPLE_FEATURES = {
            "outbound": (str, ["YES", "NO"]),
            "outports": (str, check_outports_format),
            "provider_id": (str, None)
        }
        self.check_simple(SIMPLE_FEATURES, radl)

    def isPublic(self):
        """Return true if outbound = yes."""
        return self.getValue("outbound") == "yes"

    @staticmethod
    def createNetwork(name, public=False):
        """Return a network with id being ``name`` and with outbound=yes if ``public``."""

        return network(name, [Feature("outbound", "=", "yes" if public else "no")])

    def getOutPorts(self):
        """
        Get the outports of this network.
        outports format: 22/tcp-22/tcp,8899/tcp,8800
        Returns a list of outport objects
        """
        outports = self.getValue("outports")
        if outports:
            return outport.parseOutPorts(outports)
        else:
            return None


class FeaturesApp(Features):
    """Store an RADL application."""

    def __init__(self, features):
        Features.__init__(self, features)

    @staticmethod
    def from_str(app_name, app_version=None):
        if app_name is not None:
            res = FeaturesApp([])
            res.addFeature(Feature(prop="name", operator="=", value=app_name))
            if app_version:
                res.addFeature(Feature(prop="version", operator="=", value=app_version))
            return res
        else:
            return None

    def isNewerThan(self, other):
        """ Compare if the version of this app is newer that the other """
        if self.getValue("name") == other.getValue("name"):
            if other.getValue("version"):
                if not other.getValue("version"):
                    return False
                else:
                    return Version(self.getValue("version")) > Version(other.getValue("version"))
            else:
                return True
        else:
            return False

    def check(self, radl):
        """Check the features in this application."""
        SIMPLE_FEATURES = {
            "name": (str, lambda x, _: bool(x.value)),
            "path": (str, lambda x, _: bool(x.value)),
            "version": (str, is_version),
            "preinstalled": (str, ["YES", "NO"])
        }
        self.check_simple(SIMPLE_FEATURES, radl)


class Credentials:
    pass


class UserPassCredential(Credentials):
    def __init__(self, user, passwd):
        self.username = user
        self.password = passwd


class UserKeyCredential(Credentials):
    def __init__(self, user, public, private=None):
        self.username = user
        self.public_key = public
        self.private_key = private


class system(Features, Aspect):
    """Store a RADL ``system``."""

    def __init__(self, name, features=None, reference=False, line=None):
        self.name = name
        """System id."""
        self.reference = reference
        """True if it is only a reference and it isn't a definition."""
        Features.__init__(self, features)
        self.line = line

    def getId(self):
        return self.name

    def __str__(self):
        return "system %s %s" % (self.name, "" if self.reference else "(\n%s\n)\n" % Features.__str__(self))

    def hasIP(self, ip):
        """Return True if some system has this IP."""

        for f in self.features:
            if (f.prop.startswith("net_interface.") and
                    f.prop.endswith(".ip") and f.value == ip):
                return True
        return False

    def getIfaceIP(self, iface_num):
        """Return IP in the interface with that number."""

        ip = self.getValue("net_interface.%d.ip" % iface_num)
        if ip:
            return ip
        return None

    def getNumNetworkIfaces(self):
        """Return the number of network interfaces defined."""

        i = 0
        while self.hasFeature("net_interface.%d.connection" % i):
            i += 1
        return i

    def getNumNetworkWithConnection(self, connection):
        """Return the number of network interfaces with id ``connection``."""

        i = 0
        while True:
            value = self.getValue("net_interface.%d.connection" % i, None)
            if not value:
                return None
            if value == connection:
                return i
            i += 1

    def getRequestedNameIface(self, iface_num=0, num=None, default_hostname=None, default_domain=None):
        """Return the dns name associated to the net interface."""

        full_name = self.getValue("net_interface.%d.dns_name" % iface_num)

        if full_name:
            replaced_full_name = system.replaceTemplateName(full_name, num)
            (hostname, domain) = replaced_full_name
            if not domain:
                domain = default_domain
            return (hostname, domain)
        else:
            if default_hostname:
                (hostname, _) = system.replaceTemplateName(default_hostname, num)
                return (hostname, default_domain)
            else:
                return None

    @staticmethod
    def replaceTemplateName(full_name, num=None):
        if full_name:
            if num is not None:
                full_name = full_name.replace("#N#", str(num))
            dot_pos = full_name.find('.')
            if dot_pos != -1:
                domain = full_name[dot_pos + 1:]
                name = full_name[:dot_pos]
                return (name, domain)
            else:
                return (full_name, None)
        else:
            return full_name

    def getNetworkIDs(self):
        """Return a list of network id of this system."""

        res = []
        i = 0
        while True:
            netid = self.getValue("net_interface.%d.connection" % i)
            if not netid:
                return res
            res.append(netid)
            i += 1

    def remove_net_interface(self, iface_num):
        """Remove the network interface with that number."""
        for f in self.features:
            if (f.prop.startswith("net_interface.%d" % iface_num)):
                self.delValue(f.prop)

        i = iface_num + 1
        while self.getValue("net_interface.%d.connection" % i):
            for f in self.features:
                if (f.prop.startswith("net_interface.%d" % i)):
                    self.setValue(f.prop.replace(str(i), str(i - 1)), f.value)
                    self.delValue(f.prop)
            i += 1

    def getCredentialValues(self, new=False):
        """Return the values in disk.0.os.credentials.*."""

        credentials_base = "disk.0.os.credentials."
        if new:
            credentials_base = "disk.0.os.credentials.new."
        return tuple([self.getValue(credentials_base + p) for p in [
            "username", "password", "public_key", "private_key"]])

    def updateNewCredentialValues(self):
        """
        Set the new credential values to the credentials to use, and delete the new ones
        """

        credentials_base = "disk.0.os.credentials."
        new_credentials_base = "disk.0.os.credentials.new."

        for elem in ['password', 'public_key', 'private_key']:
            if self.getValue(new_credentials_base + elem):
                self.setValue(credentials_base + elem, self.getValue(new_credentials_base + elem))
                self.delValue(new_credentials_base + elem)

    def setCredentialValues(self, username=None, password=None, public_key=None, private_key=None, new=False):
        """Set the values in disk.0.os.credentials.*."""

        credentials_base = "disk.0.os.credentials."
        if new:
            credentials_base = "disk.0.os.credentials.new."

        if username:
            self.setValue(credentials_base + "username", username)
        if password:
            self.setValue(credentials_base + "password", password)
        if public_key:
            self.setValue(credentials_base + "public_key", public_key)
        if private_key:
            self.setValue(credentials_base + "private_key", private_key)

    def getCredentials(self):
        """Return UserKeyCredential or UserPassCredential."""

        (username, password, public_key, private_key) = self.getCredentialValues()

        if public_key or private_key:
            return UserKeyCredential(username, public_key, private_key)

        if username or password:
            return UserPassCredential(username, password)

        return None

    def setCredentials(self, creds):
        """Set values in UserKeyCredential or UserPassCredential."""

        if isinstance(creds, UserKeyCredential):
            self.setUserKeyCredentials(creds.username, creds.public_key, creds.private_key)
        elif isinstance(creds, UserPassCredential):
            self.setUserPasswdCredentials(creds.username, creds.password)

    def setUserPasswdCredentials(self, username, password):
        """Set username and password in ``disk.0.os.credentials``."""

        self.setCredentialValues(username=username, password=password)

    def setUserKeyCredentials(self, username, public_key=None, private_key=None):
        """Set these properties in ``disk.0.os.credentials``."""

        self.setCredentialValues(username=username, public_key=public_key, private_key=private_key)

    def getApplications(self):
        """Return a list of Application with the specified apps in this system."""

        res = []
        for f in self.features:
            if isinstance(f, Feature) and f.prop == "disk.0.applications":
                res.append(FeaturesApp(f.value.features))

        return res

    def addApplication(self, name, version=None, path=None, disk_num=0, soft=-1):
        """Add a new application in some disk."""

        fapp = Features()
        fapp.features.append(Feature("name", "=", name))
        if version:
            fapp.features.append(Feature("version", "=", version))
        if path:
            fapp.features.append(Feature("path", "=", path))
        self.features.append(Feature("disk.%d.applications" % disk_num, "contains", fapp, soft > 0))

    def check(self, radl):
        """Check the features in this system."""

        def positive(f, _):
            return f.value >= 0

        def check_ansible_host(f, radl0):
            if radl0.get_ansible_by_id(f.value) is None:
                return False
            return True

        mem_units = ["", "B", "K", "M", "G", "KB", "MB", "GB"]
        SIMPLE_FEATURES = {
            "spot": (str, ["YES", "NO"]),
            "image_type": (str, ["VMDK", "QCOW", "QCOW2", "RAW"]),
            "virtual_system_type": (str, system._check_virtual_system_type),
            "price": (float, positive, None),
            "cpu.count": (float, positive, None),
            "cpu.arch": (str, ['I386', 'X86_64']),
            "cpu.performance": (float, positive, ["ECU", "GCEU", "HRZ"]),
            "memory.size": (int, positive, mem_units),
            "disk.0.os.credentials.new.password": (str, check_password),
            "ansible_host": (str, check_ansible_host),
            SoftFeatures.SOFT: (SoftFeatures, lambda x, r: x.check(r))
        }
        self.check_simple(SIMPLE_FEATURES, radl)

        net_connections = set()

        def check_net_interface_connection(f, radl0):
            if radl0.get_network_by_id(f.value) is None:
                return False
            net_connections.add(f.prop)
            return True

        def check_app(f, x):
            FeaturesApp(f.value.features).check(x)
            return True

        NUM_FEATURES = {
            "net_interface": {
                "connection": (str, check_net_interface_connection),
                "ip": (str, None),
                "dns_name": (str, None)},
            "disk": {
                "image.url": ((str, unicode, list), system._check_disk_image_url),
                "image.name": (str, None),
                "type": (str, None),
                "device": (str, None),
                "mount_path": (str, None),
                "fstype": (str, None),
                "size": (float, positive, mem_units),
                "free_size": (float, positive, mem_units),
                "os.name": (str, ["LINUX", "WINDOWS", "MAC OS X"]),
                "os.flavour": (str, None),
                "os.version": (str, is_version),
                "os.credentials.username": (str, None),
                "os.credentials.password": (str, None),
                "os.credentials.private_key": (str, None),
                "os.credentials.public_key": (str, None),
                "applications": (Features, check_app)
            }
        }
        prefixes = self.check_num(NUM_FEATURES, radl)

        # Check all interfaces
        if len(net_connections) != len(prefixes.get("net_interface", set())):
            raise RADLParseException("Some net_interface does not have a connection")

        return True

    @staticmethod
    def _check_disk_image_url(f, radl):
        return True

    @staticmethod
    def _check_virtual_system_type(f, radl):
        return True

    def concrete(self, other=None):
        """
        Return copy and score after being applied other system and soft features.

        Args:

        - other(system, optional): system to apply just before soft features.

        Return(tuple): tuple of the resulting system and its score.
        """

        new_system = self.clone()
        if other:
            new_system.applyFeatures(other, missing="other")
        soft_features = self.getValue(SoftFeatures.SOFT, [])
        score = 0
        for f in sorted(soft_features, key=lambda f: f.soft, reverse=True):
            try:
                new_system.applyFeatures(f, missing="other")
                score += f.soft
            except Exception:
                pass
        new_system.delValue(SoftFeatures.SOFT)
        return new_system, score


class SoftFeatures(system, Feature):
    """
    Assign a weight to a group of features.

    Args:
    - soft: weight of matching the containing features.
    """

    SOFT = "__soft__"
    """Fake property name."""

    def __init__(self, soft, features, line=None):
        self.soft = soft
        system.__init__(self, None, features, line=line)
        Feature.__init__(self, SoftFeatures.SOFT, "contains", self)

    def __str__(self):
        return "soft %s ( %s )" % (self.soft, Features.__str__(self))


class RADL:
    """Parsed RADL document."""

    def __init__(self):
        self.networks = []
        """List of network."""
        self.ansible_hosts = []
        """List of ansible_hosts."""
        self.systems = []
        """List of system."""
        self.deploys = []
        """List of deploy."""
        self.configures = []
        """List of configure."""
        self.description = None
        """Description item."""
        self.contextualize = contextualize()
        """List of contextualize."""

    def __str__(self):
        res = ""
        if self.description:
            res = str(self.description) + "\n"
        res += "\n".join([str(f) for fs in [self.ansible_hosts, self.networks, self.systems, self.configures,
                                            [self.contextualize], self.deploys] for f in fs])
        return res

    def add(self, aspect, ifpresent="error"):
        """
        Add a network, ansible_host, system, deploy, configure or contextualize.

        Args:
        - aspect(network, system, deploy, configure or contextualize): thing to add.
        - ifpresent(str): if it has been defined, do:

           - ``"ignore"``: not add the aspect.
           - ``"replace"``: replace by the old defined.
           - ``"error"``: raise an error.

        Return(bool): True if aspect was added.
        """

        # If aspect is a contextualization, it is treated separately
        if isinstance(aspect, contextualize):
            self.contextualize.update(aspect)
            return True

        # if the aspect is a description, it is also treated separately
        if isinstance(aspect, description):
            if self.description is None:
                self.description = aspect
            elif ifpresent == "error":
                raise Exception("Description can only be defined once.")
            elif ifpresent == "replace":
                self.description = aspect
            elif ifpresent == "ignore":
                return False
            else:
                raise ValueError
            return True

        classification = [(network, self.networks), (system, self.systems), (ansible, self.ansible_hosts),
                          (deploy, self.deploys), (configure, self.configures)]
        aspect_list = [l for t, l in classification if isinstance(aspect, t)]
        assert len(aspect_list) == 1, "Unexpected aspect for RADL."
        aspect_list = aspect_list[0]

        old_aspect = [a for a in aspect_list if a.getId() == aspect.getId()]
        if old_aspect:
            # If some aspect with the same id is found
            if ifpresent == "error":
                raise Exception("Aspect with the same id was found.")
            elif ifpresent == "replace":
                for i, elem in enumerate(aspect_list):
                    if elem.getId() == old_aspect[0].getId():
                        del aspect_list[i]
                        break
                aspect_list.append(aspect)
                return True
            elif ifpresent == "ignore":
                return False
            else:
                raise ValueError
        else:
            # Otherwise add aspect
            aspect_list.append(aspect)
            return True

    def get(self, aspect):
        """Get a network, system or configure or contextualize with the same id as aspect passed."""

        classification = [(network, self.networks), (system, self.systems),
                          (configure, self.configures)]
        aspect_list = [l for t, l in classification if isinstance(aspect, t)]
        assert len(aspect_list) == 1, "Unexpected aspect for RADL."
        aspect_list = aspect_list[0]

        old_aspect = [a for a in aspect_list if a.getId() == aspect.getId()]
        return old_aspect[0] if old_aspect else None

    def clone(self):
        return copy.deepcopy(self)

    def __getIP(self, public):
        """Return the first net_interface.%d.ip for a system in a public/private network."""

        maybeNot = (lambda x: x) if public else (lambda x: not x)
        nets_id = [net.id for net in self.networks if maybeNot(net.isPublic())]
        for s in self.systems:
            i = 0
            while True:
                value = s.getValue("net_interface.%d.connection" % i)
                if not value:
                    break
                if value in nets_id:
                    ip = s.getValue("net_interface.%d.ip" % i)
                    if ip:
                        return ip
                i += 1
        return None

    def getPublicIP(self):
        """Return the first net_interface.%d.ip for a system in a public network."""

        return self.__getIP(True)

    def getPrivateIP(self):
        """Return the first net_interface.%d.ip for a system in a private network."""

        return self.__getIP(False)

    def hasPublicNet(self, system_name):
        """ Return true if some system has a public network."""

        nets_id = [net.id for net in self.networks if net.isPublic()]
        system = self.get_system_by_name(system_name)
        if system:
            i = 0
            while True:
                f = system.getFeature("net_interface.%d.connection" % i)
                if not f:
                    break
                if f.value in nets_id:
                    return True
                i += 1

        return False

    def check(self):
        """Check if it is a valid RADL document."""

        for i in [f for fs in [self.networks, self.ansible_hosts, self.systems, self.deploys,
                               self.configures, [self.contextualize]] for f in fs]:
            i.check(self)

        snames = [s.name for s in self.systems]
        if len(set(snames)) != len(snames):
            raise RADLParseException("Some System name duplicated.")

        nids = [s.id for s in self.networks]
        if len(set(nids)) != len(nids):
            raise RADLParseException("Some Network name duplicated.")

        return True

    def get_system_by_name(self, name):
        """Return a system with that name or None."""

        for elem in self.systems:
            if elem.name == name:
                return elem
        return None

    def get_deploy_by_id(self, dep_id):
        """Return a deploy with that system id or None."""

        for elem in self.deploys:
            if elem.id == dep_id:
                return elem
        return None

    def get_configure_by_name(self, name):
        """Return a configure with that id or None."""

        for elem in self.configures:
            if elem.name == name:
                return elem
        return None

    def get_network_by_id(self, net_id):
        """Return a network with that id or None."""

        for elem in self.networks:
            if elem.id == net_id:
                return elem
        return None

    def get_ansible_by_id(self, ansible_id):
        """Return a ansible with that id or None."""

        for elem in self.ansible_hosts:
            if elem.id == ansible_id:
                return elem
        return None


class ansible(Features, Aspect):
    """Store a RADL ``ansible``."""

    def __init__(self, name, features, line=None):
        self.id = name
        """Ansible host id."""
        Features.__init__(self, features)
        self.line = line
        self.reference = False

    def __str__(self):
        return "ansible %s (%s)" % (self.id, Features.__str__(self))

    def getId(self):
        """Return the id of the aspect."""

        return self.id

    def check(self, radl):
        """Check the features in this network."""

        SIMPLE_FEATURES = {
            "host": (str, None),
            "credentials.username": (str, None),
            "credentials.password": (str, None),
            "credentials.private_key": (str, None)
        }
        self.check_simple(SIMPLE_FEATURES, radl)

        if not self.getHost():
            raise RADLParseException("Ansible host must have a host", line=self.line)
        (username, password, private_key) = self.getCredentialValues()
        if not username:
            raise RADLParseException("Ansible host must have a credentials.username", line=self.line)
        if not password and not private_key:
            raise RADLParseException("Ansible host must have a credentials.password or credentials.private_key",
                                     line=self.line)

    def getHost(self):
        return self.getValue("host")

    def getCredentials(self):
        """Return UserKeyCredential or UserPassCredential."""

        (username, password, private_key) = self.getCredentialValues()

        if private_key:
            return UserKeyCredential(username, None, private_key)

        if username or password:
            return UserPassCredential(username, password)

        return None

    def getCredentialValues(self, new=False):
        """Return the values in credentials.*."""

        credentials_base = "credentials."
        return tuple([self.getValue(credentials_base + p) for p in [
            "username", "password", "private_key"]])


class outport():
    """Store OutPorts data"""

    def __init__(self, port_init, port_end, protocol, is_range=False, remote_cidr=None):
        self.port_init = int(port_init)
        self.port_end = int(port_end)
        self.protocol = protocol
        self.range = is_range
        self.remote_cidr = remote_cidr

    def __eq__(self, other):
        return (self.port_init == other.port_init and self.port_end == other.port_end and
                self.protocol == other.protocol and self.range == other.range and
                self.remote_cidr == other.remote_cidr)

    def __str__(self):
        if self.is_range:
            res = "%d:%d/%s" % (self.port_init, self.port_end, self.protocol)
        else:
            res = "%d-%d/%s" % (self.port_init, self.port_end, self.protocol)
        if self.remote_cidr:
            res = "%s-%s" % (self.remote_cidr, res)
        return res

    def is_range(self):
        return self.range

    def get_port_init(self):
        return self.port_init

    def get_port_end(self):
        return self.port_end

    def get_local_port(self):
        return self.port_end

    def get_remote_port(self):
        return self.port_init

    def get_protocol(self):
        return self.protocol

    def get_remote_cidr(self):
        if self.remote_cidr:
            return self.remote_cidr
        else:
            return '0.0.0.0/0'

    @staticmethod
    def parseOutPorts(outports):
        """
        Parse the outports string
        Valid formats:
        0.0.0.0/0-8899/tcp-8899/tcp
        8899/tcp-8899/tcp,22/tcp-22/tcp
        8899/tcp-8899,22/tcp-22
        0.0.0.0/0-8899-8899,22-22
        8899-8899,22-22
        8899/tcp,22/udp
        8899,22
        1:10/tcp,9:22/udp
        1:10,9:22
        Returns a list of outport objects
        """
        res = []
        ports = outports.split(',')
        for port in ports:
            expr = (r"^((\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}\/\d{1,2})-){0,1}((\d+)((:)\d+){0,1}((\/tcp|\/udp){0,1}))"
                    r"((-)((\d+)((:)\d+){0,1}((\/tcp|\/udp){0,1}))){0,1}$")
            match = re.search(expr, port)
            if not match:
                raise RADLParseException('Invalid outports format.')
            groups = match.groups()

            local_protocol = groups[6][1:] if groups[6] else "tcp"
            remote_protocol = groups[14][1:] if groups[14] else local_protocol
            if remote_protocol != local_protocol:
                raise RADLParseException("Different protocols used in local and remote outports.")

            if groups[6] == "-":
                if groups[3] == ":":
                    raise RADLParseException('Port range (:) and port mapping (-) cannot be combined.')
                if remote_protocol != local_protocol:
                    raise RADLParseException("Different protocols used in local and remote outports.")
                res.append(outport(groups[3], groups[11], local_protocol, False, groups[1]))
            else:
                # We are in a range
                if groups[5] == ":":
                    res.append(outport(groups[3], groups[4][1:], local_protocol, True, groups[1]))
                else:
                    res.append(outport(groups[3], groups[11] or groups[3], local_protocol, False, groups[1]))

        return res


class description(Features, Aspect):
    """Store a RADL ``description``."""

    def __init__(self, name, features=None, reference=False, line=None):
        self.id = name
        """description name."""
        self.reference = reference
        """True if it is only a reference and it isn't a definition."""
        Features.__init__(self, features)
        self.line = line

    def __str__(self):
        return "description %s (%s)" % (self.id, Features.__str__(self))
