// 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 Settings = @import("settings.zig");
const PathBuilder = @import("path_builder.zig");
const Shard = @import("shard.zig");
const Utilities = @import("utilities.zig");
const ProcessManager = @import("process_manager.zig");

const RegistryManager = @This();

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

/// Update Registry: "Save Path"
///
/// The traditional saves directory location (1.00 - 1.13d). This would normally
/// be set to the "Save" folder in the Diablo II Root Directory.
///
/// Caller owns memory.
pub fn setSavePath(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    path_builder: *const PathBuilder,
    root_dir: std.Io.Dir,
    shard: *const Shard,
) !void {
    const data =
        \\ Windows Registry Editor Version 5.00
        \\
        \\ [HKEY_CURRENT_USER\Software\Blizzard Entertainment\Diablo II]
        \\ "Save Path"="{s}"
    ;
    try setFormattedPath(
        allocator,
        io,
        settings,
        local_map,
        root_dir,
        data,
        try path_builder.getWineFormatSavePath(allocator, shard),
    );
}

/// Update Registry: "InstallPath"
///
/// The InstallPath is always set on first launch to the Diablo II Root Directory,
/// which in our case is the Shard Root. The setting is also automatically adjusted
/// by the game when it detects that it's different. However, we will proactively
/// and defensively set this in the hopes that it increases program stability and
/// helps align things with the game's expected behavior.
///
/// Caller owns memory.
pub fn setInstallPath(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    path_builder: *const PathBuilder,
    root_dir: std.Io.Dir,
    shard: *const Shard,
) !void {
    const data =
        \\ Windows Registry Editor Version 5.00
        \\
        \\ [HKEY_CURRENT_USER\Software\Blizzard Entertainment\Diablo II]
        \\ "InstallPath"="{s}"
    ;
    try setFormattedPath(
        allocator,
        io,
        settings,
        local_map,
        root_dir,
        data,
        try path_builder.getWineFormatShardPath(allocator, shard),
    );
}

/// Update Registry: "PopupHireling"
///
/// Prevent the Hireling UI from automatically showing up the first time you
/// enter the game with a character that has a merc that is alive on a fresh
/// install (which also includes a fresh prefix).
///
/// Caller owns memory.
pub fn setPopupHireling(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
) !void {
    try setKeyValue(allocator, io, settings, local_map, root_dir, "PopupHireling", "1");
}

/// Update Registry: "Always Run"
///
/// Enable the Always Run flag by default. We are normally in walk mode on a fresh prefix.
///
/// Caller owns memory.
pub fn enableAlwaysRun(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
) !void {
    try setKeyValue(allocator, io, settings, local_map, root_dir, "Always Run", "1");
}

/// Update Registry: "Mini Panel"
///
/// Hide the Mini Panel by default. The panel is raised on a fresh prefix.
///
/// Caller owns memory.
pub fn hideMiniPanel(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
) !void {
    try setKeyValue(allocator, io, settings, local_map, root_dir, "Mini Panel", "1");
}

/// Update Registry: "Help Menu"
///
/// Hide the "Help" menu icon by default. The "Help" button is normally displayed
/// on a fresh prefix.
///
/// Caller owns memory.
pub fn hideHelpMenu(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
) !void {
    try setKeyValue(allocator, io, settings, local_map, root_dir, "Help Menu", "1");
}

/// Update Windows Compatibility to Windows XP.
///
/// These values were copied from what winecfg sets when you manually change the
/// compatibility mode. I need to do this here since it doesn't seem Wine has a
/// flag to set the Windows compatibility version programmatically.
///
/// Caller owns memory.
pub fn setCompatibilityVersion(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
) !void {
    const data =
        \\ Windows Registry Editor Version 5.00
        \\
        \\ [HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion]
        \\ "CSDVersion"="Service Pack 3"
        \\ "CurrentBuild"="2600"
        \\ "CurrentBuildNumber"="2600"
        \\ "CurrentMajorVersionNumber"=dword:00000005
        \\ "CurrentMinorVersionNumber"=dword:00000001
        \\ "CurrentVersion"="5.1"
        \\ "ProductName"="Microsoft Windows XP"
    ;
    const file_name = try Utilities.createRandomFileName(allocator, io, "reg");
    try importRegistryFile(io, settings, local_map, root_dir, file_name, data);
}

/// Caller owns memory.
fn setFormattedPath(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
    comptime data: []const u8,
    data_argument: []const u8,
) !void {
    const formatted_data = try std.fmt.allocPrint(allocator, data, .{data_argument});
    const file_name = try Utilities.createRandomFileName(allocator, io, "reg");
    try importRegistryFile(io, settings, local_map, root_dir, file_name, formatted_data);
}

/// Caller owns memory.
fn setKeyValue(
    allocator: std.mem.Allocator,
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
    key: []const u8,
    value: []const u8,
) !void {
    const data =
        \\ Windows Registry Editor Version 5.00
        \\
        \\ [HKEY_CURRENT_USER\Software\Blizzard Entertainment\Diablo II]
        \\ "{s}"=dword:0000000{s}
    ;
    const formatted_data = try std.fmt.allocPrint(allocator, data, .{ key, value });
    const file_name = try Utilities.createRandomFileName(allocator, io, "reg");
    try importRegistryFile(io, settings, local_map, root_dir, file_name, formatted_data);
}

/// Writes and imports the registry data into the prefix's wine registry.
fn importRegistryFile(
    io: std.Io,
    settings: *const Settings,
    local_map: std.process.Environ.Map,
    root_dir: std.Io.Dir,
    registry_file_name: []const u8,
    data: []const u8,
) !void {
    try root_dir.writeFile(io, .{ .sub_path = registry_file_name, .data = data });

    // Import the given registry file into the x32 and x64 registry paths.
    for ([_][]const u8{ "/reg:32", "/reg:64" }) |bitness| {
        var command = [_][]const u8{ "wine", "reg", "import", registry_file_name, bitness };
        try ProcessManager.runAndWait(io, settings, &command, &local_map);
    }

    // Delete the temporary file once we are done. We can't use this call inside
    // a defer at the top because you can't use try inside a defer expression.
    try root_dir.deleteFile(io, registry_file_name);
}
