/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

package org.grails.core.io.support

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import org.springframework.core.OrderComparator
import org.springframework.util.Assert
import org.springframework.util.ClassUtils

import org.grails.io.support.FactoriesLoaderSupport

/**
 * General purpose factory loading mechanism for internal use within the framework.
 *
 * <p>Based on {@link org.springframework.core.io.support.SpringFactoriesLoader}, but applies caching and uses a different file name</p>
 *
 * @since 2.4
 * @author Graeme Rocher
 */
@Slf4j
@CompileStatic
class GrailsFactoriesLoader extends FactoriesLoaderSupport {

    private static final Object[] NO_ARGUMENTS = [] as Object[]

    /**
     * Load the factory implementations of the given type from the default location,
     * using the given class loader.
     * <p>The returned factories are ordered in accordance with the {@link org.springframework.core.OrderComparator}.
     * @param factoryClass the interface or abstract class representing the factory
     */
    static <T> List<T> loadFactories(Class<T> factoryClass) {
        (List<T>) loadFactoriesWithArguments(factoryClass, GrailsFactoriesLoader.classLoader)
    }

    /**
     * Load the factory implementations of the given type from the default location,
     * using the given class loader.
     * <p>The returned factories are ordered in accordance with the {@link org.springframework.core.OrderComparator}.
     * @param factoryClass the interface or abstract class representing the factory
     * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
     */
    static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        (List<T>) loadFactoriesWithArguments(factoryClass, classLoader, NO_ARGUMENTS)
    }

    /**
     * Load the factory implementations of the given type from the given resource location,
     * using the given class loader.
     * <p>The returned factories are ordered in accordance with the {@link org.springframework.core.OrderComparator}.
     * @param factoryClass the interface or abstract class representing the factory
     * @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
     * @param resourceLocation the classpath location of the factories files to read
     *          (e.g. {@link #CLI_FACTORIES_RESOURCE_LOCATION})
     */
    static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader, String resourceLocation) {
        List<T> results = []
        for (Class<? extends T> clazz : loadFactoryClasses(factoryClass, classLoader, resourceLocation)) {
            results.add(clazz.getDeclaredConstructor().newInstance())
        }

        // This list should always be rather small, so sort the handlers by class name.  This will provide
        // a deterministic order before accounting for PriorityOrdering
        results.sort { T a, T b ->
            a?.getClass()?.name <=> b?.getClass()?.name
        }
        OrderComparator.sort((List<T>) results)
        results
    }

    static <T> List<T> loadFactoriesWithArguments(Class<T> factoryClass, ClassLoader classLoader, Object[] arguments) {
        boolean hasArguments = !(arguments != null && arguments.length == 0)
        List<T> results = new ArrayList<T>()
        for (Class<? extends T> clazz : loadFactoryClasses(factoryClass, classLoader)) {
            results.add(hasArguments ? clazz.newInstance(arguments) : clazz.getDeclaredConstructor().newInstance())
        }

        // This list should always be rather small, so sort the handlers by class name.  This will provide
        // a deterministic order before accounting for PriorityOrdering
        results.sort { T a, T b ->
            a?.getClass()?.name <=> b?.getClass()?.name
        }
        OrderComparator.sort((List<T>) results)
        results
    }

    /**
     * Loads factory class names grouped by the resource that declares them without loading the
     * classes themselves. This lets callers that need diagnostic context apply stricter class
     * loading semantics than the framework-wide best-effort factory loader.
     */
    static Map<String, List<String>> loadFactoryDeclarations(
            Class<?> factoryClass,
            ClassLoader classLoader = GrailsFactoriesLoader.classLoader,
            String resourceLocation = FACTORIES_RESOURCE_LOCATION) {
        Assert.notNull(factoryClass, "'factoryClass' must not be null")
        loadFactoryDeclarations(factoryClass.name, classLoader, resourceLocation)
    }

    static Map<String, List<String>> loadFactoryDeclarations(
            String factoryClassName,
            ClassLoader classLoader = GrailsFactoriesLoader.classLoader,
            String resourceLocation = FACTORIES_RESOURCE_LOCATION) {
        Assert.hasText(factoryClassName, "'factoryClassName' must not be empty")
        ClassLoader factoryClassLoader = classLoader ?: GrailsFactoriesLoader.classLoader
        Map<String, List<String>> declarations = new LinkedHashMap<>()
        Enumeration<URL> resources = factoryClassLoader.getResources(resourceLocation)
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement()
            Properties properties = new Properties()
            try {
                resource.openStream().withCloseable { InputStream input ->
                    properties.load(input)
                }
            }
            catch (IOException | IllegalArgumentException e) {
                log.warn('Unable to read factory declarations from \'{}\'; skipping it. Any factories declared in that resource are unavailable.',
                        resource.toExternalForm(), e)
                continue
            }

            List<String> factoryNames = []
            String declaredFactories = properties.getProperty(factoryClassName)
            if (declaredFactories) {
                for (String factoryName : declaredFactories.split(',')) {
                    String trimmedName = factoryName.trim()
                    if (trimmedName) {
                        factoryNames.add(trimmedName)
                    }
                }
            }
            if (factoryNames) {
                declarations.putIfAbsent(resource.toExternalForm(), factoryNames)
            }
        }
        declarations
    }

    static <T> List<Class<T>> loadFactoryClasses(Class<T> factoryClass, ClassLoader classLoader = GrailsFactoriesLoader.classLoader,
                                                 String resourceLocation = FACTORIES_RESOURCE_LOCATION) {
        Assert.notNull(factoryClass, "'factoryClass' must not be null")

        def factoryNames = loadFactoryNames(factoryClass, classLoader, resourceLocation)

        List<Class<T>> result = []
        for (String factoryName in factoryNames) {
            def clazz = loadFactoryClass(factoryName, factoryClass, classLoader)
            if (clazz) {
                result.add(clazz)
            }
        }
        return result
    }

    private static <T> Class<? extends T> loadFactoryClass(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
        try {
            def instanceClass = ClassUtils.forName(instanceClassName, classLoader)
            if (!factoryClass.isAssignableFrom(instanceClass)) {
                throw new IllegalArgumentException(
                    "Class [$instanceClassName] is not assignable to [$factoryClass.name]")
            }
            return (Class<? extends T>) instanceClass
        }
        catch (Throwable ex) {
            // not creatable in current context, ignore
            return null
        }
    }

    static <T> T loadFactory(Class<T> factoryClass, ClassLoader classLoader = GrailsFactoriesLoader.classLoader) {
        def all = loadFactories(factoryClass, classLoader)
        if (all) {
            return all.get(0)
        }
    }

    static <T> T loadFactory(Class<T> factoryClass, Object... arguments) {
        loadFactory(factoryClass, GrailsFactoriesLoader.classLoader, arguments)
    }

    static <T> T loadFactory(Class<T> factoryClass, ClassLoader classLoader, Object... arguments) {
        def all = loadFactoriesWithArguments(factoryClass, classLoader, arguments)
        if (all) {
            return (T) all.get(0)
        }
    }
}
