#!/usr/bin/env bash
# Description:
#   - Finds audio and video files with specified extensions up to a max depth of 8.
#   - Uses ffprobe to measure their durations.
#   - Creates a playlist starting at a random location within the total runtime.
#   - Stores durations and file paths in a dotfile for faster subsequent runs.
#   - Prompts the user whether to use the cached data or regenerate it.
#   - Uses an EDL file to specify the starting offset for the first file.
# Usage: rand_media_pl.sh [DIR]
# Version: 0.1.0
# Dependencies: Bash 4, ffprobe, mpv, bc, shuf, GNU Parallel

yell() { echo "$0: $*" >&2; } # Print script path and all args to stderr
die() { yell "$*"; exit 111; } # Same as yell() but exits with code 111
must() { "$@" || die "cannot $*"; } # Runs args as command, reports args if command fails

# Configurable variables
SEARCH_DIR="${1:-.}"  # Default to current directory if no argument is provided
MAX_DEPTH=8;
EXTENSIONS=("*.flac" "*.mp3" "*.opus" "*.m4a" "*.m4b" "*.mp4" "*.mkv" "*.webm");
CACHE_FILE=".playlist_cache";

declare -gA file_durations;
declare total_duration;

# Function to prompt the user
prompt_yes_no() {
    yell "STATUS: User input required.";
    local prompt_message="$1";
    local user_input;
    while true; do
        read -rp "$prompt_message [y/n]: " user_input;
        case "$user_input" in
            [Yy]*) return 0 ;;
            [Nn]*) return 1 ;;
            *) echo "Please answer yes or no." ;;
        esac;
    done;
};

# Function to find files with specified extensions
find_media_files() {
    yell "STATUS: Finding media files.";
    local find_cmd=("find" "-L" "$SEARCH_DIR" "-maxdepth" "$MAX_DEPTH" "-type" "f" "(");
    for ext in "${EXTENSIONS[@]}"; do
        find_cmd+=("-iname" "$ext" "-o");
    done;
    # Remove the last "-o" and add ")";
    unset 'find_cmd[-1]';
    find_cmd+=(")");
    "${find_cmd[@]}";
};

# Function to generate the playlist cache
generate_playlist_cache() {
    yell "Generating playlist cache. This may take a while..."
    # Initialize or empty the cache file
    : > "$CACHE_FILE";  # Truncate or create the cache file

    total_duration=0;
    while IFS= read -r file; do
        # Get the duration using ffprobe
        duration=$(ffprobe -v error -show_entries format=duration \
            -of default=noprint_wrappers=1:nokey=1 "$file");
        declare -p file duration 1>&2;  # Debugging statement

        # Validate the duration
        if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
            # Append the file path and duration to the cache file
            printf "%s|%s\n" "$file" "$duration" >> "$CACHE_FILE";
            total_duration=$(echo "$total_duration + $duration" | bc);
        else
            yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2;
        fi;
    done < <(find_media_files | parallel readlink -f '{}' | sort -u; );

    total_duration_s="$(printf "%.1f" "$total_duration"; )";
    total_duration_h="$(echo "scale=1; $total_duration / 3600" | bc -l; )";

    yell "Total duration of playlist: ${total_duration_s} seconds (${total_duration_h} hours).";
};
# Function to read the playlist cache
read_playlist_cache() {
    # Input: file_durations associative array
    #        total_duration scalar
    yell "STATUS: Reading playlist cache."

    total_duration=0
    while IFS='|' read -r file duration; do
        declare -p file duration 1>&2  # Debugging statement

        # Validate the duration
        if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
            file_durations["$file"]="$duration"
            total_duration=$(echo "$total_duration + $duration" | bc)
        else
            yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
        fi
    done < "$CACHE_FILE";
};
# Function to get byte count of file name
prepend_path_bc() {
    # Desc: Prepends a file path with %[path bytecount]%
    # Usage: prepend_filename_bc [FILE]
    # Input:  arg1    str  file path
    # Output: stdout  str  %[int path length]%path
    # Example: 'foo.txt' yields '%7%foo.txt'
    # Depends: GNU Coreutils 8.32 (for 'wc')
    #          BK-2020-03: yell(), die(), must()
    # Ref/Attrib: See “Syntax of mpv EDL files” https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#syntax-of-mpv-edl-files
    fin="$1";

    if [[ ! -f "$fin" ]]; then
        yell "WARNING:File does not exist:${fin}";
    fi;

    bytecount="$(printf "%s" "$fin" | wc -c; )";
    re='[0-9]';
    if [[ ! "$bytecount" =~ $re ]]; then
        die "FATAL:Not an int:${bytecount}; $(declare -p fin bytecount)";
    fi;
    printf "%%%s%%%s" "$bytecount" "$fin";
};

# Main script
if [[ -f "$CACHE_FILE" ]]; then
    if prompt_yes_no "Playlist cache detected. Do you want to use it? (Faster)"; then
        read_playlist_cache;
    else
        generate_playlist_cache;
        read_playlist_cache;
    fi;
else
    generate_playlist_cache;
    read_playlist_cache;
fi;

# Check if any files were found
if [[ ${#file_durations[@]} -eq 0 ]]; then
    declare -p file_durations 1>&2;
    die "FATAL: No media files found.";
fi;

# Ensure total_duration is not empty
if [[ -z "$total_duration" ]]; then
    die "FATAL: total_duration is empty.";
fi;

# Convert total_duration to an integer
total_duration_int=$(printf "%.0f" "$total_duration");

# Check if total_duration_int is a valid integer
if ! [[ "$total_duration_int" =~ ^[0-9]+$ ]]; then
    die "FATAL: total_duration_int is not a valid integer.";
fi;

# Generate a random integer between 0 and total_duration_int - 1
random_point=$(shuf -i 0-$((total_duration_int - 1)) -n1)
yell "DEBUG: total_duration=$total_duration, total_duration_int=$total_duration_int, random_point=$random_point" 1>&2;

# Get sorted list of files
mapfile -t sorted_files < <(printf '%s\n' "${!file_durations[@]}" | sort; );

# Find the file and offset corresponding to the random starting point
accumulated_duration=0;
accumulated_duration_int=0;

for idx in "${!sorted_files[@]}"; do
    file="${sorted_files[$idx]}";
    duration="${file_durations["$file"]}";
    declare -p idx file duration accumulated_duration 1>&2;  # Debugging statement

    # Validate the duration
    if [[ -n "$duration" && "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
        new_accumulated_duration=$(echo "$accumulated_duration + $duration" | bc)
        # Convert accumulated durations to integers for comparison
        accumulated_duration_int=$(printf "%.0f" "$accumulated_duration")
        new_accumulated_duration_int=$(printf "%.0f" "$new_accumulated_duration")
    else
        yell "WARNING: Invalid duration '$duration' for file '$file', skipping." 1>&2
        continue
    fi

    if (( random_point < new_accumulated_duration_int )); then
        offset=$(echo "$random_point - $accumulated_duration_int" | bc; );
        yell "Starting playback from $offset seconds into file: $file";

        # Create an EDL file
        edl_file=$(mktemp);
        yell "DEBUG:EDL file at:${edl_file}"; # debug
        echo "# mpv EDL v0" > "$edl_file";
        
        # Add first file to start playback at random offset position
        file_bc="$(prepend_path_bc "$file")"; # See https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#syntax-of-mpv-edl-files
        printf '%s,%s\n' "$file_bc" "$offset" >> "$edl_file";

        # Append the rest of the files
        declare -p file offset idx sorted_files edl_file 1>&2; # debug
        for (( i=idx+1; i<${#sorted_files[@]}; i++ )); do
            next_file="${sorted_files[$i]}";
            next_file_bc="$(prepend_path_bc "$next_file")";
            yell "STATUS:Adding:$next_file";
            printf '%s\n' "$next_file_bc" >> "$edl_file";
        done;

        # Start playback using mpv
        mpv "$edl_file" || exit 1;

        # Securely delete the EDL file
        if [[ -n "$edl_file" && -f "$edl_file" ]]; then
            rm "$edl_file";
            exit 0;
        else
            die "FATAL: edl_file is either unset or not a valid file, skipping deletion.";
        fi;
    fi;
    accumulated_duration="$new_accumulated_duration";
    accumulated_duration_int="$new_accumulated_duration_int";
done;

die "FATAL: Could not find file corresponding to random point.";
