package org.jboss.windup.exec.configuration;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.forge.furnace.Furnace;
import org.jboss.forge.furnace.addons.Addon;
import org.jboss.forge.furnace.services.Imported;
import org.jboss.forge.furnace.util.Predicate;
import org.jboss.windup.config.ConfigurationOption;
import org.jboss.windup.config.RuleProvider;
import org.jboss.windup.config.furnace.FurnaceHolder;
import org.jboss.windup.exec.WindupProcessor;
import org.jboss.windup.exec.WindupProgressMonitor;
import org.jboss.windup.exec.configuration.options.ExportCSVOption;
import org.jboss.windup.exec.configuration.options.InputPathOption;
import org.jboss.windup.exec.configuration.options.OnlineModeOption;
import org.jboss.windup.exec.configuration.options.OutputPathOption;
import org.jboss.windup.exec.configuration.options.UserIgnorePathOption;
import org.jboss.windup.exec.configuration.options.UserRulesDirectoryOption;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.util.PathUtil;
import org.ocpsoft.rewrite.config.Rule;

/**
 * Configuration of WindupProcessor.
 *
 * @author <a href="mailto:ozizka@redhat.com">Ondrej Zizka</a>
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public class WindupConfiguration
{
    private static final String DEFAULT_USER_RULES_DIRECTORIES_OPTION = "defaultUserRulesDirectories";
    private static final String DEFAULT_USER_IGNORE_DIRECTORIES_OPTION = "defaultUserIgnorePaths";
    public static final boolean DEFAULT_ONLINE = false;

    private Predicate<RuleProvider> ruleProviderFilter;
    private WindupProgressMonitor progressMonitor = new NullWindupProgressMonitor();
    private final Map<String, Object> configurationOptions = new HashMap<>();
    private boolean alwaysHaltOnException;

    private GraphContext context;

    public WindupConfiguration()
    {
    }

    public void useDefaultDirectories() throws IOException
    {
        Path userRulesDir = PathUtil.getUserRulesDir();
        if (userRulesDir != null && !Files.isDirectory(userRulesDir))
        {
            Files.createDirectories(userRulesDir);
        }
        if (userRulesDir != null)
        {
            addDefaultUserRulesDirectory(userRulesDir);
        }

        Path userIgnoreDir = PathUtil.getUserIgnoreDir();
        if (userIgnoreDir != null && !Files.isDirectory(userIgnoreDir))
        {
            Files.createDirectories(userIgnoreDir);
        }
        if (userIgnoreDir != null)
        {
            addDefaultUserIgnorePath(userIgnoreDir);
        }

        Path windupHomeRulesDir = PathUtil.getWindupRulesDir();
        if (windupHomeRulesDir != null && !Files.isDirectory(windupHomeRulesDir))
        {
            Files.createDirectories(windupHomeRulesDir);
        }
        if (windupHomeRulesDir != null)
        {
            addDefaultUserRulesDirectory(windupHomeRulesDir);
        }

        Path windupHomeIgnoreDir = PathUtil.getWindupIgnoreDir();
        if (windupHomeIgnoreDir != null && !Files.isDirectory(windupHomeIgnoreDir))
        {
            Files.createDirectories(windupHomeIgnoreDir);
        }
        if (windupHomeIgnoreDir != null)
        {
            addDefaultUserIgnorePath(windupHomeIgnoreDir);
        }
    }

    /**
     * Sets a configuration option to the specified value.
     */
    public WindupConfiguration setOptionValue(String name, Object value)
    {
        configurationOptions.put(name, value);
        return this;
    }

    /**
     * Indicates that we should always halt on a {@link Rule} failure, regardless of whether or not we would normally bypass the failure.
     *
     * This can be useful in test scenarios where we don't want errors to be ignored at all.
     */
    public boolean isAlwaysHaltOnException()
    {
        return alwaysHaltOnException;
    }

    /**
     * Indicates that we should always halt on a {@link Rule} failure, regardless of whether or not we would normally bypass the failure.
     *
     * This can be useful in test scenarios where we don't want errors to be ignored at all.
     */
    public void setAlwaysHaltOnException(boolean alwaysHaltOnException)
    {
        this.alwaysHaltOnException = alwaysHaltOnException;
    }

    /**
     * Returns the configuration value with the specified name.
     */
    @SuppressWarnings("unchecked")
    public <T> T getOptionValue(String name)
    {
        return (T) configurationOptions.get(name);
    }

    /**
     * Returns all configuration options as an immutable {@link Map}.
     */
    public Map<String, Object> getOptionMap()
    {
        return Collections.unmodifiableMap(configurationOptions);
    }

    /**
     * Returns all of the {@link ConfigurationOption} in all currently available {@link Addon}s.
     */
    public static Iterable<ConfigurationOption> getWindupConfigurationOptions()
    {
        return getWindupConfigurationOptions(FurnaceHolder.getFurnace());
    }

    /**
     * Returns all of the {@link ConfigurationOption} in all currently available {@link Addon}s.
     */
    public static Iterable<ConfigurationOption> getWindupConfigurationOptions(Furnace furnace)
    {
        List<ConfigurationOption> results = new ArrayList<>();
        for (ConfigurationOption option : furnace.getAddonRegistry().getServices(ConfigurationOption.class))
        {
            results.add(option);
        }
        Collections.sort(results, new Comparator<ConfigurationOption>()
        {
            @Override
            public int compare(ConfigurationOption o1, ConfigurationOption o2)
            {
                // if the 1st is required and...
                if (o1.isRequired())
                {
                    // the 2nd isn't, the 1st is "before" than the 2nd
                    if (!o2.isRequired()) return -1;
                    // otherwise if also the 2nd is required, then order is priority-based
                    else return o2.getPriority() - o1.getPriority();
                }
                // if the 1st is not required and...
                else
                {
                    // the 2nd is, the 1st is "after" than the 2nd
                    if (o2.isRequired()) return 1;
                    // otherwise also the 2nd isn't and order is priority-based
                    else return o2.getPriority() - o1.getPriority();
                }
            }
        });
        return results;
    }

    /**
     * Returns all of the {@link ConfigurationOption} in the specified {@link Addon}.
     */
    public static Iterable<ConfigurationOption> getWindupConfigurationOptions(Addon addon)
    {
        IdentityHashMap<ClassLoader, Addon> classLoaderToAddon = new IdentityHashMap<>();
        for (Addon loadedAddon : FurnaceHolder.getAddonRegistry().getAddons())
        {
            classLoaderToAddon.put(loadedAddon.getClassLoader(), loadedAddon);
        }

        List<ConfigurationOption> results = new ArrayList<>();
        Imported<ConfigurationOption> options = FurnaceHolder.getAddonRegistry()
                    .getServices(ConfigurationOption.class);
        for (ConfigurationOption option : options)
        {
            ClassLoader optionClassLoader = option.getClass().getClassLoader();
            Addon optionAddon = classLoaderToAddon.get(optionClassLoader);
            if (optionAddon.equals(addon))
            {
                results.add(option);
            }
        }
        return results;
    }

    /**
     * Contains the path to the input file (or directory) to be processed
     */
    public WindupConfiguration addInputPath(Path inputPath)
    {
        Set<Path> inputPaths = getOptionValue(InputPathOption.NAME);
        if (inputPaths == null)
        {
            inputPaths = new LinkedHashSet<>();
            setOptionValue(InputPathOption.NAME, inputPaths);
        }
        inputPaths.add(inputPath);
        return this;
    }

    /**
     * Contains the path to the input file (or directory) to be processed
     */
    public Collection<Path> getInputPaths()
    {
        Collection<Path> inputPaths = getOptionValue(InputPathOption.NAME);
        return inputPaths;
    }
    
    /**
     * Contains the directory to put the output to (migration report, temporary files, exported graph data...).
     */
    public Path getOutputDirectory()
    {
        File file = getOptionValue(OutputPathOption.NAME);
        return file == null ? null : file.toPath();
    }

    /**
     * Contains the directory to put the output to (migration report, temporary files, exported graph data...).
     */
    public WindupConfiguration setOutputDirectory(Path outputDirectory)
    {
        setOptionValue(OutputPathOption.NAME, outputDirectory.toFile());
        return this;
    }

    /**
     * Gets all user rule directories. This includes both the ones that they specify (eg, /path/to/rules) as well as ones that Windup provides by
     * default (eg, WINDUP_HOME/rules and ~/.windup/rules).
     */
    public Iterable<Path> getAllUserRulesDirectories()
    {
        Set<Path> results = new HashSet<>();
        results.addAll(getDefaultUserRulesDirectories());
        
        Collection<File> userSpecifiedFiles = getOptionValue(UserRulesDirectoryOption.NAME);
        if (userSpecifiedFiles != null && !userSpecifiedFiles.isEmpty())
        {
            userSpecifiedFiles.stream().forEach(file -> {
                results.add(file.toPath());
            });
        }
        return results;
    }

    /**
     * Gets all the directories/files in which the regexes for ignoring the files is placed. This includes the file/directory specified by the user
     * and the default paths that are WINDUP_HOME/ignore and ~/.windup/ignore.
     *
     * @return
     */
    public Iterable<Path> getAllIgnoreDirectories()
    {
        Set<Path> results = new HashSet<>();
        results.addAll(getDefaultUserIgnoreDirectories());
        File userSpecifiedFile = getOptionValue(UserIgnorePathOption.NAME);
        if (userSpecifiedFile != null)
        {
            results.add(userSpecifiedFile.toPath());
        }
        return results;
    }

    /**
     * Contains a list of {@link Path}s with directories that contains user provided rules.
     */
    public List<Path> getDefaultUserRulesDirectories()
    {
        List<Path> paths = getOptionValue(DEFAULT_USER_RULES_DIRECTORIES_OPTION);
        if (paths == null)
        {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(paths);
    }

    /**
     * Contains a default list of {@link Path}s with directories/files that contains files having regexes of file names to be ignored.
     */
    public List<Path> getDefaultUserIgnoreDirectories()
    {
        List<Path> paths = getOptionValue(DEFAULT_USER_IGNORE_DIRECTORIES_OPTION);
        if (paths == null)
        {
            return Collections.emptyList();
        }
        return Collections.unmodifiableList(paths);
    }

    /**
     * Contains a list of {@link Path}s with the directory that contains user provided rules.
     *
     * This method does guard against duplicate directories.
     */
    public WindupConfiguration addDefaultUserRulesDirectory(Path path)
    {
        List<Path> paths = getOptionValue(DEFAULT_USER_RULES_DIRECTORIES_OPTION);
        if (paths == null)
        {
            paths = new ArrayList<>();
            setOptionValue(DEFAULT_USER_RULES_DIRECTORIES_OPTION, paths);
        }

        Iterable<File>  userRulesDirs= getOptionValue(UserRulesDirectoryOption.NAME);
        if (userRulesDirs != null) 
        {
            for (File userSpecifiedRuleFile : userRulesDirs)
            {
                
                if (userSpecifiedRuleFile != null && userSpecifiedRuleFile.toPath().equals(path))
                {
                    return this;
                }
        
                for (Path existingPath : paths)
                {
                    if (existingPath.equals(path))
                    {
                        return this;
                    }
                }
            }
        }
        paths.add(path);
        return this;
    }

    /**
     * Adds a path to the list of default {@link Path}s with directories/files that contain files with regexes of file names to be ignored.
     *
     * This method does guard against duplicate directories.
     */
    public WindupConfiguration addDefaultUserIgnorePath(Path path)
    {
        List<Path> paths = getOptionValue(DEFAULT_USER_IGNORE_DIRECTORIES_OPTION);
        if (paths == null)
        {
            paths = new ArrayList<>();
            setOptionValue(DEFAULT_USER_IGNORE_DIRECTORIES_OPTION, paths);
        }

        File userSpecifiedIgnorePath = getOptionValue(UserIgnorePathOption.NAME);
        if (userSpecifiedIgnorePath != null && userSpecifiedIgnorePath.toPath().equals(path))
        {
            return this;
        }

        for (Path existingPath : paths)
        {
            if (existingPath.equals(path))
            {
                return this;
            }
        }
        paths.add(path);
        return this;
    }

    public Predicate<RuleProvider> getRuleProviderFilter()
    {
        return ruleProviderFilter;
    }

    /**
     * A filter to limit which rule providers' rules will be executed.
     */
    public WindupConfiguration setRuleProviderFilter(Predicate<RuleProvider> ruleProviderFilter)
    {
        this.ruleProviderFilter = ruleProviderFilter;
        return this;
    }

    public WindupProgressMonitor getProgressMonitor()
    {
        return progressMonitor;
    }

    /**
     * A progress monitor which will get notification of the rule execution progress.
     */
    public WindupConfiguration setProgressMonitor(WindupProgressMonitor progressMonitor)
    {
        this.progressMonitor = progressMonitor;
        return this;
    }

    /**
     * Sets the {@link GraphContext} instance on which the {@link WindupProcessor} should execute.
     */
    public WindupConfiguration setGraphContext(GraphContext context)
    {
        this.context = context;
        return this;
    }

    /**
     * Gets the {@link GraphContext} instance on which the {@link WindupProcessor} should execute.
     */
    public GraphContext getGraphContext()
    {
        return context;
    }

    /**
     * Set Windup to run online or offline (with respect to an internet connection).
     */
    public WindupConfiguration setOnline(boolean online)
    {
        setOptionValue(OnlineModeOption.NAME, online);
        return this;
    }

    /**
     * Returns true if Windup is operating in {@link OnlineModeOption} == true. (with respect to an internet connection)
     */
    public boolean isOnline()
    {
        Boolean online = getOptionValue(OnlineModeOption.NAME);
        return online == null ? DEFAULT_ONLINE : online;
    }

    /**
     * Set Windup to export CSV file containing the migration information (classifications, hints).
     */
    public WindupConfiguration setExportingCSV(boolean export)
    {
        setOptionValue(ExportCSVOption.NAME, export);
        return this;
    }

    /**
     * Returns true if Windup is operating in {@link ExportCSVOption} == true.
     */
    public boolean isExportingCSV()
    {
        Boolean export = getOptionValue(ExportCSVOption.NAME);
        return export == null ? false : export;
    }
}
