package org.metaborg.core.context;

import java.io.IOException;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.vfs2.FileObject;
import org.metaborg.core.language.ILanguageImpl;
import org.metaborg.core.language.LanguageImplChange;
import org.metaborg.core.project.IProject;
import org.metaborg.util.log.ILogger;
import org.metaborg.util.log.LoggerUtils;

import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Injector;

public class ContextService implements IContextService, IContextProcessor {
    private static final ILogger logger = LoggerUtils.logger(ContextService.class);

    private final Injector injector;

    private final ConcurrentMap<ContextIdentifier, IContextInternal> idToContext = Maps.newConcurrentMap();
    private final ConcurrentMap<ILanguageImpl, ContextIdentifier> langToContextId = Maps.newConcurrentMap();

    @Inject public ContextService(Injector injector) {
        this.injector = injector;
    }


    @Override public IContext get(FileObject resource, IProject project, ILanguageImpl language)
        throws ContextException {
        if(available(language)) {
            final ContextFacet facet = getFacet(resource, language);
            final ContextIdentifier identifier = facet.strategy.get(resource, project, language);
            return getOrCreate(facet.factory, identifier);
        } else {
            return createNullContext(project, language);
        }
    }

    @Override public ITemporaryContext getTemporary(FileObject resource, IProject project, ILanguageImpl language)
        throws ContextException {
        if(available(language)) {
            final ContextFacet facet = getFacet(resource, language);
            ContextIdentifier identifier;
            try {
                identifier = facet.strategy.get(resource, project, language);
            } catch(ContextException e) {
                logger.debug("Could not create a context via context strategy of language {} (see exception)"
                    + ", creating context with given resource {} instead", e, language, resource);
                identifier = new ContextIdentifier(resource, project, language);
            }
            return createTemporary(facet.factory, identifier);
        } else {
            return createNullContext(project, language);
        }
    }

    @Override public void unload(IContext context) {
        final IContextInternal contextInternal = (IContextInternal) context;
        contextInternal.unload();
        final ContextIdentifier identifier = contextInternal.identifier();
        idToContext.remove(identifier);
        langToContextId.remove(identifier.language);
    }

    @Override public void update(LanguageImplChange change) {
        switch(change.kind) {
            case Remove:
                final ContextIdentifier id = langToContextId.remove(change.impl);
                if(id != null) {
                    final IContextInternal removed = idToContext.remove(id);
                    if(removed != null) {
                        removed.unload();
                        logger.debug("Removing {}", removed);
                    }
                }
                break;
            default:
                // Ignore other changes
                break;
        }
    }


    private boolean available(ILanguageImpl language) {
        final ContextFacet facet = language.facet(ContextFacet.class);
        return facet != null;
    }

    private ContextFacet getFacet(FileObject resource, ILanguageImpl language) throws ContextException {
        final ContextFacet facet = language.facet(ContextFacet.class);
        if(facet == null) {
            final String message = logger.format("Cannot get a context, {} does not have a context facet", language);
            throw new ContextException(resource, language, message);
        }
        return facet;
    }

    private IContextInternal getOrCreate(IContextFactory factory, ContextIdentifier identifier) {
        final IContextInternal newContext = create(factory, identifier);
        final IContextInternal prevContext = idToContext.putIfAbsent(identifier, newContext);
        langToContextId.putIfAbsent(identifier.language, identifier);
        if(prevContext == null) {
            return newContext;
        } else if (!prevContext.getClass().equals(newContext.getClass())) {
            /* If the language implementation changed its context settings, we
             * should really return the new context, and discard the previous.
             */
            logger.info("Context for {} changed type, ignoring existing context.", identifier);
            if(idToContext.replace(identifier, prevContext, newContext)) {
                try {
                    prevContext.reset();
                } catch (IOException e) {
                    logger.warn("Error occurred while resetting {}",prevContext,e);
                }
            } else {
                logger.warn("Race condition while replacing context {} with {}",prevContext,newContext);
            }
            return newContext;
        }
        return prevContext;
    }

    private IContextInternal create(IContextFactory factory, ContextIdentifier identifier) {
        return factory.create(identifier);
    }

    private ITemporaryContextInternal createTemporary(IContextFactory factory, ContextIdentifier identifier) {
        return factory.createTemporary(identifier);
    }

    private NullContext createNullContext(IProject project, ILanguageImpl language) {
        return new NullContext(project.location(), project, language, injector);
    }

}

@@ -404,17 +404,151 @@
 collect.
-M
+HashMultimap;%0Aimport com.google.common.collect.Maps;%0Aimport com.google.common.collect.Multimap;%0Aimport com.google.common.collect.Multim
 aps;%0Aimp
@@ -1028,16 +1028,161 @@
 ntMap();
+%0A    private final Multimap%3CILanguageImpl, ContextIdentifier%3E langToDownstreamContextIds = Multimaps.synchronizedMultimap(HashMultimap.create());
 %0A%0A    @I
@@ -3140,17 +3140,24 @@
 ntifier 
-i
+removedI
 d = lang
@@ -3208,17 +3208,24 @@
      if(
-i
+removedI
 d != nul
@@ -3301,17 +3301,24 @@
 .remove(
-i
+removedI
 d);%0A    
@@ -3514,32 +3514,622 @@
          break;%0A
+            case Reload:%0A                for(ContextIdentifier reloadedId : langToDownstreamContextIds.get(change.impl)) %7B%0A                    final IContextInternal reloaded = idToContext.remove(reloadedId);%0A                    if(reloaded != null) %7B%0A                        try %7B%0A                            reloaded.reset();%0A                            logger.debug(%22Resetting %7B%7D%22, reloaded);%0A                        %7D catch(IOException e) %7B%0A                            logger.debug(%22Resetting %7B%7D failed.%22, e, reloaded);%0A                        %7D%0A                    %7D%0A                %7D%0A
             defa
@@ -5135,32 +5135,107 @@
 e, identifier);%0A
+        langToDownstreamContextIds.put(newContext.language(), identifier);%0A
         if(prevC
@@ -6043,32 +6043,115 @@
 ;%0A            %7D%0A
+            langToDownstreamContextIds.remove(prevContext.language(), identifier);%0A
             retu

Reset contexts whose language is reloaded.