// 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 ShardEssence = @import("shard.zig").ShardEssence;
const Filesystem = @import("filesystem.zig");
const Enums = @import("enums.zig");

const Settings = @This();

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

/// This struct must be serializable.
const SettingsEssence = struct {
    shards: []ShardEssence,
    use_wayland: bool,
    show_stderr: bool,
    wine_dll_overrides: ?[]const u8,
    enable_automatic_backups: bool,
    last_selected_shard: ?usize,

    // Flags introduced after initial 5.0.0 launch. Users will not have these
    // values so we will provide deserialization defaults.

    // Introduced in 5.1.0 (2026-07-02).
    backup_path: ?[]const u8 = null,

    // Introduced in 5.5.0 (2026-07-11).
    should_enable_running: bool = false,
    should_hide_mini_panel: bool = false,
    should_hide_help_menu_button: bool = false,

    // Introduced in 5.6.0 (2026-07-12).
    use_dark_mode: bool = false,
};

// The list of shards and their configuration settings.
shards: std.ArrayList(Shard) = .empty,

// Should the Wine Wayland Driver be used. If disabled, will use X11 backend.
use_wayland: bool,

// Should we show the wine stderr in the CLI?
show_stderr: bool = true,

// List of wine dll overrides to inject into the prefix.
wine_dll_overrides: ?[]const u8,

// Controls whether automatic backups are enabled.
enable_automatic_backups: bool = true,

// The index of the last selected shard.
last_selected_shard: ?usize = null,

// Backup Path. If no backup path is set, will default to the Cactus Root Path's
// Backups folder.
backup_path: ?[]const u8 = null,

// Controls whether we should start off in "Running" state when a Shard is Reset.
should_enable_running: bool = false,

// Controls whether the in game mini panel should be hidden when a Shard is Reset.
should_hide_mini_panel: bool = false,

// Controls whether the in game help menu button should be hidden when a Shard is Reset.
should_hide_help_menu_button: bool = false,

// Should Dark Mode be used?
use_dark_mode: bool = false,

/// This is the default settings file information and will be automatically
/// serialized and saved on the first launch.
///
/// Caller owns memory.
pub fn init(allocator: std.mem.Allocator, environ_map: *const std.process.Environ.Map) !Settings {
    // Detect X11 or Wayland
    const display_server_optional = environ_map.get("XDG_SESSION_TYPE");
    const use_wayland: bool = if (display_server_optional) |display_server| blk: {
        log.info("Detected Display Server: {s}", .{display_server});

        if (std.mem.eql(u8, display_server, "wayland")) {
            log.info("Using Wine Wayland Driver", .{});
            break :blk true;
        } else if (std.mem.eql(u8, display_server, "x11")) {
            log.info("Using Normal Wine X11 Driver", .{});
            break :blk false;
        }

        log.warn("Unknown display server detected. Defaulting to X11.", .{});
        break :blk false;
    } else blk: {
        log.warn("Unable to detect display server via XDG_SESSION_TYPE. Defaulting to X11.", .{});
        break :blk false;
    };

    return .{
        .use_wayland = use_wayland,
        .wine_dll_overrides = try allocator.dupe(u8, "ddraw,dsound=n"),
    };
}

pub fn deinit(self: *Settings, allocator: std.mem.Allocator) void {
    for (self.shards.items) |shard| {
        shard.deinit(allocator);
    }
    self.shards.deinit(allocator);

    if (self.wine_dll_overrides) |wdo| {
        allocator.free(wdo);
    }

    if (self.backup_path) |bp| {
        allocator.free(bp);
    }
}

/// Used to deserialize an existing settings file.
///
/// Caller owns memory.
pub fn fromSettingsEssence(allocator: std.mem.Allocator, io: std.Io, essence: SettingsEssence) !Settings {
    var shards: std.ArrayList(Shard) = .empty;

    for (essence.shards) |shard| {
        try shards.append(allocator, try Shard.fromShardEssence(allocator, io, shard));
    }

    return .{
        .shards = shards,
        .use_wayland = essence.use_wayland,
        .show_stderr = essence.show_stderr,
        .wine_dll_overrides = if (essence.wine_dll_overrides) |wdo| try allocator.dupe(u8, wdo) else null,
        .enable_automatic_backups = essence.enable_automatic_backups,
        .last_selected_shard = essence.last_selected_shard,
        .backup_path = if (essence.backup_path) |bp| try allocator.dupe(u8, bp) else null,
        .should_enable_running = essence.should_enable_running,
        .should_hide_mini_panel = essence.should_hide_mini_panel,
        .should_hide_help_menu_button = essence.should_hide_help_menu_button,
        .use_dark_mode = essence.use_dark_mode,
    };
}

pub fn toSettingsEssence(self: Settings, allocator: std.mem.Allocator) !SettingsEssence {
    return .{
        .shards = try self.shardsAsShardEssences(allocator),
        .use_wayland = self.use_wayland,
        .show_stderr = self.show_stderr,
        .wine_dll_overrides = self.wine_dll_overrides,
        .enable_automatic_backups = self.enable_automatic_backups,
        .last_selected_shard = self.last_selected_shard,
        .backup_path = self.backup_path,
        .should_enable_running = self.should_enable_running,
        .should_hide_mini_panel = self.should_hide_mini_panel,
        .should_hide_help_menu_button = self.should_hide_help_menu_button,
        .use_dark_mode = self.use_dark_mode,
    };
}

/// Get the settings from disk.
///
/// Caller owns memory.
pub fn getSettings(allocator: std.mem.Allocator, io: std.Io, path_builder: PathBuilder) !Settings {
    var aa: std.heap.ArenaAllocator = .init(allocator);
    const arena = aa.allocator();
    defer aa.deinit();

    const root_dir = try std.Io.Dir.openDirAbsolute(io, path_builder.getRootPath(), .{});
    defer root_dir.close(io);

    log.info("Loading settings from {s} ...\n", .{path_builder.getSettingsPath()});

    const data = try root_dir.readFileAlloc(io, PathBuilder.SETTINGS_NAME, arena, .unlimited);

    log.debug("Loaded Settings", .{});
    log.debug("{s}", .{data});

    // parse it through json library
    const essence = std.json.parseFromSliceLeaky(SettingsEssence, arena, data, .{}) catch |e| {
        log.err("Failed to parse settings file due to: {}", .{e});
        std.process.exit(1);
    };

    log.debug("[ Start Printing Settings ]\n", .{});
    log.debug("[ Shards ]\n ", .{});
    if (essence.shards.len == 0) {
        log.debug("< No Shards Detected >\n", .{});
    } else {
        for (essence.shards, 0..) |shard, index| {
            // Add a new line to the last log message to separate the last shard
            // from the next message.
            if (index == essence.shards.len - 1) {
                log.debug("({s}) ({s}) ({s}) ({s}) ({s})\n", .{
                    shard.worldstone,
                    shard.name,
                    shard.launcher,
                    shard.flags,
                    shard.notes,
                });
            } else {
                log.debug("({s}) ({s}) ({s}) ({s}) ({s})", .{
                    shard.worldstone,
                    shard.name,
                    shard.launcher,
                    shard.flags,
                    shard.notes,
                });
            }
        }
    }
    log.debug("Use Wayland: {}\n", .{essence.use_wayland});
    log.debug("Show Standard Error Messages: {}\n", .{essence.show_stderr});
    log.debug("Enable Automatic Backups: {}\n", .{essence.enable_automatic_backups});
    log.debug("Last Selected Shard: {?}", .{essence.last_selected_shard});
    log.debug("Should Enable Running: {}", .{essence.should_enable_running});
    log.debug("Should Hide Mini Panel: {}", .{essence.should_hide_mini_panel});
    log.debug("Should Hide Help Menu Button: {}", .{essence.should_hide_help_menu_button});
    log.debug("Use Dark Mode: {}", .{essence.use_dark_mode});

    log.debug("[ End Printing Settings ]", .{});

    // Convert SettingsBean object to the full Settings object and return it.
    // We are taking ownership of all of the data.
    return try Settings.fromSettingsEssence(allocator, io, essence);
}

/// Save the settings to disk.
///
/// Caller owns memory.
pub fn save(self: Settings, allocator: std.mem.Allocator, io: std.Io, path_builder: PathBuilder) !void {
    // Get target save location.
    const settings_path = path_builder.getSettingsPath();
    log.debug("Saving settings to {s} ...", .{settings_path});

    // Convert settings into bean.
    const essence = try self.toSettingsEssence(allocator);

    // Save the bean.
    var out: std.Io.Writer.Allocating = .init(allocator);
    var serializer = std.json.Stringify{
        .writer = &out.writer,
        .options = .{ .whitespace = .indent_4 },
    };
    try serializer.write(essence);

    const serialized_data = out.written();
    log.debug("Serialized Data", .{});
    log.debug("{s}", .{serialized_data});

    const root_dir = try std.Io.Dir.openDirAbsolute(io, path_builder.getRootPath(), .{});
    defer root_dir.close(io);

    try root_dir.writeFile(io, .{ .sub_path = PathBuilder.SETTINGS_NAME, .data = serialized_data });
}

pub const UpdateSettingsOptions = struct {
    use_wayland: ?bool = null,
    show_stderr: ?bool = null,
    enable_automatic_backups: ?bool = null,
    wine_dll_overrides: ?[]const u8 = null,
    last_selected_shard: ??usize = null,
    backup_path: ?[]const u8 = null,
    should_enable_running: ?bool = null,
    should_hide_mini_panel: ?bool = null,
    should_hide_help_menu_button: ?bool = null,
    use_dark_mode: ?bool = null,
};

pub fn update(
    self: *Settings,
    allocator: std.mem.Allocator,
    io: std.Io,
    path_builder: PathBuilder,
    options: UpdateSettingsOptions,
) !Enums.UpdateSettingsStatusCodes {
    var aa: std.heap.ArenaAllocator = .init(allocator);
    const arena = aa.allocator();
    defer aa.deinit();

    // Backup path validation should happen before we do anything with memory
    // allocations and other settings. This also allows us to give the user
    // feedback earlier and not have them play the game and later find out they
    // can't backup to their desired location (even though we have emergency
    // backup fallback).
    if (options.backup_path) |new_bp| {
        if (!std.mem.eql(u8, new_bp, "")) {
            // The below logic attempts to automatically create the backup path
            // directory and confirm read, write, execute permissions. Any
            // missing parent directories will be properly created.
            log.debug("Testing if we can create the backup path ...", .{});
            _ = Filesystem.createDirectoryIfNeeded(io, new_bp) catch |e| switch (e) {
                error.NotAbsolutePathError => return Enums.UpdateSettingsStatusCodes.not_absolute_path,
                error.AccessDenied => return Enums.UpdateSettingsStatusCodes.access_denied,
                else => return e,
            };
            log.debug("Backup path is an absolute path and was either created successfully or already exists.", .{});

            log.debug("Testing for read, write, and execute access for: {s} ...", .{new_bp});
            Filesystem.testDirectoryAccess(io, new_bp) catch |e| switch (e) {
                error.AccessDenied => return Enums.UpdateSettingsStatusCodes.access_denied,
                else => return e,
            };
            log.debug("Access test was successful! You have read, write, and execute permissions for the backup path.", .{});
        }
    }

    // ------------------------------------------------------------------------

    if (options.use_wayland) |use_wayland| {
        self.use_wayland = use_wayland;
    }

    if (options.show_stderr) |show_stderr| {
        self.show_stderr = show_stderr;
    }

    if (options.enable_automatic_backups) |enable_automatic_backups| {
        self.enable_automatic_backups = enable_automatic_backups;
    }

    if (options.should_enable_running) |should_enable_running| {
        self.should_enable_running = should_enable_running;
    }

    if (options.should_hide_mini_panel) |should_hide_mini_panel| {
        self.should_hide_mini_panel = should_hide_mini_panel;
    }

    if (options.should_hide_help_menu_button) |should_hide_help_menu_button| {
        self.should_hide_help_menu_button = should_hide_help_menu_button;
    }

    if (options.use_dark_mode) |use_dark_mode| {
        self.use_dark_mode = use_dark_mode;
    }

    // This (new_wdo) will always be coming in from the frontend as an "". We
    // need to check for this and set it to null since we want a ?[]const u8 in
    // this case. Even though I can assert that here, I'm prefering the safety
    // that the application will continue to function at the expense of an extra
    // comparison over tying the backend more to the frontend, and potentially
    // risking a crash if some requirement were to change in the future.
    if (options.wine_dll_overrides) |new_wdo| {
        if (self.wine_dll_overrides) |old_wdo| {
            allocator.free(old_wdo);
            self.wine_dll_overrides = null;
        }

        if (!std.mem.eql(u8, new_wdo, "")) {
            self.wine_dll_overrides = try allocator.dupe(u8, new_wdo);
        }
    }

    if (options.last_selected_shard) |last_selected_shard| {
        self.last_selected_shard = last_selected_shard;
    }

    // The same behavior as wine_dll_overrides in terms of whitespace/null.
    if (options.backup_path) |new_bp| {
        if (self.backup_path) |old_bp| {
            allocator.free(old_bp);
            self.backup_path = null;
        }

        if (!std.mem.eql(u8, new_bp, "")) {
            self.backup_path = try allocator.dupe(u8, new_bp);
        }
    }

    try self.save(arena, io, path_builder);

    return .success;
}

/// These are the settings that we will launch Wine's process environment in.
///
/// Caller owns memory.
pub fn getWineSettings(
    self: Settings,
    allocator: std.mem.Allocator,
    environ: std.process.Environ,
    prefix_path: []const u8,
) !std.process.Environ.Map {
    var local_map = try environ.createMap(allocator);
    if (self.use_wayland) {
        try local_map.put("DISPLAY", "");
    }
    try local_map.put("WINEPREFIX", prefix_path);

    if (self.wine_dll_overrides) |wine_dll_overrides| {
        try local_map.put("WINEDLLOVERRIDES", wine_dll_overrides);
    }

    return local_map;
}

/// Add a new shard to the shards list. If an insertAtIndex is given, will insert
/// at that position rather than inserting at the end.
///
/// The same allocator that was used to keep the rest of the shards upon settings
/// loaded should be used.
pub fn addShard(self: *Settings, allocator: std.mem.Allocator, io: std.Io, shard: Shard, insertAtIndex: ?usize) !void {
    const cloned_shard = try Shard.copy(allocator, io, shard);
    if (insertAtIndex) |index| {
        try self.shards.insert(allocator, index, cloned_shard);
    } else {
        try self.shards.append(allocator, cloned_shard);
    }
}

/// Remove and deallocate a shard from the shards list.
pub fn removeShard(self: *Settings, allocator: std.mem.Allocator, index: usize) void {
    const removed_shard = self.shards.orderedRemove(index);
    defer removed_shard.deinit(allocator);
}

pub fn shiftShard(self: *Settings, index: usize, new_index: usize) !void {
    log.debug("Shifting shard from {d} to {d} ...", .{ index, new_index });
    const shard = self.shards.orderedRemove(index);
    try self.shards.insertBounded(new_index, shard);
}

/// Find a shard by the shard id and return a mutable pointer. This function is
/// designed with the assumption that there will definitely be a shard with
/// this shard id.
pub fn getShardById(self: Settings, id: []const u8) *Shard {
    for (self.shards.items) |*shard| {
        if (std.mem.eql(u8, id, shard.id)) {
            return shard;
        }
    }
    unreachable;
}

/// Get a pointer to the requested shard.
pub fn getShard(self: Settings, index: usize) *Shard {
    return &self.shards.items[index];
}

/// Get the length of the shards list.
pub fn getShardsLength(self: Settings) usize {
    return self.shards.items.len;
}

/// Check to see if any other shards have the same shard key. shards with the same
/// shard key are considered to be the same shard.
///
/// Caller owns memory.
pub fn isShardAliased(self: Settings, alias: []const u8) bool {
    for (self.shards.items) |shard| {
        if (std.mem.eql(u8, shard.alias, alias)) {
            log.debug("Shard is aliased.", .{});
            return true;
        }
    }
    log.debug("Shard not aliased.", .{});
    return false;
}

/// Check to see if any other shards are using the same platform.
///
/// Caller owns memory.
pub fn isWorldstoneAliased(self: Settings, worldstone: []const u8) bool {
    for (self.shards.items) |shard| {
        if (std.mem.eql(u8, shard.worldstone, worldstone)) {
            log.debug("Worldstone is aliased.", .{});
            return true;
        }
    }
    log.debug("Worldstone not aliased.", .{});
    return false;
}

fn shardsAsShardEssences(self: Settings, allocator: std.mem.Allocator) ![]ShardEssence {
    var essences: std.ArrayList(ShardEssence) = .empty;
    defer essences.deinit(allocator);
    for (self.shards.items) |shard| {
        try essences.append(allocator, shard.toShardEssence());
    }
    return try essences.toOwnedSlice(allocator);
}

pub fn areAnyAliasedShardsActive(self: Settings, shard: *const Shard) !bool {
    for (self.shards.items) |current_shard| {
        if (std.mem.eql(u8, current_shard.id, shard.id))
            continue;

        if (std.mem.eql(u8, current_shard.alias, shard.alias) and current_shard.active) {
            return true;
        }
    }
    return false;
}
