package com.aionemu.gameserver.ai;

import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.commons.scripting.ScriptManager;
import com.aionemu.commons.scripting.classlistener.AggregatedClassListener;
import com.aionemu.commons.scripting.classlistener.OnClassLoadUnloadListener;
import com.aionemu.commons.scripting.classlistener.ScheduledTaskClassListener;
import com.aionemu.gameserver.GameServerError;
import com.aionemu.gameserver.configs.main.AIConfig;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.model.GameEngine;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.templates.npc.NpcTemplate;

/**
 * @author ATracer
 */
public class AIEngine implements GameEngine {

	private static final Logger log = LoggerFactory.getLogger(AIEngine.class);
	private final ScriptManager scriptManager = new ScriptManager();
	private final Map<String, Class<? extends AbstractAI<? extends Creature>>> aiHandlers = new HashMap<>();

	private AIEngine() {
	}

	@Override
	public void init() {
		AggregatedClassListener acl = new AggregatedClassListener();
		acl.addClassListener(new OnClassLoadUnloadListener());
		acl.addClassListener(new ScheduledTaskClassListener());
		acl.addClassListener(new AIHandlerClassListener());
		scriptManager.setGlobalClassListener(acl);
		scriptManager.load(AIConfig.HANDLER_DIRECTORY);
		validateScripts();
		log.info("Loaded " + aiHandlers.size() + " AI handlers.");
	}

	public void reload() {
		scriptManager.shutdown();
		aiHandlers.clear();
		init();
	}

	public void registerAI(Class<AbstractAI<? extends Creature>> aiClass) {
		AIName nameAnnotation = aiClass.getAnnotation(AIName.class);
		if (nameAnnotation != null) {
			Class<?> presentClass = aiHandlers.putIfAbsent(nameAnnotation.value(), aiClass);
			if (presentClass != null)
				throw new IllegalArgumentException("Duplicate AIs with name " + nameAnnotation.value() + " (" + aiClass + ", " + presentClass + ")");
		}
	}

	public <T extends Creature> AbstractAI<? extends Creature> newAI(String name, T owner) {
		AbstractAI<? extends Creature> aiInstance;
		if (name == null) {
			aiInstance = new DummyAI<>(owner);
		} else {
			Class<? extends AbstractAI<? extends Creature>> aiClass = aiHandlers.get(name);
			if (aiClass == null)
				throw new IllegalArgumentException("No AI found for name " + name);
			Constructor<? extends AbstractAI<? extends Creature>> constructor = findConstructor(aiClass, owner.getClass(), false);
			if (constructor == null)
				throw new IllegalArgumentException(aiClass + " cannot be instantiated with " + owner.getClass().getSimpleName() + " as the owner");
			try {
				aiInstance = constructor.newInstance(owner);
			} catch (Exception e) {
				throw new IllegalArgumentException("Could not instantiate AI for class " + aiClass + " (owner: " + owner + ")", e);
			}
		}
		if (AIConfig.ONCREATE_DEBUG)
			aiInstance.setLogging(true);
		return aiInstance;
	}

	@SuppressWarnings("unchecked")
	private <T> Constructor<T> findConstructor(Class<T> aiClass, Class<? extends Creature> searchParamType, boolean isSuperType) {
		for (Constructor<?> constructor : aiClass.getDeclaredConstructors()) {
			if (constructor.getParameterCount() == 1) {
				Class<?> constructorParamType = constructor.getParameterTypes()[0];
				if (isSuperType) {
					if (searchParamType.isAssignableFrom(constructorParamType))
						return (Constructor<T>) constructor;
				} else if (constructorParamType.isAssignableFrom(searchParamType)) {
					return (Constructor<T>) constructor;
				}
			}
		}
		return null;
	}

	private void validateScripts() {
		aiHandlers.values().forEach(aiClass -> {
			Class<? extends Creature> ownerType = findDefaultOwnerType(aiClass);
			if (ownerType == null)
				throw new GameServerError("Faulty AI handler: " + aiClass + " is missing generic owner type info");
			if (findConstructor(aiClass, ownerType, true) == null)
				throw new GameServerError(
					"Faulty AI handler: " + aiClass + " is missing constructor taking owner of type " + ownerType + " as the only argument");
		});
		Set<String> npcAINames = DataManager.NPC_DATA.getNpcData().stream().map(NpcTemplate::getAiName).filter(Objects::nonNull).collect(Collectors.toSet());
		npcAINames.removeAll(aiHandlers.keySet());
		if (!npcAINames.isEmpty())
			throw new GameServerError("No AIs could be found for the following npc_template AI names: " + String.join(", ", npcAINames));
	}

	@SuppressWarnings("unchecked")
	private Class<? extends Creature> findDefaultOwnerType(Class<? extends AbstractAI<? extends Creature>> aiClass) {
		Class<?> currentClass = aiClass;
		while (currentClass.getSuperclass() != AbstractAI.class) {
			Type genericSuperClass = currentClass.getGenericSuperclass();
			if (genericSuperClass instanceof ParameterizedType ownerTypeHolder) {
				if (ownerTypeHolder.getActualTypeArguments().length == 1) {
					Type type = ownerTypeHolder.getActualTypeArguments()[0];
					if (type instanceof TypeVariable<?>)
						type = ((TypeVariable<?>) type).getBounds()[0];
					if (type instanceof Class && Creature.class.isAssignableFrom((Class<?>) type))
						return (Class<? extends Creature>) type;
				}
			}
			currentClass = currentClass.getSuperclass();
		}
		return null;
	}

	public static AIEngine getInstance() {
		return SingletonHolder.instance;
	}

	private static class SingletonHolder {

		protected static final AIEngine instance = new AIEngine();
	}

	private static class DummyAI<T extends Creature> extends AITemplate<T> {

		private DummyAI(T owner) {
			super(owner);
		}
	}
}
