
#ifndef BEATRAT_TRACK_H
#define BEATRAT_TRACK_H

#include <pthread.h>
#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>

#define MAX_TRACKS 16

/* A clip owns its sample buffer outright -- whoever creates it (the
 * record thread or the import thread) hands ownership to the Track via
 * tracklist_add_track() and must not touch the pointer afterward. */
typedef struct {
    int16_t *samples;
    size_t   num_samples;
    uint32_t sample_rate;
} Clip;

typedef struct {
    char name[64];
    Clip clip;
    /* PLAYBACK-only mute (main.c's ACTION_TOGGLE_MUTE, bare 'm') --
     * never touches clip.samples, never affects import/export/copy/
     * paste/anything else that reads the actual audio. Just whether
     * playback_thread_fn()'s mix includes this track. atomic since
     * it's toggled from the main thread while playback_thread_fn may
     * be reading it from a Track SNAPSHOT taken at playback start
     * (see that function's comment -- like solo-via-selection, a
     * mute toggled mid-playback only takes effect on the next play,
     * not live). */
    atomic_bool muted;
} Track;

/* Tracks are append-only for now: once added, a track's contents never
 * move or get freed while the app is running. That's what lets the
 * playback thread safely snapshot-copy the track array without holding
 * the lock for the duration of playback. */
typedef struct {
    Track tracks[MAX_TRACKS];
    int num_tracks;
    pthread_mutex_t mutex;
} TrackList;

void tracklist_init(TrackList *tl);

/* Takes ownership of `samples` on success (do not free it yourself
 * afterward). Returns the new track's index, or -1 if the list is full. */
int tracklist_add_track(TrackList *tl, const char *name, int16_t *samples,
                         size_t num_samples, uint32_t sample_rate);

void tracklist_lock(TrackList *tl);
void tracklist_unlock(TrackList *tl);

/* Removes track idx, freeing its clip and shifting later tracks down by
 * one. Returns 0 on success, -1 if idx is out of range. Only safe to
 * call when no background thread is reading the track list (the app's
 * STATE_IDLE gate already ensures this). */
int tracklist_delete_track(TrackList *tl, int idx);

/* Frees every clip and resets the list to empty, WITHOUT touching the
 * mutex itself (unlike tracklist_free, meant for shutdown) -- safe to
 * call mid-session since the lock/unlock discipline stays intact for
 * any other thread concurrently waiting on it. Used by New Project and
 * before loading a different project via Open Project. */
void tracklist_clear(TrackList *tl);

/* Replaces track idx's clip in place (freeing the old one), keeping its
 * name and position -- this is what "re-record" uses instead of
 * appending a new track. Takes ownership of `samples` on success; frees
 * it itself if idx is out of range. Returns idx on success, -1 on
 * failure. */
int tracklist_replace_clip(TrackList *tl, int idx, int16_t *samples,
                            size_t num_samples, uint32_t sample_rate);

/* Swaps track idx with its neighbor in the given direction (-1 = up,
 * +1 = down). Returns the track's new index, or -1 if it's already at
 * that end of the list (nothing to do). */
int tracklist_move_track(TrackList *tl, int idx, int direction);

/* Like tracklist_move_track() above, but for a contiguous block
 * [*lo, *hi] (inclusive) moved as a unit one slot in `direction` (-1 =
 * up, +1 = down) -- the single track just past that end of the block
 * hops to the other side of it, same as dragging a multi-track
 * selection past its neighbor. Used by the keyboard track/group
 * "move mode" (leader+m, main.c's ACTION_TOGGLE_TRACK_MOVE). On
 * success, *lo and *hi are updated to the block's new position. Returns
 * 0 on success, -1 (lo/hi unchanged) if the block is already at that
 * end of the list. */
int tracklist_move_block(TrackList *tl, int *lo, int *hi, int direction);

/* Renames track idx in place. Returns 0 on success, -1 if idx is out
 * of range. */
int tracklist_rename_track(TrackList *tl, int idx, const char *new_name);

/* Frees every clip's sample buffer. Call once at shutdown. */
void tracklist_free(TrackList *tl);

/* ---------------------------------------------------------------- */
/* Region helpers -- used by the copy/cut/void/paste register system  */
/* (main.c). These operate on one Clip directly; the caller is        */
/* responsible for holding the TrackList's lock around any call that  */
/* mutates a clip that's reachable from the playback/record threads.  */
/* ---------------------------------------------------------------- */

/* Mallocs and returns a copy of clip's samples in [start, end), clamped
 * to the clip's actual length (so a region that runs past a shorter
 * track just yields whatever exists, never garbage or a crash). Also
 * covers "duplicate the whole clip" by passing start=0, end=SIZE_MAX
 * (or any end >= num_samples). Returns 0 on success (even an empty
 * region -- *out_samples may be NULL and *out_count 0 in that case,
 * which the caller should treat as "nothing to copy", not an error),
 * -1 only on allocation failure for a non-empty region. */
int track_clip_copy_region(const Clip *clip, size_t start, size_t end,
                            int16_t **out_samples, size_t *out_count);

/* Ripple-deletes [start, end) from clip in place: later samples shift
 * left to close the gap and the buffer shrinks (realloc). Clamped the
 * same way as track_clip_copy_region. Returns 0 on success (including
 * a no-op empty region), -1 on realloc failure (clip is left
 * unmodified in that case). */
int track_clip_cut_region(Clip *clip, size_t start, size_t end);

/* Ripple-inserts `count` samples at position `at`. If `at` is past the
 * clip's current length, the gap is auto-extended with real silence
 * out to `at` first (so a paste or whitespace insertion can land
 * anywhere the cursor sits, not just immediately past wherever the
 * track currently ends) -- NOT clamped back to the true end. Does NOT
 * take ownership of `samples` -- copies from it. Returns 0 on success,
 * -1 on realloc failure (clip is left unmodified). */
int track_clip_insert_region(Clip *clip, size_t at, const int16_t *samples, size_t count);

/* Swaps the playback order of two ADJACENT regions [a0, mid) and
 * [mid, b1) in place -- the "snippet move" primitive (main.c's
 * ACTION_TOGGLE_TRACK_MOVE, NAV_CONTEXT_PLAYBACK branch): afterward,
 * what used to play second (the old [mid, b1) region) plays first,
 * and vice versa. Total length [a0, b1) is unchanged -- this only
 * ever reorders samples that are already there, nothing is inserted
 * or removed, so it can't fail on allocation for any size this ever
 * gets called with in practice, though it still reports one. Calling
 * it a second time with the SAME (a0, mid, b1) undoes it exactly --
 * it's its own inverse. Returns 0 on success, -1 if the range is
 * invalid (a0 >= mid, mid >= b1, or b1 > the clip's length) or on
 * allocation failure (clip is left unmodified either way). */
int track_clip_swap_adjacent(Clip *clip, size_t a0, size_t mid, size_t b1);

#endif

