package ru.yandex.canvas;

import java.text.MessageFormat;
import java.util.Iterator;

import javax.validation.constraints.NotNull;

import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;

import ru.yandex.canvas.service.TankerKeySet;

/**
 * Функциональный врапер над BindingResult позволяющий заполнять его сразу локализованными по указанному кейсету
 * сообщениями, так сделано потому что знание о том, какой кейсет использовать есть в момент валидации.
 * Ключи в кейсете должны быть совместимы с {@link MessageFormat}
 * Лямбды нужны для того чтобы не забывать делать pop на путь, который уже не актуален.
 * Перед запуском лямбды валидатора к path до текущего поля добавляется имя валидируемого объекта, при выходе из лябмбы
 * со стека пути снимается верхний элемент.
 * <p>
 * Пример:
 * AdditionData additionData = new AdditionData();
 * AdditionDataBundle bundle = new AdditionDataBundle();
 * bundle.setName("testName");
 * additionData.setBundle(bundle);
 * Addition addition = new Addition();
 * addition.setData(additionData);
 * <p>
 * LocalizedBindingResultBuilder binder = new LocalizedBindingResultBuilder(addition, "addition", TankerKeySet.VIDEO);
 * binder.rejectValue("name", "video_file_not_found");
 * binder.rejectValue("archive", "archive");
 * binder.reject("Everything failed");
 * binder.validate("data", (o, r) -> {
 * r.reject("data failed");
 * r.validate("bundle", (data_o2, r2) -> {
 * r2.rejectValue("name", "wrong bundle name {1} {0}", "firstArgTest", "secondArgTest");
 * });
 * });
 * <p>
 * if ("die".equals(cause)) {
 * throw new CanvasValidationException("Addition validation failed" , binder.build());
 * }
 * <p>
 * Результат после обработки в GlobalExceptionHandler и конвертировании в ValidationError:
 * {
 * "message": "Addition validation failed",
 * "properties": {
 * "": [
 * "Everything failed",
 * "data failed"
 * ],
 * "name": [
 * "Видео не найдено"
 * ],
 * "archive": [
 * "archive"
 * ],
 * "data.bundle.name": [
 * "wrong bundle name secondArgTest firstArgTest"
 * ]
 * }
 * }
 * <p>
 * По ключу "" - общие на объект ошибки валидации (у нас как правило не используются)
 */
public class LocalizedBindingResultBuilder {
    private BindingResult bindingResult;
    private TankerKeySet keyset;

    public LocalizedBindingResultBuilder(Object target, String objectName, TankerKeySet keyset) {
        bindingResult = new BeanPropertyBindingResult(target, objectName);
        this.keyset = keyset;
    }

    public void validate(String path, ValidatorFunc validator) {
        Object o = bindingResult.getRawFieldValue(path);
        if (o instanceof Iterable) {
            validateIterable(path, validator, (Iterable) o);
        } else {
            validateScalar(path, validator, o);
        }

    }

    /**
     * Если нужно проверить один конкретный элемент списка, указываем путь к списку и номер элемента
     *
     * @param path
     * @param i
     * @param validator
     */
    public void validate(String path, int i, ValidatorFunc validator) {
        validate(ListNthElementPath(path, i), validator);
    }

    private void validateScalar(String path, ValidatorFunc validator, Object o) {
        bindingResult.pushNestedPath(path);
        validator.validate(o, this);
        bindingResult.popNestedPath();
    }

    private void validateIterable(String path, ValidatorFunc validator, Iterable iterable) {
        Iterator iter = iterable.iterator();
        int i = 0;
        while (iter.hasNext()) {
            bindingResult.pushNestedPath(ListNthElementPath(path, i));
            validator.validate(iter.next(), this);
            bindingResult.popNestedPath();
            i++;
        }
    }

    private String ListNthElementPath(@NotNull String path, int i) {
        return String.format("%s[%d]", path, i);
    }

    /**
     * Ошибка на поле field, указывается только имя поля, весь путь автоматически
     * будет подставлен
     */
    public void rejectValue(String field, String message, Object... messageArgs) {
        String interpolatedLocalizedMessage = interpolate(message, messageArgs);
        bindingResult.rejectValue(field, null, interpolatedLocalizedMessage);
    }

    /**
     * Ошибка на весь объект валидации TODO: проведить что фронт умеет с этим работать, если нет, то удалить/закоментировать
     */
    public void reject(String message, Object... messageArgs) {
        String interpolatedLocalizedMessage = interpolate(message, messageArgs);
        bindingResult.reject(interpolatedLocalizedMessage, interpolatedLocalizedMessage);
    }

    private String interpolate(String message, Object... messageArgs) {
        String localizedMessage = keyset.key(message);
        return MessageFormat.format(localizedMessage, messageArgs);
    }

    public BindingResult build() {
        return bindingResult;
    }

    @FunctionalInterface
    public interface ValidatorFunc<T> {
        void validate(T o, LocalizedBindingResultBuilder bindingResult);
    }
}
