from django.core.exceptions import ValidationError, NON_FIELD_ERRORS

__ALL__ = (
    '_base_manager', '_default_manager', 'full_clean', '_get_unique_checks',
    '_perform_unique_checks', '_perform_date_checks', 'validate_unique',
    'clean', 'clean_fields'
)


@property
def _base_manager(cls):
    return cls._meta.base_manager


@property
def _default_manager(cls):
    return cls._meta.default_manager


def full_clean(self, exclude=None, validate_unique=True):
    """
    Calls clean_fields, clean, and validate_unique, on the model,
    and raises a ``ValidationError`` for any errors that occurred.
    """
    errors = {}
    if exclude is None:
        exclude = []
    else:
        exclude = list(exclude)

    try:
        self.clean_fields(exclude=exclude)
    except ValidationError as e:
        errors = e.update_error_dict(errors)

    # Form.clean() is run even if other validation fails, so do the
    # same with Model.clean() for consistency.
    try:
        self.clean()
    except ValidationError as e:
        errors = e.update_error_dict(errors)

    # Run unique checks, but only for fields that passed validation.
    if validate_unique:
        for name in errors.keys():
            if name != NON_FIELD_ERRORS and name not in exclude:
                exclude.append(name)
        try:
            self.validate_unique(exclude=exclude)
        except ValidationError as e:
            errors = e.update_error_dict(errors)

    if errors:
        raise ValidationError(errors)


def _get_unique_checks(self, exclude=None):
    """
    Gather a list of checks to perform. Since validate_unique could be
    called from a ModelForm, some fields may have been excluded; we can't
    perform a unique check on a model that is missing fields involved
    in that check.
    Fields that did not validate should also be excluded, but they need
    to be passed in via the exclude argument.
    """
    if exclude is None:
        exclude = []
    unique_checks = []

    unique_togethers = [(self.__class__, self._meta.unique_together)]
    for parent_class in self._meta.get_parent_list():
        if parent_class._meta.unique_together:
            unique_togethers.append((parent_class, parent_class._meta.unique_together))

    for model_class, unique_together in unique_togethers:
        for check in unique_together:
            for name in check:
                # If this is an excluded field, don't add this check.
                if name in exclude:
                    break
            else:
                unique_checks.append((model_class, tuple(check)))

    # These are checks for the unique_for_<date/year/month>.
    date_checks = []

    # Gather a list of checks for fields declared as unique and add them to
    # the list of checks.

    fields_with_class = [(self.__class__, self._meta.local_fields)]
    for parent_class in self._meta.get_parent_list():
        fields_with_class.append((parent_class, parent_class._meta.local_fields))

    for model_class, fields in fields_with_class:
        for f in fields:
            name = f.name
            if name in exclude:
                continue
            if f.unique:
                unique_checks.append((model_class, (name,)))
            if f.unique_for_date and f.unique_for_date not in exclude:
                date_checks.append((model_class, 'date', name, f.unique_for_date))
            if f.unique_for_year and f.unique_for_year not in exclude:
                date_checks.append((model_class, 'year', name, f.unique_for_year))
            if f.unique_for_month and f.unique_for_month not in exclude:
                date_checks.append((model_class, 'month', name, f.unique_for_month))
    return unique_checks, date_checks


def _perform_unique_checks(self, unique_checks):
    errors = {}

    for model_class, unique_check in unique_checks:
        # Try to look up an existing object with the same values as this
        # object's values for all the unique field.

        lookup_kwargs = {}
        for field_name in unique_check:
            f = self._meta.get_field(field_name)
            lookup_value = getattr(self, f.attname)
            if lookup_value is None:
                # no value, skip the lookup
                continue
            if f.primary_key and not self._state.adding:
                # no need to check for unique primary key when editing
                continue
            lookup_kwargs[str(field_name)] = lookup_value

        # some fields were skipped, no reason to do the check
        if len(unique_check) != len(lookup_kwargs):
            continue

        qs = model_class._default_manager.filter(**lookup_kwargs)

        # Exclude the current object from the query if we are editing an
        # instance (as opposed to creating a new one)
        # Note that we need to use the pk as defined by model_class, not
        # self.pk. These can be different fields because model inheritance
        # allows single model to have effectively multiple primary keys.
        # Refs #17615.
        model_class_pk = self._get_pk_val(model_class._meta)
        if not self._state.adding and model_class_pk is not None:
            qs = qs.exclude(pk=model_class_pk)
        if qs.exists():
            if len(unique_check) == 1:
                key = unique_check[0]
            else:
                key = NON_FIELD_ERRORS
            errors.setdefault(key, []).append(self.unique_error_message(model_class, unique_check))

    return errors

def _perform_date_checks(self, date_checks):
    errors = {}
    for model_class, lookup_type, field, unique_for in date_checks:
        lookup_kwargs = {}
        # there's a ticket to add a date lookup, we can remove this special
        # case if that makes it's way in
        date = getattr(self, unique_for)
        if date is None:
            continue
        if lookup_type == 'date':
            lookup_kwargs['%s__day' % unique_for] = date.day
            lookup_kwargs['%s__month' % unique_for] = date.month
            lookup_kwargs['%s__year' % unique_for] = date.year
        else:
            lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(date, lookup_type)
        lookup_kwargs[field] = getattr(self, field)

        qs = model_class._default_manager.filter(**lookup_kwargs)
        # Exclude the current object from the query if we are editing an
        # instance (as opposed to creating a new one)
        if not self._state.adding and self.pk is not None:
            qs = qs.exclude(pk=self.pk)

        if qs.exists():
            errors.setdefault(field, []).append(
                self.date_error_message(lookup_type, field, unique_for)
            )
    return errors


def validate_unique(self, exclude=None):
    """
    Checks unique constraints on the model and raises ``ValidationError``
    if any failed.
    """
    unique_checks, date_checks = self._get_unique_checks(exclude=exclude)

    errors = self._perform_unique_checks(unique_checks)
    date_errors = self._perform_date_checks(date_checks)
    for k, v in date_errors.items():
        errors.setdefault(k, []).extend(v)

    if errors:
        raise ValidationError(errors)


def clean(self):
    pass


def clean_fields(self, exclude=None):
    """
    Cleans all fields and raises a ValidationError containing a dict
    of all validation errors if any occur.
    """
    if exclude is None:
        exclude = []

    errors = {}
    for f in self._meta.fields:
        if f.name in exclude:
            continue
        # Skip validation for empty fields with blank=True. The developer
        # is responsible for making sure they have a valid value.
        raw_value = getattr(self, f.attname)
        if f.blank and raw_value in f.empty_values:
            continue
        try:
            setattr(self, f.attname, f.clean(raw_value, self))
        except ValidationError as e:
            errors[f.name] = e.error_list

    if errors:
        raise ValidationError(errors)
