package net.ranides.assira.reflection;

import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.generic.Tuple;
import net.ranides.assira.reflection.impl.CGAdapter;

import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
 * This is factory for interface adapters.
 *
 * You can use it to convert any object into any "adapter interface" as long as methods declared
 * inside your interface have roughly compatible signatures.
 *
 * Compatible signature means that name, return value and argument types should be convertible
 * or declared as another "adapter interface".
 *
 * This factory creates proxy class using one of classloaders defined in application.
 * You can use factory for default classloader, or you can used named factories.
 *
 * Factory can be used in two ways: you can invoke cast method or you can request for a
 * function, which casts objects from one type to another.
 */
public class AdapterFactory implements Serializable {

    private static final AdapterFactory SYSTEM = new AdapterFactory(IClassLoader.getSystem());

    private static final Map<String, AdapterFactory> FACTORIES = new ConcurrentHashMap<>();

    private final IClassLoader loader;

    private final Map<Tuple, IClass<?>> generated = new OpenMap<>();

    static {

        FACTORIES.put(SYSTEM.loader().name(), SYSTEM);

    }

    private AdapterFactory(IClassLoader loader) {
        this.loader = loader;
    }

    /**
     * Returns AdapterFactory which defines proxy classes using default system classloader
     *
     * @return singleton factory
     */
    public static AdapterFactory getSystem() {
        return SYSTEM;
    }

    /**
     * Returns AdapterFactory which defines proxy classes using provided classloader.
     * It inherits name from classloader.
     *
     * @param loader classloader
     * @return reused factory
     */
    public static AdapterFactory getInstance(IClassLoader loader) {
        return FACTORIES.computeIfAbsent(loader.name(), k -> new AdapterFactory(loader));
    }

    /**
     * Returns AdapterFactory which defines proxy classes using classloader with provided name.
     * Please note, that classloader must be already created.
     *
     * @param name name of classloader
     * @return reused factory
     */
    public static AdapterFactory getInstance(String name) {
        return getInstance(IClassLoader.getInstance(name));
    }

    /**
     * Returns classloader used by this factory.
     *
     * @return classloader
     */
    public IClassLoader loader() {
        return loader;
    }

    /**
     * Returns convert function, which casts argument of type S into type T.
     *
     * @param target target type
     * @param source source type
     * @param <T> type
     * @param <S> type
     * @return function
     */
    public <T,S> Function<S,T> adapter(IClass<T> target, IClass<S> source) {
        Tuple key = new Tuple(target, source);
        IClass<?> ac = generated.computeIfAbsent(key, $0 -> CGAdapter.generate(this, target, source));
        return a -> target.cast(ac.construct(a));
    }

    /**
     * Casts object into defined type.
     * It is pseudo-cast operation. In fact, proxy class will be created in the background.
     *
     * @param type target type
     * @param that source object
     * @param <T> type
     * @return object of type T which delegates work to source object
     */
    public <T> T cast(Class<T> type, Object that) {
        return cast(IClass.typeinfo(type), that);
    }

    /**
     * Casts object into defined type.
     * It is pseudo-cast operation. In fact, proxy class will be created in the background.
     *
     * @param type target type
     * @param that source object
     * @param <T> T
     * @param <S> S
     * @return object of type T which delegates work to source object
     */
    public <T,S> T cast(IClass<T> type, S that) {
        return adapter(type, IClass.typefor(that)).apply(that);
    }

    protected Object writeReplace() {
        return SerializationUtils.proxy(this, loader);
    }

}
