package com.brilliance.mda.support.cache.serializer.impl;

import com.brilliance.mda.runtime.mda.*;
import com.brilliance.mda.runtime.mda.driver.MdaEnv;
import com.brilliance.mda.runtime.mda.impl.AbstractModule;
import com.brilliance.mda.runtime.mda.impl.ModuleList;
import com.brilliance.mda.support.cache.serializer.ICacheSerializer;
import com.esotericsoftware.kryo.kryo5.Kryo;
import com.esotericsoftware.kryo.kryo5.io.Input;
import com.esotericsoftware.kryo.kryo5.io.Output;
import com.esotericsoftware.kryo.kryo5.objenesis.strategy.StdInstantiatorStrategy;
import com.esotericsoftware.kryo.kryo5.util.DefaultInstantiatorStrategy;
import com.esotericsoftware.kryo.kryo5.util.Pool;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;

public class KryoSerializer implements ICacheSerializer {

    private static final Logger logger = LoggerFactory.getLogger(KryoSerializer.class);

    private static final String NAME = "Kryo";

    private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.toString();

    private static final int bufferSize = 8192;

    private static volatile Pool<Output> outputPool = new Pool<Output>(true, false, 50) {
        @Override
        protected Output create() {
            return new Output(bufferSize, -1);
        }
    };

    private static volatile Pool<Kryo> kryoPool = new Pool<Kryo>(true, false, 50) {
        @Override
        protected Kryo create() {
            Kryo kryo = new Kryo();
            kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
            kryo.setReferences(true);
            kryo.setRegistrationRequired(false);
            return kryo;
        }
    };

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public <T> byte[] writeToByteArray(T obj) {
        Output output = null;
        Kryo kryo = null;
        try {
            output = outputPool.obtain();
            kryo = kryoPool.obtain();
            kryo.writeClassAndObject(output, obj);
            byte[] buff = output.toBytes();
            return buff;
        } catch (Exception e) {
            logger.error("Kryo序列化失败", e);
            throw new RuleExecuteException("kryo 序列化异常!", e);
        } finally {
            if (kryo != null) {
                kryoPool.free(kryo);
            }
            if (output != null) {
                outputPool.free(output);
            }
        }
    }

    static AtomicInteger count = new AtomicInteger(0);

    @Override
    @SuppressWarnings("unchecked")
    public <T> T readFromByteArray(byte[] bytes) {
        Input input = new Input(bytes);
        Kryo kryo = null;
        try {
            kryo = kryoPool.obtain();
            long time2 = System.currentTimeMillis();
            T t = (T) kryo.readClassAndObject(input);
            if (t instanceof IContext) {
                IContext context = (IContext) t;
                IModule root = context.getRoot();
                context.setEmitter(MdaEnv.getBean(IRuleEmitter.EMITTER_PRE + root.getClass().getSimpleName().toLowerCase(), IRuleEmitter.class));
                moduleSetting(root);
                logger.debug("bytes to module timing:{}ms", System.currentTimeMillis() - time2);
            }
            return t;
        } catch (Exception e) {
            logger.error("Kryo反序列化失败", e);
            int i = count.incrementAndGet();
            try {
                File file = new File(MdaEnv.getRootPath(), "kryo_ex_" + i + ".ky");
                Files.write(bytes, file);
            } catch (Exception ex) {
                logger.warn("", ex);
            }
            throw new RuleExecuteException("kryo反序列化异常！", e);
        } finally {
            if (kryo != null) {
                kryoPool.free(kryo);
            }
        }
    }

    private void moduleSetting(IModule module) {
        invokeSetArgumentMethod(module);
        Field[] fields = module.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if (Modifier.isStatic(field.getModifiers())) {
                continue;
            }
            Class<?> clazz = field.getType();
            try {
                Object o = field.get(module);
                if (null == o) continue;

                if (AbstractModule.class.isAssignableFrom(clazz)) {
                    moduleSetting((IModule) o);
                } else if (ModuleList.class.isAssignableFrom(clazz)) {
                    fillModuleList(module, field, (ModuleList<?>) o);
                }
            } catch (IllegalAccessException ignored) {

            }
        }
    }

    private void invokeSetArgumentMethod(IModule module) {
        try {
            Method method = module.getClass().getMethod("setArgument");
            method.invoke(module);
        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException ignored) {

        }
    }

    private void fillModuleList(IModule parentModule, Field field, ModuleList<?> moduleList) {
        try {
            Class<? extends ModuleList> moduleListClass = moduleList.getClass();
            Field parentField = moduleListClass.getDeclaredField("_parent");
            Field nameField = moduleListClass.getDeclaredField("_name");
            Field pathField = moduleListClass.getDeclaredField("_path");
            Field dataClassField = moduleListClass.getDeclaredField("dataClass");
            Field moduleTypeField = moduleListClass.getDeclaredField("moduleType");

            parentField.setAccessible(true);
            nameField.setAccessible(true);
            pathField.setAccessible(true);
            dataClassField.setAccessible(true);
            moduleTypeField.setAccessible(true);

            parentField.set(moduleList, parentModule);
            nameField.set(moduleList, field.getName());

            String prefix = parentModule.getPath();
            if (prefix == null || prefix.length() == 0 || prefix.equals(IBaseObject.separator)) {
                pathField.set(moduleList, IBaseObject.separator + field.getName());
            } else {
                pathField.set(moduleList, prefix + IBaseObject.separator + field.getName());
            }

            Type type = field.getGenericType();
            if (type instanceof ParameterizedType) {
                ParameterizedType t = (ParameterizedType) type;
                Type[] actualTypeArguments = t.getActualTypeArguments();
                if (null != actualTypeArguments && actualTypeArguments.length > 0) {
                    Class<?> aClass = (Class<?>) actualTypeArguments[0];
                    dataClassField.set(moduleList, aClass);
                    moduleTypeField.set(moduleList, aClass.getSimpleName().toLowerCase());
                }
            }

            for (IModule module : moduleList) {
                moduleSetting(module);
            }
        } catch (NoSuchFieldException | IllegalAccessException ignored) {

        }
    }
}
