package org.metaborg.spoofax.core.analysis.legacy;

import java.util.Collection;

import org.apache.commons.vfs2.FileObject;
import org.metaborg.core.MetaborgException;
import org.metaborg.core.analysis.AnalysisException;
import org.metaborg.core.context.IContext;
import org.metaborg.core.language.FacetContribution;
import org.metaborg.core.language.ILanguageImpl;
import org.metaborg.core.messages.IMessage;
import org.metaborg.core.messages.MessageFactory;
import org.metaborg.core.messages.MessageSeverity;
import org.metaborg.spoofax.core.analysis.AnalysisCommon;
import org.metaborg.spoofax.core.analysis.AnalysisFacet;
import org.metaborg.spoofax.core.analysis.ISpoofaxAnalyzeResult;
import org.metaborg.spoofax.core.analysis.ISpoofaxAnalyzeResults;
import org.metaborg.spoofax.core.analysis.ISpoofaxAnalyzer;
import org.metaborg.spoofax.core.analysis.SpoofaxAnalyzeResult;
import org.metaborg.spoofax.core.analysis.SpoofaxAnalyzeResults;
import org.metaborg.spoofax.core.stratego.IStrategoCommon;
import org.metaborg.spoofax.core.stratego.IStrategoRuntimeService;
import org.metaborg.spoofax.core.unit.AnalyzeContrib;
import org.metaborg.spoofax.core.unit.ISpoofaxAnalyzeUnit;
import org.metaborg.spoofax.core.unit.ISpoofaxParseUnit;
import org.metaborg.spoofax.core.unit.ISpoofaxUnitService;
import org.metaborg.util.iterators.Iterables2;
import org.metaborg.util.log.ILogger;
import org.metaborg.util.log.LoggerUtils;
import org.metaborg.util.task.ICancel;
import org.metaborg.util.task.IProgress;
import org.metaborg.util.time.Timer;
import org.spoofax.interpreter.terms.IStrategoString;
import org.spoofax.interpreter.terms.IStrategoTerm;
import org.spoofax.interpreter.terms.IStrategoTuple;
import org.spoofax.interpreter.terms.ITermFactory;
import org.spoofax.terms.util.TermUtils;
import org.strategoxt.HybridInterpreter;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.Inject;

/**
 * Analyzer for legacy Stratego projects. Calls the analysis strategy for each input.
 */
public class StrategoAnalyzer implements ISpoofaxAnalyzer {
    public static final String name = "stratego";

    private static final ILogger logger = LoggerUtils.logger(StrategoAnalyzer.class);

    private final ISpoofaxUnitService unitService;
    private final ITermFactory termFactory;
    private final IStrategoRuntimeService runtimeService;

    private final IStrategoCommon strategoCommon;
    private final AnalysisCommon analysisCommon;


    @Inject public StrategoAnalyzer(ISpoofaxUnitService unitService, ITermFactory termFactory,
            IStrategoRuntimeService runtimeService, IStrategoCommon strategoCommon,
        AnalysisCommon analysisCommon) {
        this.unitService = unitService;
        this.termFactory = termFactory;
        this.runtimeService = runtimeService;
        this.strategoCommon = strategoCommon;
        this.analysisCommon = analysisCommon;
    }


    @Override public ISpoofaxAnalyzeResult analyze(ISpoofaxParseUnit input, IContext context, IProgress progress,
        ICancel cancel) throws AnalysisException, InterruptedException {
        cancel.throwIfCancelled();
        
        if(!input.valid()) {
            final String message = logger.format("Parse input for {} is invalid, cannot analyze", input.source());
            throw new AnalysisException(context, message);
        }

        final ILanguageImpl language = context.language();

        final FacetContribution<AnalysisFacet> facetContribution = language.facetContribution(AnalysisFacet.class);
        if(facetContribution == null) {
            logger.debug("No analysis required for {}", language);
            final ISpoofaxAnalyzeUnit emptyUnit = unitService.emptyAnalyzeUnit(input, context);
            return new SpoofaxAnalyzeResult(emptyUnit, context);
        }
        final AnalysisFacet facet = facetContribution.facet;

        cancel.throwIfCancelled();
        final HybridInterpreter runtime;
        try {
            runtime = runtimeService.runtime(facetContribution.contributor, context);
        } catch(MetaborgException e) {
            throw new AnalysisException(context, "Failed to get Stratego runtime", e);
        }

        cancel.throwIfCancelled();
        final ISpoofaxAnalyzeUnit result = analyze(input, context, runtime, facet.strategyName, termFactory);
        return new SpoofaxAnalyzeResult(result, context);
    }

    @Override public ISpoofaxAnalyzeResults analyzeAll(Iterable<ISpoofaxParseUnit> inputs, IContext context,
        IProgress progress, ICancel cancel) throws AnalysisException, InterruptedException {
        cancel.throwIfCancelled();
        
        final ILanguageImpl language = context.language();

        final FacetContribution<AnalysisFacet> facetContribution = language.facetContribution(AnalysisFacet.class);
        if(facetContribution == null) {
            logger.debug("No analysis required for {}", language);
            return new SpoofaxAnalyzeResults(context);
        }
        final AnalysisFacet facet = facetContribution.facet;

        cancel.throwIfCancelled();
        final HybridInterpreter runtime;
        try {
            runtime = runtimeService.runtime(facetContribution.contributor, context);
        } catch(MetaborgException e) {
            throw new AnalysisException(context, "Failed to get Stratego runtime", e);
        }

        final int size = Iterables.size(inputs);
        progress.setWorkRemaining(size);
        final Collection<ISpoofaxAnalyzeUnit> results = Lists.newArrayListWithCapacity(size);
        for(ISpoofaxParseUnit input : inputs) {
            cancel.throwIfCancelled();
            if(!input.valid()) {
                logger.warn("Parse input for {} is invalid, cannot analyze", input.source());
                // TODO: throw exception instead?
                progress.work(1);
                continue;
            }
            final ISpoofaxAnalyzeUnit result = analyze(input, context, runtime, facet.strategyName, termFactory);
            results.add(result);
            progress.work(1);
        }
        return new SpoofaxAnalyzeResults(results, context);
    }

    private ISpoofaxAnalyzeUnit analyze(ISpoofaxParseUnit input, IContext context, HybridInterpreter runtime,
        String strategy, ITermFactory termFactory) throws AnalysisException {
        final FileObject source = input.source();

        final IStrategoString contextPath = strategoCommon.locationTerm(context.location());
        final IStrategoString resourcePath = strategoCommon.resourceTerm(source, context.location());
        final IStrategoTuple inputTerm = termFactory.makeTuple(input.ast(), resourcePath, contextPath);

        try {
            logger.trace("Analysing {}", source);
            final Timer timer = new Timer(true);
            final IStrategoTerm resultTerm = strategoCommon.invoke(runtime, inputTerm, strategy);
            final long duration = timer.stop();
            if(resultTerm == null) {
                logger.trace("Analysis for {} failed", source);
                return result(analysisCommon.analysisFailedMessage(runtime), input, context, null, duration);
            } else if(!(TermUtils.isTuple(resultTerm))) {
                logger.trace("Analysis for {} has unexpected result, not a tuple", source);
                final String message = logger.format("Unexpected results from analysis {}", resultTerm);
                return result(message, input, context, null, duration);
            } else if(resultTerm.getSubtermCount() == 4) {
                logger.trace("Analysis for {} done", source);
                return result(resultTerm, input, context, duration);
            } else if(resultTerm.getSubtermCount() == 3) {
                logger.trace("Analysis for {} done", source);
                return resultNoAst(resultTerm, input, context, duration);
            } else {
                logger.trace("Analysis for {} has unexpected result; tuple with more than 4 or less than 2 elements",
                    source);
                final String message = logger.format("Unexpected results from analysis {}", resultTerm);
                return result(message, input, context, null, duration);
            }
        } catch(MetaborgException e) {
            final String message = logger.format("Analysis for {} failed", source);
            logger.trace(message, e);
            throw new AnalysisException(context, message, e);
        }
    }


    private ISpoofaxAnalyzeUnit result(IStrategoTerm result, ISpoofaxParseUnit input, IContext context, long duration) {
        final IStrategoTerm ast = result.getSubterm(0);
        final FileObject source = input.source();

        final Collection<IMessage> errors =
            analysisCommon.messages(source, MessageSeverity.ERROR, result.getSubterm(1));
        final Collection<IMessage> warnings =
            analysisCommon.messages(source, MessageSeverity.WARNING, result.getSubterm(2));
        final Collection<IMessage> notes = analysisCommon.messages(source, MessageSeverity.NOTE, result.getSubterm(3));
        final Collection<IMessage> ambiguities = analysisCommon.ambiguityMessages(source, ast);

        final Collection<IMessage> messages =
            Lists.newArrayListWithCapacity(errors.size() + warnings.size() + notes.size() + ambiguities.size());
        messages.addAll(errors);
        messages.addAll(warnings);
        messages.addAll(notes);
        messages.addAll(ambiguities);

        return unitService.analyzeUnit(input, new AnalyzeContrib(true, errors.isEmpty(), true, ast, messages, duration),
            context);
    }

    private ISpoofaxAnalyzeUnit resultNoAst(IStrategoTerm result, ISpoofaxParseUnit input, IContext context,
        long duration) {
        final FileObject source = input.source();

        final Collection<IMessage> errors =
            analysisCommon.messages(source, MessageSeverity.ERROR, result.getSubterm(0));
        final Collection<IMessage> warnings =
            analysisCommon.messages(source, MessageSeverity.WARNING, result.getSubterm(1));
        final Collection<IMessage> notes = analysisCommon.messages(source, MessageSeverity.NOTE, result.getSubterm(2));

        final Collection<IMessage> messages =
            Lists.newArrayListWithCapacity(errors.size() + warnings.size() + notes.size());
        messages.addAll(errors);
        messages.addAll(warnings);
        messages.addAll(notes);

        return unitService.analyzeUnit(input,
            new AnalyzeContrib(true, errors.isEmpty(), false, null, messages, duration), context);
    }

    private ISpoofaxAnalyzeUnit result(String error, ISpoofaxParseUnit input, IContext context, Throwable e,
        long duration) {
        final FileObject source = input.source();
        final IMessage message = MessageFactory.newAnalysisErrorAtTop(source, error, e);
        return unitService.analyzeUnit(input,
            new AnalyzeContrib(false, false, true, null, Iterables2.singleton(message), duration), context);
    }
}