import yaml

from conans.errors import ConanException
from conans.model.values import Values


def bad_value_msg(name, value, value_range):
    tip = ""
    if "settings" in name:
        tip = '\nRead "http://docs.conan.io/en/latest/faq/troubleshooting.html' \
              '#error-invalid-setting"'

    return ("Invalid setting '%s' is not a valid '%s' value.\nPossible values are %s%s"
            % (value, name, value_range, tip))


def undefined_field(name, field, fields=None, value=None):
    value_str = " for '%s'" % value if value else ""
    result = ["'%s.%s' doesn't exist%s" % (name, field, value_str),
              "'%s' possible configurations are %s" % (name, fields or "none")]
    return ConanException("\n".join(result))


def undefined_value(name):
    return ConanException("'%s' value not defined" % name)


class SettingsItem(object):
    """ represents a setting value and its child info, which could be:
    - A range of valid values: [Debug, Release] (for settings.compiler.runtime of VS)
    - "ANY", as string to accept any value
    - List ["None", "ANY"] to accept None or any value
    - A dict {subsetting: definition}, e.g. {version: [], runtime: []} for VS
    """
    def __init__(self, definition, name):
        self._name = name  # settings.compiler
        self._value = None  # gcc
        if isinstance(definition, dict):
            self._definition = {}
            # recursive
            for k, v in definition.items():
                k = str(k)
                self._definition[k] = Settings(v, name, k)
        elif definition == "ANY":
            self._definition = "ANY"
        else:
            # list or tuple of possible values
            self._definition = [str(v) for v in definition]

    def __contains__(self, value):
        return value in (self._value or "")

    def copy(self):
        """ deepcopy, recursive
        """
        result = SettingsItem({}, name=self._name)
        result._value = self._value
        if self.is_final:
            result._definition = self._definition[:]
        else:
            result._definition = {k: v.copy() for k, v in self._definition.items()}
        return result

    def copy_values(self):
        if self._value is None and "None" not in self._definition:
            return None

        result = SettingsItem({}, name=self._name)
        result._value = self._value
        if self.is_final:
            result._definition = self._definition[:]
        else:
            result._definition = {k: v.copy_values() for k, v in self._definition.items()}
        return result

    @property
    def is_final(self):
        return not isinstance(self._definition, dict)

    def __bool__(self):
        if not self._value:
            return False
        return self._value.lower() not in ["false", "none", "0", "off"]

    def __nonzero__(self):
        return self.__bool__()

    def __str__(self):
        return str(self._value)

    def _not_any(self):
        return self._definition != "ANY" and "ANY" not in self._definition

    def __eq__(self, other):
        if other is None:
            return self._value is None
        other = str(other)
        if self._not_any() and other not in self.values_range:
            raise ConanException(bad_value_msg(self._name, other, self.values_range))
        return other == self.__str__()

    def __ne__(self, other):
        return not self.__eq__(other)

    def __delattr__(self, item):
        """ This is necessary to remove libcxx subsetting from compiler in config()
           del self.settings.compiler.stdlib
        """
        try:
            self._get_child(self._value).remove(item)
        except Exception:
            pass

    def remove(self, values):
        if not isinstance(values, (list, tuple, set)):
            values = [values]
        for v in values:
            v = str(v)
            if isinstance(self._definition, dict):
                self._definition.pop(v, None)
            elif self._definition == "ANY":
                if v == "ANY":
                    self._definition = []
            elif v in self._definition:
                self._definition.remove(v)

        if self._value is not None and self._value not in self._definition and self._not_any():
            raise ConanException(bad_value_msg(self._name, self._value, self.values_range))

    def _get_child(self, item):
        if not isinstance(self._definition, dict):
            raise undefined_field(self._name, item, None, self._value)
        if self._value is None:
            raise undefined_value(self._name)
        return self._definition[self._value]

    def __getattr__(self, item):
        item = str(item)
        sub_config_dict = self._get_child(item)
        return getattr(sub_config_dict, item)

    def __setattr__(self, item, value):
        if item[0] == "_" or item.startswith("value"):
            return super(SettingsItem, self).__setattr__(item, value)

        item = str(item)
        sub_config_dict = self._get_child(item)
        return setattr(sub_config_dict, item, value)

    def __getitem__(self, value):
        value = str(value)
        try:
            return self._definition[value]
        except Exception:
            raise ConanException(bad_value_msg(self._name, value, self.values_range))

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        v = str(v)
        if self._not_any() and v not in self.values_range:
            raise ConanException(bad_value_msg(self._name, v, self.values_range))
        self._value = v

    @property
    def values_range(self):
        try:
            return sorted(list(self._definition.keys()))
        except Exception:
            return self._definition

    @property
    def values_list(self):
        if self._value is None:
            return []
        result = []
        partial_name = ".".join(self._name.split(".")[1:])
        result.append((partial_name, self._value))
        if isinstance(self._definition, dict):
            sub_config_dict = self._definition[self._value]
            result.extend(sub_config_dict.values_list)
        return result

    def validate(self):
        if self._value is None and "None" not in self._definition:
            raise undefined_value(self._name)
        if isinstance(self._definition, dict):
            key = "None" if self._value is None else self._value
            self._definition[key].validate()


class Settings(object):
    def __init__(self, definition=None, name="settings", parent_value=None):
        if parent_value == "None" and definition:
            raise ConanException("settings.yml: None setting can't have subsettings")
        definition = definition or {}
        self._name = name  # settings, settings.compiler
        self._parent_value = parent_value  # gcc, x86
        self._data = {str(k): SettingsItem(v, "%s.%s" % (name, k))
                      for k, v in definition.items()}

    def get_safe(self, name, default=None):
        try:
            tmp = self
            for prop in name.split("."):
                tmp = getattr(tmp, prop, None)
        except ConanException:
            return default
        if tmp is not None and tmp.value and tmp.value != "None":  # In case of subsettings is None
            return str(tmp)
        return default

    def copy(self):
        """ deepcopy, recursive
        """
        result = Settings({}, name=self._name, parent_value=self._parent_value)
        for k, v in self._data.items():
            result._data[k] = v.copy()
        return result

    def copy_values(self):
        """ deepcopy, recursive
        """
        result = Settings({}, name=self._name, parent_value=self._parent_value)
        for k, v in self._data.items():
            value = v.copy_values()
            if value is not None:
                result._data[k] = value
        return result

    @staticmethod
    def loads(text):
        try:
            return Settings(yaml.safe_load(text) or {})
        except (yaml.YAMLError, AttributeError) as ye:
            raise ConanException("Invalid settings.yml format: {}".format(ye))

    def validate(self):
        for field in self.fields:
            child = self._data[field]
            child.validate()

    @property
    def fields(self):
        return sorted(list(self._data.keys()))

    def remove(self, item):
        if not isinstance(item, (list, tuple, set)):
            item = [item]
        for it in item:
            it = str(it)
            self._data.pop(it, None)

    def clear(self):
        self._data = {}

    def _check_field(self, field):
        if field not in self._data:
            raise undefined_field(self._name, field, self.fields, self._parent_value)

    def __getattr__(self, field):
        assert field[0] != "_", "ERROR %s" % field
        self._check_field(field)
        return self._data[field]

    def __delattr__(self, field):
        assert field[0] != "_", "ERROR %s" % field
        self._check_field(field)
        del self._data[field]

    def __setattr__(self, field, value):
        if field[0] == "_" or field.startswith("values"):
            return super(Settings, self).__setattr__(field, value)

        self._check_field(field)
        self._data[field].value = value

    @property
    def values(self):
        return Values.from_list(self.values_list)

    @property
    def values_list(self):
        result = []
        for field in self.fields:
            config_item = self._data[field]
            result.extend(config_item.values_list)
        return result

    def items(self):
        return self.values_list

    def iteritems(self):
        return self.values_list

    def update_values(self, vals):
        """ receives a list of tuples (compiler.version, value)
        This is more an updated than a setter
        """
        assert isinstance(vals, list), vals
        for (name, value) in vals:
            list_settings = name.split(".")
            attr = self
            for setting in list_settings[:-1]:
                attr = getattr(attr, setting)
            setattr(attr, list_settings[-1], str(value))

    @values.setter
    def values(self, vals):
        assert isinstance(vals, Values)
        self.update_values(vals.as_list())

    def constraint(self, constraint_def):
        """ allows to restrict a given Settings object with the input of another Settings object
        1. The other Settings object MUST be exclusively a subset of the former.
           No additions allowed
        2. If the other defines {"compiler": None} means to keep the full specification
        """
        if isinstance(constraint_def, (list, tuple, set)):
            constraint_def = {str(k): None for k in constraint_def or []}
        else:
            constraint_def = {str(k): v for k, v in constraint_def.items()}

        fields_to_remove = []
        for field, config_item in self._data.items():
            if field not in constraint_def:
                fields_to_remove.append(field)
                continue

            other_field_def = constraint_def[field]
            if other_field_def is None:  # Means leave it as is
                continue
            if isinstance(other_field_def, str):
                other_field_def = [other_field_def]

            values_to_remove = []
            for value in config_item.values_range:  # value = "Visual Studio"
                if value not in other_field_def:
                    values_to_remove.append(value)
                else:  # recursion
                    if (not config_item.is_final and isinstance(other_field_def, dict) and
                            other_field_def[value] is not None):
                        config_item[value].constraint(other_field_def[value])

            # Sanity check of input constraint values
            for value in other_field_def:
                if value not in config_item.values_range:
                    raise ConanException(bad_value_msg(field, value, config_item.values_range))

            config_item.remove(values_to_remove)

        # Sanity check for input constraint wrong fields
        for field in constraint_def:
            if field not in self._data:
                raise undefined_field(self._name, field, self.fields)

        # remove settings not defined in the constraint
        self.remove(fields_to_remove)
