package com.jfireel.expression.node.impl;

import com.jfireel.expression.node.CalculateNode;
import com.jfireel.expression.node.MethodNode;
import com.jfireel.expression.token.Token;
import com.jfireel.expression.token.TokenType;
import com.jfirer.baseutil.reflect.ReflectUtil;
import com.jfirer.baseutil.smc.SmcHelper;
import com.jfirer.baseutil.smc.compiler.CompileHelper;
import com.jfirer.baseutil.smc.model.ClassModel;
import com.jfirer.baseutil.smc.model.MethodModel;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class CompileObjectMethodNode implements MethodNode {
	public static interface Invoker {
		Object invoke(Object host, Object[] params);
	}

	private static final CompileHelper COMPILER = new CompileHelper();
	private static final AtomicInteger counter = new AtomicInteger(0);
	private final CalculateNode beanNode;
	private final String methodName;
	private volatile Invoker invoker;
	private volatile Class<?> beanType;
	protected final boolean recognizeEveryTime;
	private CalculateNode[] argsNodes;
	private Token type;

	public CompileObjectMethodNode(String literals, CalculateNode beanNode, boolean recognizeEveryTime) {
		methodName = literals;
		type = Token.METHOD;
		this.beanNode = beanNode;
		this.recognizeEveryTime = recognizeEveryTime;
	}

	@Override
	public Object calculate(Map<String, Object> variables) {
		Object value = beanNode.calculate(variables);
		if (value == null) {
			return value;
		}
		Object[] args = new Object[argsNodes.length];
		try {
			for (int i = 0; i < args.length; i++) {
				args[i] = argsNodes[i].calculate(variables);
			}
			Invoker invoke = getMethod(value, args);
			return invoke.invoke(value, args);
		} catch (Exception e) {
			ReflectUtil.throwException(e);
			return null;
		}
	}

	@Override
	public TokenType type() {
		return type;
	}

	private Invoker getMethod(Object value, Object[] args) {
		if (recognizeEveryTime) {
			Invoker invoker = this.invoker;
			if (invoker == null || beanType.isAssignableFrom(value.getClass()) == false) {
				synchronized (this) {
					if ((invoker = this.invoker) == null || beanType.isAssignableFrom(value.getClass()) == false) {
						invoker = this.invoker = buildInvoker(value, args);
						return invoker;
					}
				}
			}
			return invoker;
		} else {
			if (invoker == null) {
				synchronized (this) {
					if (invoker == null) {
						invoker = buildInvoker(value, args);
						return invoker;
					}
				}
			}
			return invoker;
		}
	}

	private Invoker buildInvoker(Object value, Object[] args) {
		nextmethod: for (Method each : value.getClass().getMethods()) {
			if (each.getName().equals(methodName) && each.getParameterTypes().length == args.length) {
				Class<?>[] parameterTypes = each.getParameterTypes();
				for (int i = 0; i < args.length; i++) {
					if (parameterTypes[i].isPrimitive()) {
						if (args[i] == null
								|| MethodNodeUtil.isWrapType(parameterTypes[i], args[i].getClass()) == false) {
							continue nextmethod;
						}
					} else {
						if (args[i] != null && parameterTypes[i].isAssignableFrom(args[i].getClass()) == false) {
							continue nextmethod;
						}
					}
				}
				beanType = value.getClass();
				return buildInvoker(args, each);
			}
		}
		ReflectUtil.throwException(new NullPointerException());
		return null;
	}

	private Invoker buildInvoker(Object[] args, Method method) {
		try {
			ClassModel classModel = new ClassModel("Invoke_" + method.getName() + "_" + counter.incrementAndGet(),
					Object.class, Invoker.class);
			MethodModel methodModel = new MethodModel(classModel);
			methodModel.setAccessLevel(MethodModel.AccessLevel.PUBLIC);
			methodModel.setMethodName("invoke");
			methodModel.setParamterTypes(Object.class, Object[].class);
			methodModel.setReturnType(Object.class);
			StringBuilder body = new StringBuilder(
					" return ((" + SmcHelper.getReferenceName(method.getDeclaringClass(), classModel) + ")$0)."
							+ method.getName() + "(");
			int length = body.length();
			Class<?>[] parameterTypes = method.getParameterTypes();
			for (int i = 0; i < parameterTypes.length; i++) {
				Class<?> parameterType = parameterTypes[i];
				if (parameterType == int.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).intValue(),");
				} else if (parameterType == Integer.class) {
					if (args[i].getClass() == Integer.class) {
						body.append("((java.lang.Integer)$").append(i).append("),");
					} else {
						body.append("((java.lang.Number)$1[").append(i).append("]).intValue(),");
					}
				} else if (parameterTypes[i] == short.class || parameterTypes[i] == short.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).shortValue(),");
				} else if (parameterTypes[i] == long.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).longValue(),");
				} else if (parameterType == Long.class) {
					if (args[i].getClass() == Long.class) {
						body.append("((java.lang.Long)$").append(i).append("),");
					} else {
						body.append("((java.lang.Number)$1[").append(i).append("]).longValue(),");
					}
				} else if (parameterTypes[i] == float.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).floatValue(),");
				} else if (parameterType == Float.class) {
					if (args[i].getClass() == Float.class) {
						body.append("((java.lang.Float)$").append(i).append("),");
					} else {
						body.append("((java.lang.Number)$1[").append(i).append("]).floatValue(),");
					}
				} else if (parameterTypes[i] == double.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).doubleValue(),");
				} else if (parameterType == Double.class) {
					body.append("((java.lang.Double)$").append(i).append("),");
				} else if (parameterTypes[i] == byte.class || parameterTypes[i] == Byte.class) {
					body.append("((java.lang.Number)$1[").append(i).append("]).byteValue(),");
				} else if (parameterTypes[i] == boolean.class) {
					body.append("((java.lang.Boolean)$1[").append(i).append("]).booleanValue(),");
				} else if (parameterType == Boolean.class) {
					body.append("((java.lang.Boolean)$").append(i).append("),");
				} else {
					body.append("((" + SmcHelper.getReferenceName(parameterTypes[i], classModel) + ")$1[").append(i)
							.append("]),");
				}
			}
			if (body.length() != length) {
				body.setLength(body.length() - 1);
			}
			body.append(");");
			methodModel.setBody(body.toString());
			classModel.putMethodModel(methodModel);
			Class<?> compile = COMPILER.compile(classModel);
			return (Invoker) compile.newInstance();
		} catch (Exception e) {
			ReflectUtil.throwException(e);
			return null;
		}
	}

	@Override
	public void setArgsNodes(CalculateNode[] argsNodes) {
		this.argsNodes = argsNodes;
		type = Token.METHOD_RESULT;
	}

	public CalculateNode[] getArgsNodes(){
		return this.argsNodes;
	}

	public String getMethodName(){
		return this.methodName;
	}

	@Override
	public String literals() {
		StringBuilder cache = new StringBuilder();
		cache.append(beanNode.literals()).append('.').append(methodName).append('(');
		if (argsNodes != null) {
			for (CalculateNode each : argsNodes) {
				cache.append(each.literals()).append(',');
			}
			if (cache.charAt(cache.length() - 1) == ',') {
				cache.setLength(cache.length() - 1);
			}
		}
		cache.append(')');
		return cache.toString();
	}

	@Override
	public String toString() {
		return literals();
	}
}
