// Minimal repro of the lock shape from the t15 ledger run: two account
// mutexes taken in a fixed order, the second one inside `if (low != high)`.
// One thread transfers 0 -> 1, another deposits into 1. Same Debug build
// mode the benchmark scored in. Zig 0.16.0.
const std = @import("std");
const Io = std.Io;

const Account = struct {
    mutex: Io.Mutex = .init,
    balance: i64 = 1_000_000,
};

const Variant = enum { buggy, fixed };

const Ledger = struct {
    io: Io,
    accounts: [2]Account = .{ .{}, .{} },

    fn deposit(l: *Ledger, id: usize, amount: i64) void {
        const a = &l.accounts[id];
        a.mutex.lockUncancelable(l.io);
        defer a.mutex.unlock(l.io);
        const before = a.balance;
        a.balance = before + amount;
    }

    // The shape from the run: the second lock's defer lives inside the if block.
    fn transferBuggy(l: *Ledger, from: usize, to: usize, amount: i64) void {
        const low = &l.accounts[@min(from, to)];
        const high = &l.accounts[@max(from, to)];
        low.mutex.lockUncancelable(l.io);
        defer low.mutex.unlock(l.io);
        if (low != high) {
            high.mutex.lockUncancelable(l.io);
            defer high.mutex.unlock(l.io);
        }
        l.move(from, to, amount);
    }

    // Same guard, moved inside the defer instead of around it.
    fn transferFixed(l: *Ledger, from: usize, to: usize, amount: i64) void {
        const low = &l.accounts[@min(from, to)];
        const high = &l.accounts[@max(from, to)];
        low.mutex.lockUncancelable(l.io);
        defer low.mutex.unlock(l.io);
        if (low != high) high.mutex.lockUncancelable(l.io);
        defer if (low != high) high.mutex.unlock(l.io);
        l.move(from, to, amount);
    }

    fn move(l: *Ledger, from: usize, to: usize, amount: i64) void {
        const src = l.accounts[from].balance;
        const dst = l.accounts[to].balance;
        l.accounts[from].balance = src - amount;
        l.accounts[to].balance = dst + amount;
    }
};

const n = 1_000_000;

fn transfers(l: *Ledger, v: Variant) void {
    for (0..n) |_| switch (v) {
        .buggy => l.transferBuggy(0, 1, 1),
        .fixed => l.transferFixed(0, 1, 1),
    };
}

fn deposits(l: *Ledger) void {
    for (0..n) |_| l.deposit(1, 1);
}

fn run(io: Io, v: Variant) !void {
    var l: Ledger = .{ .io = io };
    const t1 = try std.Thread.spawn(.{}, transfers, .{ &l, v });
    const t2 = try std.Thread.spawn(.{}, deposits, .{&l});
    t1.join();
    t2.join();
    const want: i64 = 1_000_000 + 2 * n;
    const got = l.accounts[1].balance;
    std.debug.print("{t}: account 1 = {d}, want {d}, missing {d}\n", .{ v, got, want, want - got });
}

pub fn main(init: std.process.Init) !void {
    try run(init.io, .buggy);
    try run(init.io, .fixed);
}
