#!/bin/bash

# Use Homebrew's full FFmpeg build, which includes drawtext.
FFMPEG="$(brew --prefix ffmpeg-full)/bin/ffmpeg"
FFPROBE="$(brew --prefix ffmpeg-full)/bin/ffprobe"

# Files longer than this get split into chunks and stitched back together,
# so peak memory per ffmpeg pass stays bounded regardless of source length.
CHUNK_SEC=90

OUTDIR="burnin_proxies"
MANIFEST="$OUTDIR/manifest.csv"

# --quality standard|smaller|tiny (default standard), --no-burnin to export
# a plain scaled proxy with no drawtext overlay at all. Flags must come
# before the file/folder arguments.
QUALITY="standard"
BURN_IN=1

while [[ $# -gt 0 ]]; do
    case "$1" in
        --quality)
            QUALITY="$2"
            shift 2
            ;;
        --no-burnin)
            BURN_IN=0
            shift
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo "Unknown option: $1" >&2
            echo "Usage: $0 [--quality standard|smaller|tiny] [--no-burnin] [files or folders...]" >&2
            exit 1
            ;;
        *)
            break
            ;;
    esac
done

case "$QUALITY" in
    standard)
        WIDTH=960; VIDEO_BITRATE=2M; MAXRATE=2.8M; BUFSIZE=4M; AUDIO_BITRATE=128k
        ;;
    smaller)
        WIDTH=720; VIDEO_BITRATE=1.2M; MAXRATE=1.7M; BUFSIZE=2.4M; AUDIO_BITRATE=96k
        ;;
    tiny)
        WIDTH=480; VIDEO_BITRATE=700k; MAXRATE=1M; BUFSIZE=1.4M; AUDIO_BITRATE=64k
        ;;
    *)
        echo "Unknown --quality: $QUALITY (expected standard, smaller, or tiny)" >&2
        exit 1
        ;;
esac

mkdir -p "$OUTDIR"

if [ ! -f "$MANIFEST" ]; then
    echo "source_filename,proxy_filename,source_timecode,frame_rate,duration_seconds,audio_channels,file_date,status,processed_at" > "$MANIFEST"
fi

shopt -s nullglob

# Args can be individual files and/or folders (folders get expanded to the
# video files directly inside them, so you can point this at a whole day's
# footage). MXF for pro cameras, MOV/MP4 for DJI and most everything else.
if [ $# -gt 0 ]; then
    files=()
    for arg in "$@"; do
        if [ -d "$arg" ]; then
            for m in "$arg"/*.MXF "$arg"/*.mxf "$arg"/*.MOV "$arg"/*.mov "$arg"/*.MP4 "$arg"/*.mp4; do
                files+=("$m")
            done
        else
            files+=("$arg")
        fi
    done
else
    files=( *.MXF *.mxf *.MOV *.mov *.MP4 *.mp4 )
fi

if [ ${#files[@]} -eq 0 ]; then
    echo "No video files found."
    exit 1
fi

# Add $2 seconds (float ok) to timecode $1, at nominal frame rate $3. Prints HH:MM:SS:FF.
tc_add() {
    awk -v tc="$1" -v offset="$2" -v fps="$3" 'BEGIN {
        split(tc, a, ":");
        total = (a[1]*3600 + a[2]*60 + a[3]) * fps + a[4];
        total += int(offset * fps + 0.5);
        hh = int(total / (3600*fps)); total %= (3600*fps);
        mm = int(total / (60*fps));   total %= (60*fps);
        ss = int(total / fps);        ff = total % fps;
        printf("%02d:%02d:%02d:%02d", hh, mm, ss, ff);
    }'
}

# Build the drawtext/amix/loudnorm filter for one pass.
# Args: filename, source_tc, elapsed_tc, rate, audio_count, file_date (static, "date time" or "unavailable")
build_filter() {
    local filename="$1" src_tc="$2" elapsed_tc="$3" rate="$4" audio_count="$5" file_date="$6"
    local src_tc_esc elapsed_tc_esc file_date_esc audio_inputs i video_chain
    local font="fontfile='/System/Library/Fonts/SFNSMono.ttf'"

    if [ "$BURN_IN" = "0" ]; then
        echo "[0:v:0]scale=${WIDTH}:-2[v]"
        return
    fi

    src_tc_esc=$(printf '%s' "$src_tc" | sed 's/:/\\:/g')
    elapsed_tc_esc=$(printf '%s' "$elapsed_tc" | sed 's/:/\\:/g')
    file_date_esc=$(printf '%s' "$file_date" | sed 's/:/\\:/g')

    if [ "$QUALITY" = "tiny" ]; then
        # standard/smaller's ratios and margins were tuned for a wide canvas;
        # carried over unchanged to a 480px-wide frame, the FILE DATE line
        # shrinks to illegible and the label/value pairs collide. Tiny gets
        # its own tuned layout: less proportional shrink on font size,
        # tighter margins, and per-line label offsets (CLIP ELAPSED is wider
        # than SOURCE TC and needs to start further left to leave room for
        # its value on the right).
        video_chain="[0:v:0]scale=${WIDTH}:-2,\
drawtext=${font}:text='$filename':\
x=w-tw-w*0.012:y=h*0.03:\
fontsize=h/17:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:text='SOURCE TC':\
x=w*0.44:y=h*0.11:\
fontsize=h/19:fontcolor=white:borderw=1:bordercolor=black,\
drawtext=${font}:timecode='$src_tc_esc':rate=$rate:\
x=w-tw-w*0.012:y=h*0.11:\
fontsize=h/19:fontcolor=white:borderw=1:bordercolor=black,\
drawtext=${font}:text='CLIP ELAPSED':\
x=w*0.36:y=h*0.19:\
fontsize=h/19:fontcolor=white:borderw=1:bordercolor=black,\
drawtext=${font}:timecode='$elapsed_tc_esc':rate=$rate:\
x=w-tw-w*0.012:y=h*0.19:\
fontsize=h/19:fontcolor=white:borderw=1:bordercolor=black,\
drawtext=${font}:text='FILE DATE  $file_date_esc':\
x=w-tw-w*0.012:y=h*0.28:\
fontsize=h/24:fontcolor=white:borderw=1:bordercolor=black[v]"
    else
        video_chain="[0:v:0]scale=${WIDTH}:-2,\
drawtext=${font}:text='$filename':\
x=w-tw-w*0.0234375:y=h*0.041667:\
fontsize=h/26:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:text='SOURCE TC':\
x=w*0.609375:y=h*0.1:\
fontsize=h/29:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:timecode='$src_tc_esc':rate=$rate:\
x=w-tw-w*0.0234375:y=h*0.1:\
fontsize=h/29:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:text='CLIP ELAPSED':\
x=w*0.609375:y=h*0.152778:\
fontsize=h/29:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:timecode='$elapsed_tc_esc':rate=$rate:\
x=w-tw-w*0.0234375:y=h*0.152778:\
fontsize=h/29:fontcolor=white:borderw=2:bordercolor=black,\
drawtext=${font}:text='FILE DATE  $file_date_esc':\
x=w-tw-w*0.0234375:y=h*0.201389:\
fontsize=h/46:fontcolor=white:borderw=1:bordercolor=black[v]"
    fi

    # DJI clips are frequently silent (mic disabled, common in high-fps
    # modes). No audio in, no audio out; the video chain above is the whole
    # filter graph.
    if [ "$audio_count" -lt 1 ]; then
        echo "$video_chain"
        return
    fi

    audio_inputs=""
    for ((i=0; i<audio_count; i++)); do
        audio_inputs+="[0:a:$i]"
    done
    echo "${video_chain};\
${audio_inputs}amix=inputs=${audio_count}:duration=longest:normalize=0,\
loudnorm=I=-16:TP=-1.5:LRA=11[a]"
}

# Append one row to the manifest. Args: filename, tc, rate, duration, audio_count, file_date, status
log_manifest() {
    local filename="$1" tc="$2" rate="$3" duration="$4" audio_count="$5" file_date="$6" status="$7"
    local proxy_name now
    proxy_name="${filename%.*}_proxy.mp4"
    now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    echo "${filename},${proxy_name},${tc},${rate},${duration},${audio_count},${file_date},${status},${now}" >> "$MANIFEST"
}

file_total=${#files[@]}
file_idx=0

for f in "${files[@]}"; do
    file_idx=$((file_idx + 1))

    filename="$(basename "$f")"
    stem="${filename%.*}"
    final_output="$OUTDIR/${stem}_proxy.mp4"
    tmp_output="$OUTDIR/.tmp_${stem}_proxy.mp4"

    echo
    echo "============================================"
    echo "File $file_idx of $file_total: $filename"

    if [ -f "$final_output" ]; then
        echo "SKIP: $final_output already exists."
        echo "============================================"
        continue
    fi

    rm -f "$tmp_output"

    # Read original camera source timecode. Consumer/prosumer cameras and NLE
    # exports frequently don't embed one at all (pro MXF cameras reliably do, but
    # it's not universal) -- missing isn't an error, SOURCE TC just starts at zero.
    tc=$("$FFPROBE" -v error \
        -show_entries format_tags=timecode \
        -of default=nw=1:nk=1 \
        "$f" | head -n 1)
    if [ -z "$tc" ]; then
        tc="00:00:00:00"
    fi

    # Read actual video frame rate.
    rate=$("$FFPROBE" -v error \
        -select_streams v:0 \
        -show_entries stream=avg_frame_rate \
        -of default=nw=1:nk=1 \
        "$f" | head -n 1)

    # Count audio streams. Can legitimately be 0: DJI drones in particular are
    # frequently flown with the mic disabled (especially in high-fps modes),
    # which produces a perfectly valid video-only file, not a broken one.
    audio_count=$("$FFPROBE" -v error \
        -select_streams a \
        -show_entries stream=index \
        -of csv=p=0 \
        "$f" | wc -l | tr -d ' ')

    # Total duration, for chunk planning.
    duration=$("$FFPROBE" -v error \
        -show_entries format=duration \
        -of default=nw=1:nk=1 \
        "$f" | head -n 1)

    # Camera's own internal clock at record time. Not verified against real
    # time-of-day; cameras with an unset/dead clock battery report garbage
    # (e.g. 2019 dates on recent footage), so this is best-effort only, and
    # static (it's a QC flag more than a reliable readout; if it's obviously
    # wrong, that's a nudge to jam-sync the camera clock next time).
    file_date_raw=$("$FFPROBE" -v error \
        -show_entries format_tags=modification_date \
        -of default=nw=1:nk=1 \
        "$f" | head -n 1)
    if [ -z "$file_date_raw" ]; then
        # modification_date is the MXF-world tag; DJI and others use creation_time instead.
        file_date_raw=$("$FFPROBE" -v error \
            -show_entries format_tags=creation_time \
            -of default=nw=1:nk=1 \
            "$f" | head -n 1)
    fi
    if [ -n "$file_date_raw" ]; then
        file_date=$(printf '%s' "$file_date_raw" | sed -E 's/T/ /; s/\.[0-9]+Z$//')
    else
        file_date="unavailable"
    fi

    if [ -z "$rate" ]; then
        echo "ERROR: No frame rate found in $filename; skipping."
        log_manifest "$filename" "$tc" "" "$duration" "$audio_count" "$file_date" "failed: no frame rate"
        echo "============================================"
        continue
    fi

    echo "Source TC:  $tc"
    echo "Frame rate: $rate"
    echo "Audio:      $audio_count streams"
    echo "Duration:   ${duration}s"
    echo "File date:  $file_date (unverified)"
    echo "Quality:    $QUALITY (${WIDTH}px wide)"
    echo "Burn-in:    $([ "$BURN_IN" = "1" ] && echo "on" || echo "off")"
    echo "============================================"

    # Nominal frame rate for timecode math (e.g. 24000/1001 -> 24).
    nominal_fps=$(awk -v r="$rate" 'BEGIN { split(r, p, "/"); f = p[2] ? p[1]/p[2] : p[1]; printf("%d", f + 0.5) }')

    needs_chunking=$(awk -v d="$duration" -v c="$CHUNK_SEC" 'BEGIN { print (d > c) ? "1" : "0" }')

    # -map "[a]" and -c:a only apply when the source actually has audio;
    # ffmpeg errors if you map an audio stream that was never created.
    map_args=(-map "[v]")
    audio_encode_args=()
    if [ "$audio_count" -ge 1 ]; then
        map_args+=(-map "[a]")
        audio_encode_args=(-c:a aac -b:a "$AUDIO_BITRATE" -ar 48000 -ac 2)
    fi

    if [ "$needs_chunking" = "0" ]; then
        filter=$(build_filter "$filename" "$tc" "00:00:00:00" "$rate" "$audio_count" "$file_date")

        if "$FFMPEG" -y -loglevel warning -stats -hwaccel videotoolbox -i "$f" \
            -filter_complex "$filter" \
            "${map_args[@]}" \
            -c:v h264_videotoolbox \
            -b:v "$VIDEO_BITRATE" \
            -maxrate "$MAXRATE" \
            -bufsize "$BUFSIZE" \
            -pix_fmt yuv420p \
            "${audio_encode_args[@]}" \
            -timecode "$tc" \
            -movflags +faststart \
            "$tmp_output"
        then
            mv "$tmp_output" "$final_output"
            echo "DONE: $final_output"
            log_manifest "$filename" "$tc" "$rate" "$duration" "$audio_count" "$file_date" "done"
        else
            rm -f "$tmp_output"
            echo "FAILED: $filename"
            log_manifest "$filename" "$tc" "$rate" "$duration" "$audio_count" "$file_date" "failed: encode error"
        fi

    else
        total_chunks=$(awk -v d="$duration" -v c="$CHUNK_SEC" 'BEGIN { print int((d + c - 1) / c) }')
        echo "Large/long file; processing in ${CHUNK_SEC}s chunks (${total_chunks} total) to keep memory bounded."

        tmpdir="$OUTDIR/.chunks_${stem}"
        rm -rf "$tmpdir"
        mkdir -p "$tmpdir"
        concat_list="$tmpdir/concat.txt"
        : > "$concat_list"

        offset=0
        part=0
        chunk_ok=1
        while awk -v o="$offset" -v d="$duration" 'BEGIN { exit !(o < d) }'; do
            remaining=$(awk -v d="$duration" -v o="$offset" 'BEGIN { printf("%.3f", d - o) }')
            length=$(awk -v r="$remaining" -v c="$CHUNK_SEC" 'BEGIN { print (r < c) ? r : c }')

            chunk_src_tc=$(tc_add "$tc" "$offset" "$nominal_fps")
            chunk_elapsed_tc=$(tc_add "00:00:00:00" "$offset" "$nominal_fps")
            part_file=$(printf "%s/part_%04d.mp4" "$tmpdir" "$part")

            echo "  chunk $((part + 1))/${total_chunks}: offset=${offset}s length=${length}s src_tc=$chunk_src_tc"

            filter=$(build_filter "$filename" "$chunk_src_tc" "$chunk_elapsed_tc" "$rate" "$audio_count" "$file_date")

            if "$FFMPEG" -y -loglevel warning -stats -hwaccel videotoolbox -ss "$offset" -i "$f" -t "$length" \
                -filter_complex "$filter" \
                "${map_args[@]}" \
                -c:v h264_videotoolbox \
                -b:v "$VIDEO_BITRATE" \
                -maxrate "$MAXRATE" \
                -bufsize "$BUFSIZE" \
                -pix_fmt yuv420p \
                "${audio_encode_args[@]}" \
                -movflags +faststart \
                "$part_file"
            then
                printf "file '%s'\n" "$(basename "$part_file")" >> "$concat_list"
            else
                echo "  FAILED on chunk $((part + 1))/${total_chunks} (offset ${offset}s)"
                chunk_ok=0
                break
            fi

            offset=$(awk -v o="$offset" -v c="$CHUNK_SEC" 'BEGIN { printf("%.3f", o + c) }')
            part=$((part + 1))
        done

        if [ "$chunk_ok" = "1" ]; then
            if (cd "$tmpdir" && "$FFMPEG" -y -loglevel warning -f concat -safe 0 -i concat.txt -c copy -timecode "$tc" "../.tmp_${stem}_proxy.mp4"); then
                mv "$tmp_output" "$final_output"
                echo "DONE: $final_output (stitched from $part chunks)"
                log_manifest "$filename" "$tc" "$rate" "$duration" "$audio_count" "$file_date" "done"
                rm -rf "$tmpdir"
            else
                echo "FAILED: concat step for $filename; chunks left in $tmpdir for inspection."
                log_manifest "$filename" "$tc" "$rate" "$duration" "$audio_count" "$file_date" "failed: concat error"
            fi
        else
            echo "FAILED: $filename; chunks left in $tmpdir for inspection."
            log_manifest "$filename" "$tc" "$rate" "$duration" "$audio_count" "$file_date" "failed: chunk encode error"
        fi
    fi
done

echo
echo "Manifest: $MANIFEST"
