package jalse.entities;

import static jalse.entities.Entities.isEntitySubtype;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import jalse.entities.functions.DefaultFunction;
import jalse.entities.functions.EntityFunction;
import jalse.entities.functions.EntityFunctionResolver;
import jalse.entities.functions.EntityMethodFunction;
import jalse.entities.functions.GetAttributeFunction;
import jalse.entities.functions.GetEntitiesFunction;
import jalse.entities.functions.GetEntityFunction;
import jalse.entities.functions.KillEntitiesFunction;
import jalse.entities.functions.KillEntityFunction;
import jalse.entities.functions.MarkAsTypeFunction;
import jalse.entities.functions.NewEntityFunction;
import jalse.entities.functions.ScheduleForActorFunction;
import jalse.entities.functions.SetAttributeFunction;
import jalse.entities.functions.StreamEntitiesFunction;
import jalse.entities.functions.UnmarkAsTypeFunction;
import jalse.entities.methods.EntityMethod;

/**
 * This is the default {@link EntityProxyFactory} implementation for JALSE. This proxy factory will
 * use {@link EntityFunctionResolver} to resolve an {@link EntityFunction} to translate the proxy
 * {@link Method} calls to {@link EntityMethod}s. Override {@link #addResolverFunctions()} to change
 * what {@link EntityMethodFunction}s are added to the resolver. <br>
 * <br>
 * NOTE: Proxies are cached per {@link Entity} using {@link EntityProxyCache}.
 *
 * @author Elliot Ford
 *
 * @see #uncacheProxyOfEntity(Entity, Class)
 * @see #uncacheProxiesOfEntity(Entity)
 * @see #uncacheAllProxies()
 *
 */
public class DefaultEntityProxyFactory implements EntityProxyFactory {

    /**
     * A {@link WeakHashMap} cache for {@link Entity} proxies.
     *
     * @author Elliot Ford
     *
     * @see Collections#synchronizedMap(Map)
     *
     */
    protected class EntityProxyCache {

	private final Map<Entity, Map<Class<?>, Object>> proxyMap;

	/**
	 * Creates a new entity proxy cache.
	 */
	private EntityProxyCache() {
	    proxyMap = Collections.synchronizedMap(new WeakHashMap<>());
	}

	/**
	 * Either retrieves a proxy from the cache or creates (and caches) a new entity proxy.
	 *
	 * @param e
	 *            Entity to cache type for.
	 * @param type
	 *            Entity type to proxy.
	 * @return Proxy entity of type.
	 *
	 * @see EntityProxyHandler
	 */
	@SuppressWarnings("unchecked")
	public <T extends Entity> T getOrNew(final Entity e, final Class<T> type) {
	    final Map<Class<?>, Object> proxies = proxyMap.computeIfAbsent(e, k -> new ConcurrentHashMap<>());
	    return (T) proxies.computeIfAbsent(type, k -> newEntityProxy(e, k));
	}

	/**
	 * Uncaches all proxies.
	 */
	public void invalidateAll() {
	    proxyMap.clear();
	}

	/**
	 * Uncaches all proxies for an entity.
	 *
	 * @param e
	 *            Entity key.
	 */
	public void invalidateEntity(final Entity e) {
	    proxyMap.remove(e);
	}

	/**
	 * Uncaches a the proxy of the specified type for the entity.
	 *
	 * @param e
	 *            Entity key.
	 * @param type
	 *            Entity type to remove.
	 */
	public void invalidateType(final Entity e, final Class<? extends Entity> type) {
	    final Map<Class<?>, Object> proxies = proxyMap.get(e);
	    if (proxies != null) {
		proxies.remove(type);
		proxyMap.computeIfPresent(e, (k, v) -> v.isEmpty() ? null : v);
	    }
	}

	private Object newEntityProxy(final Entity e, final Class<?> type) {
	    logger.fine(String.format("Creating proxy of type %s for entity %s", type, e.getID()));
	    return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { type },
		    new EntityProxyHandler(e));
	}
    }

    /**
     * The {@link InvocationHandler} for the entity proxies. The {@link Entity} is wrapped in a
     * {@link WeakReference}.
     *
     * @author Elliot Ford
     *
     */
    protected class EntityProxyHandler implements InvocationHandler {

	private final WeakReference<Entity> entityRef;

	/**
	 * Creates a new entity proxy handler.
	 *
	 * @param entity
	 *            Entity to proxy events for.
	 */
	private EntityProxyHandler(final Entity entity) {
	    entityRef = new WeakReference<Entity>(entity);
	}

	/**
	 * Gets the entity this proxy is for.
	 *
	 * @return Host entity.
	 *
	 * @see WeakReference
	 */
	public Entity getEntity() {
	    final Entity entity = entityRef.get();
	    if (entity == null) {
		throw new IllegalStateException("Entity reference lost");
	    }
	    return entity;
	}

	@Override
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
	    /*
	     * Get Entity reference.
	     */
	    final Entity entity = getEntity();

	    /*
	     * Not an Entity subclass.
	     */
	    final Class<?> declaringClazz = method.getDeclaringClass();
	    if (!isEntitySubtype(declaringClazz)) {
		return method.invoke(entity, args);
	    }

	    /*
	     * Invoke resolved method.
	     */
	    @SuppressWarnings("unchecked")
	    final EntityFunction resolvedType = resolver.resolveType((Class<? extends Entity>) declaringClazz);
	    final EntityMethod entityMethod = resolvedType.apply(method);
	    return entityMethod.invoke(proxy, entity, args);
	}
    }

    private static final Logger logger = Logger.getLogger(DefaultEntityProxyFactory.class.getName());

    /**
     * Resolver for entity types.
     */
    protected final EntityFunctionResolver resolver;

    /**
     * Proxy cache for entities.
     */
    protected final EntityProxyCache cache;

    /**
     * Creates a new DefaultEntityProxyFactory.
     *
     * @see #addResolverFunctions()
     */
    public DefaultEntityProxyFactory() {
	resolver = new EntityFunctionResolver();
	cache = new EntityProxyCache();
	addResolverFunctions();
    }

    /**
     * Adds the {@link EntityMethodFunction}s to the resolver.
     *
     * @see DefaultFunction
     * @see GetAttributeFunction
     * @see SetAttributeFunction
     * @see NewEntityFunction
     * @see GetEntityFunction
     * @see StreamEntitiesFunction
     * @see GetEntitiesFunction
     * @see KillEntityFunction
     * @see KillEntitiesFunction
     * @see MarkAsTypeFunction
     * @see UnmarkAsTypeFunction
     * @see ScheduleForActorFunction
     */
    protected void addResolverFunctions() {
	resolver.addMethodFunction(new DefaultFunction());
	resolver.addMethodFunction(new GetAttributeFunction());
	resolver.addMethodFunction(new SetAttributeFunction());
	resolver.addMethodFunction(new NewEntityFunction());
	resolver.addMethodFunction(new GetEntityFunction());
	resolver.addMethodFunction(new StreamEntitiesFunction());
	resolver.addMethodFunction(new GetEntitiesFunction());
	resolver.addMethodFunction(new KillEntityFunction());
	resolver.addMethodFunction(new KillEntitiesFunction());
	resolver.addMethodFunction(new MarkAsTypeFunction());
	resolver.addMethodFunction(new UnmarkAsTypeFunction());
	resolver.addMethodFunction(new ScheduleForActorFunction());
    }

    @Override
    public boolean isProxyEntity(final Entity e) {
	return Proxy.isProxyClass(e.getClass()) && Proxy.getInvocationHandler(e) instanceof EntityProxyHandler;
    }

    @Override
    public <T extends Entity> T proxyOfEntity(final Entity e, final Class<T> type) {
	validateType(type);
	return cache.getOrNew(e, type);
    }

    /**
     * Uncaches all proxies.
     */
    public void uncacheAllProxies() {
	resolver.unresolveAllTypes();
	cache.invalidateAll();
    }

    /**
     * Uncaches all proxies for an entity.
     *
     * @param e
     *            Entity to uncache for.
     */
    public void uncacheProxiesOfEntity(final Entity e) {
	cache.invalidateEntity(e);
    }

    /**
     * Uncaches the specific type proxy for an entity.
     *
     * @param e
     *            Entity to uncache for.
     * @param type
     *            Proxy type.
     */
    public void uncacheProxyOfEntity(final Entity e, final Class<? extends Entity> type) {
	cache.invalidateType(e, type);
    }

    @Override
    public void validateType(final Class<? extends Entity> type) {
	resolver.resolveType(type);
    }
}