import actors.ProcessCPOCsvEntry;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.UntypedActor;
import akka.actor.UntypedActorFactory;
import org.mongodb.morphia.logging.MorphiaLoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.AbstractService;
import com.google.common.util.concurrent.Service;
import com.google.inject.*;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.google.inject.spi.InjectionListener;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import org.mongodb.morphia.logging.slf4j.SLF4JLoggerImplFactory;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import play.Application;
import play.GlobalSettings;
import play.Logger;
import play.libs.Akka;
import play.mvc.Controller;
import utils.MoreMatchers;

import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author Mathias Bogaert
 */
public class Global extends GlobalSettings {
    static {
        MorphiaLoggerFactory.reset();
        MorphiaLoggerFactory.registerLogger(SLF4JLoggerImplFactory.class);
    }

    static final class ActorProvider<T extends UntypedActor> implements Provider<ActorRef> {
        private final TypeLiteral<T> uta;
        private final Injector injector;

        @Inject
        public ActorProvider(TypeLiteral<T> uta, Injector injector) {
            this.uta = uta;
            this.injector = injector;
        }

        @Override
        public ActorRef get() {
            return Akka.system().actorOf(new Props(new UntypedActorFactory() {
                public T create() {
                    return injector.getInstance(Key.get(uta));
                }
            }));
        }
    }

    private Injector injector;
    private final List<Module> modules = Lists.newArrayList();

    private final List<OnStartListener> onStartListeners = new CopyOnWriteArrayList<OnStartListener>();
    private final List<OnStopListener> onStopListeners = new CopyOnWriteArrayList<OnStopListener>();

    @Override
    public void beforeStart(final Application application) {
        final Reflections reflections = new Reflections(new ConfigurationBuilder()
                .filterInputsBy(new FilterBuilder.Exclude(FilterBuilder.prefix("com.google")))
                .addUrls(ClasspathHelper.forClassLoader(application.classloader()))
                .addScanners(
                        new SubTypesScanner()
                ));

        // automatic Guice module detection
        Set<Class<? extends AbstractModule>> guiceModules = reflections.getSubTypesOf(AbstractModule.class);
        for (Class<? extends Module> moduleClass : guiceModules) {
            try {
                if (!moduleClass.isAnonymousClass()) {
                    modules.add(moduleClass.newInstance());
                }
            } catch (InstantiationException e) {
                throw Throwables.propagate(e);
            } catch (IllegalAccessException e) {
                throw Throwables.propagate(e);
            }
        }

        modules.add(new AbstractModule() {
            @Override
            protected void configure() {
                bind(Application.class).toInstance(application);
                bind(Reflections.class).toInstance(reflections);

                Names.bindProperties(this.binder(), fromKeys(application.configuration().keys(), new Function<String, String>() {
                    @Override
                    public String apply(String key) {
                        if (key.contains("akka")) return null;

                        return application.configuration().getString(key);
                    }
                }));

                for (Class<? extends Controller> controllerClass : reflections.getSubTypesOf(Controller.class)) {
                    Logger.info("Static injection for " + controllerClass);

                    requestStaticInjection(controllerClass);
                }

                // bind all services
                Multibinder<Service> serviceBinder = Multibinder.newSetBinder(binder(), Service.class);
                for (Class<? extends Service> serviceImplClass : reflections.getSubTypesOf(AbstractService.class)) {
                    serviceBinder.addBinding().to(serviceImplClass).asEagerSingleton();
                }
                for (Class<? extends Service> serviceImplClass : reflections.getSubTypesOf(AbstractIdleService.class)) {
                    serviceBinder.addBinding().to(serviceImplClass).asEagerSingleton();
                }
                for (Class<? extends Service> serviceImplClass : reflections.getSubTypesOf(AbstractExecutionThreadService.class)) {
                    serviceBinder.addBinding().to(serviceImplClass).asEagerSingleton();
                }

                // bind actor - todo use reflections for this
                bind(ActorRef.class).annotatedWith(Names.named("ProcessCPOCsvEntry"))
                        .toProvider(new TypeLiteral<ActorProvider<ProcessCPOCsvEntry>>() {
                        });

                // start/stop services after injection and on shutdown of the Play app
                bindListener(MoreMatchers.subclassesOf(Service.class), new TypeListener() {
                    @Override
                    public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {
                        typeEncounter.register(new InjectionListener<I>() {
                            @Override
                            public void afterInjection(final I i) {
                                onStartListeners.add(new OnStartListener() {
                                    @Override
                                    public void onApplicationStart(Application application, Injector injector) {
                                        Logger.info(String.format("Starting %s", i.toString()));
                                        ((Service) i).start();

                                        onStopListeners.add(new OnStopListener() {
                                            @Override
                                            public void onApplicationStop(Application application) {
                                                Logger.info(String.format("Stopping %s", i.toString()));
                                                ((Service) i).stop();
                                            }
                                        });
                                    }
                                });
                            }
                        });
                    }
                });
            }
        });
    }

    @Override
    public <A> A getControllerInstance(Class<A> controllerClass) throws Exception {
        return injector.getInstance(controllerClass);
    }

    @Override
    public void onStart(Application app) {
        Logger.info("Creating injector with " + modules.size() + " modules.");
        injector = Guice.createInjector(Stage.PRODUCTION, modules);

        for (OnStartListener listener : onStartListeners) {
            listener.onApplicationStart(app, injector);
        }
    }

    @Override
    public void onStop(Application app) {
        for (OnStopListener listener : onStopListeners) {
            listener.onApplicationStop(app);
        }
    }

    /**
     * Listener that will get invoked after the application is started.
     */
    static interface OnStartListener {
        void onApplicationStart(Application application, Injector injector);
    }

    /**
     * Listener that will get invoked before the application is stopped.
     */
    static interface OnStopListener {
        void onApplicationStop(Application application);
    }

    private static <K, V> ImmutableMap<K, V> fromKeys(Iterable<K> keys, Function<? super K, V> valueFunction) {
        ImmutableMap.Builder<K, V> builder = ImmutableMap.builder();
        for (K key : keys) {
            V value = valueFunction.apply(key);
            if (value != null) {
                builder.put(key, value);
            }
        }
        return builder.build();
    }
}