from .datapath import DataPath
from .validationresults import ValidationResult
from .. import syntax, util
from .. import validators as val


class Schema(object):
    """
    Makes a Schema object from a schema dict.
    Still acts like a dict.
    """

    def __init__(self, schema_dict, name="", validators=None, includes=None):
        self.validators = validators or val.DefaultValidators
        self.dict = schema_dict
        self.name = name
        self._schema = self._process_schema(DataPath(), schema_dict, self.validators)
        # if this schema is included it shares the includes with the top level
        # schema
        self.includes = {} if includes is None else includes

    def add_include(self, type_dict):
        for include_name, custom_type in type_dict.items():
            t = Schema(custom_type, name=include_name, validators=self.validators, includes=self.includes)
            self.includes[include_name] = t

    def _process_schema(self, path, schema_data, validators):
        """
        Go through a schema and construct validators.
        """
        if util.is_map(schema_data) or util.is_list(schema_data):
            for key, data in util.get_iter(schema_data):
                schema_data[key] = self._process_schema(path + DataPath(key), data, validators)
        else:
            schema_data = self._parse_schema_item(path, schema_data, validators)
        return schema_data

    def _parse_schema_item(self, path, expression, validators):
        try:
            return syntax.parse(expression, validators)
        except SyntaxError as e:
            # Tack on some more context and rethrow.
            error = str(e) + " at node '%s'" % str(path)
            raise SyntaxError(error)

    def validate(self, data, data_name, strict):
        path = DataPath()
        errors = self._validate(self._schema, data, path, strict)
        return ValidationResult(data_name, self.name, errors)

    def _validate_item(self, validator, data, path, strict, key):
        """
        Fetch item from data at the position key and validate with validator.

        Returns an array of errors.
        """
        errors = []
        path = path + DataPath(key)
        try:  # Pull value out of data. Data can be a map or a list/sequence
            data_item = data[key]
        except (KeyError, IndexError):  # Oops, that field didn't exist.
            # Optional? Who cares.
            if isinstance(validator, val.Validator) and validator.is_optional:
                return errors
            # SHUT DOWN EVERYTHING
            errors.append("%s: Required field missing" % path)
            return errors

        return self._validate(validator, data_item, path, strict)

    def _validate(self, validator, data, path, strict):
        """
        Validate data with validator.
        Special handling of non-primitive validators.

        Returns an array of errors.
        """

        if util.is_list(validator) or util.is_map(validator):
            return self._validate_static_map_list(validator, data, path, strict)

        errors = []
        # Optional field with optional value? Who cares.
        if data is None and validator.is_optional and validator.can_be_none:
            return errors

        errors += self._validate_primitive(validator, data, path)

        if errors:
            return errors

        if isinstance(validator, val.Include):
            errors += self._validate_include(validator, data, path, strict)

        elif isinstance(validator, (val.Map, val.List)):
            errors += self._validate_map_list(validator, data, path, strict)

        elif isinstance(validator, val.Any):
            errors += self._validate_any(validator, data, path, strict)

        elif isinstance(validator, val.Subset):
            errors += self._validate_subset(validator, data, path, strict)

        return errors

    def _validate_static_map_list(self, validator, data, path, strict):
        if util.is_map(validator) and not util.is_map(data):
            return ["%s : '%s' is not a map" % (path, data)]

        if util.is_list(validator) and not util.is_list(data):
            return ["%s : '%s' is not a list" % (path, data)]

        errors = []

        if strict:
            data_keys = set(util.get_keys(data))
            validator_keys = set(util.get_keys(validator))
            for key in data_keys - validator_keys:
                error_path = path + DataPath(key)
                errors += ["%s: Unexpected element" % error_path]

        for key, sub_validator in util.get_iter(validator):
            errors += self._validate_item(sub_validator, data, path, strict, key)
        return errors

    def _validate_map_list(self, validator, data, path, strict):
        errors = []

        if not validator.validators:
            return errors  # No validators, user just wanted a map.

        for key in util.get_keys(data):
            sub_errors = []
            for v in validator.validators:
                err = self._validate_item(v, data, path, strict, key)
                if err:
                    sub_errors.append(err)

            if len(sub_errors) == len(validator.validators):
                # All validators failed, add to errors
                for err in sub_errors:
                    errors += err

        return errors

    def _validate_include(self, validator, data, path, strict):
        include_schema = self.includes.get(validator.include_name)
        if not include_schema:
            return [("Include '%s' has not been defined." % validator.include_name)]
        strict = strict if validator.strict is None else validator.strict
        return include_schema._validate(include_schema._schema, data, path, strict)

    def _validate_any(self, validator, data, path, strict):
        if not validator.validators:
            return []

        errors = []

        sub_errors = []
        for v in validator.validators:
            err = self._validate(v, data, path, strict)
            if err:
                sub_errors.append(err)

        if len(sub_errors) == len(validator.validators):
            # All validators failed, add to errors
            for err in sub_errors:
                errors += err

        return errors

    def _validate_subset(self, validator, data, path, strict):
        def _internal_validate(internal_data):
            sub_errors = []
            for val in validator.validators:
                err = self._validate(val, internal_data, path, strict)
                if not err:
                    break
                sub_errors += err
            else:
                return sub_errors
            return []

        if not validator.validators:
            return []

        errors = []
        if util.is_map(data):
            for k, v in data.items():
                errors += _internal_validate({k: v})
        elif util.is_list(data):
            for k in data:
                errors += _internal_validate(k)
        else:
            errors += _internal_validate(data)
        return errors

    def _validate_primitive(self, validator, data, path):
        errors = validator.validate(data)

        for i, error in enumerate(errors):
            errors[i] = ("%s: " % path) + error

        return errors
