/*
 *  @author Ranides Atterwim {@literal <ranides@gmail.com>}
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.generic;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.maps.MapCollectors;
import net.ranides.assira.reflection.*;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.ToIntFunction;

/**
 * Helper functions for enum values.
 */
@UtilityClass public class EnumUtils {

    /**
     * Creates inverted mapping between enum valus and values assigned to each enum.
     * Every value computed by provided "function" could be used later to quickly find "enum"
     *
     * Created mapping is returned as "function", not ordinary map.
     * You could think about it as a map degenerated to "get" method.
     *
     * @param eenum eenum
     * @param function function
     * @param <K> K
     * @param <E> E
     * @return mapping
     */
    public static <K,E extends Enum<E>> Function<K, E> lookup(Class<E> eenum, Function<E,? extends K> function) {
        Map<K, E> map = Arrays.stream(eenum.getEnumConstants()).collect(MapCollectors.unique(function));
        return map::get;
    }

    /**
     * Creates reversed mapping between enum and value computed for each enum.
     * Every value computed by provided "function" could be used later to quickly find "enum"
     *
     * Created mapping is returned as "function", not ordinary map.
     * You could think about it as a map degenerated to "get" method.
     *
     * @param eenum eenum
     * @param function function
     * @param <E> E
     * @return mapping
     */
    public static <E extends Enum<E>> IntFunction<E> lookupInt(Class<E> eenum, ToIntFunction<E> function) {
        E[] values = eenum.getEnumConstants();
        int[] keys = Arrays.stream(values).mapToInt(function).toArray();
        return key -> {
            int i = Arrays.binarySearch(keys, key);
            return i<0 ? null : values[i];
        };

    }

    /**
     * Creates mapping between enum and value computed for each enum.
     * Every value computed by provided "function" could be found later using "enum" as a key.
     *
     * @param eenum eenum
     * @param function function
     * @param <E> E
     * @param <V> V
     * @return immutable map
     */
    public static <E extends Enum<E>, V> Map<E,V> index(Class<E> eenum, Function<E,? extends V> function) {
        return Arrays.stream(eenum.getEnumConstants()).collect(MapCollectors.unique(e -> e, function::apply));
    }

    /**
     * Creates mapping between enum and corresponding value.
     * Every value could be found later using "enum" as a key.
     *
     * @param eenum eenum
     * @param values values
     * @param <E> E
     * @param <V> V
     * @return immutable map
     */
    public static <E extends Enum<E>, V> Map<E,V> index(Class<E> eenum, V[] values) {
        E[] keys = eenum.getEnumConstants();
        if(keys.length != values.length) {
            throw new IllegalArgumentException("Different size of enum ("+keys.length+" and values array (" + values.length + ")");
        }
        return Arrays.stream(keys).collect(MapCollectors.unique(e -> e, e -> values[e.ordinal()]));
    }

    /**
     * Returns annotation put on provided enum value.
     * Returns null, if enum isn't annotated.
     *
     * Every enum value can be annotated independently, this method allows to read those annotations.
     *
     * @param value value
     * @param type type
     * @param <A> A
     * @return annotation
     */
    public static <A extends Annotation> Ref<A> getAnnotation(Enum<?> value, Class<A> type) {
        return getAnnotations(value).first(type);
    }

    /**
     * Returns all annotations put on provided enum value.
     *
     * Every enum value can be annotated independently, this method allows to read those annotations.
     *
     * @param value value
     * @return annotations
     */
    public static IAnnotations getAnnotations(Enum<?> value) {
        return IClass.typefor(value)
            .field(value.name())
            .map(IElement::annotations)
            .orElse(IAnnotations.empty());
    }

    /**
     * Returns all enum constants for provided type.
     * Throws IReflectiveException if type is not enum.
     *
     * @param type type
     * @param <E> enum
     * @return list of Enum
     */
    @SuppressWarnings("unchecked")
    public static <E extends Enum<E>> List<E> getItems(IClass<?> type) {
        if (!type.isEnum()) {
            throw new IReflectiveException(type.name() + " is not enum");
        }
        return (List)Arrays.asList(type.raw().getEnumConstants());
    }

}
