package ru.yandex.webmaster3.core.http.autodoc;

import org.jetbrains.annotations.NotNull;
import ru.yandex.webmaster3.core.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

/**
 * @author avhaliullin
 */
public class EnumComparators {
    private static boolean canUseField(Field field) {
        int mod = field.getModifiers();
        return Modifier.isPublic(field.getModifiers()) && !Modifier.isStatic(mod);
    }

    private static boolean canUseGetter(Method method) {
        int mod = method.getModifiers();
        return Modifier.isPublic(mod) && !Modifier.isStatic(mod) && method.getParameterCount() == 0;
    }

    private static <T> Comparator<T> createComparator(Function<T, Object> accessor, Class<T> clazz, boolean reverse,
                                                      NullMode nullMode, Map<Class, Comparator> comparatorIndex,
                                                      Set<Class> promised) {
        Comparator cmp;
        if (clazz.isEnum()) {
            cmp = createComparator((Class<Enum>) clazz, comparatorIndex, promised);
        } else if (Comparable.class.isAssignableFrom(clazz)) {
            cmp = Comparator.naturalOrder();
        } else if (clazz.isPrimitive()) {
            return createComparator(accessor, ReflectionUtils.primitiveToBoxed(clazz), reverse, nullMode, comparatorIndex, promised);
        } else {
            throw new RuntimeException("Don't know how to compare objects of type " + clazz);
        }
        if (reverse) {
            cmp = cmp.reversed();
        }
        switch (nullMode) {
            case FIRST:
                cmp = Comparator.nullsFirst(cmp);
                break;
            case LAST:
                cmp = Comparator.nullsLast(cmp);
                break;
            default:
                throw new RuntimeException("Unknown null mode " + nullMode);
        }
        return Comparator.comparing(accessor, cmp);
    }

    private static <T extends Enum<T>> Comparator<T> comparatorForField(Class<T> enumClass, FieldOrdering orderBy,
                                                                        Map<Class, Comparator> comparatorsIndex,
                                                                        Set<Class> promised) {
        for (Method method : enumClass.getMethods()) {
            if (canUseGetter(method) && method.getName().equals(orderBy.name)) {
                return createComparator(t -> {
                    try {
                        return method.invoke(t);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        throw new RuntimeException("Failed to access order field " + orderBy.name, e);
                    }
                }, (Class<T>) method.getReturnType(), orderBy.reverse, orderBy.nullMode, comparatorsIndex, promised);
            }
        }
        for (Field field : enumClass.getFields()) {
            if (canUseField(field) && field.getName().equals(orderBy.name)) {
                return createComparator(t -> {
                    try {
                        return field.get(t);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException("Failed to access order field " + orderBy.name, e);
                    }
                }, (Class<T>) field.getType(), orderBy.reverse, orderBy.nullMode, comparatorsIndex, promised);
            }
        }
        return null;
    }

    public static <T extends Enum<T>> Comparator<T> createComparator(Class<T> enumClass) {
        return createComparator(enumClass, new HashMap<Class, Comparator>(), new HashSet<>());
    }

    @NotNull
    private static <T extends Enum<T>> Comparator<T> createComparator(Class<T> enumClass,
                                                                      Map<Class, Comparator> comparatorsIndex,
                                                                      Set<Class> promisedComparators) {
        if (promisedComparators.contains(enumClass)) {
            return (o1, o2) -> comparatorsIndex.get(enumClass).compare(o1, o2);
        }
        promisedComparators.add(enumClass);
        OrderBy[] ordering = enumClass.getAnnotationsByType(OrderBy.class);
        List<FieldOrdering> fields = new ArrayList<>();
        if (ordering == null || ordering.length == 0) {
            fields.add(new FieldOrdering("ordinal", false, NullMode.FIRST));
        } else {
            for (OrderBy orderBy : ordering) {
                boolean reverse = orderBy.reverse();
                NullMode fNullMode;
                switch (orderBy.nullMode()) {
                    case FIRST:
                        fNullMode = NullMode.FIRST;
                        break;
                    case LAST:
                        fNullMode = NullMode.LAST;
                        break;
                    default:
                        throw new RuntimeException("Unknown ordering null mode " + orderBy.nullMode());
                }
                fields.add(new FieldOrdering(orderBy.value(), reverse, fNullMode));
            }
        }
        Comparator<T> result = null;
        for (FieldOrdering orderBy : fields) {
            Comparator<T> newComp = comparatorForField(enumClass, orderBy, comparatorsIndex, promisedComparators);
            if (newComp == null) {
                throw new RuntimeException("Failed to find accessible field or method " + orderBy.name + " of class " + enumClass);
            }

            if (result == null) {
                result = newComp;
            } else {
                result = result.thenComparing(newComp);
            }
        }
        comparatorsIndex.put(enumClass, result);
        return result;
    }

    private enum NullMode {
        FIRST,
        LAST,
    }

    private static class FieldOrdering {
        final String name;
        final boolean reverse;
        final NullMode nullMode;

        public FieldOrdering(String name, boolean reverse, NullMode nullMode) {
            this.name = name;
            this.reverse = reverse;
            this.nullMode = nullMode;
        }
    }
}
