package emu.grasscutter.scripts.serializer;

import com.esotericsoftware.reflectasm.ConstructorAccess;
import com.esotericsoftware.reflectasm.MethodAccess;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.scripts.ScriptUtils;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class LuaSerializer implements Serializer {

	private final static Map<Class<?>, MethodAccess> methodAccessCache = new ConcurrentHashMap<>();
	private final static Map<Class<?>, ConstructorAccess<?>> constructorCache = new ConcurrentHashMap<>();
	private final static Map<Class<?>, Map<String, FieldMeta>> fieldMetaCache = new ConcurrentHashMap<>();

	@Override
	public <T> List<T> toList(Class<T> type, Object obj) {
		return serializeList(type, (LuaTable) obj);
	}

	@Override
	public <T> T toObject(Class<T> type, Object obj) {
		return serialize(type, (LuaTable) obj);
	}

    @Override
    public <T> Map<String, T> toMap(Class<T> type, Object obj) {
        return serializeMap(type, (LuaTable) obj);
    }

    private <T> Map<String,T> serializeMap(Class<T> type, LuaTable table) {
        Map<String,T> map = new HashMap<>();

        if (table == null) {
            return map;
        }

        try {
            LuaValue[] keys = table.keys();
            for (LuaValue k : keys) {
                try {
                    LuaValue keyValue = table.get(k);

                    T object = null;

                    if (keyValue.istable()) {
                        object = serialize(type, keyValue.checktable());
                    } else if (keyValue.isint()) {
                        object = (T) (Integer) keyValue.toint();
                    } else if (keyValue.isnumber()) {
                        object = (T) (Float) keyValue.tofloat(); // terrible...
                    } else if (keyValue.isstring()) {
                        object = (T) keyValue.tojstring();
                    } else if (keyValue.isboolean()) {
                        object = (T) (Boolean) keyValue.toboolean();
                    } else {
                        object = (T) keyValue;
                    }

                    if (object != null) {
                        map.put(String.valueOf(k),object);
                    }
                } catch (Exception ex) {

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return map;
    }

    public <T> List<T> serializeList(Class<T> type, LuaTable table) {
		List<T> list = new ArrayList<>();

		if (table == null) {
			return list;
		}

		try {
			LuaValue[] keys = table.keys();
			for (LuaValue k : keys) {
				try {
					LuaValue keyValue = table.get(k);

					T object = null;

					if (keyValue.istable()) {
						object = serialize(type, keyValue.checktable());
				    } else if (keyValue.isint()) {
				    	object = (T) (Integer) keyValue.toint();
				    } else if (keyValue.isnumber()) {
				    	object = (T) (Float) keyValue.tofloat(); // terrible...
				    } else if (keyValue.isstring()) {
				    	object = (T) keyValue.tojstring();
                    } else if (keyValue.isboolean()) {
                        object = (T) (Boolean) keyValue.toboolean();
                    } else {
				    	object = (T) keyValue;
				    }

					if (object != null) {
						list.add(object);
					}
				} catch (Exception ex) {

				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		return list;
	}

	public <T> T serialize(Class<T> type, LuaTable table) {
		T object = null;

		if (type == List.class) {
			try {
				Class<T> listType = (Class<T>) type.getTypeParameters()[0].getClass();
				return (T) serializeList(listType, table);
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}

		try {
			if (!methodAccessCache.containsKey(type)) {
				cacheType(type);
			}
			var methodAccess = methodAccessCache.get(type);
			var fieldMetaMap = fieldMetaCache.get(type);

			object = (T) constructorCache.get(type).newInstance();

			if (table == null) {
				return object;
			}

			LuaValue[] keys = table.keys();
			for (LuaValue k : keys) {
				try {
					var keyName = k.checkjstring();
					if(!fieldMetaMap.containsKey(keyName)){
						continue;
					}
					var fieldMeta = fieldMetaMap.get(keyName);
					LuaValue keyValue = table.get(k);

					if (keyValue.istable()) {
						methodAccess.invoke(object, fieldMeta.index, serialize(fieldMeta.getType(), keyValue.checktable()));
				    } else if (fieldMeta.getType().equals(float.class)) {
						methodAccess.invoke(object, fieldMeta.index, keyValue.tofloat());
				    } else if (fieldMeta.getType().equals(int.class)) {
						methodAccess.invoke(object, fieldMeta.index, keyValue.toint());
				    } else if (fieldMeta.getType().equals(String.class)) {
						methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
                    } else if (fieldMeta.getType().equals(boolean.class)) {
                        methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean());
                    } else {
						methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
				    }
				} catch (Exception ex) {
					//ex.printStackTrace();
					continue;
				}
			}
		} catch (Exception e) {
			Grasscutter.getLogger().info(ScriptUtils.toMap(table).toString());
			e.printStackTrace();
		}

		return object;
	}

	public <T> Map<String, FieldMeta> cacheType(Class<T> type){
		if(fieldMetaCache.containsKey(type)) {
			return fieldMetaCache.get(type);
		}
		if(!constructorCache.containsKey(type)){
			constructorCache.putIfAbsent(type, ConstructorAccess.get(type));
		}
		var methodAccess = Optional.ofNullable(methodAccessCache.get(type)).orElse(MethodAccess.get(type));
		methodAccessCache.putIfAbsent(type, methodAccess);

		var fieldMetaMap = new HashMap<String, FieldMeta>();
		var methodNameSet = new HashSet<>(Arrays.stream(methodAccess.getMethodNames()).toList());

		Arrays.stream(type.getDeclaredFields())
				.filter(field -> methodNameSet.contains(getSetterName(field.getName())))
				.forEach(field -> {
					var setter = getSetterName(field.getName());
					var index = methodAccess.getIndex(setter);
					fieldMetaMap.put(field.getName(), new FieldMeta(field.getName(), setter, index, field.getType()));
				});

		Arrays.stream(type.getFields())
				.filter(field -> !fieldMetaMap.containsKey(field.getName()))
				.filter(field -> methodNameSet.contains(getSetterName(field.getName())))
				.forEach(field -> {
					var setter = getSetterName(field.getName());
					var index = methodAccess.getIndex(setter);
					fieldMetaMap.put(field.getName(), new FieldMeta(field.getName(), setter, index, field.getType()));
				});

		fieldMetaCache.put(type, fieldMetaMap);
		return fieldMetaMap;
	}

	public String getSetterName(String fieldName){
		if(fieldName == null || fieldName.length() == 0){
			return null;
		}
		if(fieldName.length() == 1){
			return "set" + fieldName.toUpperCase();
		}
		return "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
	}

	@Data
	@AllArgsConstructor
	@FieldDefaults(level = AccessLevel.PRIVATE)
	static class FieldMeta{
		String name;
		String setter;
		int index;
		Class<?> type;
	}
}
