const std = @import("std");

fn readStr(rd: *std.io.Reader, len: usize) ![]const u8 {
    return std.mem.trimEnd(u8, try rd.take(len), "\x00");
}

const Sample = extern struct {
    len: u16,
    fine: u8,
    vol: u8,
    rep_offs: u16,
    rep_len: u16,
};

pub fn main() !void {
    const alloc = std.heap.smp_allocator;
    var file = try std.fs.cwd().openFile("sirens.mod", .{});
    var rbuf: [1 << 16]u8 = undefined;
    var frd2 = file.reader(&rbuf);
    var frd = &frd2.interface;
    const buf = try frd.allocRemaining(alloc, .unlimited);
    var rd = std.io.Reader.fixed(buf);
    const mod_name = try readStr(&rd, 20);
    std.debug.print("name: {s}\n", .{mod_name});

    var sample_info: [31]Sample = undefined;
    const SampleData = struct {
        name: []const u8,
        data: ?[]const u8,
    };
    var sample_data: [31]SampleData = .{SampleData{ .name = undefined, .data = null }} ** 31;

    for (&sample_info, &sample_data) |*d, *d2| {
        d2.name = try readStr(&rd, 22);
        d.* = try rd.takeStruct(Sample, .big);
    }
    const song_len = try rd.takeByte();
    _ = try rd.takeByte();
    const pattern_seq = try rd.take(128);
    _ = try rd.take(4); // M.K.
    const num_patterns = std.mem.max(u8, pattern_seq) + 1;

    const patterns = try alloc.alloc(*const [1024]u8, num_patterns);

    for (patterns) |*d| {
        const pattern = try rd.takeArray(1024);
        d.* = pattern;
        for (0..256) |i| {
            const cmd = std.mem.readInt(u32, pattern[i * 4 ..][0..4], .big);
            if (cmd & 0xF00 == 0) continue;
            // x |= @as(u16, 1) << @intCast((cmd >> 8) & 0xF);
            // std.debug.print("{x}\n", .{cmd & 0xFFF});
        }
    }
    for (&sample_info, &sample_data) |s, *d| {
        if (s.len == 0) continue;
        // std.debug.print("{any}\n", .{try rd.take(2)});
        _ = try rd.take(2);
        d.data = @ptrCast(try rd.take((s.len - 1) * 2));
    }

    var wr2 = std.fs.File.stdout().writer(&rbuf);
    const wr = &wr2.interface;

    const ar = 48000.0 * 4.0;

    var bpm: u16 = 125;
    var tpd: u8 = 6;

    var song_pos: u8 = 0;
    var curr_div: u8 = 0;
    var tick_ctr: u8 = 0;

    // if (true) return;

    var slide: [4]bool = .{false} ** 4;

    const Channel = struct {
        idx: f64 = 0,
        speed: f64 = 0,
        period: u16 = 0,
        sample: u8 = 0,
        volume: u8 = 0,
        sliderate: u8 = 0,
        rep: bool = false,

        pub fn updateSpeed(ch: *@This(), fine: u8) void {
            const fine_: i4 = @truncate(@as(i8, @bitCast(fine)));
            ch.speed = (7093789.2 / ar) / (@as(f64, @floatFromInt(ch.period * 4))) * @exp2(1.0 / (12.0 * 8.0) * @as(f64, @floatFromInt(fine_)));
            // _ = fine_;
            // ch.speed = (7093789.2 / ar) / (@as(f64, @floatFromInt(ch.period * 4)));
        }
    };
    var channels: [4]Channel = .{Channel{}} ** 4;

    _ = &channels;

    var rem: f64 = undefined;
    var sched_change: ?u8 = null;
    while (song_pos < song_len) {
        // std.debug.print("{} {} {}\n", .{ tick_ctr, curr_div, song_pos });
        const pattern = patterns[pattern_seq[song_pos]];
        const cmds = pattern[@as(usize, curr_div) * 16 ..][0..16];
        for (0..4) |i| {
            const ch = &channels[i];
            // ch.volume -|= 16;
            if (!(tick_ctr == 0 or slide[i])) continue;
            const cmd: packed struct {
                eff: u12,
                samp_l: u4,
                param: u12,
                samp_h: u4,
            } = @bitCast(std.mem.readInt(u32, cmds[i * 4 ..][0..4], .big));
            const eff = cmd.eff;
            const sample = @as(u8, cmd.samp_h) * 16 + cmd.samp_l;
            const param = cmd.param;
            const x: u4 = @truncate(eff);
            const y: u4 = @truncate(eff >> 4);
            const xy: u8 = @truncate(eff);
            if (tick_ctr == 0 and sample != 0) {
                ch.sample = sample;
                ch.rep = false;
                ch.idx = 0;
                ch.volume = sample_info[sample - 1].vol;
            }
            var param_used: bool = false;
            switch (@as(u4, @intCast(eff >> 8))) {
                0 => {
                    if (xy != 0) unreachable;
                },
                3 => {
                    param_used = true;
                    slide[i] = true;
                    if (xy != 0) ch.sliderate = xy;
                    if (param > ch.period) {
                        // std.debug.print("{}\n", .{ch.sliderate});
                        ch.period = @min(ch.period +| ch.sliderate, param);
                    } else {
                        ch.period = @max(ch.period -| ch.sliderate, param);
                    }
                    // ch.speed = (7093789.2 / ar) / (@as(f64, @floatFromInt(ch.period * 4)));
                    ch.updateSpeed(sample_info[ch.sample - 1].fine);
                },
                10 => {
                    slide[i] = true;
                    if (tick_ctr == 0) continue;
                    if (x != 0) {
                        ch.volume += x;
                        ch.volume = @min(ch.volume, 64);
                    } else if (y != 0) {
                        ch.volume -|= y;
                    }
                },
                12 => {
                    // std.debug.print("{} {}\n", .{ i, xy });
                    ch.volume = @min(xy, 64);
                },
                13 => {
                    sched_change = @as(u8, x) * 10 + y;
                },
                15 => {
                    if (xy <= 32) {
                        tpd = @max(xy, 1);
                    } else {
                        bpm = xy;
                    }
                },
                else => unreachable,
            }
            if ((!param_used) and param != 0) {
                ch.period = param;
                ch.updateSpeed(sample_info[ch.sample - 1].fine);
                // ch.speed = (7093789.2 / ar) / (@as(f64, @floatFromInt(ch.period * 4)));
            }
            // const asdf: u32 = @as(u32, @bitCast(cmd));
            // if (asdf != 0) std.debug.print("{x}\n", .{asdf});
            // if (param == 0 and sample != 0) std.debug.print("{x}\n", .{asdf});
        }

        tick_ctr += 1;

        if (tick_ctr >= tpd) {
            tick_ctr = 0;
            slide = .{false} ** 4;

            if (sched_change) |i| {
                song_pos += 1;
                curr_div = i;
            } else {
                curr_div += 1;
                if (curr_div >= 64) {
                    curr_div = 0;
                    song_pos += 1;
                }
            }
        }

        const wait: f64 = tck_len(bpm) * ar + rem;
        const rwait = @round(wait);
        rem = wait - rwait;
        // std.Thread.sleep(@intFromFloat(wait * 1000000000));
        // const samples: usize = @round();
        // for (&channels) |ch| {
        //     std.debug.print("{} {} | ", .{ ch.rep, sample_info[ch.sample -| 0].rep_len });
        // }
        // std.debug.print("\n", .{});
        for (0..@intFromFloat(rwait)) |_| {
            var out: [2]f32 = .{ 0, 0 };
            for (&channels, [4]u8{ 0, 1, 1, 0 }) |*ch, lr| {
                if (ch.sample == 0) continue;
                const info = sample_info[ch.sample - 1];
                if (ch.rep and info.rep_len <= 1) continue;
                ch.idx = @mod(ch.idx, @as(f64, @floatFromInt(if (ch.rep) info.rep_len else info.len)));
                if (sample_data[ch.sample - 1].data) |sd| {
                    const idx = @as(u32, @intFromFloat(ch.idx * 2)) + (if (ch.rep) @as(u32, info.rep_offs) * 2 else 2) - 2;
                    const sample: f32 = @as(f32, @floatFromInt(@as(i8, @bitCast(sd[idx])))) / 127.0;
                    out[lr] += sample * (@as(f32, @floatFromInt(ch.volume)) / 64.0) * 0.2;
                }
                ch.idx += ch.speed;
                // if ((!ch.rep) and ) {
                //     ch.rep = true;
                // }
                ch.rep = ch.rep or ch.idx >= @as(f64, @floatFromInt(info.len - 2));
            }
            const out2 = .{ out[0] + out[1] * 0.5, out[1] + out[0] * 0.5 };
            try wr.writeSliceEndian(f32, &out2, .little);
            // try wr.writeInt(u32, @bitCast((out[0] + out[1]) * 0.1), .little);
        }
    }

    // const a = sample_data[13].data;
    // for (0..@max(24000 / a.len, 2)) |_| {
    //     try wr2.writeAll(@ptrCast(a));
    // }
    // try wr2.flush();
}

fn tck_len(bpm: u16) f64 {
    const v: f64 = @floatFromInt(bpm);
    return 2.5 / v;
}
