package org.jboss.windup.exec;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.jboss.forge.furnace.services.Imported;
import org.jboss.forge.furnace.util.Assert;
import org.jboss.forge.furnace.util.Predicate;
import org.jboss.windup.exec.rulefilters.NotPredicate;
import org.jboss.windup.config.DefaultEvaluationContext;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.KeepWorkDirsOption;
import org.jboss.windup.config.PreRulesetEvaluation;
import org.jboss.windup.config.RuleLifecycleListener;
import org.jboss.windup.config.RuleProvider;
import org.jboss.windup.config.RuleSubset;
import org.jboss.windup.config.SkipReportsRenderingOption;
import org.jboss.windup.config.loader.RuleLoader;
import org.jboss.windup.config.loader.RuleLoaderContext;
import org.jboss.windup.config.metadata.RuleProviderRegistry;
import org.jboss.windup.config.metadata.TechnologyReference;
import org.jboss.windup.config.metadata.TechnologyReferenceTransformer;
import org.jboss.windup.config.phase.PostReportGenerationPhase;
import org.jboss.windup.config.phase.PostReportRenderingPhase;
import org.jboss.windup.config.phase.PreReportGenerationPhase;
import org.jboss.windup.config.phase.ReportGenerationPhase;
import org.jboss.windup.config.phase.ReportRenderingPhase;
import org.jboss.windup.exec.configuration.WindupConfiguration;
import org.jboss.windup.exec.configuration.options.ExcludeTagsOption;
import org.jboss.windup.exec.configuration.options.IncludeTagsOption;
import org.jboss.windup.exec.configuration.options.SourceOption;
import org.jboss.windup.exec.configuration.options.TargetOption;
import org.jboss.windup.exec.rulefilters.AndPredicate;
import org.jboss.windup.exec.rulefilters.RuleProviderPhasePredicate;
import org.jboss.windup.exec.rulefilters.SourceAndTargetPredicate;
import org.jboss.windup.exec.rulefilters.TaggedRuleProviderPredicate;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.GraphContextFactory;
import org.jboss.windup.graph.model.TechnologyReferenceModel;
import org.jboss.windup.graph.model.WindupConfigurationModel;
import org.jboss.windup.graph.model.resource.FileModel;
import org.jboss.windup.graph.service.FileService;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.graph.service.WindupConfigurationService;
import org.jboss.windup.util.Checks;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.exception.WindupException;
import org.ocpsoft.rewrite.config.Configuration;
import org.ocpsoft.rewrite.config.RuleVisit;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.param.DefaultParameterValueStore;
import org.ocpsoft.rewrite.param.ParameterValueStore;

/**
 * Loads and executes the Rules from RuleProviders according to given WindupConfiguration.
 *
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 * @author <a href="mailto:zizka@seznam.cz">Ondrej Zizka</a>
 */
public class WindupProcessorImpl implements WindupProcessor
{
    private static final Logger LOG = Logging.get(WindupProcessorImpl.class);

    @Inject
    private RuleLoader ruleLoader;

    @Inject
    private GraphContextFactory graphContextFactory;

    @Inject
    private Imported<RuleLifecycleListener> listeners;

    @Override
    public void execute()
    {
        execute(new WindupConfiguration());
    }

    @Override
    public void execute(WindupConfiguration configuration)
    {
        long startTime = System.currentTimeMillis();

        validateConfig(configuration);

        boolean autoCloseGraph = false;
        if (configuration.getGraphContext() == null)
        {
            // Since we created it, we should clean it up
            autoCloseGraph = true;
            Path graphPath = configuration.getOutputDirectory().resolve(GraphContextFactory.DEFAULT_GRAPH_SUBDIRECTORY);
            GraphContext graphContext = this.graphContextFactory.create(graphPath);
            configuration.setGraphContext(graphContext);
        }

        try
        {
            printConfigInfo(configuration);

            GraphContext context = configuration.getGraphContext();
            context.setOptions(configuration.getOptionMap());

            WindupConfigurationModel configurationModel = WindupConfigurationService.getConfigurationModel(context);

            Set<FileModel> inputPathModels = new LinkedHashSet<>();
            for (Path inputPath : configuration.getInputPaths()) {
                inputPathModels.add(getFileModel(context, inputPath));
            }
            configurationModel.setInputPaths(inputPathModels);

            configurationModel.setOutputPath(getFileModel(context, configuration.getOutputDirectory()));
            configurationModel.setOnlineMode(configuration.isOnline());
            configurationModel.setExportingCSV(configuration.isExportingCSV());
            configurationModel.setKeepWorkDirectories(configuration.getOptionValue(KeepWorkDirsOption.NAME));
            for (Path path : configuration.getAllUserRulesDirectories()) {
                System.out.println("Using user rules dir: " + path);
                if (path == null) {
                    throw new WindupException("Null path found (all paths are: "
                            + configuration.getAllUserRulesDirectories() + ")");
                }
                configurationModel.addUserRulesPath(getFileModel(context, path));
            }

            for (Path path : configuration.getAllIgnoreDirectories()) {
                configurationModel.addUserIgnorePath(getFileModel(context, path));
            }

            List<RuleLifecycleListener> listeners = new ArrayList<>();
            for (RuleLifecycleListener listener : this.listeners) {
                listeners.add(listener);
            }

            final GraphRewrite event = new GraphRewrite(listeners, context);
            RuleLoaderContext ruleLoaderContext = new RuleLoaderContext(event.getRewriteContext(), configuration.getAllUserRulesDirectories(), configuration.getRuleProviderFilter());
            ruleLoaderContext = configureRuleProviderAndTagFilters(ruleLoaderContext, configuration);
            addSourceAndTargetInformation(event, configuration, configurationModel);

            RuleProviderRegistry providerRegistry = ruleLoader.loadConfiguration(ruleLoaderContext);
            Configuration rules = providerRegistry.getConfiguration();

            if (configuration.getProgressMonitor() != null)
                listeners.add(new DefaultRuleLifecycleListener(configuration.getProgressMonitor(), rules));

            event.getRewriteContext().put(RuleProviderRegistry.class, providerRegistry);

            RuleSubset ruleSubset = RuleSubset.create(rules);
            ruleSubset.setAlwaysHaltOnFailure(configuration.isAlwaysHaltOnException());

            for (RuleLifecycleListener listener : listeners) {
                ruleSubset.addLifecycleListener(listener);
            }

            new RuleVisit(ruleSubset).accept((rule) ->
            {
                if (rule instanceof PreRulesetEvaluation)
                    ((PreRulesetEvaluation) rule).preRulesetEvaluation(event);
            });

            ruleSubset.perform(event, createEvaluationContext());

            if (event.getWindupStopException() != null)
                LOG.log(Level.WARNING, "Windup stopped on request before finishing, see the exception for where.", event.getWindupStopException());
        }
        finally
        {
            if (autoCloseGraph)
            {
                try
                {
                    configuration.getGraphContext().close();
                } catch (Throwable t)
                {
                    LOG.log(Level.WARNING, "Failed to close graph due to: " + t.getMessage(), t);
                }
            }

            long endTime = System.currentTimeMillis();
            long seconds = (endTime - startTime) / 1000L;
            LOG.info("Windup execution took " + seconds + " seconds to execute on input: " + configuration.getInputPaths() + "!");

            ExecutionStatistics.get().reset();
        }
    }

    private void addSourceAndTargetInformation(GraphRewrite event, WindupConfiguration configuration, WindupConfigurationModel configurationModel)
    {
        @SuppressWarnings("unchecked")
        Collection<String> sources = (Collection<String>) configuration.getOptionMap().get(SourceOption.NAME);
        @SuppressWarnings("unchecked")
        Collection<String> targets = (Collection<String>) configuration.getOptionMap().get(TargetOption.NAME);

        GraphService<TechnologyReferenceModel> technologyReferenceService = new GraphService<>(event.getGraphContext(), TechnologyReferenceModel.class);
        Function<String, TechnologyReferenceModel> createReferenceModel = (techID) -> {
            TechnologyReference reference = TechnologyReference.parseFromIDAndVersion(techID);
            TechnologyReferenceModel technologyReferenceModel = technologyReferenceService.create();
            technologyReferenceModel.setTechnologyID(reference.getId());
            technologyReferenceModel.setVersionRange(reference.getVersionRangeAsString());
            return technologyReferenceModel;
        };

        if (sources != null)
        {
            sources.forEach((sourceID) -> {
                configurationModel.addSourceTechnology(createReferenceModel.apply(sourceID));
            });
        }

        if (targets != null)
        {
            targets.forEach((targetID) -> {
                configurationModel.addTargetTechnology(createReferenceModel.apply(targetID));
            });
        }
    }

    @SuppressWarnings("unchecked")
    private RuleLoaderContext configureRuleProviderAndTagFilters(RuleLoaderContext ruleLoaderContext, WindupConfiguration config)
    {
        Collection<String> includeTags = (Collection<String>) config.getOptionMap().get(IncludeTagsOption.NAME);
        Collection<String> excludeTags = (Collection<String>) config.getOptionMap().get(ExcludeTagsOption.NAME);
        Collection<String> sources = (Collection<String>) config.getOptionMap().get(SourceOption.NAME);
        Collection<String> targets = (Collection<String>) config.getOptionMap().get(TargetOption.NAME);

        if(includeTags != null && includeTags.isEmpty()) includeTags = null;
        if(excludeTags != null && excludeTags.isEmpty()) excludeTags = null;
        if(sources != null && sources.isEmpty()) sources = null;
        if(targets != null && targets.isEmpty()) targets = null;

        if (includeTags != null || excludeTags != null || sources != null || targets != null)
        {
            Predicate<RuleProvider> configuredPredicate = config.getRuleProviderFilter();

            final TaggedRuleProviderPredicate tagPredicate = new TaggedRuleProviderPredicate(includeTags, excludeTags);

            List<TechnologyReferenceTransformer> transformers = TechnologyReferenceTransformer.getTransformers(ruleLoaderContext);

            /*
             * Create a Map based upon the ID of the original to be transformed.
             */
            Map<String, List<TechnologyReferenceTransformer>> transformerMap = transformers.stream()
                .collect(Collectors.toMap(
                    // This is the Map key
                    transformer -> transformer.getOriginal().getId(),

                    // Create a List for each entry
                    (transformer) -> {
                        List<TechnologyReferenceTransformer> list = new ArrayList<>();
                        list.add(transformer);
                        return list;
                    },

                    // Merge the list if there are multiple entries for the same id
                    (old, latest) -> {
                        old.addAll(latest);
                        return old;
                    })
                );

            // Use the tech transformers to transform the IDs
            Function<String, String> transformTechFunction = (technologyID) -> {
                if (transformerMap.containsKey(technologyID)) {
                    for (TechnologyReferenceTransformer transformer : transformerMap.get(TechnologyReference.parseFromIDAndVersion(technologyID).getId()))
                        technologyID = transformer.transform(technologyID).toString();
                }
                return technologyID;
            };

            sources = (sources == null) ? null : sources.stream().map(transformTechFunction).collect(Collectors.toSet());
            config.setOptionValue(SourceOption.NAME, sources);
            targets = (targets== null) ? null : targets.stream().map(transformTechFunction).collect(Collectors.toSet());
            config.setOptionValue(TargetOption.NAME, targets);

            final SourceAndTargetPredicate sourceAndTargetPredicate = new SourceAndTargetPredicate(sources, targets);

            Predicate<RuleProvider> providerFilter = new AndPredicate(tagPredicate, sourceAndTargetPredicate);
            if (configuredPredicate != null)
                providerFilter = new AndPredicate(configuredPredicate, tagPredicate, sourceAndTargetPredicate);

            LOG.info("RuleProvider filter: " + providerFilter);
            config.setRuleProviderFilter(providerFilter);
        }

        Boolean skipReports = false;
        // if skipReportsRendering option is set filter rules in related phases
        if (config.getOptionMap().containsKey(SkipReportsRenderingOption.NAME))
        {
            skipReports = (Boolean) config.getOptionMap().get(SkipReportsRenderingOption.NAME);
        }
        if (skipReports)
        {
            NotPredicate skipReportsProviderFilter = new NotPredicate(
                        new RuleProviderPhasePredicate(PreReportGenerationPhase.class, ReportGenerationPhase.class,
                                    ReportRenderingPhase.class, PostReportGenerationPhase.class, PostReportRenderingPhase.class));
            Predicate<RuleProvider> configuredProvider = config.getRuleProviderFilter();
            Predicate<RuleProvider> providerFilter = new AndPredicate(skipReportsProviderFilter);
            if (configuredProvider != null)
            {
                providerFilter = new AndPredicate(configuredProvider, skipReportsProviderFilter);
            }

            LOG.info("Adding RuleProvider filter for skipping reports: " + providerFilter);
            config.setRuleProviderFilter(providerFilter);
        }

        return new RuleLoaderContext(ruleLoaderContext.getContext(), ruleLoaderContext.getRulePaths(), config.getRuleProviderFilter());
    }

    private FileModel getFileModel(GraphContext context, Path file)
    {
        return new FileService(context).createByFilePath(file.toString());
    }

    private EvaluationContext createEvaluationContext()
    {
        final DefaultEvaluationContext evaluationContext = new DefaultEvaluationContext();
        final DefaultParameterValueStore values = new DefaultParameterValueStore();
        evaluationContext.put(ParameterValueStore.class, values);
        return evaluationContext;
    }

    private void validateConfig(WindupConfiguration windupConfiguration)
    {
        Assert.notNull(windupConfiguration,
                    "Windup configuration must not be null. (Call default execution if no configuration is required.)");

        Collection<Path> inputPaths = windupConfiguration.getInputPaths();
        Assert.notNull(inputPaths, "Path to the application must not be null!");
        for (Path inputPath : inputPaths)
        {
            Assert.notNull(inputPath, "Path to the application must not be null!");
            Checks.checkFileOrDirectoryToBeRead(inputPath.toFile(), "Application");
        }

        Path outputDirectory = windupConfiguration.getOutputDirectory();
        Assert.notNull(outputDirectory, "Output directory must not be null!");
        Checks.checkDirectoryToBeFilled(outputDirectory.toFile(), "Output directory");
    }

    private void printConfigInfo(WindupConfiguration windupConfiguration)
    {
        LOG.info("");
        if (windupConfiguration.getInputPaths().size() == 1)
        {
            LOG.info("Input Application:" + windupConfiguration.getInputPaths().iterator().next());
        }
        else
        {
            LOG.info("Input Applications:");
            for (Path inputPath : windupConfiguration.getInputPaths())
            {
                LOG.info("\t" + inputPath);
            }
            LOG.info("");
        }
        LOG.info("Output Path:" + windupConfiguration.getOutputDirectory());
        LOG.info("");

        for (Map.Entry<String, Object> entrySet : windupConfiguration.getOptionMap().entrySet())
        {
            LOG.info("\t" + entrySet.getKey() + ": " + entrySet.getValue());
        }
    }

}
