package org.netbeans.gradle.build;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.internal.file.AbstractFileCollection;
import org.gradle.api.internal.tasks.DefaultTaskDependency;
import org.gradle.api.tasks.TaskDependency;

public final class ZippedJarsFileCollection extends AbstractFileCollection {
    private final Charset UTF8 = Charset.forName("UTF-8");

    private final Project project;
    private final String dependencyNotation;
    private final Configuration dependencyContainer;
    private final Path unzipDir;
    private final List<String> subDirPath;

    public ZippedJarsFileCollection(
            Project project,
            String dependencyNotation,
            List<String> unzipPath,
            List<String> subDirPath) {
        this.project = project;
        Dependency dependency = project.getDependencies().create(dependencyNotation);
        this.dependencyContainer = project.getConfigurations().detachedConfiguration(dependency);
        this.unzipDir = resolve(project.getProjectDir().toPath(), unzipPath);
        this.dependencyNotation = dependencyNotation;
        this.subDirPath = new ArrayList<>(subDirPath);
    }

    @Override
    public TaskDependency getBuildDependencies() {
        return new DefaultTaskDependency();
    }

    private static Path resolve(Path dir, List<String> subDirs) {
        Path result = dir;
        for (String subDir: subDirs) {
            result = result.resolve(subDir);
        }
        return result;
    }

    private Path getExtractedFilesDir() {
        return unzipDir.resolve("files");
    }

    private Path getExtractedMarker() {
        return unzipDir.resolve("EXTRACTED");
    }

    private void extractSafe() {
        try {
            extract();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private static void safeCopy(Path src, Path dest) throws IOException {
        try (InputStream input = Files.newInputStream(src)) {
            Files.copy(input, dest, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    private void markExtracted() throws IOException {
        Path extractedMarker = getExtractedMarker();
        List<String> lines = Collections.singletonList(dependencyNotation);
        Files.write(extractedMarker, lines, UTF8);
    }

    private boolean isExtracted() throws IOException {
        Path extractedMarker = getExtractedMarker();
        if (!Files.isRegularFile(extractedMarker)) {
            return false;
        }

        List<String> lines = Files.readAllLines(extractedMarker, UTF8);
        if (lines.isEmpty()) {
            return false;
        }

        return dependencyNotation.equals(lines.get(0));
    }

    private void tryDeleteDirectory(Path directory) {
        project.delete(directory.toFile());
    }

    private void copyDir(final Path srcDir, final Path destDir) throws IOException {
        tryDeleteDirectory(destDir);
        Files.createDirectories(destDir);

        Files.walkFileTree(srcDir, new FileVisitor<Path>() {
            private Path currentDir = destDir;

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                if (!srcDir.equals(dir)) {
                    currentDir = currentDir.resolve(dir.getFileName().toString());
                    Files.createDirectory(currentDir);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                String fileName = file.getFileName().toString();
                String normName = fileName.toLowerCase(Locale.ROOT);
                if (normName.endsWith(".jar")) {
                    safeCopy(file, currentDir.resolve(fileName));
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                currentDir = currentDir != null ? currentDir.getParent() : null;
                return FileVisitResult.CONTINUE;
            }
        });
    }

    private void extract() throws IOException {
        if (isExtracted()) {
            return;
        }

        Path zipFile = dependencyContainer.getSingleFile().toPath();
        FileSystem zipFS = FileSystems.newFileSystem(zipFile, null);
        Path root = zipFS.getRootDirectories().iterator().next();
        Path libDir = resolve(root, subDirPath);

        copyDir(libDir, getExtractedFilesDir());

        markExtracted();
    }

    @Override
    public String getDisplayName() {
        return "UnZip: " + dependencyNotation;
    }

    @Override
    public Set<File> getFiles() {
        extractSafe();

        return project.fileTree(getExtractedFilesDir().toFile()).getFiles();
    }
}
