package org.jboss.windup.bootstrap.commands.windup;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.jboss.windup.bootstrap.commands.AbstractListCommand;
import org.jboss.windup.bootstrap.commands.Command;
import org.jboss.windup.bootstrap.commands.CommandPhase;
import org.jboss.windup.bootstrap.commands.CommandResult;
import org.jboss.windup.bootstrap.commands.FurnaceDependent;
import org.jboss.windup.exec.configuration.options.InputPathOption;
import org.jboss.windup.rules.apps.java.scan.operation.packagemapping.PackageNameMappingRegistry;
import org.jboss.windup.util.ClassNameUtil;
import org.jboss.windup.util.PackageComparator;
import org.jboss.windup.util.PackageFrequencyTrie;
import org.jboss.windup.util.PackageFrequencyTrieVisitor;
import org.jboss.windup.util.PathUtil;
import org.jboss.windup.util.ZipUtil;

/**
 * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
 */
public class DiscoverPackagesCommand extends AbstractListCommand implements Command, FurnaceDependent
{
    private final List<String> arguments;

    public DiscoverPackagesCommand(List<String> arguments)
    {
        this.arguments = arguments;
    }

    @Override
    public CommandResult execute()
    {
        String input = null;
        for (int i = 0; i < this.arguments.size(); i++)
        {
            String argument = this.arguments.get(i);
            if (argument.equalsIgnoreCase("--" + InputPathOption.NAME))
            {
                if (this.arguments.size() > (i + 1))
                {
                    input = this.arguments.get(i + 1);
                    break;
                }
            }
        }

        if (input == null)
        {
            System.err.println();
            System.err.println("ERROR: --input must be specified");
            return CommandResult.EXIT;
        }

        Path inputPath = Paths.get(input);
        if (!Files.isDirectory(inputPath) && !Files.isRegularFile(inputPath))
        {
            System.err.println();
            System.err.println("ERROR: --input must exist");
            return CommandResult.EXIT;
        }

        if (Files.isRegularFile(inputPath) && !Files.isReadable(inputPath))
        {
            System.err.println();
            System.err.println("ERROR: --input must be readable");
            return CommandResult.EXIT;
        }

        final Map<String, Integer> classes = findClasses(Paths.get(input));
        PackageNameMappingRegistry packageNameMappingRegistry = getFurnace().getAddonRegistry().getServices(PackageNameMappingRegistry.class).get();
        packageNameMappingRegistry.loadPackageMappings();

        Map<String, String> packageToOrganization = new TreeMap<>(new PackageComparator());
        PackageFrequencyTrie frequencyTrie = new PackageFrequencyTrie();

        for (String qualifiedName : classes.keySet())
        {
            String packageName = ClassNameUtil.getPackageName(qualifiedName);
            String organization = packageNameMappingRegistry.getOrganizationForPackage(packageName);
            if (organization == null)
                frequencyTrie.addClass(qualifiedName);
            else
                packageToOrganization.put(packageName, organization);
        }

        System.out.println("Known Packages:");
        System.out.println("=======================");
        System.out.println();
        for (Map.Entry<String, String> organizationPackage : packageToOrganization.entrySet())
        {
            System.out.println(organizationPackage.getKey() + " - " + organizationPackage.getValue());
        }

        System.out.println();
        System.out.println("Unknown Packages:");
        System.out.println("=======================");
        frequencyTrie.visit(new PackageFrequencyTrieVisitor()
        {

            @Override
            public void visit(PackageFrequencyTrie trie, int depth)
            {
                String packageName = trie.getPackageName();
                int recursiveClassCount = trie.getClassCount(true);
                if (depth == 1 || (depth > 1 && recursiveClassCount > 100))
                    System.out.println(packageName + " - Classes: " + recursiveClassCount);

                if (depth == 0 && trie.getClassCount(false) > 0)
                {
                    System.out.println("Default Package - Classes: " + trie.getClassCount(false));
                }
            }
        });

        return CommandResult.EXIT;
    }

    @Override
    public CommandPhase getPhase()
    {
        return CommandPhase.PRE_EXECUTION;
    }

    /**
     * Recursively scan the provided path and return a list of all Java packages contained therein.
     */

    private static Map<String, Integer> findClasses(Path path)
    {
        List<String> paths = findPaths(path, true);
        Map<String, Integer> results = new HashMap<>();
        for (String subPath : paths)
        {
            if (subPath.endsWith(".java") || subPath.endsWith(".class"))
            {
                String qualifiedName = PathUtil.classFilePathToClassname(subPath);
                addClassToMap(results, qualifiedName);
            }
        }
        return results;
    }

    private static void addClassToMap(Map<String, Integer> map, String className)
    {
        Integer count = map.get(className);
        if (count == null)
            map.put(className, 1);
        else
            map.put(className, count + 1);
    }

    /**
     * Find all paths within the given file (or folder).
     */
    private static Collection<String> findPaths(Path path)
    {
        List<String> paths = findPaths(path, false);
        Collections.sort(paths);
        return paths;
    }


    private static List<String> findPaths(Path path, boolean relativeOnly)
    {
        List<String> results = new ArrayList<>();
        results.add(path.normalize().toAbsolutePath().toString());
        if (Files.isDirectory(path))
        {
            try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path))
            {
                for (Path child : directoryStream)
                {
                    results.addAll(findPaths(child, relativeOnly));
                }
            }
            catch (IOException e)
            {
                System.err.println("Could not read file: " + path + " due to: " + e.getMessage());
            }
        }
        else if (Files.isRegularFile(path) && ZipUtil.endsWithZipExtension(path.toString()))
        {
            results.addAll(ZipUtil.scanZipFile(path, relativeOnly));
        }
        return results;
    }

}
