package emu.grasscutter.utils;

import emu.grasscutter.Grasscutter;
import lombok.val;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class FileUtils {
    private static final Path DATA_DEFAULT_PATH;
    private static final Path DATA_USER_PATH = Path.of(Grasscutter.config.folderStructure.data);
    private static final Path PACKETS_PATH = Path.of(Grasscutter.config.folderStructure.packets);
    private static final Path PLUGINS_PATH = Path.of(Grasscutter.config.folderStructure.plugins);
    private static final Path RESOURCES_PATH;
    private static final Path SCRIPTS_PATH;
    static {
        FileSystem fs = null;
        Path path = null;
        // Setup access to jar resources
        try {
            var uri = Grasscutter.class.getResource("/defaults/data").toURI();
            switch (uri.getScheme()) {
                case "jar":  // When running normally, as a jar
                case "zip":  // Honestly I have no idea what setup would result in this, but this should work regardless
                    fs = FileSystems.newFileSystem(uri, Map.of());  // Have to mount zip filesystem. This leaks, but we want to keep it forever anyway.
                    // Fall-through
                case "file":  // When running in an IDE
                    path = Path.of(uri);  // Can access directly
                    break;
                default:
                Grasscutter.getLogger().error("Invalid URI scheme for class resources: "+uri.getScheme());
                    break;
            }
        } catch (URISyntaxException | IOException e) {
            // Failed to load this jar. How?
            Grasscutter.getLogger().error("Failed to load jar?!");
        } finally {
            DATA_DEFAULT_PATH = path;
            Grasscutter.getLogger().debug("Setting path for default data: "+path.toAbsolutePath());
        }

        // Setup Resources path
        final String resources = Grasscutter.config.folderStructure.resources;
        fs = null;
        path = Path.of(resources);
        if (resources.endsWith(".zip")) {  // Would be nice to support .tar.gz too at some point, but it doesn't come for free in Java
            try {
                fs = FileSystems.newFileSystem(path);
            } catch (IOException e) {
                Grasscutter.getLogger().error("Failed to load resources zip \"" + resources + "\"");
            }
        }

        if (fs != null) {
            var root = fs.getPath("");
            try (Stream<Path> pathStream = Files.find(root, 3, (p, a) -> {
                        var filename = p.getFileName();
                        if (filename == null) return false;
                        return filename.toString().equals("ExcelBinOutput");
            })) {
                var excelBinOutput = pathStream.findFirst();
                if (excelBinOutput.isPresent()) {
                    path = excelBinOutput.get().getParent();
                    if (path == null)
                        path = root;
                    Grasscutter.getLogger().debug("Resources will be loaded from \"" + resources + "/" + path.toString() + "\"");
                } else {
                    Grasscutter.getLogger().error("Failed to find ExcelBinOutput in resources zip \"" + resources + "\"");
                }
            } catch (IOException e) {
                Grasscutter.getLogger().error("Failed to scan resources zip \"" + resources + "\"");
            }
        }
        RESOURCES_PATH = path;

        // Setup Scripts path
        final String scripts = Grasscutter.config.folderStructure.scripts;
        SCRIPTS_PATH = (scripts.startsWith("resources:"))
            ? RESOURCES_PATH.resolve(scripts.substring("resources:".length()))
            : Path.of(scripts);
    };

    private static final String[] TSJ_JSON_TSV = {"tsj", "json", "tsv"};
    private static final Path[] DATA_PATHS = {DATA_USER_PATH, DATA_DEFAULT_PATH};
    public static Path getDataPathTsjJsonTsv(String filename) {
        return getDataPathTsjJsonTsv(filename, true);
    }
    public static Path getDataPathTsjJsonTsv(String filename, boolean fallback) {
        val name = getFilenameWithoutExtension(filename);
        for (val data_path : DATA_PATHS) {
            for (val ext : TSJ_JSON_TSV) {
                val path = data_path.resolve(name + "." + ext);
                if (Files.exists(path)) return path;
            }
        }
        return fallback ? DATA_USER_PATH.resolve(name + ".tsj") : null;  // Maybe they want to write to a new file
    }

    public static Path getDataPath(String path) {
        Path userPath = DATA_USER_PATH.resolve(path);
        if (Files.exists(userPath)) return userPath;
        Path defaultPath = DATA_DEFAULT_PATH.resolve(path);
        if (Files.exists(defaultPath)) return defaultPath;
        return userPath;  // Maybe they want to write to a new file
    }

    public static Path getDataUserPath(String path) {
        return DATA_USER_PATH.resolve(path);
    }

    public static Path getPacketPath(String path) {
        return PACKETS_PATH.resolve(path);
    }

    public static Path getPluginPath(String path) {
        return PLUGINS_PATH.resolve(path);
    }

    public static Path getResourcePath(String path) {
        return RESOURCES_PATH.resolve(path);
    }

    public static Path getExcelPath(String filename) {
        return getTsjJsonTsv(RESOURCES_PATH.resolve("ExcelBinOutput"), filename);
    }

    // Gets path of a resource.
    // If multiple formats of it exist, priority is TSJ > JSON > TSV
    // If none exist, return the TSJ path, in case it wants to create a file
    public static Path getTsjJsonTsv(Path root, String filename) {
        val name = getFilenameWithoutExtension(filename);
        for (val ext : TSJ_JSON_TSV) {
            val path = root.resolve(name + "." + ext);
            if (Files.exists(path)) return path;
        }
        return root.resolve(name + ".tsj");
    }

    public static Path getScriptPath(String path) {
        return SCRIPTS_PATH.resolve(path);
    }

    public static void write(String dest, byte[] bytes) {
        Path path = Path.of(dest);

        try {
            Files.write(path, bytes);
        } catch (IOException e) {
            Grasscutter.getLogger().warn("Failed to write file: " + dest);
        }
    }

    public static byte[] read(String dest) {
        return read(Path.of(dest));
    }

    public static byte[] read(Path path) {
        try {
            return Files.readAllBytes(path);
        } catch (IOException e) {
            Grasscutter.getLogger().warn("Failed to read file: " + path);
        }

        return new byte[0];
    }

    public static InputStream readResourceAsStream(String resourcePath) {
        return Grasscutter.class.getResourceAsStream(resourcePath);
    }

    public static byte[] readResource(String resourcePath) {
        try (InputStream is = Grasscutter.class.getResourceAsStream(resourcePath)) {
            return is.readAllBytes();
        } catch (Exception exception) {
            Grasscutter.getLogger().warn("Failed to read resource: " + resourcePath);
            exception.printStackTrace();
        }

        return new byte[0];
    }

    public static byte[] read(File file) {
        return read(file.getPath());
    }

    public static void copyResource(String resourcePath, String destination) {
        try {
            byte[] resource = FileUtils.readResource(resourcePath);
            FileUtils.write(destination, resource);
        } catch (Exception exception) {
            Grasscutter.getLogger().warn("Failed to copy resource: " + resourcePath + "\n" + exception);
        }
    }

    @Deprecated  // Misnamed legacy function
    public static String getFilenameWithoutPath(String filename) {
        return getFilenameWithoutExtension(filename);
    }
    public static String getFilenameWithoutExtension(String filename) {
        int i = filename.lastIndexOf(".");
        return (i < 0) ? filename : filename.substring(0, i);
    }

    public static String getFileExtension(Path path) {
        val filename = path.toString();
        int i = filename.lastIndexOf(".");
        return (i < 0) ? "" : filename.substring(i+1);
    }

    public static List<Path> getPathsFromResource(String folder) throws URISyntaxException {
        try {
            // file walks JAR
            return Files.walk(Path.of(Grasscutter.class.getResource(folder).toURI()))
                    .filter(Files::isRegularFile)
                    .collect(Collectors.toList());
        } catch (IOException e) {
            // Eclipse puts resources in its bin folder
            try {
                return Files.walk(Path.of(System.getProperty("user.dir"), folder))
                        .filter(Files::isRegularFile)
                        .collect(Collectors.toList());
            } catch (IOException ignored) {
                return null;
            }
        }
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static String readToString(InputStream file) throws IOException {
        byte[] content = file.readAllBytes();

        return new String(content, StandardCharsets.UTF_8);
    }
}
