package com.hotent.base.groovy;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.hotent.base.constants.DataSourceConsts;
import com.hotent.base.datasource.DatabaseContext;
import com.hotent.base.datasource.DatabaseSwitchResult;
import com.hotent.base.feign.BpmRuntimeFeignService;
import com.hotent.base.model.ExtendClassCheckEvent;
import com.hotent.base.util.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

import com.hotent.base.exception.BaseException;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import groovy.lang.Script;

import javax.sql.DataSource;

/**
 * groovy脚本引擎
 * <pre>
 *
 * bindingInterface:表示系统中继承了这些接口的类可以在脚本引擎中进行使用。
 *
 * 上面的配置，说明：
 * 1.实现了IScript接口的类;
 * 2.实现了IUserScript接口的类;
 * 3.实现了Manager接口的类。
 * 可以在脚本中使用。
 *
 * </pre>
 *
 * @company 广州宏天软件股份有限公司
 * @author heyifan
 * @email heyf@jee-soft.cn
 * @date 2018年4月11日
 */
public class GroovyScriptEngine implements BeanPostProcessor {
	private Log logger = LogFactory.getLog(getClass());

	private static ConcurrentHashMap<String, Class<Script>> zlassMaps = new ConcurrentHashMap<String, Class<Script>>();
	private GroovyBinding binding = new GroovyBinding();
	private final String CommonScript="scriptImpl";
	/** 绑定接口 */
	private List<String> bindingInterface;

	/**
	 * @return the bindingInterface
	 */
	public List<String> getBindingInterface() {
		return bindingInterface;
	}

	/**
	 * @param bindingInterface
	 *            the bindingInterface to set
	 */
	public void setBindingInterface(List<String> bindingInterface) {
		this.bindingInterface = bindingInterface;
	}

	/**
	 * 执行groovy代码无返回。
	 *
	 * @param script
	 */
	public void execute(String script) {
		executeObject(script, null);
	}

	/**
	 * 执行groovy代码无返回。
	 *
	 * @param script
	 *            脚本
	 * @param vars
	 *            变量
	 */
	public void execute(String script, Map<String, Object> vars) {
		executeObject(script, vars);
	}

	/**
	 * 执行groovy代码返回布尔型。
	 *
	 * @param script
	 * @return
	 * @throws Exception
	 */
	public boolean executeBoolean(String script, Map<String, Object> vars) {
		return (Boolean) executeObject(script, vars);
	}

	/**
	 * 执行脚本返回字符串数据。
	 *
	 * @param script
	 * @return 字符串数据
	 */
	public String executeString(String script, Map<String, Object> vars) {
		return BeanUtils.ObjectToString(executeObject(script, vars));
	}

	/**
	 * 执行脚本返回整形数据。
	 *
	 * @param script
	 * @return 整形数据
	 */
	public int executeInt(String script, Map<String, Object> vars) {
		return (Integer) executeObject(script, vars);
	}

	/**
	 * 执行脚本返回浮点型数据。
	 *
	 * @param script
	 * @return 浮点型数据
	 */
	public float executeFloat(String script, Map<String, Object> vars) {
		return (Float) executeObject(script, vars);
	}

	/**
	 * 执行脚本返回对象数据。
	 *
	 * @param script
	 * @return 对象数据
	 */
	@SuppressWarnings("unchecked")
	public Object executeObject(String script, Map<String, Object> vars) {
		DatabaseContext databaseContext = AppUtil.getBean(DatabaseContext.class);
			checkScriptIsUseExtendClass(script);
			//判断是否需要切换数据源
			if (isCommonScript(script)) {
				ObjectNode scriptNode = AppUtil.getBean(BpmRuntimeFeignService.class).getByScript(script);
				if (BeanUtils.isNotEmpty(scriptNode)&& StringUtil.isNotEmpty(JsonUtil.getString(scriptNode, "dsName"))) {
					return databaseContext.switchDS(JsonUtil.getString(scriptNode, "dsName"),()-> executeScript(script,vars), e->{
						throw new BaseException(e.getMessage());
					});
				}
			}
			return executeScript(script, vars);
	}

	/**
	 * 执行脚本返回对象数据。
	 *
	 * @param script
	 * @return 对象数据
	 */
	@SuppressWarnings("unchecked")
	public Object executeScript(String script, Map<String, Object> vars) {
		try {
			script = script.replace("&apos;", "'").replace("&quot;", "\"")
					.replace("&gt;", ">").replace("&lt;", "<")
					.replace("&nuot;", "\n").replace("&amp;", "&");

			String key = fingerKey(script);
			Class<Script> scriptShell = zlassMaps.get(key);
			if (scriptShell == null) {
				synchronized (key.intern()) {
					// Double Check
					scriptShell = zlassMaps.get(key);
					if (scriptShell == null) {
						try(GroovyClassLoader classLoader = new GroovyClassLoader()){
							scriptShell = classLoader.parseClass(script);
							zlassMaps.put(key, scriptShell);
						}
					}
				}
			}
			this.setParameters(vars);
			Script scriptObj = InvokerHelper.createScript(scriptShell, binding);
			Object rtn =  scriptObj.run();
			return rtn;
		}catch (Exception e) {
			e.printStackTrace();
			throw new BaseException("执行脚本" + script + "报错,请检查脚本");
		}
	}

	/**
	 * 判断脚本中是否使用了动态扩展的class。用了则发布事件，做版本校验
	 * @param script
	 */
	private void checkScriptIsUseExtendClass(String script){
		if (BeanUtils.isEmpty(script) || !script.contains(".")) {
			return;
		}
		Map<String, String> cacheClassMd5Map = ExtendClassLoader.getClassMd5Map();
		if (BeanUtils.isNotEmpty(cacheClassMd5Map)) {
			Set<String> usedExtendClass = new HashSet<>();
			for (Entry<String, String> next : cacheClassMd5Map.entrySet()) {
				String[] split = script.split(next.getKey() + "\\.");
				if (script.startsWith(next.getKey() + ".")) {
					usedExtendClass.add(next.getKey());
				} else if (split.length > 1) {
					char charAt = split[0].charAt(split[0].length() - 1);
					if (!Character.isDigit(charAt) && !Character.isLetter(charAt)) {
						usedExtendClass.add(next.getKey());
					}
				}
			}
			if (BeanUtils.isNotEmpty(usedExtendClass)) {
				ExtendClassCheckEvent event = new ExtendClassCheckEvent(usedExtendClass);
				AppUtil.publishEvent(event);
			}
		}
	}

	// 为脚本代码生成md5指纹
    private String fingerKey(String scriptText) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(scriptText.getBytes(StandardCharsets.UTF_8));

            final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
            StringBuilder ret = new StringBuilder(bytes.length * 2);
			for (byte aByte : bytes) {
				ret.append(HEX_DIGITS[(aByte >> 4) & 0x0f]);
				ret.append(HEX_DIGITS[aByte & 0x0f]);
			}
            return ret.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

	/**
	 * 判断是否是常用脚本
	 * @param script
	 * @return
	 */
	private boolean isCommonScript(String script){
		return script.contains(CommonScript);
	}

    private void setParameters(Map<String, Object> vars) {
		if (vars == null) return;
		Set<Map.Entry<String, Object>> set = vars.entrySet();
		for (Entry<String, Object> entry : set) {
			binding.setVariable(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * 设置执行参数。
	 *
	 * @param shell
	 * @param vars
	 */
	private void setParameters(GroovyShell shell, Map<String, Object> vars) {
		if (vars == null) return;
		Set<Map.Entry<String, Object>> set = vars.entrySet();
		for (Entry<String, Object> entry : set) {
			shell.setVariable(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * 给groovy脚本引擎注入对象的引用。
	 */
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName)
			throws BeansException {
		List<Class<?>> list=new ArrayList<Class<?>>();
		for (String str : bindingInterface) {
			try{
				Class<?> clazz = Class.forName(str);
				list.add(clazz);
			}
			catch (ClassNotFoundException e) {
				e.printStackTrace();
				logger.debug(e.getException());
			}
		}
		for(Class<?> clazz:list){
			boolean isImpl = BeanUtils.isInherit(bean.getClass(), clazz);
			if(!isImpl) {
				continue;
			}
			binding.setProperty(beanName, bean);
		}
		return bean;
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		return bean;
	}

}
