package org.rug.data.characteristics.comps;

import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

/**
 * This class manages the retrieval of the source code of a class from
 * multiple sources.
 * This class handles the logic to retrieve the source code files.
 */
public abstract class SourceCodeRetriever {

    private final static Logger logger = LoggerFactory.getLogger(SourceCodeRetriever.class);

    protected Map<String, String> classesCache;
    protected Map<String, Optional<Path>> filesCache;
    protected Path sourcePath;
    protected Set<Path> deepPaths;

    protected final static String NOT_FOUND = "";

    /**
     * Instantiates a source code retriever.
     * @param sourcePath the path to the source directory.
     */
    public SourceCodeRetriever(Path sourcePath) {
        this.classesCache = new HashMap<>(200);
        this.filesCache = new HashMap<>(200);
        this.sourcePath = sourcePath;
        try( var stream = Files.walk(sourcePath)){
            this.deepPaths = stream.filter(p -> p.toFile().isFile()).collect(Collectors.toSet());
        } catch (IOException e) {
            this.deepPaths = new HashSet<>();
        }
    }

    /**
     * Retrieves the source code as a string from the given class name.
     * @param elementName the full name of the element (suffix may or may not be necessary based on the implementation).
     * @return the source code of the element as string or {@link #NOT_FOUND} if no class is found.
     */
    public String getSource(String elementName, String extension) {
        String key = toFileName(elementName, extension);
        if (!classesCache.containsKey(key)) {
            var classFile = getPathOf(elementName, extension);
            try {
                if (classFile.isPresent()) {
                    var source = Files.readString(classFile.get());
                    classesCache.putIfAbsent(key, source);
                } else {
                    throw new IOException();
                }
            } catch (IOException e) {
                logger.error("Could not read source from: {}", key);
            }
        }

        return classesCache.getOrDefault(key, NOT_FOUND);
    }


    /**
     * Returns the source code of the given vertex element as described by {@link #getSource(String, String)}.
     * @param element the element to retrieve the source code of.
     * @return the source code of the element as string or {@link #NOT_FOUND} if no class is found.
     */
    public String getSource(Vertex element) {
        if (element.values("ClassType", "Type").next().toString().toLowerCase().contains("retrieved")) {
            return "";
        }
        var fileName = toFileName(element);
        var dotIndex = fileName.lastIndexOf(".");
        var extension = dotIndex < 0 ? "" : fileName.substring(dotIndex);
        fileName = dotIndex < 0 ? fileName : fileName.substring(0, dotIndex);
        return getSource(fileName, extension);
    }

    /**
     * Clear cached classes' source code.
     */
	public void clearCache(){
	    classesCache.clear();
    }

    /**
     * Transform the given element into a file name based on the specific implementation.
     * For example, C/CPP projects might attach the file extension to the `name` property, whereas
     * Java project may get the full path based on the full name of the class/package retrieved.
     * Please refer to actual implementations for more details.
     * @param element the element to extract the file path from.
     * @return a string containing the file name.
     */
    protected abstract String toFileName(Vertex element);

    /**
     * Default implementation pastes the element name and extension together.
     * @param elementName the name of the element
     * @param extension the extension with a . as a prefix.
     * @return a string in the following format `#elementName.#extension`.
     */
    protected String toFileName(String elementName, String extension) {
        return String.format("%s%s", elementName, extension);
    }

    /**
     * Return the path of a component with the given `name` property.
     * @param elementName the name of the component.
     * @return the Path object to the given element or null if no element was found.
     */
    public Optional<Path> getPathOf(String elementName, String extension){
        return findFile(toFileName(elementName, extension));
    }

    /**
     * Returns the path of a component based on its `name` property and using {@link #toFileName(Vertex)}.
     * This method ignores retrieved classes (e.g. Java lang classes, primitives, etc.).
     * @param component the vertex (please note that only vertices that refer to files/packages/etc. will work.
     * @return the Path instance of the given component or null if no element was found.
     */
    public Optional<Path> getPathOf(Vertex component) {
        if (component.values("ClassType", "Type").next().toString().toLowerCase().contains("retrieved")) {
            return Optional.empty();
        }
        return findFile(toFileName(component));
    }

    /**
     * Finds a file by searching recursively in the current source path for a file path that
     * ends with the given file name(or path) suffix.
     * @param fileName the file (optionally including the path) to find.
     * @return an optional Path.
     */
    protected Optional<Path> findFile(String fileName){
        if (!filesCache.containsKey(fileName)) {
            Optional<Path> elementFile = deepPaths.stream().filter(p -> p.endsWith(fileName)).findFirst();
            filesCache.put(fileName, elementFile);
        }
        return filesCache.getOrDefault(fileName, Optional.empty());
    }

    /**
     * Returns the given path as a relative path starting from the current {@link #sourcePath} directory.
     * @param path the path to relativize
     * @return the path relative to the source path or an empty optional otherwise.
     */
    public Optional<Path> relativize(Optional<Path> path){
        if (path.isEmpty())
            return Optional.empty();
        return path.map(value -> sourcePath.relativize(value));
    }
}
