// 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 = @This();

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

/// This struct must be serializable.
pub const ShardEssence = struct {
    worldstone: []const u8,
    name: []const u8,
    launcher: []const u8,
    flags: []const u8,
    notes: []const u8,
};

// ===== The following are internal properties that are not serialized =====

// The id uniquely identifies the shard. It is primarily used for process
// management and other safety checks.
id: []const u8,

// This represents whether or not there is an active process running from this
// shard. It is primarily used for process management and other safety checks.
active: bool = false,

// The alias is a combination of the worldstone + shard. Any shards using the same
// alias are considered to be the same shard. Essentially it allows you to have
// multiple shard entries pointing to the same files and same saves, but you
// can adjust your flags (one with sound and one without sound for example).
//
// The alias is also the subpath to the Prefix and Saves. For example, if the
// worldstone is: "1.05b" and the shard is "Leslie", then the alias is
// "1.05b/Leslie", which correspondings to Saves/1.05b/Leslie and Prefixes/1.05b/Leslie.
alias: []const u8,

// ===== The following properties are serialized and exposed to the user =====

// The core worldstone that should be used for the DLLs, etc.
worldstone: []const u8,

// The shard name. This allows you to have separate shards and saves while
// sharing the same foundational worldstone.
name: []const u8,

// The executable that should be executed when running the game.
// This is usually "Game.exe".
launcher: []const u8,

// The flags to launch the game with: -ns, -w, -direct -txt, ...
flags: []const u8,

// A field for your own notes.
notes: []const u8,

/// Caller owns memory.
pub fn init(
    allocator: std.mem.Allocator,
    io: std.Io,
    worldstone: []const u8,
    name: []const u8,
    launcher: []const u8,
    flags: []const u8,
    notes: []const u8,
) !Shard {
    // Check if all required fields are provided.
    if (std.mem.eql(u8, worldstone, "")) return error.RequiredFieldMissing;
    if (std.mem.eql(u8, name, "")) return error.RequiredFieldMissing;
    if (std.mem.eql(u8, launcher, "")) return error.RequiredFieldMissing;

    // Ensure only valid characters are used. Notes can have any value. We just
    // don't want to have any path or launcher process issues with UNIX or Wine.
    if (!isValueValid(worldstone)) return error.InvalidCharacterDetected;
    if (!isValueValid(name)) return error.InvalidCharacterDetected;
    if (!isValueValid(launcher)) return error.InvalidCharacterDetected;
    if (!isValueValid(flags)) return error.InvalidCharacterDetected;

    return .{
        .id = try createRandomShardKey(allocator, io),
        .worldstone = try allocator.dupe(u8, worldstone),
        .name = try allocator.dupe(u8, name),
        .alias = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ worldstone, name }),
        .launcher = try allocator.dupe(u8, launcher),
        .flags = try allocator.dupe(u8, flags),
        .notes = try allocator.dupe(u8, notes),
    };
}

/// Caller owns memory.
pub fn copy(allocator: std.mem.Allocator, io: std.Io, shard: Shard) !Shard {
    return try init(
        allocator,
        io,
        shard.worldstone,
        shard.name,
        shard.launcher,
        shard.flags,
        shard.notes,
    );
}

/// Caller owns memory.
pub fn fromShardEssence(allocator: std.mem.Allocator, io: std.Io, essence: ShardEssence) !Shard {
    return try init(
        allocator,
        io,
        essence.worldstone,
        essence.name,
        essence.launcher,
        essence.flags,
        essence.notes,
    );
}

pub fn toShardEssence(self: Shard) ShardEssence {
    return .{
        .worldstone = self.worldstone,
        .name = self.name,
        .launcher = self.launcher,
        .flags = self.flags,
        .notes = self.notes,
    };
}

pub fn modify(
    self: *Shard,
    allocator: std.mem.Allocator,
    launcher: []const u8,
    flags: []const u8,
    notes: []const u8,
) !void {
    // Run validations before we do any deallocations, etc.
    if (std.mem.eql(u8, launcher, "")) return error.RequiredFieldMissing;

    if (!isValueValid(launcher)) return error.InvalidCharacterDetected;
    if (!isValueValid(flags)) return error.InvalidCharacterDetected;

    // Deallocate the old values and take ownership of the new ones.
    allocator.free(self.launcher);
    allocator.free(self.flags);
    allocator.free(self.notes);

    self.launcher = try allocator.dupe(u8, launcher);
    self.flags = try allocator.dupe(u8, flags);
    self.notes = try allocator.dupe(u8, notes);
}

pub fn deinit(self: Shard, allocator: std.mem.Allocator) void {
    allocator.free(self.id);
    allocator.free(self.worldstone);
    allocator.free(self.name);
    allocator.free(self.alias);
    allocator.free(self.launcher);
    allocator.free(self.flags);
    allocator.free(self.notes);
}

pub fn print(self: Shard) void {
    log.debug("{s} - ({s}) ({s}) ({s}) ({s}) ({s}) ({s})", .{
        self.id,
        self.worldstone,
        self.name,
        self.alias,
        self.launcher,
        self.flags,
        self.notes,
    });
}

/// Check to see if the text valid. Only a limited set of characters are allowed
/// since they all need to work for both file names and wine prefix. We aren't
/// adding additional strictness to the Flags at the moment.
///
/// Allowed characters:
///
/// Alphanumeric, Spaces, Dashes, Underscores, Single Quotes, Dots, and Parenthesis.
///
/// Example:
///
/// (Leslie) In (Space)  _ Ok -- Cool 123's Cool .. -- __ ' cool '
fn isValueValid(text: []const u8) bool {
    for (text) |letter| {
        if (!std.ascii.isAlphanumeric(letter)) {
            log.debug("Not alphanumeric but may still be valid. Performing further checks: {c}", .{letter});
            var is_valid = false;
            const allowed_symbols = [_]u8{ ' ', '-', '_', '\'', '.', '(', ')' };
            for (allowed_symbols) |symbol| {
                if (letter == symbol) {
                    log.debug("{c} is a valid allowed symbol. Allowing it through.", .{letter});
                    is_valid = true;
                    break;
                }
            }
            if (!is_valid) {
                log.warn("Invalid Character Detected!\n\nText: {s}\nInvalid Value: {c}\n", .{ text, letter });
                return false;
            }
        }
    }
    return true;
}

/// Create a random shard key. This should be universally unique. Ideally a UUID,
/// but I don't want to bring in another dependency into the project atm. We
/// are not currently implementing any verification as to whether or not the
/// generated key is actually universal. What are the chances? lol.
///
/// Caller owns memory.
pub fn createRandomShardKey(allocator: std.mem.Allocator, io: std.Io) ![]const u8 {
    var rng: std.Random.IoSource = .{ .io = io };
    const random = rng.interface();

    const part_1 = random.intRangeAtMostBiased(u32, 1_000_000_000, 3_999_999_999);
    const part_2 = random.intRangeAtMostBiased(u32, 1_000_000_000, 3_999_999_999);
    const key = try std.fmt.allocPrint(allocator, "{d}{d}", .{ part_1, part_2 });
    log.debug("generated shard key: {s}", .{key});
    return key;
}

// TODO: possibly add verification to ensure two shards cannot have the same id,
// even if the chances are really low.

// TODO: maybe make both copy functions generic? We may need it to be different
// if we want to keep any specific Shard internal values.
