package org.update4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.module.ModuleFinder;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ProviderNotFoundException;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.update4j.mapper.FileMapper;

public class Archive {

    private Path location;
    private Configuration config;
    private List<FileMetadata> files;

    private static final String RESERVED_DIR = "reserved";
    private static final String CONFIG_PATH = "config";
    private static final String FILES_DIR = "files";

    public static Archive read(Path location) throws IOException {
        Archive archive = new Archive(location);
        archive.load();

        return archive;
    }

    public static Archive read(String location) throws IOException {
        return read(Paths.get(location));
    }

    Archive(Path location) {
        this.location = location;
    }

    public Configuration getConfiguration() {
        return config;
    }

    private void load() throws IOException {
        try (FileSystem zip = openConnection()) {
            Path filesPath = zip.getPath(FILES_DIR);
            Path reservedPath = zip.getPath(RESERVED_DIR);
            Path configPath = reservedPath.resolve(CONFIG_PATH);
            
            if (Files.notExists(configPath))
                throw new NoSuchFileException(configPath.toString(), null, "Configuration file is missing");

            try (BufferedReader in = Files.newBufferedReader(configPath)) {
                config = Configuration.read(in);
            }

            try (Stream<Path> stream = Files.walk(filesPath)) {
                files = stream.filter(p -> !Files.isDirectory(p))
                                .map(p -> filesPath.relativize(p))
                                .peek(System.out::println)
                                .map(Path::toString)
                                .map(p -> OS.CURRENT != OS.WINDOWS ? "/" + p : p)
                                .map(p -> getConfiguration()
                                                .getFiles()
                                                .stream()
                                                .filter(file -> file.getNormalizedPath()
                                                                .toString()
                                                                .replace("\\", "/")
                                                                .equals(p.toString()))
                                                .findAny()
                                                .orElseThrow(() -> new IllegalStateException(p
                                                                + ": Archive entry cannot be linked to a file in the configuration")))
                                .collect(Collectors.toList());

                // Collectors.toUnmodifiableList() was added in JDK 10
                files = Collections.unmodifiableList(files);
            }

            for (FileMetadata file : getFiles()) {
                Path p = filesPath.resolve(file.getNormalizedPath().toString());
                if (FileMapper.getChecksum(p) != file.getChecksum()) {
                    throw new IOException(p + ": File has been tampered with");
                }
            }
        }
    }

    public List<FileMetadata> getFiles() {
        return files;
    }

    public Path getLocation() {
        return location;
    }

    public FileSystem openConnection() throws IOException {
        if (Files.notExists(getLocation())) {
            // I can't use Map.of("create", "true") since the overload taking a path was only added in JDK 13
            // and using URI overload doesn't support nested zip files
            try (OutputStream out = Files.newOutputStream(getLocation(), StandardOpenOption.CREATE_NEW)) {
                // End of Central Directory Record (EOCD)
                out.write(new byte[] { 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
            }
        }

        try {
            return FileSystems.newFileSystem(getLocation(), (ClassLoader) null);
        } catch (ProviderNotFoundException e) {
            ModuleFinder.ofSystem()
                            .find("jdk.zipfs")
                            .orElseThrow(() -> new ProviderNotFoundException(
                                            "Accessing the archive depends on the jdk.zipfs module which is missing from the JRE image"));

            throw e;
        }
    }
}