package com.alibaba.qlexpress4.runtime;

import com.alibaba.qlexpress4.exception.ErrorReporter;
import com.alibaba.qlexpress4.exception.QLErrorCodes;
import com.alibaba.qlexpress4.exception.QLRuntimeException;
import com.alibaba.qlexpress4.member.FieldHandler;
import com.alibaba.qlexpress4.member.MethodHandler;
import com.alibaba.qlexpress4.runtime.data.DataValue;
import com.alibaba.qlexpress4.runtime.data.FieldValue;
import com.alibaba.qlexpress4.runtime.data.MapItemValue;
import com.alibaba.qlexpress4.runtime.function.ExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.FilterExtensionFunction;
import com.alibaba.qlexpress4.runtime.function.MapExtensionFunction;
import com.alibaba.qlexpress4.security.QLSecurityStrategy;
import com.alibaba.qlexpress4.security.StrategyIsolation;
import com.alibaba.qlexpress4.utils.BasicUtil;

import java.io.Serializable;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * java reflect util with cache
 * Author: DQinYuan
 */
public class ReflectLoader {
    
    private final QLSecurityStrategy securityStrategy;
    
    private final boolean allowPrivateAccess;
    
    private final Map<List<Class<?>>, Constructor<?>> constructorCache = new ConcurrentHashMap<>();
    
    private final Map<List<?>, FieldReflectCache> fieldCache = new ConcurrentHashMap<>();
    
    private final Map<MethodCacheKey, IMethod> staticMethodCache = new ConcurrentHashMap<>();
    
    private final Map<MethodCacheKey, IMethod> memberMethodCache = new ConcurrentHashMap<>();
    
    /**
     * default extension functions
     */
    private final List<ExtensionFunction> extensionFunctions =
        new CopyOnWriteArrayList<>(Arrays.asList(FilterExtensionFunction.INSTANCE, MapExtensionFunction.INSTANCE));
    
    public ReflectLoader(QLSecurityStrategy securityStrategy, boolean allowPrivateAccess) {
        this.securityStrategy = securityStrategy;
        this.allowPrivateAccess = allowPrivateAccess;
    }
    
    public void addExtendFunction(ExtensionFunction extensionFunction) {
        extensionFunctions.add(extensionFunction);
    }
    
    public Constructor<?> loadConstructor(Class<?> cls, Class<?>[] paramTypes) {
        if (securityStrategy instanceof StrategyIsolation) {
            return null;
        }
        
        List<Class<?>> cacheKey = new ArrayList<>(paramTypes.length + 1);
        cacheKey.add(cls);
        cacheKey.addAll(Arrays.asList(paramTypes));
        
        Constructor<?> cachedConstructor = constructorCache.get(cacheKey);
        if (cachedConstructor != null) {
            return cachedConstructor;
        }
        
        Constructor<?> constructor = securityFilter(MemberResolver.resolveConstructor(cls, paramTypes));
        if (constructor == null) {
            return null;
        }
        constructorCache.put(cacheKey, constructor);
        return constructor;
    }
    
    public Value loadField(Object bean, String fieldName, boolean skipSecurity, ErrorReporter errorReporter) {
        if (bean.getClass().isArray() && BasicUtil.LENGTH.equals(fieldName)) {
            return new DataValue(((Object[])bean).length);
        }
        else if (bean instanceof List && BasicUtil.LENGTH.equals(fieldName)) {
            return new DataValue(((List<?>)bean).size());
        }
        else if (bean instanceof Map) {
            return new MapItemValue((Map<?, ?>)bean, fieldName);
        }
        else if (!skipSecurity && securityStrategy instanceof StrategyIsolation) {
            return null;
        }
        else if (bean instanceof MetaClass) {
            MetaClass metaClass = (MetaClass)bean;
            if (BasicUtil.CLASS.equals(fieldName)) {
                return new DataValue(metaClass.getClz());
            }
            return loadJavaField(metaClass.getClz(), null, fieldName, skipSecurity, errorReporter);
        }
        else {
            return loadJavaField(bean.getClass(), bean, fieldName, skipSecurity, errorReporter);
        }
    }
    
    public IMethod loadMethod(Object bean, String methodName, Class<?>[] argTypes) {
        boolean isStaticMethod = bean instanceof MetaClass;
        Class<?> clz = isStaticMethod ? ((MetaClass)bean).getClz() : bean.getClass();
        // only support member extension method
        if (!isStaticMethod) {
            IMethod extendFunction = loadExtendFunction(clz, methodName, argTypes);
            if (extendFunction != null) {
                return extendFunction;
            }
        }
        
        if (securityStrategy instanceof StrategyIsolation) {
            return null;
        }
        
        MethodCacheKey cacheKey = new MethodCacheKey(clz, methodName, argTypes);
        Map<MethodCacheKey, IMethod> methodCache = isStaticMethod ? staticMethodCache : memberMethodCache;
        IMethod cachedMethod = methodCache.get(cacheKey);
        if (cachedMethod != null) {
            return cachedMethod;
        }
        
        IMethod method = securityFilterIMethod(
            MemberResolver.resolveMethod(clz, methodName, argTypes, isStaticMethod, allowPrivateAccess));
        if (method == null) {
            return null;
        }
        
        methodCache.put(cacheKey, method);
        return method;
    }
    
    private IMethod securityFilterIMethod(IMethod iMethod) {
        if (iMethod instanceof JvmIMethod) {
            Method filterResult = securityFilter(((JvmIMethod)iMethod).getMethod());
            return filterResult == null ? null : iMethod;
        }
        else {
            return iMethod;
        }
    }
    
    private IMethod loadExtendFunction(Class<?> clz, String methodName, Class<?>[] argTypes) {
        List<ExtensionFunction> assignableExtensionFunctions = extensionFunctions.stream()
            .filter(extensionFunction -> extensionFunction.getDeclaringClass().isAssignableFrom(clz)
                && methodName.equals(extensionFunction.getName()))
            .collect(Collectors.toList());
        if (assignableExtensionFunctions.isEmpty()) {
            return null;
        }
        return MemberResolver.resolveMethod(assignableExtensionFunctions, argTypes);
    }
    
    private Value loadJavaField(Class<?> cls, Object bean, String fieldName, boolean skipSecurity,
        ErrorReporter errorReporter) {
        FieldReflectCache fieldReflectCache = loadFieldReflectCache(cls, fieldName, skipSecurity);
        if (fieldReflectCache == null) {
            return null;
        }
        Supplier<Object> getterOp = fieldReflectCache.getterSupplier.apply(errorReporter, bean);
        if (fieldReflectCache.setterSupplier == null) {
            return new DataValue(getterOp.get());
        }
        Consumer<Object> setterOp = fieldReflectCache.setterSupplier.apply(errorReporter, bean);
        return new FieldValue(getterOp, setterOp, fieldReflectCache.defType);
    }
    
    private FieldReflectCache loadFieldReflectCache(Class<?> cls, String fieldName, boolean skipSecurity) {
        List<Serializable> cacheKey = Arrays.asList(cls, fieldName);
        FieldReflectCache cachedField = fieldCache.get(cacheKey);
        if (cachedField != null) {
            return cachedField;
        }
        
        FieldReflectCache fieldReflect = loadJavaFieldInner(cls, fieldName, skipSecurity);
        if (fieldReflect != null) {
            fieldCache.put(cacheKey, fieldReflect);
        }
        return fieldReflect;
    }
    
    private FieldReflectCache loadJavaFieldInner(Class<?> cls, String fieldName, boolean skipSecurity) {
        String preHandledName = FieldHandler.Preferred.preHandleAlias(cls, fieldName);
        Method getMethod = skipSecurity ? MethodHandler.getGetter(cls, preHandledName)
            : securityFilter(MethodHandler.getGetter(cls, preHandledName));
        Field field = skipSecurity ? FieldHandler.Preferred.gatherFieldRecursive(cls, preHandledName)
            : securityFilter(FieldHandler.Preferred.gatherFieldRecursive(cls, preHandledName));
        BiFunction<ErrorReporter, Object, Supplier<Object>> getterSupplier = fieldGetter(getMethod, field);
        if (getterSupplier == null) {
            return null;
        }
        Method setMethod = securityFilter(MethodHandler.getSetter(cls, preHandledName));
        BiFunction<ErrorReporter, Object, Consumer<Object>> setterSupplier = fieldSetter(setMethod, field);
        return new FieldReflectCache(getterSupplier, setterSupplier, fieldDefCls(setMethod, field));
    }
    
    private <T extends Member> T securityFilter(T member) {
        return member == null ? null : (securityStrategy.check(member) ? member : null);
    }
    
    private Class<?> fieldDefCls(Method setMethod, Field field) {
        return setMethod != null ? setMethod.getParameterTypes()[0] : field != null ? field.getType() : Object.class;
    }
    
    private BiFunction<ErrorReporter, Object, Consumer<Object>> fieldSetter(Method setMethod, Field field) {
        if (setMethod != null) {
            if (BasicUtil.isPublic(setMethod)) {
                return setMethodAccessible(setMethod);
            }
            if (allowPrivateAccess) {
                return setMethodUnAccessible(setMethod);
            }
        }
        if (field != null) {
            if (BasicUtil.isPublic(field)) {
                return setFieldAccessible(field);
            }
            if (allowPrivateAccess) {
                return setFieldUnAccessible(field);
            }
        }
        return null;
    }
    
    private BiFunction<ErrorReporter, Object, Supplier<Object>> fieldGetter(Method getMethod, Field field) {
        if (getMethod != null) {
            if (BasicUtil.isPublic(getMethod)) {
                return getMethodAccessible(getMethod);
            }
            if (allowPrivateAccess) {
                return getMethodUnAccessible(getMethod);
            }
        }
        if (field != null) {
            if (BasicUtil.isPublic(field)) {
                return getFieldAccessible(field);
            }
            if (allowPrivateAccess) {
                return getFieldUnAccessible(field);
            }
        }
        return null;
    }
    
    private BiFunction<ErrorReporter, Object, Consumer<Object>> setMethodAccessible(Method setMethod) {
        return (errorReporter, bean) -> newValue -> {
            try {
                setMethod.invoke(bean, newValue);
            }
            catch (Exception e) {
                throw unwrapMethodInvokeEx(errorReporter, setMethod.getName(), e);
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Consumer<Object>> setMethodUnAccessible(Method setMethod) {
        return (errorReporter, bean) -> newValue -> {
            try {
                setMethod.setAccessible(true);
                setMethod.invoke(bean, newValue);
            }
            catch (Exception e) {
                throw unwrapMethodInvokeEx(errorReporter, setMethod.getName(), e);
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Consumer<Object>> setFieldAccessible(Field field) {
        return (errorReporter, bean) -> newValue -> {
            try {
                field.set(bean, newValue);
            }
            catch (Exception e) {
                throw errorReporter.report(e,
                    QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.name(),
                    String.format(QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName()));
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Consumer<Object>> setFieldUnAccessible(Field field) {
        return (errorReporter, bean) -> newValue -> {
            try {
                field.setAccessible(true);
                field.set(bean, newValue);
            }
            catch (Exception e) {
                throw errorReporter.report(e,
                    QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.name(),
                    String.format(QLErrorCodes.SET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName()));
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Supplier<Object>> getMethodAccessible(Method getMethod) {
        return (errorReporter, bean) -> () -> {
            try {
                return getMethod.invoke(bean);
            }
            catch (Exception e) {
                throw unwrapMethodInvokeEx(errorReporter, getMethod.getName(), e);
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Supplier<Object>> getMethodUnAccessible(Method getMethod) {
        return (errorReporter, bean) -> () -> {
            try {
                getMethod.setAccessible(true);
                return getMethod.invoke(bean);
            }
            catch (Exception e) {
                throw unwrapMethodInvokeEx(errorReporter, getMethod.getName(), e);
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Supplier<Object>> getFieldAccessible(Field field) {
        return (errorReporter, bean) -> () -> {
            try {
                return field.get(bean);
            }
            catch (Exception e) {
                throw errorReporter.report(e,
                    QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.name(),
                    String.format(QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName()));
            }
        };
    }
    
    private BiFunction<ErrorReporter, Object, Supplier<Object>> getFieldUnAccessible(Field field) {
        return (errorReporter, bean) -> () -> {
            try {
                field.setAccessible(true);
                return field.get(bean);
            }
            catch (Exception e) {
                throw errorReporter.report(e,
                    QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.name(),
                    String.format(QLErrorCodes.GET_FIELD_UNKNOWN_ERROR.getErrorMsg(), field.getName()));
            }
        };
    }
    
    public static QLRuntimeException unwrapMethodInvokeEx(ErrorReporter errorReporter, String methodName,
        Exception ex) {
        if (ex instanceof IllegalArgumentException) {
            return errorReporter.reportFormat(QLErrorCodes.INVOKE_METHOD_WITH_WRONG_ARGUMENTS.name(),
                String.format(QLErrorCodes.INVOKE_METHOD_WITH_WRONG_ARGUMENTS.getErrorMsg(), methodName));
        }
        else if (ex instanceof InvocationTargetException) {
            return errorReporter.report(((InvocationTargetException)ex).getTargetException(),
                QLErrorCodes.INVOKE_METHOD_INNER_ERROR.name(),
                String.format(QLErrorCodes.INVOKE_METHOD_INNER_ERROR.getErrorMsg(), methodName));
        }
        else {
            return errorReporter.report(ex,
                QLErrorCodes.INVOKE_METHOD_UNKNOWN_ERROR.name(),
                String.format(QLErrorCodes.INVOKE_METHOD_UNKNOWN_ERROR.getErrorMsg(), methodName));
        }
    }
    
    private static class FieldReflectCache {
        private final BiFunction<ErrorReporter, Object, Supplier<Object>> getterSupplier;
        
        private final BiFunction<ErrorReporter, Object, Consumer<Object>> setterSupplier;
        
        private final Class<?> defType;
        
        private FieldReflectCache(BiFunction<ErrorReporter, Object, Supplier<Object>> getterSupplier,
            BiFunction<ErrorReporter, Object, Consumer<Object>> setterSupplier, Class<?> defType) {
            this.getterSupplier = getterSupplier;
            this.setterSupplier = setterSupplier;
            this.defType = defType;
        }
    }
    
    private static class ExtensionMapKey {
        private final Class<?> cls;
        
        private final String methodName;
        
        public ExtensionMapKey(Class<?> cls, String methodName) {
            this.cls = cls;
            this.methodName = methodName;
        }
        
        public Class<?> getCls() {
            return cls;
        }
        
        public String getMethodName() {
            return methodName;
        }
        
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            ExtensionMapKey that = (ExtensionMapKey)o;
            return Objects.equals(cls, that.cls) && Objects.equals(methodName, that.methodName);
        }
        
        @Override
        public int hashCode() {
            return Objects.hash(cls, methodName);
        }
    }
    
    private static class MethodCacheKey {
        private final Class<?> cls;
        
        private final String methodName;
        
        private final Class<?>[] argTypes;
        
        public MethodCacheKey(Class<?> cls, String methodName, Class<?>[] argTypes) {
            this.cls = cls;
            this.methodName = methodName;
            this.argTypes = argTypes;
        }
        
        public Class<?> getCls() {
            return cls;
        }
        
        public String getMethodName() {
            return methodName;
        }
        
        public Class<?>[] getArgTypes() {
            return argTypes;
        }
        
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            MethodCacheKey that = (MethodCacheKey)o;
            return cls.equals(that.cls) && methodName.equals(that.methodName) && Arrays.equals(argTypes, that.argTypes);
        }
        
        @Override
        public int hashCode() {
            int result = Objects.hash(cls, methodName);
            result = 31 * result + Arrays.hashCode(argTypes);
            return result;
        }
    }
}
