package ch.bfh.lpdg;

import ch.bfh.lpdg.datastructure.Dependency;
import ch.bfh.lpdg.datastructure.DependencyType;

import java.io.*;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;

/**
 * This class is responsible for scanning dependencies for a file input.
 */
public class DependencyScanner {

    public static final String REGEX_DEPENDENCIES = "\\u005Cusepackage\\{(.*?)}|\\u005CRequirePackage\\{(.*?)}|\\u005Cinput\\{(.*?)}|\\u005Cinclude\\{(.*?)}";
    public final static DependencyScanner INSTANCE = new DependencyScanner();
    private String filePathWithoutFileName;

    private DependencyScanner() {
    }

    public static DependencyScanner getInstance() {
        return INSTANCE;
    }

    /**
     * This method can be called recursively, it loops through all dependencies of the given file.
     * This is done via a regex pattern -> it scans for all types in the DependencyType.java class.
     * It scans each line of the file individually and if the line matches one of the patterns in the regex the extractDependency()
     * method is called and is then added to the list of dependencies.
     *
     * @param filePath                 is needed to know the initial location of the document that has to be scanned
     * @param type                     describes what type the currently scanned document has
     * @param source                   describes what type of dependency the currently scanned dependency has
     * @param alreadyAddedDependencies is needed to avoid dependency loops
     * @param scanDepth                limits how deep the scan looks into the dependencies
     * @return returns a Dependency containing a list of all its dependencies itself
     */
    public Dependency findDependencies(final String filePath, final DependencyType type, final String source, final List<String> alreadyAddedDependencies, final int scanDepth) {
        final String fileName = Paths.get(filePath).getFileName().toString();
        final String fileNameWithoutExt = this.resolveDependencyName(fileName);
        filePathWithoutFileName = String.copyValueOf(filePath.toCharArray());
        filePathWithoutFileName = filePathWithoutFileName.replace(fileName, "");

        final Dependency dependency = new Dependency(type, fileNameWithoutExt, source);

        if(scanDepth > 0) {
            try {
                final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
                final Pattern pattern = Pattern.compile(REGEX_DEPENDENCIES);

                String line;
                while ((line = br.readLine()) != null) {
                    final Matcher matcher = pattern.matcher(line);

                    while (matcher.find()) {
                        final String usepackage = matcher.group(1);
                        final String requirepackage = matcher.group(2);
                        final String input = matcher.group(3);
                        final String include = matcher.group(4);

                        // Create dependencies for dependency
                        final List<Dependency> extractedUP = this.extractDependencies(usepackage, DependencyType.USE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedRP = this.extractDependencies(requirepackage, DependencyType.REQUIRE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedINP = this.extractDependencies(input, DependencyType.INPUT, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedINC = this.extractDependencies(include, DependencyType.INCLUDE, matcher.group(0), alreadyAddedDependencies, scanDepth);

                        // Add dependency to tree
                        final List<Dependency> extractedDependencies = Stream.of(extractedUP, extractedRP, extractedINP, extractedINC).filter(Objects::nonNull).flatMap(Collection::stream).toList();
                        extractedDependencies.forEach(dep -> dependency.getDependencyList().add(dep));
                    }
                }
                br.close();
            } catch (FileNotFoundException ex) {
                InteractionHandler.getInstance().printDebugMessage("File: " + fileName + " was not found.");
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        var cleanDependencies = removeInvalidDependencies(dependency);
        dependency.setDependencyList(cleanDependencies);

        return dependency;
    }

    /**
     * This method extracts all dependencies for a Dependency scanned in the findDependencies() method.
     *
     * @return a list of dependencies, this is required for a format like requirePackage{array, color}
     */
    private List<Dependency> extractDependencies(final String dependencyName, final DependencyType type, final String source, final List<String> alreadyAddedDependencies, final int scanDepth) {
        final List<String> currentDependencies = new ArrayList<>(alreadyAddedDependencies);
        if (dependencyName != null && !dependencyName.isEmpty()) {
            // Resolve multiline usePackage dependencies, ex. usePackage{array,color}
            if (dependencyName.contains(",")) {
                final String[] packages = dependencyName.split(",");
                final List<Dependency> foundDependencies = new ArrayList<>();
                Arrays.stream(packages).forEach(p -> {
                    if (!currentDependencies.contains(p)) {
                        // Try to resolve the path for the found dependency, if a path is found, the file has to be scanned recursively.
                        final String filePath = this.resolvePath(p, type);
                        if (filePath == null) {
                            foundDependencies.add(new Dependency(type, p, source));
                        } else {
                            currentDependencies.add(p);
                            final Dependency dependency = this.findDependencies(filePath, type, source, currentDependencies, scanDepth-1);
                            if (dependency != null) {
                                foundDependencies.add(dependency);
                            }
                        }
                    }
                });
                return foundDependencies;
            } else {
                if (!alreadyAddedDependencies.contains(dependencyName)) {
                    // Try to resolve the path for the found dependency, if a path is found, the file has to be scanned recursively.
                    final String filePath = this.resolvePath(dependencyName, type);
                    if (filePath == null) {
                        return Collections.singletonList(new Dependency(type, dependencyName, source));
                    } else {
                        currentDependencies.add(dependencyName);
                        return List.of(this.findDependencies(filePath, type, source, currentDependencies, scanDepth -1));
                    }
                }
            }
        }
        return emptyList();
    }

    /**
     * This method resolves a path for a LaTeX dependency
     * @param packageName the name of the package that should be searched for with kpsewhich
     * @param type the type of the package, this is needed to search for project dependencies for input and include
     * @return returns the path of the dependency location
     */
    private String resolvePath(final String packageName, final DependencyType type) {
        //Execute kpsewhich tool to find path of package
        Process process;
        try {
            boolean hasExtension = packageName.lastIndexOf('.') != -1;

            //If the package doesn't have an extension add the standard one
            process = Runtime.getRuntime().exec(String.format("kpsewhich %s%s", packageName, hasExtension ? "" : ".sty"));
        } catch (IOException e) {
            System.err.println("Couldn't execute kpsewhich command.");
            throw new RuntimeException(e);
        }

        StringBuilder result = new StringBuilder();
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String s;
        while (true) {
            try {
                if ((s = stdInput.readLine()) == null) break; //Path not found
            } catch (IOException e) {
                System.err.println("Couldn't read output of kpsewhich command.");
                throw new RuntimeException(e);
            }

            result.append(s);
        }

        if (result.isEmpty()) {
            if (type.equals(DependencyType.INCLUDE) || type.equals(DependencyType.INPUT)) {
                return filePathWithoutFileName + packageName + ".tex";
            }
            return null;
        }

        return Paths.get(result.toString()).toAbsolutePath().toString();
    }

    /**
     * Removes invalid dependencies like variable names.
     * Example: #1.def, \gin@driver
     *
     * @param d The List of all found dependencies
     * @return A clean list without variable names and other invalid dependencies
     */
    private List<Dependency> removeInvalidDependencies(Dependency d) {
        return d.getDependencyList().stream().filter(e -> !e.getName().contains("#") && !e.getName().contains("@")).toList();
    }

    private String resolveDependencyName(final String fileName) {
        return fileName.split("\\.")[0];
    }
}
