package ru.yandex.direct.api.v5.common;

import java.beans.Introspector;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.base.CaseFormat;
import one.util.streamex.StreamEx;

import ru.yandex.direct.common.util.PropertyFilter;

import static ru.yandex.direct.utils.FunctionalUtils.mapList;

/**
 * Используется в разнообразных Get-запросах, чтобы отфильтровать лишние свойства возвращаемых объектов.
 * <p>
 * Свойства у генерируемых классов имеют те же имена, что и {@code value()} у сгенерированных Enum'ов.
 * Это позволяет упростить организацию фильтрации свойств в Get-запросах.
 */
@ParametersAreNonnullByDefault
public class EnumPropertyFilter<F extends Enum<F>> {
    private final Map<F, String> enumToFieldMap;
    private final PropertyFilter propertyFilter;

    private EnumPropertyFilter(Map<F, String> map, PropertyFilter propertyFilter) {
        this.propertyFilter = propertyFilter;
        // без синхронизации, так как EnumMap потоко-безопасен на чтение
        enumToFieldMap = new EnumMap<>(map);
    }

    /**
     * По указанному типу Enum'а создёт экземпляр {@link EnumPropertyFilter}.
     * При этом значениям Enum'а ставится в соответствие их lower camelcase названия. Для наших сценариев, когда Enum
     * генерируется автоматически и имя и {@code value()} у элементов Enum'а идентичны с точностью до формата.
     * <p>
     * Пример:
     * <pre>
     *     IS_AVAILABLE.toString() -> "IS_AVAILABLE"
     *     IS_AVAILABLE.value() -> "isAvailable"
     *
     *     V_CARD_ID.toString() -> "V_CARD_ID"
     *     V_CARD_ID.value() -> "VCardId"
     * </pre>
     * <p>
     * Для преобразования форматов используется Guava {@link CaseFormat}:
     * <pre>Introspector.decapitalize(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, String.valueOf(value)))</pre>
     * <p>
     * Дополнительный вызов Introspector.decapitalize нужен для корректного вычисления lower camelcase названия
     * из значения Enum'а (см. JavaBeans specification, ver. 1.01, параграф 8.8 Capitalization of inferred names)
     *
     * @param enumType       класс Enum-типа
     * @param propertyFilter {@link PropertyFilter}
     * @param <T>            тип Enum'а из которого создаём {@link EnumPropertyFilter}
     * @return экземпляр {@link EnumPropertyFilter} для указанного Enum'а
     */
    public static <T extends Enum<T>> EnumPropertyFilter<T> from(Class<T> enumType, PropertyFilter propertyFilter) {
        return new EnumPropertyFilter<>(
                StreamEx.of(
                        enumType.getEnumConstants()) // toString для enum'ов возвращает их значение в виде IS_AVAILABLE (UPPER_UNDERSCORE). Нам нужно isAvailable (LOWER_CAMEL).
                        .toMap(value -> Introspector.decapitalize(
                                CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, String.valueOf(value)))),
                propertyFilter
        );
    }

    public void filterProperties(Collection<?> getItems, Iterable<F> fieldNames) {
        filterPropertiesByNames(getItems, mapList(fieldNames, enumToFieldMap::get));
    }

    public void filterPropertiesByNames(Collection<?> getItems, List<String> propertyNames) {
        getItems.forEach(item -> propertyFilter.filterProperties(item, propertyNames));
    }

    public Map<F, String> getEnumToFieldMap() {
        return Collections.unmodifiableMap(enumToFieldMap);
    }
}
