package com.atlassian.bitbucket.util;

import com.atlassian.bitbucket.scm.BaseWeightedModuleDescriptor;
import com.atlassian.plugin.ModuleDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;

/**
 * Utility methods for working with plugin {@link ModuleDescriptor ModuleDescriptors}.
 */
public class ModuleDescriptorUtils {

    private static final Logger log = LoggerFactory.getLogger(ModuleDescriptorUtils.class);

    private ModuleDescriptorUtils() {
        throw new UnsupportedOperationException(getClass().getName() +
                " is a utility class and should not be instantiated");
    }

    /**
     * @return a generic {@link Function} for picking the result of {@link ModuleDescriptor#getModule()} from a
     * descriptor.
     * <p>
     * The cast is safe as a {@link ModuleDescriptor} can only ever return a module of it's generic type.
     */
    public static <T, M extends ModuleDescriptor<T>> Function<M, T> toModule() {
        return input -> {
            try {
                return input.getModule();
            } catch (Exception e) {
                log.info("Could not instantiate module {}", input.getCompleteKey());
                log.debug("Error instantiating module", e);
                return null;
            }
        };
    }

    /**
     * Convert an {@link Iterable} of {@link ModuleDescriptor} into a {@link Stream} of {@link T}, where T is the
     * type returned by {@link ModuleDescriptor#getModule()}.
     *
     * @param descriptors the module descriptors to convert.
     * @param <T> the type of module returned by the descriptors
     * @param <M> the module descriptors' type
     * @return a Stream of modules, sourced from the descriptors.
     */
    public static <T, M extends ModuleDescriptor<T>> Stream<T> toModules(Collection<M> descriptors) {
        return toModules(descriptors.stream());
    }

    /**
     * Convert a {@link Stream} of {@link ModuleDescriptor} into a {@link Stream} of {@link T}, where T is the
     * type returned by {@link ModuleDescriptor#getModule()}.
     *
     * @param descriptors the module descriptors to convert.
     * @param <T> the type of module returned by the descriptors
     * @param <M> the module descriptors' type
     * @return a Stream of modules, sourced from the descriptors.
     */
    public static <T, M extends ModuleDescriptor<T>> Stream<T> toModules(Stream<M> descriptors) {
        return descriptors.map(toModule())
                .filter(Objects::nonNull);
    }

    /**
     * Convert a {@link Collection} of {@link BaseWeightedModuleDescriptor} into a {@link Stream} of {@link T}
     * (sorted by {@link BaseWeightedModuleDescriptor BaseWeightedModuleDescriptor's} weight), where T is the type
     * returned by {@link ModuleDescriptor#getModule()}.
     *
     * @param descriptors the weighted module descriptors to convert.
     * @param <T> the type of module returned by the descriptors
     * @param <M> the weighted module descriptors' type
     * @return a Stream of modules, sourced from the descriptors and sorted by the descriptor's weight.
     */
    public static <T, M extends BaseWeightedModuleDescriptor<T>> Stream<T> toSortedModules(Collection<M> descriptors) {
        return toSortedModules(descriptors.stream());
    }

    /**
     * Convert a {@link Stream} of {@link BaseWeightedModuleDescriptor} into a {@link Stream} of {@link T}
     * (sorted by {@link BaseWeightedModuleDescriptor BaseWeightedModuleDescriptor's} weight), where T is the type
     * returned by {@link ModuleDescriptor#getModule()}.
     *
     * @param descriptors the weighted module descriptors to convert.
     * @param <T> the type of module returned by the descriptors
     * @param <M> the weighted module descriptors' type
     * @return a Stream of modules, sourced from the descriptors and sorted by the descriptor's weight.
     */
    public static <T, M extends BaseWeightedModuleDescriptor<T>> Stream<T> toSortedModules(Stream<M> descriptors) {
        return toModules(descriptors.sorted());
    }
}
