// iOS sound-effect bank via AudioToolbox System Sound Services (P10.2).
//
// A purely additive layer: it never reads or mutates score / board / move
// state — board_view tells it an event happened and it plays one short clip.
//
// System Sound Services is plain C, so it is reached with sx's `#foreign` FFI
// exactly as uikit.sx reaches UIApplicationMain / dlsym / CACurrentMediaTime —
// no sx-library change. The AudioToolbox + CoreFoundation frameworks are linked
// per-target in build.sx. Every call is guarded by `inline if OS == .ios`, so
// other targets never reference these symbols nor need the frameworks.

#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";

// AudioToolbox — System Sound Services. SystemSoundID is a UInt32; OSStatus a
// SInt32 (0 == noErr); the clip's file is passed as a CFURLRef (opaque ptr).
AudioServicesCreateSystemSoundID :: (url: *void, out_id: *u32) -> s32 #foreign;
AudioServicesPlaySystemSound     :: (sound_id: u32) #foreign;

// CoreFoundation — build a file CFURL from an absolute path. `len` is a CFIndex
// (long); `is_dir` a Boolean (unsigned char); a NULL allocator = default.
CFURLCreateFromFileSystemRepresentation :: (allocator: *void, buffer: *u8, len: s64, is_dir: s8) -> *void #foreign;
CFRelease :: (cf: *void) #foreign;

// libc — getcwd to absolutize the bundle-relative asset path. The platform
// chdir's to the bundle's resource dir at boot, so CWD is the .app and the
// game's other relative `assets/...` loads already resolve against it.
getcwd   :: (buf: *u8, size: usize) -> *u8 #foreign;
c_strlen :: (s: *u8) -> usize #foreign "strlen";

// The cascade clips combo1..combo5, in ascending pitch. `combo_ids` is indexed
// by `cascade_cue_index`.
COMBO_CLIPS :: 5;

// The whole bank, each cue loaded once at startup into its own SystemSoundID;
// every `play_*` is then a single C call. `loaded` is true only when every cue
// loaded, so a partial/failed bank mutes rather than playing a 0 id.
GameAudio :: struct {
    swap_id:  u32;
    match_id: u32;
    combo_ids: [COMBO_CLIPS]u32;
    win_id:   u32;
    lose_id:  u32;
    loaded: bool;

    init :: (self: *GameAudio) {
        self.loaded = false;
        inline if OS != .ios { return; }

        self.swap_id      = load_cue("swap.wav",   "[sx] audio: loaded swap",   "[sx] audio: load failed swap");
        self.match_id     = load_cue("match.wav",  "[sx] audio: loaded match",  "[sx] audio: load failed match");
        self.combo_ids[0] = load_cue("combo1.wav", "[sx] audio: loaded combo1", "[sx] audio: load failed combo1");
        self.combo_ids[1] = load_cue("combo2.wav", "[sx] audio: loaded combo2", "[sx] audio: load failed combo2");
        self.combo_ids[2] = load_cue("combo3.wav", "[sx] audio: loaded combo3", "[sx] audio: load failed combo3");
        self.combo_ids[3] = load_cue("combo4.wav", "[sx] audio: loaded combo4", "[sx] audio: load failed combo4");
        self.combo_ids[4] = load_cue("combo5.wav", "[sx] audio: loaded combo5", "[sx] audio: load failed combo5");
        self.win_id       = load_cue("win.wav",    "[sx] audio: loaded win",    "[sx] audio: load failed win");
        self.lose_id      = load_cue("lose.wav",   "[sx] audio: loaded lose",   "[sx] audio: load failed lose");

        self.loaded = self.swap_id != 0 and self.match_id != 0
            and self.combo_ids[0] != 0 and self.combo_ids[1] != 0 and self.combo_ids[2] != 0
            and self.combo_ids[3] != 0 and self.combo_ids[4] != 0
            and self.win_id != 0 and self.lose_id != 0;
    }

    play_swap :: (self: *GameAudio) {
        inline if OS != .ios { return; }
        if !self.loaded { return; }
        NSLog(xx "[sx] audio: cue swap");
        AudioServicesPlaySystemSound(self.swap_id);
    }

    play_match :: (self: *GameAudio) {
        inline if OS != .ios { return; }
        if !self.loaded { return; }
        NSLog(xx "[sx] audio: cue match");
        AudioServicesPlaySystemSound(self.match_id);
    }

    // Pick the ascending cascade clip by clamping the cascade depth into the
    // combo1..combo5 range (see `cascade_cue_index`).
    play_cascade :: (self: *GameAudio, depth: s64) {
        inline if OS != .ios { return; }
        if !self.loaded { return; }
        idx := cascade_cue_index(depth);
        NSLog(xx cascade_cue_name(idx));
        AudioServicesPlaySystemSound(self.combo_ids[idx]);
    }

    play_win :: (self: *GameAudio) {
        inline if OS != .ios { return; }
        if !self.loaded { return; }
        NSLog(xx "[sx] audio: cue win");
        AudioServicesPlaySystemSound(self.win_id);
    }

    play_lose :: (self: *GameAudio) {
        inline if OS != .ios { return; }
        if !self.loaded { return; }
        NSLog(xx "[sx] audio: cue lose");
        AudioServicesPlaySystemSound(self.lose_id);
    }
}

// The cascade cue's log line, one stable literal per combo clip so the play-time
// `log show` shows the clip stepping up with cascade depth. Literals only — the
// string→NSString bridge needs NUL-terminated bytes (a formatted string may not
// be). `idx` is a clamped `cascade_cue_index`, so it is always 0..COMBO_CLIPS-1.
cascade_cue_name :: (idx: s64) -> string {
    if idx <= 0 { return "[sx] audio: cue combo1"; }
    if idx == 1 { return "[sx] audio: cue combo2"; }
    if idx == 2 { return "[sx] audio: cue combo3"; }
    if idx == 3 { return "[sx] audio: cue combo4"; }
    "[sx] audio: cue combo5"
}

// Cascade depth (number of cleared rounds) → combo clip index 0..COMBO_CLIPS-1
// (combo1..combo5). Clamps: depth <= 1 → 0, depth >= 5 → 4. Pure arithmetic and
// OS-agnostic so it can be snapshot-tested headlessly (P10.4).
cascade_cue_index :: (depth: s64) -> s64 {
    if depth <= 1 { return 0; }
    if depth >= COMBO_CLIPS { return COMBO_CLIPS - 1; }
    depth - 1
}

// Load one cue into a SystemSoundID and log the outcome. `loaded_msg` /
// `failed_msg` are string literals (NUL-terminated, so safe to bridge to
// NSString) naming the cue. Returns 0 on any failure, which the play methods
// treat as "skip" via the `loaded` gate.
load_cue :: (name: string, loaded_msg: string, failed_msg: string) -> u32 {
    inline if OS != .ios { return 0; }
    id := load_system_sound(name);
    if id != 0 { NSLog(xx loaded_msg); }
    else       { NSLog(xx failed_msg); }
    return id;
}

// Create a SystemSoundID for `assets/audio/<name>` (relative to the bundle).
// Returns 0 on any failure.
load_system_sound :: (name: string) -> u32 {
    inline if OS != .ios { return 0; }

    cwd_buf : [1024]u8 = ---;
    if getcwd(@cwd_buf[0], 1024) == null { return 0; }
    cwd : string = ---;
    cwd.ptr = @cwd_buf[0];
    cwd.len = cast(s64) c_strlen(@cwd_buf[0]);

    // CFURLCreateFromFileSystemRepresentation takes an explicit byte length, so
    // the formatted path needs no NUL terminator.
    path := format("{}/assets/audio/{}", cwd, name);

    url := CFURLCreateFromFileSystemRepresentation(null, path.ptr, path.len, 0);
    if url == null { return 0; }

    sound_id : u32 = 0;
    status := AudioServicesCreateSystemSoundID(url, @sound_id);
    CFRelease(url);
    if status != 0 { return 0; }
    sound_id
}

// The process-wide instance. main() allocates + inits it; board_view triggers
// the swap/match/cascade cues through the `sfx_*` shims on a committed gesture,
// and main's frame loop fires the win/lose stinger edge-triggered. Null until
// init, so every shim is a safe no-op before then.
g_audio : *GameAudio = null;

sfx_swap    :: ()           { if g_audio != null { g_audio.play_swap(); } }
sfx_match   :: ()           { if g_audio != null { g_audio.play_match(); } }
sfx_cascade :: (depth: s64) { if g_audio != null { g_audio.play_cascade(depth); } }
sfx_win     :: ()           { if g_audio != null { g_audio.play_win(); } }
sfx_lose    :: ()           { if g_audio != null { g_audio.play_lose(); } }
