package com.chushang.common.mybatis.config;

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;

import static java.lang.Thread.sleep;

/**
 * <p>Mybatis 映射文件热加载（发生变动后自动重新加载）.</p>
 * <p>方便开发时使用，不用每次修改xml文件后都要去重启应用.</p>
 */
@Slf4j
public class MybatisPlusMapperRefresh implements Runnable {
	/**
	 * 记录jar包存在的mapper
	 */
	private static final Map<String, List<Resource>> jarMapper = new HashMap<>();
	private final SqlSessionFactory sqlSessionFactory;
	private final Resource[] mapperLocations;
	private volatile Long beforeTime = 0L;
	private Configuration configuration;
	/**
	 * xml文件目录
	 */
	private Set<String> fileSet;
	/**
	 * 延迟加载时间
	 */
	private int delaySeconds = 10;
	/**
	 * 刷新间隔时间
	 */
	private int sleepSeconds = 20;
	private boolean refreshMapper;

	public MybatisPlusMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory, int delaySeconds,
									int sleepSeconds, boolean refreshMapper) {
		this.mapperLocations = mapperLocations.clone();
		this.sqlSessionFactory = sqlSessionFactory;
		this.delaySeconds = delaySeconds;
		this.sleepSeconds = sleepSeconds;
		this.configuration = sqlSessionFactory.getConfiguration();
		this.refreshMapper = refreshMapper;
		this.run();
	}

	public MybatisPlusMapperRefresh(Resource[] mapperLocations, SqlSessionFactory sqlSessionFactory) {
		this.mapperLocations = mapperLocations.clone();
		this.sqlSessionFactory = sqlSessionFactory;
		this.configuration = sqlSessionFactory.getConfiguration();
		this.run();
	}

	@Override
	public void run() {
		/*
		 */
		if (refreshMapper) {
			beforeTime = SystemClock.now();
			final MybatisPlusMapperRefresh runnable = this;
			new Thread(() -> {
				if (fileSet == null) {
					fileSet = new HashSet<>();
					if (mapperLocations != null) {
						for (Resource mapperLocation : mapperLocations) {
							try {
								if (ResourceUtils.isJarURL(mapperLocation.getURL())) {
									String key = new UrlResource(
											ResourceUtils.extractJarFileURL(mapperLocation.getURL())).getFile()
													.getPath();
									fileSet.add(key);
									if (jarMapper.get(key) != null) {
										jarMapper.get(key).add(mapperLocation);
									} else {
										List<Resource> resourcesList = new ArrayList<>();
										resourcesList.add(mapperLocation);
										jarMapper.put(key, resourcesList);
									}
								} else {
									fileSet.add(mapperLocation.getFile().getPath());
								}
							} catch (IOException ioException) {
								ioException.printStackTrace();
							}
						}
					}
				}
				try {
					sleep(delaySeconds * 1000L);
				} catch (InterruptedException interruptedException) {
					interruptedException.printStackTrace();
				}
				do {
					try {
						for (String filePath : fileSet) {
							File file = new File(filePath);
							if (file.isFile() && file.lastModified() > beforeTime) {
								// 记录上次重新加载时间防止重复加载已经重载的文件
								beforeTime = file.lastModified();
								List<Resource> removeList = jarMapper.get(filePath);
								if (removeList != null && !removeList.isEmpty()) {
									for (Resource resource : removeList) {
										runnable.refresh(resource);
									}
								} else {
									runnable.refresh(new FileSystemResource(file));
								}
							}
						}
					} catch (Exception exception) {
						exception.printStackTrace();
					}
					try {
						sleep(sleepSeconds * 1000L);
					} catch (InterruptedException e) {
						throw new RuntimeException(e);
					}
				} while (true);
			}, "mybatis-plus MapperRefresh").start();
		}
	}

	/**
	 * 刷新mapper
	 *
	 * @throws Exception
	 */
	@SuppressWarnings("rawtypes")
	private void refresh(Resource resource)
			throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
		this.configuration = sqlSessionFactory.getConfiguration();
		boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
		try {
			Field loadedResourcesField = isSupper
					? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
					: configuration.getClass().getDeclaredField("loadedResources");
			loadedResourcesField.setAccessible(true);
			Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
			XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
					new XMLMapperEntityResolver());
			XNode context = xPathParser.evalNode("/mapper");
			String namespace = context.getStringAttribute("namespace");
			Field field = MapperRegistry.class.getDeclaredField("knownMappers");
			field.setAccessible(true);
			Map mapConfig = (Map) field.get(configuration.getMapperRegistry());

			mapConfig.remove(Resources.classForName(namespace));
			loadedResourcesSet.remove(resource.toString());
			configuration.getCacheNames().remove(namespace);

			cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
			cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
			cleanKeyGenerators(context.evalNodes("insert|update|select|delete"), namespace);
			cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);
			XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
					sqlSessionFactory.getConfiguration(), resource.toString(),
					sqlSessionFactory.getConfiguration().getSqlFragments());
			xmlMapperBuilder.parse();
			log.debug("refresh: '" + resource + "', success!");
		} catch (IOException e) {
			log.error("Refresh IOException :" + e.getMessage());
		} finally {
			ErrorContext.instance().reset();
		}
	}

	/**
	 * 清理parameterMap
	 *
	 * @param list
	 * @param namespace
	 */
	private void cleanParameterMap(List<XNode> list, String namespace) {
		for (XNode parameterMapNode : list) {
			String id = parameterMapNode.getStringAttribute("id");
			configuration.getParameterMaps().remove(String.format("%s.%s", namespace, id));
		}
	}

	/**
	 * 清理resultMap
	 *
	 * @param list
	 * @param namespace
	 */
	private void cleanResultMap(List<XNode> list, String namespace) {
		for (XNode resultMapNode : list) {
			String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
			configuration.getResultMapNames().remove(id);
			configuration.getResultMapNames().remove(namespace + "." + id);
			clearResultMap(resultMapNode, namespace);
		}
	}

	private void clearResultMap(XNode xNode, String namespace) {
		for (XNode resultChild : xNode.getChildren()) {
			if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
					|| "case".equals(resultChild.getName())) {
				if (resultChild.getStringAttribute("select") == null) {
					configuration.getResultMapNames()
							.remove(resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
					configuration.getResultMapNames().remove(namespace + "."
							+ resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
					if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
						clearResultMap(resultChild, namespace);
					}
				}
			}
		}
	}

	/**
	 * 清理selectKey
	 *
	 * @param list
	 * @param namespace
	 */
	private void cleanKeyGenerators(List<XNode> list, String namespace) {
		for (XNode context : list) {
			String id = context.getStringAttribute("id");
			configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
			configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);

			Collection<MappedStatement> mappedStatements = configuration.getMappedStatements();
			List<MappedStatement> objects = new ArrayList<>();
			for (Object object : mappedStatements) {
				if (object instanceof MappedStatement) {
					MappedStatement mappedStatement = (MappedStatement) object;
					if (mappedStatement.getId().equals(namespace + "." + id)) {
						objects.add(mappedStatement);
					}
				}
			}
			mappedStatements.removeAll(objects);
		}
	}

	/**
	 * 清理sql节点缓存
	 *
	 * @param list
	 * @param namespace
	 */
	private void cleanSqlElement(List<XNode> list, String namespace) {
		for (XNode context : list) {
			String id = context.getStringAttribute("id");
			configuration.getSqlFragments().remove(id);
			configuration.getSqlFragments().remove(namespace + "." + id);
		}
	}
}


