/*
 *  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
 *
 *    http://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.codehaus.groovy.transform.stc;

import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.runtime.m12n.ExtensionModule;
import org.codehaus.groovy.runtime.m12n.ExtensionModuleScanner;
import org.codehaus.groovy.runtime.m12n.MetaInfExtensionModule;
import org.codehaus.groovy.runtime.memoize.EvictableCache;
import org.codehaus.groovy.runtime.memoize.StampedCommonCache;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Function;
import java.util.function.Predicate;

import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;

/**
 * @since 3.0.0
 */
public abstract class AbstractExtensionMethodCache {
    final EvictableCache<ClassLoader, Map<String, List<MethodNode>>> cache = new StampedCommonCache<>(new WeakHashMap<>());

    public Map<String, List<MethodNode>> get(ClassLoader loader) {
        return cache.getAndPut(loader, this::getMethodsFromClassLoader);
    }

    private Map<String, List<MethodNode>> getMethodsFromClassLoader(ClassLoader classLoader) {
        final List<ExtensionModule> modules = new LinkedList<>();
        ExtensionModuleScanner scanner = new ExtensionModuleScanner(
                module -> {
                    if (!(module instanceof MetaInfExtensionModule)) return;

                    boolean skip = false;
                    for (ExtensionModule extensionModule : modules) {
                        if (extensionModule.getName().equals(module.getName())) {
                            skip = true;
                            break;
                        }
                    }
                    if (!skip) modules.add(module);
                },
                classLoader
        );
        scanner.scanClasspathModules();

        return makeMethodsUnmodifiable(getMethods(modules));
    }

    /**
     * Returns a map which contains, as the key, the name of a class. The value
     * consists of a list of MethodNode, one for each groovy default method found
     * which is applicable for this class.
     *
     * @param modules extension modules
     * @return
     */
    private Map<String, List<MethodNode>> getMethods(List<ExtensionModule> modules) {
        Set<Class> instanceExtClasses = new LinkedHashSet<>();
        Set<Class> staticExtClasses = new LinkedHashSet<>();
        for (ExtensionModule module : modules) {
            MetaInfExtensionModule extensionModule = (MetaInfExtensionModule) module;
            instanceExtClasses.addAll(extensionModule.getInstanceMethodsExtensionClasses());
            staticExtClasses.addAll(extensionModule.getStaticMethodsExtensionClasses());
        }
        Map<String, List<MethodNode>> methods = new HashMap<>();

        addAdditionalClassesToScan(instanceExtClasses, staticExtClasses);

        scan(methods, staticExtClasses, true);
        scan(methods, instanceExtClasses, false);

        return methods;
    }

    private Map<String, List<MethodNode>> makeMethodsUnmodifiable(Map<String, List<MethodNode>> methods) {
        methods.replaceAll((k, v) -> Collections.unmodifiableList(v));

        return Collections.unmodifiableMap(methods);
    }

    protected abstract void addAdditionalClassesToScan(Set<Class> instanceExtClasses, Set<Class> staticExtClasses);

    private void scan(Map<String, List<MethodNode>> accumulator, Iterable<Class> allClasses, boolean isStatic) {
        Predicate<MethodNode> methodFilter = getMethodFilter();
        Function<MethodNode, String> methodMapper = getMethodMapper();

        for (Class dgmLikeClass : allClasses) {
            ClassNode cn = makeWithoutCaching(dgmLikeClass, true);
            for (MethodNode methodNode : cn.getMethods()) {
                if (!(methodNode.isStatic() && methodNode.isPublic()) || methodNode.getParameters().length == 0) continue;
                if (methodFilter.test(methodNode)) continue;

                accumulate(accumulator, isStatic, methodNode, methodMapper);
            }
        }
    }

    protected abstract Predicate<MethodNode> getMethodFilter();
    protected abstract Function<MethodNode, String> getMethodMapper();

    private void accumulate(Map<String, List<MethodNode>> accumulator, boolean isStatic, MethodNode metaMethod,
                                   Function<MethodNode, String> mapperFunction) {

        Parameter[] types = metaMethod.getParameters();
        Parameter[] parameters = new Parameter[types.length - 1];
        System.arraycopy(types, 1, parameters, 0, parameters.length);
        ExtensionMethodNode node = new ExtensionMethodNode(
                metaMethod,
                metaMethod.getName(),
                metaMethod.getModifiers(),
                metaMethod.getReturnType(),
                parameters,
                ClassNode.EMPTY_ARRAY, null,
                isStatic);
        node.setGenericsTypes(metaMethod.getGenericsTypes());
        ClassNode declaringClass = types[0].getType();
        node.setDeclaringClass(declaringClass);

        String key = mapperFunction.apply(metaMethod);

        List<MethodNode> nodes = accumulator.computeIfAbsent(key, k -> new ArrayList<>());
        nodes.add(node);
    }
}
