package org.jboss.windup.tooling;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.RemoteException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.jboss.forge.furnace.Furnace;
import org.jboss.forge.furnace.util.Lists;
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.RuleProviderRegistryCache;
import org.jboss.windup.exec.WindupProcessor;
import org.jboss.windup.exec.configuration.WindupConfiguration;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.GraphContextFactory;
import org.jboss.windup.graph.model.report.IgnoredFileRegexModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.rules.apps.java.config.ExcludePackagesOption;
import org.jboss.windup.rules.apps.java.config.ScanPackagesOption;
import org.jboss.windup.rules.apps.java.config.SourceModeOption;
import org.jboss.windup.rules.apps.java.model.WindupJavaConfigurationModel;
import org.jboss.windup.rules.apps.java.service.WindupJavaConfigurationService;
import org.jboss.windup.tooling.rules.RuleProviderRegistry;
import org.jboss.windup.tooling.rules.RuleProviderRegistryImpl;
import org.jboss.windup.util.PathUtil;
import org.jboss.windup.util.exception.WindupException;

/**
 * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
 */
public class ExecutionBuilderImpl implements ExecutionBuilder
{
    @Inject
    private GraphContextFactory graphContextFactory;

    @Inject
    private ToolingXMLService toolingXMLService;

    @Inject
    private WindupProcessor processor;

    @Inject
    private Furnace furnace;
    
    @Inject
    private RuleProviderRegistryCache ruleProviderCache;

    private String windupHome;
    private WindupToolingProgressMonitor progressMonitor;
    private String input;
    private String output;
    private Set<String> ignorePathPatterns = new HashSet<>();
    private Set<String> includePackagePrefixSet = new HashSet<>();
    private Set<String> excludePackagePrefixSet = new HashSet<>();
    private Set<String> userRulesPathSet = new HashSet<>();
    private Map<String, Object> options = new HashMap<>();

    private String version;

    @Inject
    private RuleLoader ruleLoader;

    @Override
    public void clear() throws RemoteException
    {
        this.windupHome = null;
        this.progressMonitor = null;
        this.input = null;
        this.output = null;
        this.ignorePathPatterns.clear();
        this.includePackagePrefixSet.clear();
        this.excludePackagePrefixSet.clear();
        this.userRulesPathSet.clear();
        this.options.clear();
    }

    // TODO: Should we also do UnicastRemoteObject.unexportObject(this, true)?
    @Override
    public void terminate() throws RemoteException
    {
        furnace.stop();
        try
        {
            Thread.sleep(5000);
        }
        catch (InterruptedException e)
        {
        }
        Runtime.getRuntime().halt(1);
    }

    @Override
    public void setWindupHome(String windupHome) throws RemoteException
    {
        this.windupHome = windupHome;
    }

    @Override
    public void setInput(String input) throws RemoteException
    {
        this.input = input;
    }

    @Override
    public void setOutput(String output) throws RemoteException
    {
        this.output = output;
    }

    @Override
    public void ignore(String ignorePattern) throws RemoteException
    {
        this.ignorePathPatterns.add(ignorePattern);
    }

    @Override
    public void includePackage(String packagePrefix) throws RemoteException
    {
        this.includePackagePrefixSet.add(packagePrefix);
    }

    @Override
    public void includePackages(Collection<String> includePackagePrefixes) throws RemoteException
    {
        if (includePackagePrefixes != null)
            this.includePackagePrefixSet.addAll(includePackagePrefixes);
    }

    @Override
    public void excludePackage(String packagePrefix) throws RemoteException
    {
        this.excludePackagePrefixSet.add(packagePrefix);
    }

    @Override
    public void excludePackages(Collection<String> excludePackagePrefixes) throws RemoteException
    {
        if (excludePackagePrefixes != null)
            this.excludePackagePrefixSet.addAll(excludePackagePrefixes);
    }

    @Override
    public void setProgressMonitor(WindupToolingProgressMonitor monitor) throws RemoteException
    {
        this.progressMonitor = monitor;
    }

    @Override
    public void sourceOnlyMode() throws RemoteException
    {
        options.put(SourceModeOption.NAME, true);
    }

    @Override
    public void skipReportGeneration() throws RemoteException
    {
        options.put(SkipReportsRenderingOption.NAME, true);
    }

    @Override
    public void addUserRulesPath(String rulesPath) throws RemoteException
    {
        if (rulesPath == null)
            return;

        String pathString = Paths.get(rulesPath).normalize().toAbsolutePath().toString();
        this.userRulesPathSet.add(pathString);
    }

    @Override
    public void addUserRulesPaths(Iterable<String> rulesPaths) throws RemoteException
    {
        if (rulesPaths == null)
            return;

        for (String rulesPath : rulesPaths)
        {
            this.addUserRulesPath(rulesPath);
        }
    }

    @Override
    public void setOption(String name, Object value) throws RemoteException
    {
        this.options.put(name, value);
    }

    @Override
    public String getVersion() throws RemoteException
    {
        return version;
    }

    @Override
    public void setVersion(String version) throws RemoteException
    {
        this.version = version;
    }

    @Override
    public ExecutionResults execute() throws RemoteException
    {
        PathUtil.setWindupHome(Paths.get(this.windupHome));
        WindupConfiguration windupConfiguration = new WindupConfiguration();
        try
        {
            windupConfiguration.useDefaultDirectories();
        }
        catch (IOException e)
        {
            throw new WindupException("Failed to configure windup due to: " + e.getMessage(), e);
        }
        ToolingProgressMonitorAdapter progressMonitorAdapter = new ToolingProgressMonitorAdapter(this.progressMonitor);

        windupConfiguration.addInputPath(Paths.get(this.input));
        windupConfiguration.setOutputDirectory(Paths.get(this.output));
        windupConfiguration.setProgressMonitor(progressMonitorAdapter);

        Path graphPath = Paths.get(output).resolve(GraphContextFactory.DEFAULT_GRAPH_SUBDIRECTORY);

        Logger globalLogger = Logger.getLogger("");
        WindupProgressLoggingHandler loggingHandler = null;
        if (progressMonitor instanceof WindupToolingProgressMonitor)
        {
            loggingHandler = new WindupProgressLoggingHandler((WindupToolingProgressMonitor) progressMonitor);
            globalLogger.addHandler(loggingHandler);
        }

        try (final GraphContext graphContext = graphContextFactory.create(graphPath))
        {

            GraphService<IgnoredFileRegexModel> graphService = new GraphService<>(graphContext, IgnoredFileRegexModel.class);
            for (String ignorePattern : this.ignorePathPatterns)
            {
                IgnoredFileRegexModel ignored = graphService.create();
                ignored.setRegex(ignorePattern);

                WindupJavaConfigurationModel javaCfg = WindupJavaConfigurationService.getJavaConfigurationModel(graphContext);
                javaCfg.addIgnoredFileRegex(ignored);
            }

            windupConfiguration.setOptionValue(ScanPackagesOption.NAME, Lists.toList(this.includePackagePrefixSet));
            windupConfiguration.setOptionValue(ExcludePackagesOption.NAME, Lists.toList(this.excludePackagePrefixSet));

            for (Map.Entry<String, Object> option : options.entrySet())
            {
                windupConfiguration.setOptionValue(option.getKey(), option.getValue());
            }
            windupConfiguration.setGraphContext(graphContext);
            processor.execute(windupConfiguration);

            return new ExecutionResultsImpl(graphContext, toolingXMLService);
        }
        catch (IOException e)
        {
            throw new WindupException("Failed to instantiate graph due to: " + e.getMessage(), e);
        }
        finally
        {
            if (loggingHandler != null)
                globalLogger.removeHandler(loggingHandler);
        }
    }

    @Override
    public RuleProviderRegistry getRuleProviderRegistry(List<String> pathStrings) throws RemoteException
    {
        RuleProviderRegistryImpl ruleProviderRegistry = new RuleProviderRegistryImpl();
        List<Path> paths = pathStrings.stream().map(pathString -> Paths.get(pathString)).collect(Collectors.toList());

        RuleLoaderContext ruleLoaderContext = new RuleLoaderContext(paths, null);
        org.jboss.windup.config.metadata.RuleProviderRegistry registry = this.ruleLoader.loadConfiguration(ruleLoaderContext);

        ruleProviderRegistry.buildRuleProviders(registry);
        return ruleProviderRegistry;
    }
    
    @Override
    public RuleProviderRegistry getSystemRuleProviderRegistry() throws RemoteException
    {
        RuleProviderRegistryImpl ruleProviderRegistry = new RuleProviderRegistryImpl();
        ruleProviderRegistry.buildRuleProviders(ruleProviderCache.getRuleProviderRegistry());
        return ruleProviderRegistry;
    }


    private class WindupProgressLoggingHandler extends Handler
    {
        private final WindupToolingProgressMonitor monitor;

        public WindupProgressLoggingHandler(WindupToolingProgressMonitor monitor)
        {
            this.monitor = monitor;
        }

        @Override
        public void publish(LogRecord record)
        {
            if (this.monitor == null)
                return;

            try
            {
                this.monitor.logMessage(record);
            }
            catch (RemoteException e)
            {
            }
        }

        @Override
        public void flush()
        {

        }

        @Override
        public void close() throws SecurityException
        {

        }
    }
}
