/*
 * Copyright 2017 ~ 2025 the original author or authors. <[email protected], [email protected]>
 *
 * Licensed 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 com.wl4g.devops.support.mybatis.loader;

import static com.wl4g.devops.tool.common.lang.Assert2.isTrue;
import static com.wl4g.devops.tool.common.lang.Assert2.notNull;
import static com.wl4g.devops.tool.common.lang.Assert2.state;
import static com.wl4g.devops.tool.common.log.SmartLoggerFactory.getLogger;
import static java.lang.Thread.sleep;
import static java.lang.String.format;
import static java.lang.System.*;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.replace;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

import com.wl4g.devops.tool.common.log.SmartLogger;

import static com.wl4g.devops.tool.common.reflect.ReflectionUtils2.*;

/**
 * Mybatis {@link SqlSessionFactory} developments hotspot mapper re-loader.
 * 
 * @author Wangl.sir <[email protected], [email protected]>
 * @version v1.0 2019年11月14日
 * @since
 */
public final class SqlSessionMapperHotspotLoader implements ApplicationRunner {
	final public static String TARGET_PART_PATH = "target" + File.separator + "classes";
	final public static String SRC_PART_PATH = "src" + File.separator + "main" + File.separator + "resources";

	final protected SmartLogger log = getLogger(getClass());

	/** Refresh configuration properties. */
	final protected HotspotLoadProperties config;
	/** Monitor objectives for {@link SqlSessionFactory} */
	final protected SqlSessionFactoryBean sessionFactory;

	/** Refresher of last timestamp. */
	final private AtomicLong lastRefreshTime = new AtomicLong(0L);
	/** Runner thread boss. */
	private Thread boss;

	private Configuration configuration;
	private Resource[] mapperLocations;

	public SqlSessionMapperHotspotLoader(SqlSessionFactoryBean sessionFactory, HotspotLoadProperties config) {
		notNull(sessionFactory, "SqlSessionFactory can't is null.");
		notNull(config, "MapperHotspotLoader properties config can't is null.");
		this.sessionFactory = sessionFactory;
		this.config = config;
		try {
			// Init configuration.
			init();
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Note: Start and run the refresh processing method (note that if web.xml
	 * configures spring and springmvc as two containers, it will be called
	 * twice, resulting in clear mybatis Sqlelements error.)
	 */
	@Override
	public void run(ApplicationArguments args) throws Exception {
		boss = new Thread(() -> {
			boolean stopped = false;
			while (!stopped && !boss.isInterrupted()) {
				try {
					sleep(config.getMonitorLoaderIntervalMs());
					if (isChanged()) {
						refresh(configuration);
						stopped = false;
					}
				} catch (Exception e) {
					if (config.isFastFail()) {
						stopped = true;
						log.error("", e);
					}
				}
			}
			log.warn("Stopped SqlSession mappers hotspot loader monitor!");
		});
		boss.start();

		log.info("Started SqlSession mappers hotspot loader of {}", sessionFactory.toString());
	}

	/**
	 * Initializing configuration、mapperLocations etc.
	 * 
	 * @throws Exception
	 */
	private synchronized void init() throws Exception {
		state(isNull(configuration) && isNull(mapperLocations),
				String.format("Already initialized mappers hotspot loader. configuration for: %s", configuration));
		// Obtain configuration.
		configuration = sessionFactory.getObject().getConfiguration();
		// Obtain mapperLocations.
		Field mapperLocaionsField = findField(SqlSessionFactoryBean.class, "mapperLocations", Resource[].class);
		makeAccessible(mapperLocaionsField);
		mapperLocations = (Resource[]) getField(mapperLocaionsField, sessionFactory);
		// Convert to origin resources.
		mapperLocations = getOriginResources(mapperLocations);

		notNull(configuration, "SqlSessionFactory configuration can't is null.");
		notNull(mapperLocations, "SqlSessionFactory mapperLocations can't is null.");
	}

	/**
	 * Refresh the contents of mybatis mapping files.
	 * 
	 * @param configuration
	 * @throws Exception
	 */
	private synchronized void refresh(Configuration configuration) throws Exception {
		// 清理Mybatis的所有映射文件缓存, 目前由于未找到清空被修改文件的缓存的key值, 暂时仅支持全部清理, 然后全部加载
		doCleanupOlderCacheConfig(configuration);

		long begin = currentTimeMillis();
		for (Resource rs : mapperLocations) {
			try {
				XMLMapperBuilder builder = new XMLMapperBuilder(rs.getInputStream(), configuration, rs.toString(),
						configuration.getSqlFragments()); // Reload.
				builder.parse();
				log.debug("Refreshed for: {}", rs);
			} catch (IOException e) {
				log.error(format("Failed to refresh mapper for: %s", rs), e);
			}
		}
		long now = currentTimeMillis();
		out.println(format("%s - Refreshed mappers: %s, cost: %sms", new Date(), mapperLocations.length, (now - begin)));

		// Update refresh time.
		lastRefreshTime.set(now);
	}

	/**
	 * Detect if at least one mapper file has been updated.
	 * 
	 * @return
	 * @throws IOException
	 */
	private boolean isChanged() throws IOException {
		if (lastRefreshTime.get() <= 0) { // Just initialized?
			lastRefreshTime.set(currentTimeMillis());
			return false;
		}
		for (Resource rs : mapperLocations) {
			if (nonNull(rs) && rs.getFile().lastModified() > lastRefreshTime.get()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Clear several importants caches in configuration
	 * 
	 * @param configuration
	 * @throws Exception
	 */
	private synchronized void doCleanupOlderCacheConfig(Configuration configuration) throws Exception {
		Class<?> classConfig = configuration.getClass();

		clearMap(classConfig, configuration, "mappedStatements", null);
		clearMap(classConfig, configuration, "caches", null);
		clearMap(classConfig, configuration, "resultMaps", null);
		clearMap(classConfig, configuration, "parameterMaps", null);
		clearMap(classConfig, configuration, "keyGenerators", null);
		clearMap(classConfig, configuration, "sqlFragments", null);

		clearSet(classConfig, configuration, "loadedResources", null);
	}

	/**
	 * Clear map cache in configuration.
	 * 
	 * @param classConfig
	 * @param configuration
	 * @param fieldName
	 * @param clearKey
	 * @throws Exception
	 */
	@SuppressWarnings("rawtypes")
	private synchronized void clearMap(Class<?> classConfig, Configuration configuration, String fieldName, Object clearKey)
			throws Exception {
		Field field = classConfig.getDeclaredField(fieldName);
		field.setAccessible(true);
		Map mapConfig = (Map) field.get(configuration);
		// (此用于实现只重新加载单个mapper文件的热部署, 但是目前由于未找到清空被修改文件的缓存的key值,
		// 暂无法实现单个mapper热部署)
		// mapConfig.remove(clearKey);
		mapConfig.clear();
	}

	/**
	 * Clear set cache in configuration.
	 * 
	 * @param classConfig
	 * @param configuration
	 * @param fieldName
	 * @param clearKey
	 * @throws Exception
	 */
	@SuppressWarnings("rawtypes")
	private synchronized void clearSet(Class<?> classConfig, Configuration configuration, String fieldName, Object clearKey)
			throws Exception {
		Field field = classConfig.getDeclaredField(fieldName);
		field.setAccessible(true);
		Set setConfig = (Set) field.get(configuration);
		// (此用于实现只重新加载单个mapper文件的热部署, 但是目前由于未找到清空被修改文件的缓存的key值,
		// 暂无法实现单个mapper热部署)
		// setConfig.remove(clearKey);
		setConfig.clear();
	}

	/**
	 * Because idea does not hot update the mapper.xml file in the target
	 * directory by default, it can only be converted to the source directory
	 * (original file)
	 * 
	 * @param mapperLocations
	 * @return
	 * @throws IOException
	 */
	private Resource[] getOriginResources(Resource[] mapperLocations) throws IOException {
		List<Resource> res = new ArrayList<>(mapperLocations.length);
		if (nonNull(mapperLocations)) {
			for (Resource r : mapperLocations) {
				String path = r.getFile().getAbsolutePath();
				path = replace(path, TARGET_PART_PATH, SRC_PART_PATH);
				res.add(new FileSystemResource(path));
			}
		}
		return res.toArray(new Resource[] {});
	}

	// /**
	// * 获取需要刷新的文件列表.(此用于实现只重新加载单个mapper文件的热部署, 但是目前由于未找到清空被修改文件的缓存的key值,
	// * 暂无法实现单个mapper热部署)
	// *
	// * @param beforeTime
	// * 上次刷新时间
	// * @return 刷新文件列表
	// * @throws IOException
	// */
	// private List<Resource> getRefreshResource(Long beforeTime) throws
	// IOException {
	// List<Resource> refreshResourcelist = new ArrayList<Resource>();
	// for (Resource resource : mapperLocations) {
	// if (resource != null && resource.getFile().lastModified() > beforeTime) {
	// refreshResourcelist.add(resource);
	// }
	// }
	// return refreshResourcelist;
	// }

	/**
	 * Mybatis mappers hotspot loader properties configuration.
	 * 
	 * @author Wangl.sir <[email protected], [email protected]>
	 * @version v1.0 2019年11月14日
	 * @since
	 */
	public static class HotspotLoadProperties implements Serializable {
		private static final long serialVersionUID = -2662416556401160389L;

		/** {@link SqlSessionFactory} watching intervalMs. */
		private long monitorLoaderIntervalMs = 1000L;

		/** Refresh failed processing policy. */
		private boolean fastFail = false;

		public long getMonitorLoaderIntervalMs() {
			return monitorLoaderIntervalMs;
		}

		public void setMonitorLoaderIntervalMs(long monitorIntervalMs) {
			isTrue(monitorIntervalMs >= 200, "Monitor intervalMs must >=200");
			this.monitorLoaderIntervalMs = monitorIntervalMs;
		}

		public boolean isFastFail() {
			return fastFail;
		}

		public void setFastFail(boolean fastFail) {
			this.fastFail = fastFail;
		}

	}

}