const std = @import("std");
const logu = @import("logu");
const ScRaw = @import("scraw").ScRaw;
const raylib = @import("raylib");
const zigimg = @import("zigimg");
const c332 = @import("c332");
const args = @import("args");

const capture__video: bool = false; // Whether to dump frames to TGA images during playback.
const capture__audio: bool = false; // Whether to dump audio sample to file.
const status_bar__enable: bool = false;
const gfx__block__length: u16 = 252; // From GFX applet: Gfx.java => BLOCK_LENGTH
const msx_play__block__length: u16 = 256; // From MSX applet: Msx.java => BLOCK_LENGTH
const msx_play__chunk_a__count: u16 = 127; // From MSX applet: Msx.java => SAMPLE_A_CHUNK_COUNT
const msx_play__chunk_b__count: u16 = 26; // From MSX applet: Msx.java => SAMPLE_B_CHUNK_COUNT
const msx_load__block__length: u16 = 255; // Can be anything <=255
const raylib__sampling_rate: u16 = 11025;
const raylib__audio__buffer_size: i32 = raylib__sampling_rate;

pub fn usage(arg_0: ?[]const u8) void {
    logu.logzScope(logu.logz.info(), @This(), @src()).fmt("usage", "{s} --log-path log_file_path --card-gfx-0 reader_name --card-gfx-1 reader_name --card-gfx-2 reader_name --card-gfx-3 reader_name --card-msx reader_name [--card-msx-init --sample-1 pcm_u8_11025_path --sample-2 pcm_u8_11025_path --sample-3 pcm_u8_11025_path --sample-4 pcm_u8_11025_path --sample-5 pcm_u8_11025_path]", .{arg_0 orelse "exe"}).log();
}

var audio__buffer: [@as(usize, msx_play__block__length) * @as(usize, (msx_play__chunk_a__count + msx_play__chunk_b__count)) * 5 * 1000]u8 = undefined;
var audio__buffer__reader__index: usize = 0;
var audio__buffer__writer__index: usize = 0;
var audio_stream: raylib.AudioStream = undefined;
fn audioCallback(buffer: ?*anyopaque, frames: c_uint) callconv(.C) void {
    if (audio__buffer__reader__index + frames < audio__buffer.len) {
        const d: [*]u8 = @alignCast(@ptrCast(buffer orelse return));
        std.mem.copyForwards(u8, d[0..frames], audio__buffer[audio__buffer__reader__index .. audio__buffer__reader__index + frames]);
        logu.logzScope(logu.logz.info(), @This(), @src()).string("audio", "reader").int("frames", frames).int("reader__index", audio__buffer__reader__index).log();
        audio__buffer__reader__index += frames;
    }
}

const MainError = error{
    ArgProcessFailed,
    ArgMissing,
    AppletSelectFailed,
    AppletResponseFailed,
    CardSelectFailed,
    SampleLengthMismatch,
};
pub fn main() !void {
    var allocator__gp = std.heap.GeneralPurposeAllocator(.{}){};
    defer if (allocator__gp.deinit() == .leak) {
        logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "General purpose allocator leaked!").log();
    };
    const allocator = allocator__gp.allocator();

    try logu.logz.setup(allocator, .{});
    defer logu.logz.deinit();

    const options = args.parseForCurrentProcess(struct {
        @"log-path": ?[]const u8 = null,
        @"card-gfx-0": ?[]const u8 = null,
        @"card-gfx-1": ?[]const u8 = null,
        @"card-gfx-2": ?[]const u8 = null,
        @"card-gfx-3": ?[]const u8 = null,
        @"card-msx": ?[]const u8 = null,
        @"card-msx-init": bool = false,
        @"sample-0": ?[]const u8 = null,
        @"sample-1": ?[]const u8 = null,
        @"sample-2": ?[]const u8 = null,
        @"sample-3": ?[]const u8 = null,
        @"sample-4": ?[]const u8 = null,
        @"sample-5": ?[]const u8 = null,
    }, allocator, .print) catch {
        return MainError.ArgProcessFailed;
    };
    defer options.deinit();

    if (options.options.@"log-path" == null) {
        logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Log path missing.").log();
        usage(options.executable_name);
        return MainError.ArgMissing;
    }

    try logu.logz.setup(allocator, .{ .output = .{ .file = options.options.@"log-path".? } });

    var card_msx: *ScRaw = undefined;
    var job_msx__context: JobMsxContext = undefined;
    var job_msx__thread: std.Thread = undefined;
    if (options.options.@"card-msx" != null) {
        card_msx = try cardSelect(allocator, options.options.@"card-msx".?) orelse return MainError.CardSelectFailed;
        job_msx__context = .{
            .run = true,
            .pause = true,
            .scraw = card_msx,
        };
        job_msx__thread = try std.Thread.spawn(.{}, jobMsx, .{&job_msx__context});
    }
    // Cleanup thread when we are done.
    defer {
        if (options.options.@"card-msx" != null) {
            job_msx__context.run = false;
            card_msx.deinit(allocator) catch {};
            job_msx__thread.join();
        }
    }

    if (options.options.@"card-msx" != null) {
        if (options.options.@"card-msx-init" == true) {
            // User requested to initialize the MSX card.

            // XXX: DO NOT PERFORM THIS STEP IF IT IS NOT NEEDED. THE CARD HAS A LIMITED NUMBER OF FLASH WRITE CYCLES.

            if (options.options.@"sample-0" == null) {
                // Sample 0 missing (it's not used so continuing).
                logu.logzScope(logu.logz.warn(), @This(), @src()).string("message", "Sample 0 is not used!").log();
            }
            if (options.options.@"sample-1" == null) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 1 missing when requesting MSX card init.").log();
                usage(options.executable_name);
                return MainError.ArgMissing;
            }
            if (options.options.@"sample-2" == null) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 2 missing when requesting MSX card init.").log();
                usage(options.executable_name);
                return MainError.ArgMissing;
            }
            if (options.options.@"sample-3" == null) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 3 missing when requesting MSX card init.").log();
                usage(options.executable_name);
                return MainError.ArgMissing;
            }
            if (options.options.@"sample-4" == null) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 4 missing when requesting MSX card init.").log();
                usage(options.executable_name);
                return MainError.ArgMissing;
            }
            if (options.options.@"sample-5" == null) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 5 missing when requesting MSX card init.").log();
                usage(options.executable_name);
                return MainError.ArgMissing;
            }
            const sample_1 = try std.fs.cwd().readFileAlloc(allocator, options.options.@"sample-1".?, std.math.pow(usize, 2, 16));
            defer allocator.free(sample_1);
            const sample_2 = try std.fs.cwd().readFileAlloc(allocator, options.options.@"sample-2".?, std.math.pow(usize, 2, 16));
            defer allocator.free(sample_2);
            const sample_3 = try std.fs.cwd().readFileAlloc(allocator, options.options.@"sample-3".?, std.math.pow(usize, 2, 16));
            defer allocator.free(sample_3);
            const sample_4 = try std.fs.cwd().readFileAlloc(allocator, options.options.@"sample-4".?, std.math.pow(usize, 2, 16));
            defer allocator.free(sample_4);
            const sample_5 = try std.fs.cwd().readFileAlloc(allocator, options.options.@"sample-5".?, std.math.pow(usize, 2, 16));
            defer allocator.free(sample_5);

            const sample__size = (msx_play__chunk_a__count + msx_play__chunk_b__count) * msx_play__block__length; // 11025 Hz u8 PCM.
            if (sample_1.len != sample__size) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 1 size invalud.").int("expected", sample__size).int("actual", sample_1.len).log();
                return MainError.SampleLengthMismatch;
            }
            if (sample_2.len != sample__size) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 2 size invalud.").int("expected", sample__size).int("actual", sample_2.len).log();
                return MainError.SampleLengthMismatch;
            }
            if (sample_3.len != sample__size) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 3 size invalud.").int("expected", sample__size).int("actual", sample_3.len).log();
                return MainError.SampleLengthMismatch;
            }
            if (sample_4.len != sample__size) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 4 size invalud.").int("expected", sample__size).int("actual", sample_4.len).log();
                return MainError.SampleLengthMismatch;
            }
            if (sample_5.len != sample__size) {
                logu.logzScope(logu.logz.err(), @This(), @src()).string("message", "Sample 5 size invalud.").int("expected", sample__size).int("actual", sample_5.len).log();
                return MainError.SampleLengthMismatch;
            }

            // Load samples onto the card.
            try msxLoad(card_msx, 1, sample_1);
            try msxLoad(card_msx, 2, sample_2);
            try msxLoad(card_msx, 3, sample_3);
            try msxLoad(card_msx, 4, sample_4);
            try msxLoad(card_msx, 5, sample_5);
        }

        raylib.initAudioDevice();
        raylib.setAudioStreamBufferSizeDefault(raylib__audio__buffer_size);
        audio_stream = raylib.loadAudioStream(raylib__sampling_rate, 8, 1);
        raylib.setAudioStreamVolume(audio_stream, 1.0);
        raylib.setAudioStreamCallback(audio_stream, audioCallback);

        job_msx__context.pause = false;
    }

    const card_gfx__count_max = 4;
    var card_gfx: [card_gfx__count_max]*ScRaw = undefined;
    var job_gfx__context: [card_gfx__count_max]JobGfxContext = undefined;
    var job_gfx__thread: [card_gfx__count_max]std.Thread = undefined;
    var card_gfx_select__count: u8 = 0;
    var card_gfx__count: u16 = 0;

    {
        const card_gfx__name__list = [card_gfx__count_max]?[]const u8{
            options.options.@"card-gfx-0",
            options.options.@"card-gfx-1",
            options.options.@"card-gfx-2",
            options.options.@"card-gfx-3",
        };
        for (0..card_gfx__count_max) |card_gfx__index| {
            // Stop processing cards on first null value. This prevents cards from being specified out of order.
            if (card_gfx__name__list[card_gfx__index] == null) {
                break;
            }
            card_gfx__count += 1;

            card_gfx[card_gfx__index] = cardSelect(allocator, card_gfx__name__list[card_gfx__index].?) catch {
                break;
            } orelse {
                break;
            };
            job_gfx__context[card_gfx__index] = .{
                .id = @intCast(card_gfx__index),
                .run = true,
                .pause = true,
                .scraw = card_gfx[card_gfx__index],
                .done = std.atomic.Value(bool).init(false),
                .frame__index = 0,
                .block__index = 0,
                .rapdu__scanline = undefined,
            };
            job_gfx__thread[card_gfx__index] = try std.Thread.spawn(.{}, jobGfx, .{&job_gfx__context[card_gfx__index]});
            card_gfx_select__count += 1;
        }
        // In case not every card got connected.
        if (card_gfx_select__count != card_gfx__count) {
            for (0..card_gfx_select__count) |card_gfx__index| {
                card_gfx[card_gfx__index].deinit(allocator) catch {};
            }
            return MainError.CardSelectFailed;
        }
    }
    // Cleanup threads when we are done.
    defer for (0..card_gfx__count) |card_gfx__index| {
        job_gfx__context[card_gfx__index].run = false;
        card_gfx[card_gfx__index].deinit(allocator) catch {};
        job_gfx__thread[card_gfx__index].join();
    };

    const frame__width = 36;
    const frame__height = 14;
    const packet__block__scanline__count = 7;
    const packet__block__count = frame__height / packet__block__scanline__count;

    var frame__buffer: [frame__height][frame__width]u8 = undefined;

    const display__statusbar__height = 30;
    const display__scale = 10;
    const display__width = frame__width * display__scale;
    const display__height = (frame__height * display__scale) + display__statusbar__height;

    raylib.initWindow(display__width, display__height, "Gosia");
    defer raylib.closeWindow();
    raylib.setTargetFPS(31 * 2); // Each frame requires 2 blocks to fully update.

    {
        // Set initial state for each card.
        for (0..card_gfx__count) |card_gfx__index| {
            const block__index__next__tmp: u16 = @intCast(card_gfx__index);
            const frame__index__next: u16 = block__index__next__tmp / packet__block__count;
            const block__index__next: u16 = block__index__next__tmp % packet__block__count;

            job_gfx__context[card_gfx__index].frame__index = frame__index__next;
            job_gfx__context[card_gfx__index].block__index = block__index__next;
            job_gfx__context[card_gfx__index].pause = false;
        }
    }

    var global__frame__index: u16 = 0;
    var global__block__index: u16 = 0;
    const global__time__start: i128 = std.time.nanoTimestamp();
    var frame_time_delta__last: i128 = global__time__start;
    var frame_index__last: u16 = global__frame__index;
    var frame_time_delta__list: [10]i128 = undefined;
    var frame_time_delta__list__index: usize = 0;
    for (0..frame_time_delta__list.len) |idx| {
        frame_time_delta__list[idx] = 0;
    }
    var rapdu__buffer = std.mem.zeroes([ScRaw.bufferReceiveLengthMax]u8);

    var image = try zigimg.Image.create(allocator, 1920, 1080, .rgb24);
    defer image.deinit();
    for (0..image.pixels.rgb24.len) |i| {
        image.pixels.rgb24[i] = .{
            .r = 0,
            .g = 0,
            .b = 0,
        };
    }

    var audioPlay: bool = false;
    if (card_gfx__count == 0) {
        raylib.playAudioStream(audio_stream);
        audioPlay = true;
    }
    while (!raylib.windowShouldClose()) {
        logu.logzScope(logu.logz.info(), @This(), @src()).fmt("global", "{}:{}", .{ global__frame__index, global__block__index }).log();
        {
            raylib.beginDrawing();
            defer raylib.endDrawing();

            if (capture__video) {
                for (0..frame__height) |y| {
                    for (0..frame__width) |x| {
                        const c = frame__buffer[y][x];
                        const c888 = c332.c332u8c888u8(c);

                        for (0..10) |ys| {
                            for (0..10) |xs| {
                                image.pixels.rgb24[((((y * 10) + ys) + 470) * 1920) + ((x * 10) + 780 + xs)] = .{
                                    .r = c888[0],
                                    .g = c888[1],
                                    .b = c888[2],
                                };
                            }
                        }
                    }
                }
                var image__filename__buffer: [100]u8 = undefined;
                const image__filename = try std.fmt.bufPrint(&image__filename__buffer, "{d}.tga", .{global__frame__index});
                try image.writeToFilePath(image__filename, .{ .tga = .{} });

                // ffmpeg -framerate 100 -i %d.tga -c:v libx264 -r 50 out.mov
            }

            // Outer loop ensures that the entire frame is generated before showing the frame (this way there is no tearing).
            for (0..2) |_| {
                for (0..card_gfx__count) |card_gfx__index| {
                    // Skip any card that is currently working on a different frame than the one that will be displayed.
                    if (job_gfx__context[card_gfx__index].frame__index != global__frame__index or job_gfx__context[card_gfx__index].block__index != global__block__index) {
                        continue;
                    }
                    // Wait until this scanline is done.
                    while (job_gfx__context[card_gfx__index].done.load(.seq_cst) != true) {
                        std.time.sleep(100);
                    }

                    std.mem.copyForwards(u8, &rapdu__buffer, job_gfx__context[card_gfx__index].rapdu__scanline);
                    const card__block__index = job_gfx__context[card_gfx__index].block__index;

                    // Give the card another job.
                    const block__index__next__tmp = job_gfx__context[card_gfx__index].block__index + card_gfx__count;
                    const block__index__next = block__index__next__tmp % packet__block__count;
                    const frame__index__next = job_gfx__context[card_gfx__index].frame__index + (block__index__next__tmp / packet__block__count);
                    logu.logzScope(logu.logz.info(), @This(), @src()).int("id", card_gfx__index).fmt("frame", "{}:{} -> {}:{}", .{ job_gfx__context[card_gfx__index].frame__index, job_gfx__context[card_gfx__index].block__index, frame__index__next, block__index__next }).log();
                    job_gfx__context[card_gfx__index].frame__index = frame__index__next;
                    job_gfx__context[card_gfx__index].block__index = block__index__next;
                    job_gfx__context[card_gfx__index].done.store(false, .seq_cst);

                    for (0..packet__block__scanline__count) |line| {
                        for (0..frame__width) |x| {
                            const color = rapdu__buffer[(line * frame__width) + x];
                            const r: u8 = ((color >> 5) & 0b0000_0111) * (256 / 8);
                            const g: u8 = ((color >> 2) & 0b0000_0111) * (256 / 8);
                            const b: u8 = ((color >> 0) & 0b0000_0011) * (256 / 4);
                            const xRect: i32 = @intCast(x * display__scale);
                            const yRect: i32 = @intCast((line + card__block__index * packet__block__scanline__count) * display__scale);
                            raylib.drawRectangle(xRect, yRect, display__scale, display__scale, raylib.Color.init(r, g, b, 0xFF));
                            frame__buffer[line + card__block__index * packet__block__scanline__count][x] = color;
                        }
                    }

                    if (audioPlay == false) {
                        raylib.playAudioStream(audio_stream);
                        audioPlay = true;
                    }
                    const global__block__index__tmp = global__block__index + 1;
                    global__frame__index += global__block__index__tmp / packet__block__count;
                    global__block__index = global__block__index__tmp % packet__block__count;
                    break;
                }
            }

            // Draw status bar (below the frame).
            if (status_bar__enable and (frame_index__last != global__frame__index)) {
                frame_index__last = global__frame__index;

                raylib.drawRectangle(0, frame__height * display__scale, display__width, display__statusbar__height, raylib.Color.black);

                const frame_time__tmp = std.time.nanoTimestamp();
                frame_time_delta__list[frame_time_delta__list__index] = frame_time__tmp - frame_time_delta__last;
                frame_time_delta__last = frame_time__tmp;
                frame_time_delta__list__index += 1;
                frame_time_delta__list__index = frame_time_delta__list__index % frame_time_delta__list.len;

                // We calculate FPS by finding the delta between last n frames and finding the average delta, then using that average delta to compute the average FPS.
                var frame_time_delta__sum: i128 = 0;
                for (frame_time_delta__list) |frame_time_delta| {
                    frame_time_delta__sum += frame_time_delta;
                }

                const frame_time_delta__average: f32 = @as(f32, @floatFromInt(frame_time_delta__sum)) / @as(f32, @floatFromInt(frame_time_delta__list.len));
                const fps: f32 = 1.0 / (frame_time_delta__average / std.time.ns_per_s);

                var text__buffer: [100]u8 = undefined;
                const fps__text: [*:0]const u8 = try std.fmt.bufPrintZ(&text__buffer, "{d:0>6.2}", .{fps});
                raylib.drawText(fps__text, 0, frame__height * display__scale, display__statusbar__height, raylib.Color.lime);
                const audio__text: [*:0]const u8 = try std.fmt.bufPrintZ(&text__buffer, "{d:0>6}", .{if (audio__buffer__writer__index > audio__buffer__reader__index) audio__buffer__writer__index - audio__buffer__reader__index else 0});
                raylib.drawText(audio__text, 15 * display__scale, frame__height * display__scale, display__statusbar__height, raylib.Color.dark_green);
            }
        }
    }
}

fn cardSelect(allocator: std.mem.Allocator, reader_name__search: []const u8) !?*ScRaw {
    const scraw = try ScRaw.init(allocator);
    errdefer scraw.deinit(allocator) catch {};

    logu.logzScope(logu.logz.info(), @This(), @src()).string("reader__name__search", reader_name__search).log();

    try scraw.readerSearchStart();
    var reader_found: bool = false;
    while (true) {
        const reader_name: []const u8 = scraw.readerSearchNext() catch |err| {
            if (err == ScRaw.ReaderSearchNextError.ReaderSearchEndOfList) {
                break;
            } else {
                return err;
            }
        };
        logu.logzScope(logu.logz.info(), @This(), @src()).string("reader__next", reader_name).log();
        if (std.mem.eql(u8, reader_name, reader_name__search)) {
            logu.logzScope(logu.logz.info(), @This(), @src()).string("reader__select", reader_name).log();
            scraw.readerSelect(reader_name);
            scraw.readerSearchEnd();
            reader_found = true;
            break;
        }
    }
    if (!reader_found) {
        try scraw.deinit(allocator);
        logu.logzScope(logu.logz.err(), @This(), @src()).string("Message", "Could not find the desired reader.").log();
        return null;
    }

    logu.logzScope(logu.logz.info(), @This(), @src()).string("Message", "Connecting to card...").log();
    scraw.cardConnect(.T0) catch |e| {
        logu.logzScope(logu.logz.info(), @This(), @src()).string("Message", "Connecting to card failed. Make sure that \"pcscd\" (PCSC daemon) is running, and ensure the card is being detected by the reader, e.g., with \"pcsc_scan -c\".").log();
        return e;
    };
    errdefer scraw.cardDisconnect() catch {
        logu.logzScope(logu.logz.err(), @This(), @src()).string("Message", "Disconnecting from card failed.").log();
    };

    return scraw;
}

const JobGfxContext = struct {
    id: u8,
    run: bool,
    pause: bool,
    scraw: *ScRaw,
    done: std.atomic.Value(bool),
    frame__index: u16,
    block__index: u16,
    rapdu__scanline: []u8,
};

fn jobGfx(context: *JobGfxContext) void {
    const apdu__cla = 0xA0;
    const apdu__ins__base = 0xB0;
    var rapdu__buffer = std.mem.zeroes([gfx__block__length + 2]u8);

    // Select our ADF (Application Dedicated File), i.e. the GFX applet.
    var capdu__select = [_]u8{ 0x00, 0xA4, 0x04, 0x00, 0x08, 0xF1, 0x93, 0x57, 0x11, 0x03, 0xB9, 0x05, 0x1A };
    const rapdu__select = context.scraw.cardTransceive(capdu__select[0..], rapdu__buffer[0..]) catch {
        context.run = false;
        return;
    };
    if (rapdu__select[0] != 0x90 and rapdu__select[1] != 0x00) {
        context.run = false;
        return;
    }

    while (context.run) {
        std.time.sleep(100);
        if (context.pause == true or context.done.load(.seq_cst) == true) {
            continue;
        }

        const ins: u8 = @intCast(apdu__ins__base + context.block__index);
        var capdu__scanline = [_]u8{ apdu__cla, ins, std.mem.asBytes(&context.frame__index)[1], std.mem.asBytes(&context.frame__index)[0], gfx__block__length };
        context.rapdu__scanline = context.scraw.cardTransceive(capdu__scanline[0..], rapdu__buffer[0..]) catch {
            logu.logzScope(logu.logz.info(), @This(), @src()).int("id", context.id).string("message", "Failed to transceive. Stopping...").log();
            context.run = false;
            return;
        };
        logu.logzScope(logu.logz.info(), @This(), @src()).int("id", context.id).fmt("frame", "{}:{}", .{ context.frame__index, context.block__index }).log();
        logu.logzScope(logu.logz.info(), @This(), @src()).fmt("capdu__scanline", "{s}", .{std.fmt.fmtSliceHexUpper(&capdu__scanline)}).fmt("rapdu__scanline", "{s}", .{std.fmt.fmtSliceHexUpper(context.rapdu__scanline)}).log();
        if (context.rapdu__scanline.len != gfx__block__length + 2 or !(context.rapdu__scanline[context.rapdu__scanline.len - 2] == 0x90 and context.rapdu__scanline[context.rapdu__scanline.len - 1] == 0x00)) {
            logu.logzScope(logu.logz.info(), @This(), @src()).int("id", context.id).string("message", "Response is invalid. Stopping...").log();
            context.run = false;
            return;
        }

        context.done.store(true, .seq_cst);
    }
}

const JobMsxContext = struct {
    run: bool,
    pause: bool,
    scraw: *ScRaw,
};

fn jobMsx(context: *JobMsxContext) void {
    const audio__filename = "capture__audio.u8";
    var audio__file: ?std.fs.File = null;
    if (capture__audio) {
        audio__file = std.fs.cwd().createFile(audio__filename, .{
            .read = false,
            .truncate = true,
            .exclusive = false,
            .lock = .none,
            .lock_nonblocking = false,
            .mode = std.fs.File.default_mode,
        }) catch {
            context.run = false;
            return;
        };
    }
    defer if (capture__audio) {
        audio__file.?.close();
    };

    const apdu__cla = 0xA0;
    const apdu__ins = 0xCF;
    var rapdu__buffer = std.mem.zeroes([msx_play__block__length + 2]u8);

    var message__index: u16 = 0;

    // Select our ADF (Application Dedicated File), i.e. the MSX applet.
    var capdu__select = [_]u8{ 0x00, 0xA4, 0x04, 0x00, 0x08, 0xF1, 0x93, 0x57, 0x11, 0x07, 0xB9, 0x05, 0x1A };
    const rapdu__select = context.scraw.cardTransceive(capdu__select[0..], rapdu__buffer[0..]) catch {
        context.run = false;
        return;
    };
    var sw0: u8 = rapdu__select[0];
    var sw1: u8 = rapdu__select[1];
    if (sw0 != 0x90 and sw1 != 0x00) {
        context.run = false;
        return;
    }

    while (context.run) {
        std.time.sleep(100);
        if (context.pause == true) {
            continue;
        }

        var capdu__scanline = [_]u8{ apdu__cla, apdu__ins, std.mem.asBytes(&message__index)[1], std.mem.asBytes(&message__index)[0], 0x00 };
        const rapdu__msx = context.scraw.cardTransceive(capdu__scanline[0..], rapdu__buffer[0..]) catch {
            logu.logzScope(logu.logz.info(), @This(), @src()).string("id", "msx").string("message", "Failed to transceive. Stopping...").log();
            context.run = false;
            return;
        };
        sw0 = rapdu__msx[rapdu__msx.len - 2];
        sw1 = rapdu__msx[rapdu__msx.len - 1];

        logu.logzScope(logu.logz.info(), @This(), @src()).string("id", "msx").fmt("message", "{}", .{message__index}).log();
        logu.logzScope(logu.logz.info(), @This(), @src()).fmt("sw", "{X:0>2}{X:0>2}", .{ sw0, sw1 }).fmt("capdu__msx", "{s}", .{std.fmt.fmtSliceHexUpper(&capdu__scanline)}).fmt("rapdu__msx", "{s}", .{std.fmt.fmtSliceHexUpper(rapdu__msx)}).log();
        if (rapdu__msx.len != msx_play__block__length + 2 or !(sw0 == 0x9D and ((sw1 >= 0x00 and sw1 <= 0x05) or (sw1 >= 0x10 and sw1 <= 0x15) or sw1 == 0x20))) {
            logu.logzScope(logu.logz.info(), @This(), @src()).string("id", "msx").string("message", "Response is invalid. Stopping...").log();
            context.run = false;
            return;
        } else {
            message__index += 1;
        }

        if (audio__buffer__writer__index < audio__buffer.len) {
            const msx__length: usize = rapdu__msx.len - 2;
            logu.logzScope(logu.logz.info(), @This(), @src()).string("audio", "writer").int("msx__length", msx__length).int("writer__index", audio__buffer__writer__index).log();
            std.mem.copyForwards(u8, audio__buffer[audio__buffer__writer__index..], rapdu__msx[0..msx__length]);
            if (capture__audio) {
                audio__file.?.writeAll(rapdu__msx[0..msx__length]) catch {
                    context.run = false;
                    return;
                };
            }
            audio__buffer__writer__index += msx__length;
        }
    }
}

const MsxSampleLoadError = error{
    SelectFailed,
    LoadFailed,
    ALessB,
    SampleTooShort,
};
fn msxLoad(scraw: *ScRaw, sample__index: u4, sample__data: []const u8) !void {
    const apdu__cla = 0xA0;
    const apdu__ins__load_a: u8 = 0xA0 | @as(u8, sample__index);
    const apdu__ins__load_b: u8 = 0xB0 | @as(u8, sample__index);
    var rapdu__buffer = std.mem.zeroes([2]u8);

    logu.logzScope(logu.logz.info(), @This(), @src()).string("message", "Sample Loading").int("sample__index", sample__index).fmt("ins__load_a", "{X:0>2}", .{apdu__ins__load_a}).fmt("ins__load_b", "{X:0>2}", .{apdu__ins__load_b}).log();

    // Select our ADF (Application Dedicated File), i.e. the MSX applet.
    var capdu__select = [_]u8{ 0x00, 0xA4, 0x04, 0x00, 0x08, 0xF1, 0x93, 0x57, 0x11, 0x07, 0xB9, 0x05, 0x1A };
    const rapdu__select = try scraw.cardTransceive(capdu__select[0..], rapdu__buffer[0..]);
    var sw0: u8 = rapdu__select[0];
    var sw1: u8 = rapdu__select[1];
    if (sw0 != 0x90 and sw1 != 0x00) {
        logu.logzScope(logu.logz.info(), @This(), @src()).string("message", "Select failed").fmt("sw0", "{X:0>2}", .{sw0}).fmt("sw1", "{X:0>2}", .{sw1}).fmt("capdu", "{}", .{std.fmt.fmtSliceHexUpper(&capdu__select)}).fmt("rapdu", "{}", .{std.fmt.fmtSliceHexUpper(rapdu__select)}).log();
        return MsxSampleLoadError.SelectFailed;
    }

    // We load a few samples less than the actual file, but it makes playback easier.
    const sample_a__length: u16 = msx_play__chunk_a__count * msx_play__block__length;
    const sample_b__length: u16 = msx_play__chunk_b__count * msx_play__block__length;
    if (sample_a__length < sample_b__length) {
        logu.logzScope(logu.logz.err(), @This(), @src())
            .string("message", "Sample A shorter than sample B.")
            .int("sample_a__length", sample_a__length)
            .int("sample_b__length", sample_b__length)
            .log();
        return MsxSampleLoadError.ALessB;
    }
    if (sample__data.len < sample_a__length + sample_b__length) {
        logu.logzScope(logu.logz.err(), @This(), @src())
            .string("message", "Sample data length less than parts A + B combined.")
            .int("sample__data__length", sample__data.len)
            .int("sample_a_b__length", sample_a__length + sample_b__length)
            .log();
        return MsxSampleLoadError.SampleTooShort;
    }

    var sample_a__length__remaining: usize = sample_a__length;
    var sample_b__length__remaining: usize = sample_b__length;

    while (sample_a__length__remaining > 0 or sample_b__length__remaining > 0) {
        var capdu__load: [5 + 255]u8 = undefined;
        var capdu__load__length: u16 = 0;

        if (sample_a__length__remaining > 0) {
            const sample_a__chunk__offset: usize = sample_a__length - sample_a__length__remaining;
            const sample_a__chunk__length: u8 = @min(msx_load__block__length, sample_a__length__remaining);
            sample_a__length__remaining -= sample_a__chunk__length;

            const capdu__load_a__header = [_]u8{ apdu__cla, apdu__ins__load_a, std.mem.asBytes(&sample_a__chunk__offset)[1], std.mem.asBytes(&sample_a__chunk__offset)[0], sample_a__chunk__length };
            std.mem.copyForwards(u8, capdu__load[0..], &capdu__load_a__header);
            std.mem.copyForwards(u8, capdu__load[5..], sample__data[sample_a__chunk__offset .. sample_a__chunk__offset + sample_a__chunk__length]);

            capdu__load__length = 5 + @as(u16, sample_a__chunk__length);

            logu.logzScope(logu.logz.info(), @This(), @src())
                .string("message", "Sample Loading A")
                .int("sample_a__chunk__offset", sample_a__chunk__offset)
                .int("sample_a__chunk__length", sample_a__chunk__length)
                .log();
        }

        var rapdu__load = try scraw.cardTransceive(capdu__load[0..capdu__load__length], rapdu__buffer[0..]);
        sw0 = rapdu__load[rapdu__load.len - 2];
        sw1 = rapdu__load[rapdu__load.len - 1];
        if (rapdu__load.len != 2 or !(sw0 == 0x90 and sw1 == 0x00)) {
            logu.logzScope(logu.logz.err(), @This(), @src())
                .string("message", "Load A failed")
                .fmt("sw0", "{X:0>2}", .{sw0})
                .fmt("sw1", "{X:0>2}", .{sw1})
                .fmt("capdu", "{}", .{std.fmt.fmtSliceHexUpper(capdu__load[0..capdu__load__length])})
                .fmt("rapdu", "{}", .{std.fmt.fmtSliceHexUpper(rapdu__load)})
                .log();
            return MsxSampleLoadError.LoadFailed;
        }

        if (sample_b__length__remaining > 0) {
            const sample_b__chunk__offset: usize = sample_b__length - sample_b__length__remaining;
            const sample_b__chunk__length: u8 = @min(msx_load__block__length, sample_b__length__remaining);
            sample_b__length__remaining -= sample_b__chunk__length;

            const capdu__load__b__header = [_]u8{ apdu__cla, apdu__ins__load_b, std.mem.asBytes(&sample_b__chunk__offset)[1], std.mem.asBytes(&sample_b__chunk__offset)[0], sample_b__chunk__length };
            std.mem.copyForwards(u8, capdu__load[0..], &capdu__load__b__header);
            std.mem.copyForwards(u8, capdu__load[5..], sample__data[sample_a__length + sample_b__chunk__offset .. sample_a__length + sample_b__chunk__offset + sample_b__chunk__length]);

            capdu__load__length = 5 + @as(u16, sample_b__chunk__length);

            logu.logzScope(logu.logz.info(), @This(), @src())
                .string("message", "Sample Loading B")
                .int("sample_b__chunk__offset", sample_b__chunk__offset)
                .int("sample_b__chunk__length", sample_b__chunk__length)
                .log();
        }

        rapdu__load = try scraw.cardTransceive(capdu__load[0..capdu__load__length], rapdu__buffer[0..]);
        sw0 = rapdu__load[rapdu__load.len - 2];
        sw1 = rapdu__load[rapdu__load.len - 1];
        if (rapdu__load.len != 2 or !(sw0 == 0x90 and sw1 == 0x00)) {
            logu.logzScope(logu.logz.err(), @This(), @src())
                .string("message", "Load B failed")
                .fmt("sw0", "{X:0>2}", .{sw0})
                .fmt("sw1", "{X:0>2}", .{sw1})
                .fmt("capdu", "{}", .{std.fmt.fmtSliceHexUpper(capdu__load[0..capdu__load__length])})
                .fmt("rapdu", "{}", .{std.fmt.fmtSliceHexUpper(rapdu__load)})
                .log();
            return MsxSampleLoadError.LoadFailed;
        }
    }
}
