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

const ProcessManager = @This();

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

tracked_process_count: usize = 0,
tracked_process_count_lock: std.Io.Mutex,

// This will be a very simple "check in" system that isn't using a dictionary
// for simplicity since we don't currently need the extra performance. Trading
// performance for simplicity.. still fast enough even with O(n) scan to find
// the checked in tags.
tracked_process_shard_ids: std.ArrayList([]const u8),

pub fn init() ProcessManager {
    return .{
        .tracked_process_count = 0,
        .tracked_process_count_lock = .init,
        .tracked_process_shard_ids = .empty,
    };
}

pub fn deinit(self: *ProcessManager, allocator: std.mem.Allocator) void {
    self.tracked_process_shard_ids.deinit(allocator);
}

// When running a shard as a thread, we need to pass in the shard id, not the
// shard pointer itself, this is because people can continue to create/destroy
// shards in the UI, which can cause the underlying shards list to invalidate
// pointers. Thus, we need to re-find the shard matching our shard id. We are
// guaranteeing that an active shard id cannot be removed, so we should be able
// to dynamically retrieve the new shard index for the shard id.
pub fn run(
    self: *ProcessManager,
    allocator: std.mem.Allocator,
    io: std.Io,
    environ: std.process.Environ,
    settings: *Settings,
    path_builder: PathBuilder,
    shard: *Shard,
) !void {
    var handle = try std.Thread.spawn(.{}, runGame, .{
        allocator,
        io,
        environ,
        settings,
        path_builder,
        shard.id,
        &self.tracked_process_count,
        &self.tracked_process_count_lock,
        &self.tracked_process_shard_ids,
    });
    handle.detach();
}

// This function is not part of the class since I'm not able to pass a pointer
// to a struct method to a new thread.
fn runGame(
    allocator: std.mem.Allocator,
    io: std.Io,
    environ: std.process.Environ,
    settings: *const Settings,
    path_builder: PathBuilder,
    shard_id: []const u8,
    tracked_process_count: *usize,
    tracked_process_count_lock: *std.Io.Mutex,
    tracked_process_shard_ids: *std.ArrayList([]const u8),
) !void {
    var aa: std.heap.ArenaAllocator = .init(allocator);
    const arena = aa.allocator();
    defer aa.deinit();

    // Lock everything before we start.
    try tracked_process_count_lock.lock(io);
    var shard = settings.getShardById(shard_id);
    std.log.debug("Process Count (Before Opening): {d}", .{tracked_process_count.*});
    tracked_process_count.* += 1;
    std.log.debug("Adding Id: {s} ...", .{shard.id});
    try tracked_process_shard_ids.append(allocator, shard.id);
    shard.active = true;

    // TODO: LEAK: Sometimes a leak is coming from here. Need to fix. I have
    // the stack trace since GPA tells me, but need to find a reproducible
    // scenario. I think my solution will just be to try and not use the arena
    // in here and see how far that gets me before digging to deeper solutions.
    const shard_path = try path_builder.getShardPath(arena, shard);
    const prefix_path = try path_builder.getPrefixPath(arena, shard);

    // We need to change into the shard path's directory before we run the launcher.
    // This is so that the data directory, screenshots, and other locations that
    // Diablo II expects are in the correct location.
    try std.Io.Threaded.chdir(shard_path);

    var command = [_][]const u8{ "wine", shard.launcher, shard.flags };

    // We are trying to launch the process here but in a non-blocking way
    // since people can launch other instances of the game either on the same
    // exact shard or another shard simultaneously.
    const local_map = try settings.getWineSettings(arena, environ, prefix_path);
    std.log.debug("Process Count (After Opening): {d}", .{tracked_process_count.*});
    tracked_process_count_lock.unlock(io);

    try runAndWait(io, settings, &command, &local_map);

    try tracked_process_count_lock.lock(io);
    tracked_process_count.* -= 1;
    std.log.debug("Process Count (After Closing): {d}", .{tracked_process_count.*});

    // Refetch the shard by id since something could happened to the underlying
    // shards list that caused all existing pointers to be invalidated.
    shard = settings.getShardById(shard_id);

    if (is_shard_id_in_list(shard.id, tracked_process_shard_ids)) |key_to_delete| {
        std.log.debug("Key Found: {s}. Deleting ...", .{shard.id});
        _ = tracked_process_shard_ids.orderedRemove(key_to_delete);

        // If this was the last shard key for this shard, make it inactive.
        if (is_shard_id_in_list(shard.id, tracked_process_shard_ids) == null) {
            std.log.debug("This was the last entry for {s}. Making it inactive ...", .{shard.id});
            shard.active = false;

            // Create a backup if automatic backups are enabled and no other
            // aliased are running.
            if (settings.enable_automatic_backups and !is_aliased_shard_active(
                shard_id,
                settings,
                tracked_process_shard_ids,
            )) {
                const full_path_to_backup = try Filesystem.createBackup(allocator, io, settings, shard, &path_builder);
                defer allocator.free(full_path_to_backup);
            }
        }
    }

    // We good. Time to unlock.
    tracked_process_count_lock.unlock(io);
}

/// Check to see if a given shard id in the tracked list.
pub fn is_shard_id_in_list(key: []const u8, list: *const std.ArrayList([]const u8)) ?usize {
    for (list.items, 0..) |shard_id, index| {
        if (std.mem.eql(u8, key, shard_id)) {
            std.log.debug("found: {s}", .{key});
            return index;
        }
    }
    std.log.debug("Couldn't find in list: {s}", .{key});
    return null;
}

/// Prints the currently tracked shard ids.
pub fn print_current_shard_ids(list: *const std.ArrayList([]const u8)) void {
    for (list.items) |item| {
        std.log.debug("Shard ID: {s}", .{item});
    }
}

/// Checks to see if any of the currently running shards is an alias of the given.
pub fn is_aliased_shard_active(
    shard_id: []const u8,
    settings: *const Settings,
    list: *const std.ArrayList([]const u8),
) bool {
    // Get shard for by shard id
    const shard = settings.getShardById(shard_id);

    // Go through all of the currently running shard ids and see if the alias
    // matches the alias of the given shard id.
    for (list.items) |running_shard_id| {
        const running_shard = settings.getShardById(running_shard_id);

        if (std.mem.eql(u8, running_shard.alias, shard.alias)) {
            std.log.debug("Shard alias is currently active: {s}", .{shard.alias});
            return true;
        }
    }

    std.log.debug("No other shards matching this alias are running.", .{});
    return false;
}

/// Run a command in a new process and wait for completion.
pub fn runAndWait(io: std.Io, settings: *const Settings, command: [][]const u8, environ_map: ?*const std.process.Environ.Map) !void {
    var proc = try std.process.spawn(
        io,
        switch (settings.show_stderr) {
            true => .{ .argv = command, .environ_map = environ_map },
            false => .{ .argv = command, .environ_map = environ_map, .stderr = .ignore },
        },
    );
    const term = try proc.wait(io);
    if (term != .exited) {
        return error.ProcessFailure;
    }
}
