/*
 * 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.enodeframework.common.extensions;

import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

/**
 * @author anruence@gmail.com
 */
public class ClassPathScanHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(ClassPathScanHandler.class);
    /**
     * class file extension name.
     */
    private static final String CLASS_EXTENSION_NAME = ".class";
    /**
     * 是否排除内部类 true->是 false->否.
     */
    private boolean excludeInner = true;
    /**
     * 过滤规则适用情况 true—>搜索符合规则的 false->排除符合规则的.
     */
    private boolean checkInOrEx = true;
    /**
     * 过滤规则列表 如果是null或者空，即全部符合不过滤.
     */
    private List<String> classFilters = null;
    /**
     * the reflections.
     */
    private Reflections reflections = null;

    /**
     * the classes and the scanPackages to be scanned.
     *
     * @param scanPackages scanPackages.
     */
    public ClassPathScanHandler(String... scanPackages) {
        this.reflections = new Reflections(new ConfigurationBuilder().forPackages(scanPackages));
    }

    /**
     * excludeInner: 是否排除内部类 true 是 false 否
     * checkInOrEx: 过滤规则适用情况 true: 搜索符合规则的 false: 排除符合规则的
     * classFilters: 自定义过滤规则，如果是null或者空，即全部符合不过滤
     *
     * @param excludeInner whether exclude the inner class.
     * @param checkInOrEx  whether exclude the rule checking.
     * @param classFilters the customized the classes to be filtered.
     */
    public ClassPathScanHandler(Boolean excludeInner, Boolean checkInOrEx, List<String> classFilters) {
        this.excludeInner = excludeInner;
        this.checkInOrEx = checkInOrEx;
        this.classFilters = classFilters;
    }

    /**
     * get all the classes with annotation.
     *
     * @param annotation     the specific annotation.
     * @param honorInherited honorInherited
     * @return the set of the classes.
     */
    public Set<Class<?>> getAllClassesWithAnnotation(Class<? extends Annotation> annotation, boolean honorInherited) {
        return reflections.getTypesAnnotatedWith(annotation, honorInherited);
    }

    /**
     * get all the sub classes with the specific parent class.
     *
     * @param parent the parent class.
     *               sub class's type.
     * @return the set of the sub classes found.
     */
    public <T> Set<Class<? extends T>> getAllSubClassesByParent(Class<T> parent) {
        return reflections.getSubTypesOf(parent);
    }

    /**
     * scan the package.
     *
     * @param basePackage the basic class package's string.
     * @param recursive   whether to search recursive.
     * @return Set of the found classes.
     */
    public Set<Class<?>> getPackageAllClasses(String basePackage, boolean recursive) {
        if (basePackage == null) {
            return new HashSet<>();
        }
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        String packageName = basePackage;
        if (packageName.endsWith(".")) {
            packageName = packageName.substring(0, packageName.lastIndexOf('.'));
        }
        String package2Path = packageName.replace('.', '/');
        try {
            Enumeration<URL> dirs =
                Thread.currentThread().getContextClassLoader().getResources(package2Path);
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
                    doScanPackageClassesByFile(classes, packageName, filePath, recursive);
                } else if ("jar".equals(protocol)) {
                    doScanPackageClassesByJar(packageName, url, recursive, classes);
                }
            }
        } catch (IOException e) {
            LOGGER.error("ignore this IOException", e);
        }
        TreeSet<Class<?>> sortedClasses = new TreeSet<>(new ClassNameComparator());
        sortedClasses.addAll(classes);
        return sortedClasses;
    }

    /**
     * 以jar的方式扫描包下的所有Class文件<br>.
     *
     * @param basePackage eg：michael.utils.
     * @param url         the url.
     * @param recursive   whether to search recursive.
     * @param classes     set of the found classes.
     */
    private void doScanPackageClassesByJar(
        String basePackage, URL url, final boolean recursive, Set<Class<?>> classes) {
        String package2Path = basePackage.replace('.', '/');
        JarFile jar;
        try {
            jar = ((JarURLConnection) url.openConnection()).getJarFile();
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (!name.startsWith(package2Path) || entry.isDirectory()) {
                    continue;
                }
                // 判断是否递归搜索子包
                if (!recursive && name.lastIndexOf('/') != package2Path.length()) {
                    continue;
                }
                // 判断是否过滤 inner class
                if (this.excludeInner && name.indexOf('$') != -1) {
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("exclude inner class with name: {}", name);
                    }
                    continue;
                }
                String classSimpleName = name.substring(name.lastIndexOf('/') + 1);
                // 判定是否符合过滤条件
                if (this.filterClassName(classSimpleName)) {
                    String className = name.replace('/', '.');
                    className = className.substring(0, className.length() - 6);
                    try {
                        classes.add(
                            Thread.currentThread().getContextClassLoader().loadClass(className));
                    } catch (ClassNotFoundException e) {
                        LOGGER.error("Class.forName error:URL is {}", url.getPath());
                    }
                }
            }
        } catch (IOException e) {
            LOGGER.error("IOException error:URL is {}", url.getPath());
        } catch (Throwable e) {
            LOGGER.error("ScanPackageClassesByJar error:URL is {}", url.getPath());
        }
    }

    /**
     * 以文件的方式扫描包下的所有Class文件.
     *
     * @param packageName the package name for scanning.
     * @param packagePath the package path for scanning.
     * @param recursive   whether to search recursive.
     * @param classes     set of the found classes.
     */
    private void doScanPackageClassesByFile(
        Set<Class<?>> classes, String packageName, String packagePath, final boolean recursive) {
        File dir = new File(packagePath);
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        File[] files = dir.listFiles(pathname -> filterClassFileByCustomization(pathname, recursive));
        if (null == files) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                doScanPackageClassesByFile(
                    classes, packageName + "." + file.getName(), file.getAbsolutePath(), recursive);
            } else {
                String className = file.getName().substring(0, file.getName().length() - CLASS_EXTENSION_NAME.length());
                try {
                    classes.add(
                        Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    LOGGER.error("IOException error:");
                }
            }
        }
    }

    /**
     * filter the class file from the customized rules.
     *
     * @param file      the class file to be filtered.
     * @param recursive whether search recursive.
     * @return true: match,  false: not match.
     */
    private boolean filterClassFileByCustomization(@Nonnull File file, boolean recursive) {
        if (file.isDirectory()) {
            return recursive;
        }
        String filename = file.getName();
        if (excludeInner && filename.indexOf('$') != -1) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("exclude inner class with name:{}", filename);
            }
            return false;
        }
        return filterClassName(filename);
    }

    /**
     * 根据过滤规则判断类名.
     *
     * @param className the class name.
     * @return whether to be filtered.
     */
    private boolean filterClassName(String className) {
        if (!className.endsWith(CLASS_EXTENSION_NAME)) {
            return false;
        }
        if (null == this.classFilters || this.classFilters.isEmpty()) {
            return true;
        }
        String tmpName = className.substring(0, className.length() - 6);
        boolean flag = false;
        for (String str : classFilters) {
            flag = matchInnerClassname(tmpName, str);
            if (flag) {
                break;
            }
        }
        return (checkInOrEx && flag) || (!checkInOrEx && !flag);
    }

    /**
     * check the className whether match the inner class's rule.
     *
     * @param className    the inner class name.
     * @param filterString the filter string.
     * @return true or false.
     */
    private boolean matchInnerClassname(String className, String filterString) {
        String reg = "^" + filterString.replace("*", ".*") + "$";
        Pattern p = Pattern.compile(reg);
        return p.matcher(className).find();
    }
}
