package ru.yandex.direct.api.v5.validation.constraints;

import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;

import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.core.validation.CommonDefectTranslations;
import ru.yandex.direct.i18n.types.Identity;
import ru.yandex.direct.validation.Predicates;
import ru.yandex.direct.validation.builder.Constraint;
import ru.yandex.direct.validation.builder.ListConstraint;

import static ru.yandex.direct.api.v5.validation.DefectTypes.absentElementInArray;
import static ru.yandex.direct.api.v5.validation.DefectTypes.emptyElementsList;
import static ru.yandex.direct.api.v5.validation.DefectTypes.emptyValue;
import static ru.yandex.direct.api.v5.validation.DefectTypes.invalidCollectionSize;
import static ru.yandex.direct.api.v5.validation.DefectTypes.invalidFormat;
import static ru.yandex.direct.api.v5.validation.DefectTypes.invalidId;
import static ru.yandex.direct.api.v5.validation.DefectTypes.invalidValue;
import static ru.yandex.direct.api.v5.validation.DefectTypes.maxCollectionSize;
import static ru.yandex.direct.api.v5.validation.DefectTypes.maxStringSize;
import static ru.yandex.direct.api.v5.validation.DefectTypes.minCollectionSize;
import static ru.yandex.direct.validation.Predicates.not;

public class Constraints {

    // common
    public static <T> Constraint<T, DefectType> isNull() {
        return Constraint.fromPredicateOfNullable(Objects::isNull, invalidValue());
    }

    public static <T> Constraint<T, DefectType> notNull() {
        return Constraint.fromPredicateOfNullable(Objects::nonNull, invalidValue());
    }

    public static <T> Constraint<List<T>, DefectType> eachNotNull() {
        return Constraint.fromPredicate(
                list -> list.stream().allMatch(Objects::nonNull), absentElementInArray());
    }

    public static <T> Constraint<T, DefectType> inSet(Set<T> set) {
        return Constraint.fromPredicate(set::contains, invalidValue());
    }

    public static <T> Constraint<T, DefectType> inSet(Supplier<Set<T>> setSupplier) {
        return Constraint.fromPredicate(setSupplier.get()::contains, invalidValue());
    }

    public static <T> Constraint<T, DefectType> notInSet(Set<T> set) {
        return Constraint.fromPredicate(not(set::contains), invalidValue());
    }

    // numbers

    public static Constraint<Long, DefectType> greaterThan(long min) {
        return Constraint.fromPredicate(v -> v > min, invalidValue().withDetailedMessage(
                (p, v) -> CommonDefectTranslations.INSTANCE.greaterThan(p, new Identity(min))));
    }

    public static Constraint<Integer, DefectType> greaterThan(int min) {
        return Constraint.fromPredicate(v -> v > min, invalidValue());
    }

    public static Constraint<Long, DefectType> lessThan(long max) {
        return Constraint.fromPredicate(v -> v < max, invalidValue());
    }

    public static Constraint<Integer, DefectType> lessThan(int max) {
        return Constraint.fromPredicate(v -> v < max, invalidValue());
    }

    public static Constraint<Long, DefectType> notGreaterThan(long max) {
        return Constraint.fromPredicate(v -> v <= max,
                invalidValue().withDetailedMessage(
                        (p, v) -> CommonDefectTranslations.INSTANCE.notGreaterThan(p, new Identity(max))));
    }

    public static Constraint<Integer, DefectType> notGreaterThan(int max) {
        return Constraint.fromPredicate(v -> v <= max,
                invalidValue().withDetailedMessage(
                        (p, v) -> CommonDefectTranslations.INSTANCE.notGreaterThan(p, new Identity(max))));
    }

    public static Constraint<Long, DefectType> notLessThan(long min) {
        return Constraint.fromPredicate(v -> v >= min,
                invalidValue().withDetailedMessage(
                        (p, v) -> CommonDefectTranslations.INSTANCE.notLessThan(p, new Identity(min))));
    }

    public static Constraint<Integer, DefectType> notLessThan(int min) {
        return Constraint.fromPredicate(v -> v >= min,
                invalidValue().withDetailedMessage(
                        (p, v) -> CommonDefectTranslations.INSTANCE.notLessThan(p, new Identity(min))));
    }

    public static Constraint<Integer, DefectType> inRange(int min, int max) {
        return Constraint.fromPredicate(Predicates.inRange(min, max), invalidValue());
    }

    public static Constraint<Long, DefectType> inRange(long min, long max) {
        return Constraint.fromPredicate(Predicates.inRange(min, max), invalidValue());
    }

    public static Constraint<BigDecimal, DefectType> inRange(BigDecimal min, BigDecimal max) {
        return Constraint.fromPredicate(Predicates.inRange(min, max), invalidValue());
    }

    // strings

    public static Constraint<String, DefectType> notEmpty() {
        return Constraint.fromPredicate(v -> !v.trim().isEmpty(), emptyValue());
    }

    public static Constraint<String, DefectType> notBlank() {
        return Constraint.fromPredicate(StringUtils::isNotBlank, emptyValue());
    }

    public static Constraint<String, DefectType> maxLength(int max) {
        return Constraint.fromPredicate(Predicates.maxLength(max), maxStringSize(max));
    }

    public static Constraint<String, DefectType> matches(Pattern pattern) {
        return Constraint.fromPredicate(v -> pattern.matcher(v).matches(), invalidValue());
    }

    public static Constraint<String, DefectType> contains(Pattern pattern) {
        return Constraint.fromPredicate(pattern.asPredicate(), invalidValue());
    }

    public static Constraint<String, DefectType> notContains(Pattern pattern) {
        return Constraint.fromPredicate(v -> !pattern.matcher(v).find(), invalidValue());
    }

    // collections

    public static <I> Constraint<List<I>, DefectType> notEmptyCollection() {
        return Constraint.fromPredicate(Predicates.notEmptyCollection(), emptyElementsList());
    }

    public static <I> Constraint<List<I>, DefectType> listSize(int min, int max) {
        return Constraint.fromPredicate(Predicates.listSize(min, max), invalidCollectionSize(min, max));
    }

    public static <I> Constraint<List<I>, DefectType> minListSize(int min) {
        return Constraint.fromPredicate(Predicates.listSize(min, Integer.MAX_VALUE), minCollectionSize(min));
    }

    public static <I> Constraint<List<I>, DefectType> maxListSize(int max) {
        return Constraint.fromPredicate(Predicates.listSize(Integer.MIN_VALUE, max), maxCollectionSize(max));
    }

    public static <T> ListConstraint<T, DefectType> unique() {
        return new UniqueItemsConstraint<>();
    }

    public static <T> ListConstraint<T, DefectType> unique(Function<T, ?> getter) {
        return new UniqueItemsConstraint<>(getter);
    }

    /**
     * Позволяет найти дубликаты с помощью переданного {@code comparator}'а.
     * Дубликатами считаются элементы, для которых {@link Comparator#compare(Object, Object) comparator.compare(..)}
     * возвращает {@code 0}.
     *
     * @param comparator {@link Comparator}
     * @return экземпляр {@link ListConstraint} валидирующий список на наличие дубликатов
     */
    public static <T> ListConstraint<T, DefectType> unique(Comparator<T> comparator) {
        return new UniqueComparableItemsConstraint<>(comparator);
    }

    // complex

    // todo new detailed messsage
    public static Constraint<Long, DefectType> validId() {
        return Constraint.fromPredicate(not(Predicates.lessThan(1L)), invalidId());
    }

    public static <T> Constraint<List<Long>, DefectType> eachValidId() {
        return Constraint.fromPredicate(
                list -> list.stream().allMatch(not(Predicates.lessThan(1L))), invalidId());
    }

    /**
     * Проверить корректность email с помощью Apache EmailValidator
     */
    public static Constraint<String, DefectType> validEmail() {
        return Constraint.fromPredicate(v -> EmailValidator.getInstance().isValid(v), invalidValue());
    }

    /**
     * Проверить на соответствие регулярному выражению
     *
     * @param pattern регулярное выражение
     */
    public static Constraint<String, DefectType> matchPattern(String pattern) {
        Pattern compiledPattern = Pattern.compile(pattern);
        return Constraint.fromPredicate(v -> compiledPattern.matcher(v).matches(), invalidValue());
    }

    /**
     * Проверить формат даты на соответствие ISO 8601
     */
    public static Constraint<String, DefectType> validIso8601DateTime() {
        return strdate -> {
            try {
                OffsetDateTime moment = OffsetDateTime.parse(strdate);
                return null;
            } catch (DateTimeParseException ex) {
                return invalidFormat();
            }
        };
    }
}
