package ru.yandex.wmtools.common.util.enums;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import ru.yandex.common.util.collections.Cf;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.common.util.functional.Filter;
import ru.yandex.common.util.functional.Function;

import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Stepan Koltsov
 */
public class EnumResolver<T extends Enum<T>> {
    private final T[] enums;

    protected final Class<T> enumClass;

    protected final List<T> valuesList;
    protected final Set<T> valuesSet;

    @SuppressWarnings("unused")
    private final Map<String, T> mapByName;
    protected final Map<String, T> mapByIdName;

    private final boolean hasUnknown;
    protected final T unknown;

    public EnumResolver(Class<T> enumClass) {
        Validate.notNull(enumClass);

        this.enumClass = enumClass;

        this.enums = enumClass.getEnumConstants();

        this.valuesList = Cf.list(enums);
        this.valuesSet = EnumSet.allOf(enumClass);

        mapByName = Cf.newHashMap();
        for (T t : valuesList) {
            mapByName.put(t.name(), t);
        }

        mapByIdName = Cf.newHashMap();
        for (T t : valuesList) {
            mapByIdName.put(toId(t.name()), t);
        }

        hasUnknown = !valuesList.isEmpty() && isUnknown(Cu.first(valuesList));
        unknown = hasUnknown ? Cu.first(valuesList) : null;

    }

    protected boolean isUnknown(T t) {
        return t.name().equalsIgnoreCase("UNKNOWN");
    }

    public Class<T> getEnumClass() {
        return enumClass;
    }

    public T getUnknownValue() {
        if (unknown != null) {
            return unknown;
        }
        throw new IllegalArgumentException("UNKNOWN not set");
    }
    static Function<Enum<?>, Integer> enumOrdinalM() {
        return new Function<Enum<?>, Integer>() {
            public Integer apply(Enum<?> t) {
                return t.ordinal();
            }
        };
    }

    static Function<Enum<?>, String> enumNameM() {
        return new Function<Enum<?>, String>() {
            public String apply(Enum<?> t) {
                return t.name();
            }
        };
    }

    public Set<T> allOf() {
        return valuesSet;
    }

    public List<T> valuesList() {
        return valuesList;
    }

    public List<T> knownValues() {
        return Cu.filterList(valuesList(), knownP());
    }

    protected String toId(String s) {
        return s.toLowerCase().replaceAll("[_ .\\-]", "");
    }

    public T valueOfOrNull(String s) {
        if (StringUtils.isEmpty(s)) {
            return null;
        }
        return mapByIdName.get(toId(s));
    }

    public T valueOf(String s) {
        T value = valueOfOrNull(s);
        if (value == null) {
            throw new IllegalArgumentException("unknown value for enum: " + s);
        }
        return value;
    }

    public T valueOfOrUnknown(String s) {
        T value = valueOfOrNull(s);
        if (value != null) {
            return value;
        }
        if (unknown != null) {
            return unknown;
        }
        throw new IllegalArgumentException("UNKNOWN not set");
    }

    public T valueOfKnown(String s) {
        T v = valueOf(s);
        if (unknown != null && v == unknown) throw new IllegalArgumentException("must be known");
        return v;
    }

    public Function<String, T> valueOfM() {
        return new Function<String, T>() {
            public T apply(String s) {
                return valueOf(s);
            }
        };
    }

    public Function<String, T> valueOfOrUnknownM() {
        return new Function<String, T>() {
            public T apply(String s) {
                return valueOfOrUnknown(s);
            }
        };
    }

    private Filter<T> knownP() {
        return new Filter<T>() {
            public boolean fits(T t) {
                return unknown != null && unknown != t;
            }
        };
    }

    /**
     * this method should be named {@code er} and not {@code r} because Eclipse compiler can't
     * distinguish it and {@code r} method of OrdinalIntEnumResolver
     */
    public static <E extends Enum<E>> EnumResolver<E> er(Class<E> enumClass) {
        return new EnumResolver<E>(enumClass);
    }
} //~
