package com.friday.dynamic.core;


import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.io.Resources;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.any;


/**
 * 动态加载外部jar包的自定义类加载器
 */
@Slf4j
public class ModuleClassLoader extends URLClassLoader {

    private final static String XML_SUFFIX = ".xml";
    private final static String MAPPER_SUFFIX = "mapper/";
    public final static String[] DEFAULT_EXCLUDED_PACKAGES = new String[]{"java.", "javax.", "sun.", "oracle."};
    private final Set<String> excludedPackages;
    private Set<String> overridePackages;

    //属于本类加载器加载的jar包
    private JarFile jarFile;
    //依赖包
    private List<URL> dependencies;
    private Map<String, byte[]> classBytesMap = new HashMap<>();
    private Map<String, Class<?>> classesMap = new HashMap<>();
    private Map<String, byte[]> xmlBytesMap = new HashMap<>();
    private Map<String, Class> dubboServiceClassMap = new HashMap<>();

    public ModuleClassLoader(ClassLoader parent, URL url, List<URL> dependencies) {
        super(new URL[]{url}, parent);
        this.excludedPackages = Sets.newHashSet(Arrays.asList(DEFAULT_EXCLUDED_PACKAGES.clone()));
        this.overridePackages = Sets.newHashSet();
        this.dependencies = dependencies;
        try {
            jarFile = new JarFile(url.getPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //在当前的应用类加载器里，使用URLClassLoader类的方法addURL，不过该方法是protected的，需要反射处理一下
    public void currentClassloaderAddUrl(){
        URLClassLoader urlClassLoader = (URLClassLoader) this.getParent();
        try {
            Method addUrl = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            addUrl.setAccessible(true);
            dependencies.forEach(d -> {
                try {
                    addUrl.invoke(urlClassLoader, d);
                } catch (IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void loadDubboService(List<BizDubboConfig> configs) throws ClassNotFoundException {
        for(BizDubboConfig config : configs){
            Class ref = Resources.classForName(config.getInterfaceFullyQualifiedName());
            dubboServiceClassMap.put(config.getInterfaceFullyQualifiedName(), ref);
        }
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> result = null;
        synchronized (ModuleClassLoader.class) {
            if (isEligibleForOverriding(name)) {
                log.info("Load class for overriding: {}", name);
                result = loadClassForOverriding(name);
            }
            if (Objects.nonNull(result)) {
                // 链接类
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
        }
        // 使用默认类加载方式
        return super.loadClass(name, resolve);
    }

    private Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
        // 查找已加载的类
        Class<?> result = findLoadedClass(name);
        if (Objects.isNull(result)) {
            // 加载类
            result = findClass(name);
        }
        return result;
    }

    private boolean isEligibleForOverriding(final String name) {
        checkNotNull(name, "name is null");
        return !isExcluded(name) && any(overridePackages, name::startsWith);
    }

    protected boolean isExcluded(String className) {
        checkNotNull(className, "className is null");
        for (String packageName : this.excludedPackages) {
            if (className.startsWith(packageName)) {
                return true;
            }
        }
        return false;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] buf = classBytesMap.get(name);
        if (buf == null) {
            return super.findClass(name);
        }
        if(classesMap.containsKey(name)) {
            return classesMap.get(name);
        }
        return loadClass(name,buf);
    }

    //使用反射强行将类装载的归属给当前类加载器的父类加载器也就是AppClassLoader，如果报ClassNotFoundException则递归装载
    private Class<?> loadClass(String name, byte[] bytes) throws ClassNotFoundException {

        Object[] args = new Object[]{name, bytes, 0, bytes.length};
        try {
            ClassLoader parent = this.getParent();
            Method classLoaderDefineClass = (Method) AccessController.doPrivileged((PrivilegedExceptionAction) () -> ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, Integer.TYPE, Integer.TYPE));
            if(!classLoaderDefineClass.isAccessible()) {
                classLoaderDefineClass.setAccessible(true);
            }
            return (Class<?>)classLoaderDefineClass.invoke(parent,args);
        } catch (Exception e) {
            if(e instanceof InvocationTargetException) {
                String message = ((InvocationTargetException) e).getTargetException().getCause().toString();
                if(message.startsWith("java.lang.ClassNotFoundException")) {
                    String notClassName = message.split(":")[1];
                    if(StringUtils.isEmpty(notClassName)) {
                        throw new ClassNotFoundException(message);
                    }
                    notClassName = notClassName.trim();
                    byte[] bytes1 = classBytesMap.get(notClassName);
                    if(bytes1 == null) {
                        throw new ClassNotFoundException(message);
                    }
                    //递归装载未找到的类
                    Class<?> notClass = loadClass(notClassName, bytes1);
                    if(notClass == null) {
                        throw new ClassNotFoundException(message);
                    }
                    classesMap.put(notClassName,notClass);
                    return loadClass(name,bytes);
                }
            } else {
                log.error("loadClassError", e);
            }
        }
        return null;
    }

    public Map<String,byte[]> getXmlBytesMap() {
        return xmlBytesMap;
    }

    public Map<String, Class> getDubboServiceClassMap() {
        return this.dubboServiceClassMap;
    }


    /**
     * 方法描述 初始化类加载器，保存字节码
     */
    public void loadMapper() {
        Enumeration<JarEntry> en = jarFile.entries();
        InputStream input = null;
        String mapperName = "";
        try {
            while (en.hasMoreElements()) {
                JarEntry je = en.nextElement();
                String name = je.getName();
                if(name.endsWith(XML_SUFFIX) && name.startsWith(MAPPER_SUFFIX)) {
                    mapperName = name;
                    input = jarFile.getInputStream(je);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int bufferSize = 4096;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    byte[] xmlBytes = baos.toByteArray();
                    xmlBytesMap.put(name, xmlBytes);
                }
            }
        } catch (IOException e) {
            log.error("load mapper {} error", mapperName, e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}