package net.peierls.restlet.util;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Stage;

import static java.util.Collections.emptyList;

import org.restlet.Context;
import org.restlet.Finder;
import org.restlet.Handler;
import org.restlet.Restlet;
import org.restlet.data.Request;
import org.restlet.data.Response;

/**
 * A Guice module that doubles as a factory for creating Finders that
 * look up Handler or Resource instances from a given class or Guice Key.
 * On first use of these Finders, if the module hasn't been used to create
 * an Injector, this module creates its own Injector.
 */
public class FinderFactoryModule extends AbstractModule implements FinderFactory {

    /**
     * Creates a FinderFactoryModule that will install the given modules.
     */
    public FinderFactoryModule(Module... modules) {
        this.moduleArray = modules;
        this.modules     = emptyList();
    }

    /**
     * Creates a FinderFactoryModule that will install the given modules.
     */
    public FinderFactoryModule(Iterable<Module> modules) {
        this.moduleArray = new Module[0];
        this.modules     = modules;
    }

    //
    // FinderFactory methods
    //

    public Finder finderFor(Key<? extends Handler> key) {
        return new KeyFinder(key);
    }

    public Finder finderFor(Class<? extends Handler> cls) {
        return new KeyFinder(Key.get(cls));
    }


    /**
     * This method must be called by any method that overrides it.
     */
    @Override protected final void configure() {

        if (injector != null) {
            throw new IllegalStateException("can't reconfigure with existing Injector");
        }

        if (!alreadyBound.get()) {
            alreadyBound.set(true);

            bind(Context.class)
                .toProvider(new Provider<Context>() {
                    public Context get() { return Context.getCurrent(); }
                });
            bind(Request.class)
                .toProvider(new Provider<Request>() {
                    public Request get() { return Request.getCurrent(); }
                });
            bind(Response.class)
                .toProvider(new Provider<Response>() {
                    public Response get() { return Response.getCurrent(); }
                });
            bind(FinderFactory.class)
                .toInstance(this);
        }

        for (Module module : moduleArray) install(module);
        for (Module module : modules)     install(module);
    }


    class KeyFinder extends Finder {
        private final Key<? extends Handler> key;

        KeyFinder(Key<? extends Handler> key) {
            this.key = key;
        }

        @Override protected Handler findTarget(Request request, Response response) {
            return getInjector().getInstance(key);
        }

        private Injector getInjector() {
            Injector inj = injector;
            if (inj == null) {
                synchronized (FinderFactoryModule.this) {
                    inj = injector;
                    if (inj == null) {
                        inj = Guice.createInjector(FinderFactoryModule.this);
                    }
                }
            }
            return inj;
        }
    }

    private final Module[] moduleArray;
    private final Iterable<Module> modules;
    @Inject private volatile Injector injector;

    @Inject private void clearAlreadyBound() {
        alreadyBound.set(false);
    }

    private static ThreadLocal<Boolean> alreadyBound = new ThreadLocal<Boolean>() {
        protected Boolean initialValue() {
            return false;
        }
    };
}
