// Copyright © 2013-2026 Jonathan Vasquez <jon@xyinn.org>
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

const std = @import("std");
const PathBuilder = @import("path_builder.zig");
const Shard = @import("shard.zig");
const Settings = @import("settings.zig");
const ProcessManager = @import("process_manager.zig");
const Utilities = @import("utilities.zig");
const RegistryManager = @import("registry_manager.zig");

const Filesystem = @This();

const log = std.log.scoped(.filesystem);

// Determines the type of symlink path we are going to point to.
const RelativeRequestType = enum { core, worldstone, overlay };

const REQUIRED_FILES = [_][]const u8{
    // Core: Base MPQs
    "d2char.mpq",
    "d2data.mpq",
    "d2music.mpq",
    "d2sfx.mpq",
    "d2speech.mpq",
    "d2video.mpq",

    // Core: Expansion MPQs
    "d2exp.mpq",
    "d2xmusic.mpq",
    "d2xvideo.mpq",
    "d2xtalk.mpq",

    // Core: Language Server
    "D2.LNG",

    // Platform: Minimum Expected Files
    "binkw32.dll",
    "Bnclient.dll",
    "D2Client.dll",
    "D2CMP.dll",
    "D2Common.dll",
    "D2DDraw.dll",
    "D2Direct3D.dll",
    "D2Game.dll",
    "D2Gdi.dll",
    "D2gfx.dll",
    "D2Glide.dll",
    "D2Lang.dll",
    "D2Launch.dll",
    "D2MCPClient.dll",
    "D2Multi.dll",
    "D2Net.dll",
    "D2sound.dll",
    "D2Win.dll",
    "Fog.dll",
    "Game.exe",
    "ijl11.dll",
    "Patch_D2.mpq",
    "SmackW32.dll",
    "Storm.dll",
};

// These methods are used by the Cactus initialization logic and cannot depend
// on the cached internal resources.

/// Check if directory exists. Must use an absolute path.
pub fn doesDirectoryExist(io: std.Io, target_absolute_path: []const u8) !bool {
    // Check if directory is absolute since if we try to use the absolute path
    // functions the app will panic and we can't catch that.
    if (!std.mem.startsWith(u8, target_absolute_path, "/")) {
        log.warn("This is not an absolute path. Cannot use this location for current operation: {s}", .{target_absolute_path});
        return error.NotAbsolutePathError;
    }

    const target_dir = std.Io.Dir.openDirAbsolute(io, target_absolute_path, .{}) catch |e| switch (e) {
        error.FileNotFound => return false,
        else => return e,
    };
    defer target_dir.close(io);
    return true;
}

/// Check if file exists.
///
/// Must be an absolute path.
pub fn doesFileExist(io: std.Io, path: []const u8) !bool {
    const target_file = std.Io.Dir.openFileAbsolute(io, path, .{}) catch |e| switch (e) {
        error.FileNotFound => return false,
        else => return e,
    };
    defer target_file.close(io);
    return true;
}

/// Creates the directory if it doesn't exist.
///
/// Any parent directories that don't exist will automatically be created.
///
/// Must use an absolute path.
pub fn createDirectoryIfNeeded(io: std.Io, path: []const u8) !bool {
    if (try doesDirectoryExist(io, path)) return false;
    log.debug("Creating directory: {s} ...", .{path});
    try std.Io.Dir.cwd().createDirPath(io, path);
    return true;
}

/// Renames the old Settings.json to the new Cactus.json name if needed.
pub fn migrateOldSettingsNameToNewNameIfNeeded(io: std.Io, path_builder: PathBuilder) !void {
    log.debug("Migrating old settings name to new name if needed ...", .{});

    if (try doesFileExist(io, path_builder.getSettingsOldPath())) {
        log.info("Renaming old Settings.json to new Cactus.json ...", .{});
        try std.Io.Dir.renameAbsolute(
            path_builder.getSettingsOldPath(),
            path_builder.getSettingsPath(),
            io,
        );
    }
}

/// Create and save the default settings file if needed.
///
/// Caller owns memory.
pub fn createDefaultSettingsFileIfNeeded(
    allocator: std.mem.Allocator,
    io: std.Io,
    environ_map: *const std.process.Environ.Map,
    path_builder: PathBuilder,
) !void {
    log.info("Creating default settings file if needed ...", .{});

    if (try doesFileExist(io, path_builder.getSettingsPath())) return;
    const settings = try Settings.init(allocator, environ_map);
    try settings.save(allocator, io, path_builder);
}

/// Copies a directory.
///
/// Zig doesn't have a copyTree function, only a deleteTree. So we will implement
/// our own using cp -R.
pub fn copyTree(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    source_absolute_path: []const u8,
    target_absolute_path: []const u8,
) !void {
    const source_dir = try std.Io.Dir.openDirAbsolute(io, source_absolute_path, .{ .iterate = true });
    defer source_dir.close(io);

    var walker = try source_dir.walk(allocator);
    defer walker.deinit();

    // Copy all the directories recursively that are on level 1, and then copy
    // all the files that are on level one. This way we can optimize the copying
    // since zig doesn't have a copyTree function so we will be using cp -R.
    while (try walker.next(io)) |element| {
        const source_file = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ source_absolute_path, element.path });
        defer allocator.free(source_file);
        if (element.kind == .directory and element.depth() == 1) {
            log.debug("Directory: {s}", .{source_file});
            var command = [_][]const u8{
                "cp",
                "-R",
                source_file,
                target_absolute_path,
            };
            try ProcessManager.runAndWait(io, settings, &command, null);
        } else if (element.kind == .file and element.depth() == 1) {
            log.debug("File: {s}", .{source_file});
            var command = [_][]const u8{ "cp", source_file, target_absolute_path };
            try ProcessManager.runAndWait(io, settings, &command, null);
        }
    }
}

/// "Copies" a directory by creating a symlink tree. We use this to maximize
/// disk space savings when making shards and also allows one to simply modify a
/// single file in the Core directories and not have to update every shard
/// manually. In this situation, the Reset functionality simply recreates the
/// symlinks. There is no actual data being copied. An example would be us
/// updating our Common/Core/ddraw.ini file, and have all Shards that haven't
/// launched immediately get updated video setting updates.
///
/// "addon_relative_path" isn't used for Core. Only for Worldstones and Overlays.
pub fn linkTree(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    path_builder: *const PathBuilder,
    relative_request_type: RelativeRequestType,
    addon_relative_path: []const u8,
    source_absolute_path: []const u8,
    target_absolute_path: []const u8,
) !void {
    const source_dir = try std.Io.Dir.openDirAbsolute(io, source_absolute_path, .{ .iterate = true });
    defer source_dir.close(io);

    var walker = try source_dir.walk(allocator);
    defer walker.deinit();

    log.debug("Source Absolute Path: {s}", .{source_absolute_path});

    while (try walker.next(io)) |element| {
        const source_file = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ source_absolute_path, element.path });
        defer allocator.free(source_file);

        const relative_path = if (relative_request_type == .core) blk: {
            break :blk try std.fmt.allocPrint(
                allocator,
                "{s}/{s}",
                .{ path_builder.getCorePathRelativeToShard(), element.path },
            );
        } else blk: {
            break :blk try std.fmt.allocPrint(allocator, "{s}/{s}", .{ addon_relative_path, element.path });
        };
        defer allocator.free(relative_path);
        log.debug("Link Tree Relative Path: {s}", .{relative_path});

        if ((element.kind == .directory or element.kind == .file) and element.depth() == 1) {
            log.debug("Linking: {s} -> {s}", .{ source_file, target_absolute_path });
            var command = [_][]const u8{
                "ln",
                "-sf",
                "-t",
                target_absolute_path,
                relative_path,
            };
            try ProcessManager.runAndWait(io, settings, &command, null);
        }
    }
}

/// Create a backup for the shard alias. Only the Saves will be backed up.
///
/// Returns the path to where the backup was created.
///
/// Caller owns memory.
pub fn createBackup(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    shard: *const Shard,
    path_builder: *const PathBuilder,
) ![]const u8 {
    log.info("Creating backup for {s} ...", .{shard.alias});

    const shard_save_path = try path_builder.getSavePath(allocator, shard);
    defer allocator.free(shard_save_path);
    log.debug("Save Path: {s}", .{shard_save_path});

    // TODO: Eventually see how we can convert this timestamp into a human readable format.
    const timestamp = std.Io.Clock.real.now(io).nanoseconds;

    var full_backup_path: []const u8 = undefined;

    // TODO: See if we can simplify this logic.
    if (settings.backup_path) |backup_path| {
        log.info("Backup Path: {s}", .{backup_path});
        const backup_path_with_timestamp = try std.fmt.allocPrint(allocator, "{s}/{s}/{}", .{ backup_path, shard.alias, timestamp });
        log.debug("Backup Path w/ Timestamp: {s}", .{backup_path_with_timestamp});
        const was_created = createDirectoryIfNeeded(io, backup_path_with_timestamp) catch |e| blk: {
            log.warn("Failed to create specified backup directory. Defaulting to default backups path. Error: {}", .{e});
            allocator.free(backup_path_with_timestamp);
            break :blk false;
        };

        if (was_created) {
            full_backup_path = backup_path_with_timestamp;
        } else {
            full_backup_path = runCreateDefaultBackupDirectoryLogic(allocator, io, path_builder, shard, timestamp) catch unreachable;
        }
    } else {
        full_backup_path = runCreateDefaultBackupDirectoryLogic(allocator, io, path_builder, shard, timestamp) catch unreachable;
    }
    defer allocator.free(full_backup_path);

    try copyTree(allocator, io, settings, shard_save_path, full_backup_path);
    log.info("Backup successfully created at: {s}", .{full_backup_path});

    return try allocator.dupe(u8, full_backup_path);
}

/// Caller owns memory.
pub fn runCreateDefaultBackupDirectoryLogic(
    allocator: std.mem.Allocator,
    io: std.Io,
    path_builder: *const PathBuilder,
    shard: *const Shard,
    timestamp: i96,
) ![]const u8 {
    const shard_backup_path = try path_builder.getBackupPath(allocator, shard);
    defer allocator.free(shard_backup_path);
    const shard_backup_path_with_timestamp = try std.fmt.allocPrint(allocator, "{s}/{}", .{ shard_backup_path, timestamp });

    log.debug("Default Shard Backup Path w/ Timestamp: {s}", .{shard_backup_path_with_timestamp});

    _ = try createDirectoryIfNeeded(io, shard_backup_path_with_timestamp);

    return shard_backup_path_with_timestamp;
}

/// Verify that all requires files exist in the given path.
pub fn verifyRequiredFilesExist(allocator: std.mem.Allocator, io: std.Io, absolute_path: []const u8) !void {
    const dir = try std.Io.Dir.openDirAbsolute(io, absolute_path, .{});
    defer dir.close(io);

    for (REQUIRED_FILES) |required_file| {
        const required_file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ absolute_path, required_file });
        defer allocator.free(required_file_path);
        log.debug("Required File: {s}", .{required_file_path});
        if (!try doesFileExist(io, required_file_path)) {
            log.warn("The following required file is missing: {s}", .{required_file_path});
            return error.RequiredFileMissing;
        }
    }
}

/// Ensures that all of the required folders exist in the Cactus Root Directory.
pub fn createBaseLayout(io: std.Io, path_builder: *const PathBuilder) !void {
    log.debug("Creating base layout if needed ...", .{});

    // Common/
    // Common/Core
    // Common/Worldstones
    // Common/Overlays
    _ = try createDirectoryIfNeeded(io, path_builder.getCorePath());
    _ = try createDirectoryIfNeeded(io, path_builder.getWorldstonesPath());
    _ = try createDirectoryIfNeeded(io, path_builder.getOverlaysPath());

    // Saves/
    _ = try createDirectoryIfNeeded(io, path_builder.getSavesPath());

    // Shards/
    _ = try createDirectoryIfNeeded(io, path_builder.getShardsPath());

    // Prefixes/
    _ = try createDirectoryIfNeeded(io, path_builder.getPrefixesPath());

    // Backups/
    _ = try createDirectoryIfNeeded(io, path_builder.getBackupsPath());
}

/// Install or reset a shard if needed. If a shard doesn't need to be installed
/// or reset, then this function acts as a no-op sanity validation to ensure
/// everything that is needed to enjoy this beautiful game, is in the right place <3.
///
/// We will also automatically heal the Save Path on every summon of an alias
/// (as long as the alias isn't actively running and no aliased shards are running).
/// This is to ensure that the save path location is always correct, especially
/// in the situation that the user has moved Cactus to a new computer or a different
/// location on their machine. If we didn't do this, then the Save Path inside
/// the Wine Prefix would be out of sync.
///
/// Caller owns memory.
pub fn installIfNeeded(
    allocator: std.mem.Allocator,
    io: std.Io,
    environ: std.process.Environ,
    settings: *const Settings,
    path_builder: *const PathBuilder,
    shard: *const Shard,
    should_reset_shard: bool,
    should_heal_save_path: bool,
) !void {
    // Technically we don't need to create the Saves/Prefixes/Shards directories
    // since this already happens every time Cactus starts up if necessary.
    // However, for robustness, especially since someone (including myself) may
    // want to nuke those folders during development/runtime, we will re-create
    // these folders automatically without issues. We will use our existing base
    // layout function for this. Although due note that even though we will also
    // create Common and its hierarchy, this isn't really supported in this flow
    // since if the user didn't already have the appropriate Core/Platforms file
    // in place in Common, then by this point of execution things will fail either
    // way. We are using base layout to reduce code duplication. 99-100% of the
    // time, this call should just be a no-op.
    try createBaseLayout(io, path_builder);

    // Create a shard and prefix for the requested shard key.
    // It is a core assumption that a shard will always have a corresponding prefix.
    const shard_path = try path_builder.getShardPath(allocator, shard);
    const does_shard_path_exist = try doesDirectoryExist(io, shard_path);
    const is_new_shard = !does_shard_path_exist;
    const is_shard_being_reset = does_shard_path_exist and should_reset_shard;

    // If the shard is being reset, delete the shard and corresponding prefix
    // directories if they exist.
    if (is_shard_being_reset) {
        try deleteShardAndPrefixIfNeeded(allocator, io, settings, path_builder, shard);
    }

    // Create the shard directory and copy all required files for the game.
    if (is_new_shard or is_shard_being_reset) {
        log.info("Creating new shard for {s} ...\n", .{shard_path});

        // Create the new directory and copy files into it from Common/*.
        _ = try createDirectoryIfNeeded(io, shard_path);

        // Link files (Core + Platform + Overlay)
        try linkFiles(allocator, io, settings, path_builder, shard);
    }

    // Ensure all required files exist.
    try verifyRequiredFilesExist(allocator, io, shard_path);

    // Regardless if a new shard was created or not, we will run the following
    // safety checks and create anything that doesn't exist for these scenarios:
    // Prefixes and Saves.

    // Create prefix hierarchy. If the prefix_path was created, then we need to
    // auto configure it.
    const prefix_path = try path_builder.getPrefixPath(allocator, shard);
    const local_map = try settings.getWineSettings(allocator, environ, prefix_path);

    // Change into the root directory. Registry operations later on will be
    // expecting to be in this location.
    const root_path = path_builder.getRootPath();
    try std.Io.Threaded.chdir(root_path);

    // Set initial registry configuration options in this shard's prefix.
    const root_dir = try std.Io.Dir.openDirAbsolute(io, root_path, .{});
    defer root_dir.close(io);

    if (try createDirectoryIfNeeded(io, prefix_path)) {
        try RegistryManager.setSavePath(allocator, io, settings, local_map, path_builder, root_dir, shard);
        try RegistryManager.setInstallPath(allocator, io, settings, local_map, path_builder, root_dir, shard);
        try RegistryManager.setPopupHireling(allocator, io, settings, local_map, root_dir);
        try RegistryManager.setCompatibilityVersion(allocator, io, settings, local_map, root_dir);

        if (settings.should_enable_running) {
            try RegistryManager.enableAlwaysRun(allocator, io, settings, local_map, root_dir);
        }

        if (settings.should_hide_mini_panel) {
            try RegistryManager.hideMiniPanel(allocator, io, settings, local_map, root_dir);
        }

        if (settings.should_hide_help_menu_button) {
            try RegistryManager.hideHelpMenu(allocator, io, settings, local_map, root_dir);
        }
    }

    if (should_heal_save_path) {
        log.debug("Healing shard prefix's save path and any other needed values ...", .{});
        try RegistryManager.setSavePath(allocator, io, settings, local_map, path_builder, root_dir, shard);
        try RegistryManager.setInstallPath(allocator, io, settings, local_map, path_builder, root_dir, shard);
    }

    // Create save directory for the shard.
    _ = try createDirectoryIfNeeded(io, try path_builder.getSavePath(allocator, shard));
}

/// Link the files into the shard folder.
///
/// Caller owns memory.
fn linkFiles(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    path_builder: *const PathBuilder,
    shard: *const Shard,
) !void {
    const shard_path = try path_builder.getShardPath(allocator, shard);

    log.info("[ Linking Files ]\n", .{});

    const core_path = path_builder.getCorePath();
    const worldstone_path = try path_builder.getWorldstonePath(allocator, shard);
    const worldstone_relative_path = try path_builder.getWorldstonePathRelativeToShard(allocator, shard);
    const overlay_path = try path_builder.getOverlayPath(allocator, shard);
    const overlay_relative_path = try path_builder.getOverlayPathRelativeToShard(allocator, shard);

    log.info("Core Path: {s}", .{core_path});
    log.info("Worldstone Path: {s}", .{worldstone_path});
    log.info("Overlay Path: {s}", .{overlay_path});
    log.info("Shard Path: {s}\n", .{shard_path});

    log.debug("Worldstone Relative Path: {s}", .{worldstone_relative_path});
    log.debug("Overlay Relative Path: {s}", .{overlay_relative_path});

    for ([_][]const u8{ core_path, worldstone_path, overlay_path }) |common_path| {
        const relative_request_type: RelativeRequestType = if (std.mem.eql(u8, common_path, core_path)) blk: {
            break :blk .core;
        } else if (std.mem.eql(u8, common_path, worldstone_path)) blk: {
            break :blk .worldstone;
        } else blk: {
            break :blk .overlay;
        };

        // Overlays are optional, we only seamlessly copy them in if they exist.
        const is_overlay_install = if (std.mem.eql(u8, common_path, overlay_path)) true else false;
        const does_dir_exist = try doesDirectoryExist(io, common_path);

        log.info("Request Type: {}", .{relative_request_type});
        log.info("Linking Path: {s}\n", .{common_path});

        if (!does_dir_exist) {
            if (!is_overlay_install) {
                log.warn("Critical directory doesn't exist: {s}", .{common_path});
                return error.RequiredFileMissing;
            }
            log.info("No Overlay Detected. Skipping ...", .{});
        } else {
            if (is_overlay_install) {
                log.info("Overlay Detected. Applying ...", .{});
            }
        }

        if (does_dir_exist) {
            const addon_relative_path = if (relative_request_type == .core)
                ""
            else if (relative_request_type == .worldstone)
                worldstone_relative_path
            else
                overlay_relative_path;

            log.debug("Addon Relative Path: {s}\n", .{addon_relative_path});

            try linkTree(
                allocator,
                io,
                settings,
                path_builder,
                relative_request_type,
                addon_relative_path,
                common_path,
                shard_path,
            );
        }
    }
}

/// Delete the shard and prefix directories if needed for the specific shard alias.
/// If 'should_delete_parent_if_needed' is true, it will also delete the parent
/// directories with the given shard_platform string.
///
/// Caller owns memory.
pub fn deleteShardAndPrefixIfNeeded(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    path_builder: *const PathBuilder,
    shard: *const Shard,
) !void {
    const shard_dir = try std.Io.Dir.openDirAbsolute(io, path_builder.getShardsPath(), .{});
    defer shard_dir.close(io);
    const prefixes_dir = try std.Io.Dir.openDirAbsolute(io, path_builder.getPrefixesPath(), .{});
    defer prefixes_dir.close(io);

    const shard_path = try path_builder.getShardPath(allocator, shard);
    const prefix_path = try path_builder.getPrefixPath(allocator, shard);
    const shard_parent_path = try path_builder.getShardParentPath(allocator, shard);
    const prefix_parent_path = try path_builder.getPrefixParentPath(allocator, shard);

    // Delete the shard folder if it exists.
    if (try doesDirectoryExist(io, shard_path)) {
        log.debug("Deleting shard directory: {s} ...", .{shard_path});
        try shard_dir.deleteTree(io, shard.alias);
    }

    // Delete its prefix folder if it exists.
    if (try doesDirectoryExist(io, prefix_path)) {
        log.debug("Deleting prefix directory: {s} ...", .{prefix_path});
        try prefixes_dir.deleteTree(io, shard.alias);
    }

    const should_delete_parent_if_needed = !settings.isWorldstoneAliased(shard.worldstone);
    if (should_delete_parent_if_needed) {
        // Delete the shard parent folder if it exists.
        if (try doesDirectoryExist(io, shard_parent_path)) {
            log.debug("Deleting shard parent directory: {s} ...", .{shard_parent_path});
            try shard_dir.deleteTree(io, shard.worldstone);
        }

        // Delete its prefix parent folder if it exists.
        if (try doesDirectoryExist(io, prefix_parent_path)) {
            log.debug("Deleting prefix parent directory: {s} ...", .{prefix_parent_path});
            try prefixes_dir.deleteTree(io, shard.worldstone);
        }
    }
}

/// Test to see if we have read, write, and execute access to this directory.
///
/// Must be an absolute path.
pub fn testDirectoryAccess(io: std.Io, path: []const u8) !void {
    try std.Io.Dir.accessAbsolute(io, path, .{ .read = true, .write = true, .execute = true });
}
