package org.jboss.as.naming;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Hashtable;
import java.util.concurrent.atomic.AtomicInteger;

import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameClassPair;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.spi.ObjectFactory;

import org.jboss.invocation.proxy.ProxyConfiguration;
import org.jboss.invocation.proxy.ProxyFactory;
import org.jboss.modules.Module;
import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoadException;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * An ObjectFactory that binds an arbitrary InitialContext into JNDI.
 */
public class ExternalContextObjectFactory implements ObjectFactory {

    private static final AtomicInteger PROXY_ID = new AtomicInteger();

    public static final String CACHE_CONTEXT = "cache-context";
    public static final String INITIAL_CONTEXT_CLASS = "initial-context-class";
    public static final String INITIAL_CONTEXT_MODULE = "initial-context-module";

    /**
     * If this property is set to {@code true} in the {@code Context} environment, objects will be looked up
     * by calling its {@link javax.naming.Context#lookup(String)} instead of {@link javax.naming.Context#lookup(javax.naming.Name)}.
     */
    private static final String LOOKUP_BY_STRING = "org.jboss.as.naming.lookup.by.string";


    private volatile Context cachedObject;

    @Override
    public Object getObjectInstance(final Object obj, final Name name, final Context nameCtx, final Hashtable<?, ?> environment) throws Exception {
        String cacheString = (String) environment.get(CACHE_CONTEXT);
        boolean cache = cacheString != null && cacheString.toLowerCase().equals("true");
        if (cache) {
            if (cachedObject == null) {
                synchronized (this) {
                    if (cachedObject == null) {
                        cachedObject = createContext(environment, true);
                    }
                }
            }
            return cachedObject;
        } else {
            return createContext(environment, false);
        }
    }

    private Context createContext(final Hashtable<?, ?> environment, boolean useProxy) throws NamingException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ModuleLoadException {
        String initialContextClassName = (String) environment.get(INITIAL_CONTEXT_CLASS);
        String initialContextModule = (String) environment.get(INITIAL_CONTEXT_MODULE);
        final boolean useStringLokup = useStringLookup(environment);

        final Hashtable<?, ?> newEnvironment = new Hashtable<>(environment);
        newEnvironment.remove(CACHE_CONTEXT);
        newEnvironment.remove(INITIAL_CONTEXT_CLASS);
        newEnvironment.remove(INITIAL_CONTEXT_MODULE);
        newEnvironment.remove(LOOKUP_BY_STRING);

        ClassLoader loader;
        if (! WildFlySecurityManager.isChecking()) {
            loader = getClass().getClassLoader();
        } else {
            loader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
                @Override
                public ClassLoader run() {
                    return getClass().getClassLoader();
                }
            });
        }
        Class initialContextClass = null;
        final Context loadedContext;
        if (initialContextModule == null) {
            initialContextClass = Class.forName(initialContextClassName);
            Constructor ctor = initialContextClass.getConstructor(Hashtable.class);
            loadedContext = (Context) ctor.newInstance(newEnvironment);
        } else {
            Module module = Module.getBootModuleLoader().loadModule(ModuleIdentifier.fromString(initialContextModule));
            loader = module.getClassLoader();
            final ClassLoader currentClassLoader = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged();
            try {
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(loader);
                initialContextClass = Class.forName(initialContextClassName, true, loader);
                Constructor ctor = initialContextClass.getConstructor(Hashtable.class);
                loadedContext = (Context) ctor.newInstance(newEnvironment);
            } finally {
                WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(currentClassLoader);
            }
        }

        final Context context;
        if (useStringLokup) {
            context = new LookupByStringContext(loadedContext);
        } else {
            context = loadedContext;
        }

        if (!useProxy) {
            return context;
        }
        ProxyConfiguration config = new ProxyConfiguration();
        config.setClassLoader(loader);
        config.setSuperClass(initialContextClass);
        config.setProxyName(initialContextClassName + "$$$$Proxy" + PROXY_ID.incrementAndGet());
        ProxyFactory<?> factory = new ProxyFactory<Object>(config);
        return (Context) factory.newInstance(new CachedContext(context));
    }

    /**
     * A proxy implementation of Context that simply intercepts the
     * close() method and ignores it since the underlying Context
     * object is being maintained in memory.
     */
    static class CachedContext implements InvocationHandler {
        Context externalCtx;

        CachedContext(Context externalCtx) {
            this.externalCtx = externalCtx;
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object value = null;
            if (method.getName().equals("close")) {
                // We just ignore the close method
            } else {
                try {
                    value = method.invoke(externalCtx, args);
                } catch (InvocationTargetException e) {
                    throw e.getTargetException();
                }
            }
            return value;
        }
    }

    /**
     * @return {@code true} if the environment contains a {@code LOOKUP_BY_STRING} property with a value corresponding to a {@code true} boolean, or {@code false} in any other case.
     * @param environment
     */
    private static boolean useStringLookup(Hashtable<?, ?> environment) {
        Object val = environment.get(LOOKUP_BY_STRING);
        if (val instanceof String) {
            return Boolean.valueOf((String) val);
        }
        return false;
    }

    /**
     * A wrapper around a {@code Context} that delegates {@link javax.naming.Name}-based lookup to
     * their corresponding {@code String}-based method.
     */
    private static class LookupByStringContext implements Context {
        private final Context context;

        public LookupByStringContext(Context context) {
            this.context = context;
        }

        @Override
        public Object lookup(Name name) throws NamingException {
            return context.lookup(name.toString());
        }

        @Override
        public Object lookup(String name) throws NamingException {
            return context.lookup(name);
        }

        @Override
        public void bind(Name name, Object obj) throws NamingException {
            context.bind(name, obj);

        }

        @Override
        public void bind(String name, Object obj) throws NamingException {
            context.bind(name, obj);
        }

        @Override
        public void rebind(Name name, Object obj) throws NamingException {
            context.rebind(name, obj);
        }

        @Override
        public void rebind(String name, Object obj) throws NamingException {
            context.rebind(name, obj);
        }

        @Override
        public void unbind(Name name) throws NamingException {
            context.unbind(name);
        }

        @Override
        public void unbind(String name) throws NamingException {
            context.unbind(name);
        }

        @Override
        public void rename(Name oldName, Name newName) throws NamingException {
            context.rename(oldName, newName);
        }

        @Override
        public void rename(String oldName, String newName) throws NamingException {
            context.rename(oldName, newName);
        }

        @Override
        public NamingEnumeration<NameClassPair> list(Name name) throws NamingException {
            return context.list(name);
        }

        @Override
        public NamingEnumeration<NameClassPair> list(String name) throws NamingException {
            return context.list(name);
        }

        @Override
        public NamingEnumeration<Binding> listBindings(Name name) throws NamingException {
            return context.listBindings(name);
        }

        @Override
        public NamingEnumeration<Binding> listBindings(String name) throws NamingException {
            return context.listBindings(name);
        }

        @Override
        public void destroySubcontext(Name name) throws NamingException {
            context.destroySubcontext(name);
        }

        @Override
        public void destroySubcontext(String name) throws NamingException {
            context.destroySubcontext(name);
        }

        @Override
        public Context createSubcontext(Name name) throws NamingException {
            return context.createSubcontext(name);
        }

        @Override
        public Context createSubcontext(String name) throws NamingException {
            return context.createSubcontext(name);
        }

        @Override
        public Object lookupLink(Name name) throws NamingException {
            return context.lookupLink(name.toString());
        }

        @Override
        public Object lookupLink(String name) throws NamingException {
            return context.lookupLink(name);
        }

        @Override
        public NameParser getNameParser(Name name) throws NamingException {
            return context.getNameParser(name.toString());
        }

        @Override
        public NameParser getNameParser(String name) throws NamingException {
            return context.getNameParser(name);
        }

        @Override
        public Name composeName(Name name, Name prefix) throws NamingException {
            return context.composeName(name, prefix);
        }

        @Override
        public String composeName(String name, String prefix) throws NamingException {
            return context.composeName(name, prefix);
        }

        @Override
        public Object addToEnvironment(String propName, Object propVal) throws NamingException {
            return context.addToEnvironment(propName, propVal);
        }

        @Override
        public Object removeFromEnvironment(String propName) throws NamingException {
            return context.removeFromEnvironment(propName);
        }

        @Override
        public Hashtable<?, ?> getEnvironment() throws NamingException {
            return context.getEnvironment();
        }

        @Override
        public void close() throws NamingException {
            context.close();
        }

        @Override
        public String getNameInNamespace() throws NamingException {
            return context.getNameInNamespace();
        }
    }
}
