package org.implab.gradle.common.utils;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Spliterators;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.gradle.api.Action;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Provider;

public final class Values {

    private Values() {
    }

    /**
     * Converts values in the specified map
     *
     * @param <K>
     * @param <V>
     * @param <U>
     * @param map
     * @param mapper
     * @return
     */
    public static <K, V, U> Map<K, U> mapValues(Map<K, V> map, Function<V, U> mapper) {
        Function<Entry<K, V>, V> getter = Entry::getValue;

        return map.entrySet().stream()
                .collect(Collectors.toMap(Entry::getKey, getter.andThen(mapper)));
    }

    public static <K, V> void mergeMap(MapProperty<K,V> prop, Map<K, ? > map, Class<V> clazz) {
        map.forEach((k,v) -> {
            if(v instanceof Provider<?>)
                prop.put(k, ((Provider<?>)v).map(clazz::cast));
            else
                prop.put(k, clazz.cast(v));
        });
    }

    public static <K,V,U> void mergeMap(MapProperty<K,V> prop, Map<K, ?> map, Class<U> clazz, Function<U,V> mapper) {
        Function<Object, U> caster = clazz::cast;
        var castMap = caster.andThen(mapper);

        map.forEach((k,v) -> {
            if(v instanceof Provider<?>)
                prop.put(k, ((Provider<?>)v).map(x -> castMap.apply(x)));
            else
                prop.put(k, castMap.apply(v));
        });
    }

    public static <K,V,U> void configureMap(MapProperty<K,V> prop, Action<Map<K, ?>> configure, Class<U> clazz, Function<U,V> mapper)  {
        var map = new HashMap<K,V>();
        configure.execute(map);
        mergeMap(prop, map, clazz, mapper);
    }

    /**
     * Converts the supplied value to a string.
     */
    public static String toString(Object value) {
        if (value == null) {
            return null;
        } else if (value instanceof String string) {
            return string;
        } else if (value instanceof Provider<?> provider) {
            return toString(provider.getOrNull());
        } else if (value instanceof Supplier<?> supplier) {
            return toString(supplier.get());
        } else {
            return value.toString();
        }
    }

    public static <T> Stream<T> stream(Iterator<T> remaining) {
        return StreamSupport.stream(
                Spliterators.spliteratorUnknownSize(remaining, 0),
                false);
    }

    public static <T> Optional<T> take(Iterator<T> iterator) {
        return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();
    }

    public static <T> Iterable<T> iterable(T[] values) {
        return () -> new ArrayIterator<>(values);
    }

    public static <T> Optional<T> optional(Provider<T> provider) {
        return provider.isPresent() ? Optional.of(provider.get()) : Optional.empty();
    }

    private static class ArrayIterator<T> implements Iterator<T> {
        private final T[] data;

        private int pos = 0;

        ArrayIterator(T[] data) {
            this.data = data;
        }

        @Override
        public boolean hasNext() {
            return pos < data.length;
        }

        @Override
        public T next() {
            return data[pos++];
        }
    }

}
