#!/bin/sh

# Extracts the sharpest still frame near each regular time interval.
# Candidates within WINDOW are scored with FFmpeg blurdetect; the nearest is
# used if none has a finite score. Exact selected frames are written as lossless
# PNGs, optionally Lanczos-scaled to cover and centre-cropped to an exact size.
# No sharpening, frame interpolation, or deinterlacing; existing paths are not overwritten.

set -eu

LC_ALL=C
export LC_ALL

usage() {
    printf 'Usage: %s [-w WINDOW] [-o OUTDIR] [-s WIDTHxHEIGHT] INTERVAL INPUT\n' "$0" >&2
    exit 2
}

window=0.25
out=frames
size=
size_set=0

while getopts 'w:o:s:' opt; do
    case $opt in
        w) window=$OPTARG ;;
        o) out=$OPTARG ;;
        s) size=$OPTARG; size_set=1 ;;
        *) usage ;;
    esac
done
shift $((OPTIND - 1))

[ "$#" -eq 2 ] || usage

interval=$1
input=$2

if [ "$size_set" -eq 1 ]; then
    case $size in
        *[!0-9x]* | *x*x* | x* | *x) usage ;;
    esac

    width=${size%%x*}
    height=${size#*x}

    case $width in *[1-9]*) ;; *) usage ;; esac
    case $height in *[1-9]*) ;; *) usage ;; esac
else
    width=
    height=
fi

awk -v i="$interval" -v w="$window" '
    BEGIN {
        re = "^[0-9]+([.][0-9]+)?$"
        exit !(i ~ re && w ~ re && i > 0 && w >= 0 && w < i / 2)
    }
' || {
    printf '%s\n' \
        "INTERVAL must be > 0; WINDOW must be >= 0 and < INTERVAL/2" >&2
    exit 2
}

command -v ffmpeg >/dev/null 2>&1 || {
    printf '%s\n' "ffmpeg not found" >&2
    exit 127
}

# Force relative pathnames to be treated as local files, not FFmpeg URLs.
case $input in /*) ;; *) input=./$input ;; esac
case $out   in /*) ;; *) out=./$out ;; esac

[ -f "$input" ] || {
    printf 'Input is not a regular file: %s\n' "$input" >&2
    exit 1
}

[ ! -e "$out" ] && [ ! -L "$out" ] || {
    printf 'Output path already exists: %s\n' "$out" >&2
    exit 1
}

tmpbase=${TMPDIR:-/tmp}
case $tmpbase in /*) ;; *) tmpbase=./$tmpbase ;; esac

tmp=$(mktemp -d "$tmpbase/bestframes.XXXXXX") || {
    printf '%s\n' "Could not create temporary directory" >&2
    exit 1
}

[ -n "$tmp" ] && [ -d "$tmp" ] || {
    printf '%s\n' "mktemp did not create a temporary directory" >&2
    exit 1
}

cleanup() {
    rm -f "$tmp/meta" "$tmp/picks" "$tmp/filter" 2>/dev/null || :
    rmdir "$tmp" 2>/dev/null || :
}
trap cleanup 0
trap 'exit 1' HUP INT TERM

printf '[1/2] Analysing candidate frames...\n' >&2

ffmpeg -nostdin -hide_banner -loglevel error \
    -stats -stats_period 2 \
    -i "$input" \
    -map 0:v:0 \
    -vf "setpts=PTS-STARTPTS,select='lte(abs(t-round(t/$interval)*$interval)\,$window)',blurdetect=block_width=32:block_height=32:block_pct=80,metadata=mode=print:key=lavfi.blur:file=-" \
    -f null /dev/null \
    >"$tmp/meta"

awk -v interval="$interval" '
    /^frame:/ {
        pts = time = ""

        for (i = 1; i <= NF; i++) {
            if ($i ~ /^pts:/) {
                pts = $i
                sub(/^pts:/, "", pts)
            } else if ($i ~ /^pts_time:/) {
                time = $i
                sub(/^pts_time:/, "", time)
            }
        }
        next
    }

    /^lavfi\.blur=/ && pts != "" && time != "" {
        blur = $0
        sub(/^[^=]*=/, "", blur)

        b = int(time / interval + 0.5)
        dist = time - b * interval
        if (dist < 0)
            dist = -dist

        # Fallback: nearest candidate if no finite blur score is available.
        if (!(b in nearpts) || dist < neardist[b]) {
            nearpts[b] = pts
            neardist[b] = dist
        }

        if (blur ~ /^[+-]?(([0-9]+([.][0-9]*)?)|([.][0-9]+))([eE][+-]?[0-9]+)?$/) {
            score = blur + 0

            if (!(b in bestpts) ||
                score < bestblur[b] ||
                (score == bestblur[b] && dist < bestdist[b])) {
                bestpts[b] = pts
                bestblur[b] = score
                bestdist[b] = dist
            }
        }
    }

    END {
        for (b in nearpts) {
            if (b in bestpts)
                print bestpts[b]
            else
                print nearpts[b]
        }
    }
' "$tmp/meta" >"$tmp/picks"

count=$(awk 'END { print NR + 0 }' "$tmp/picks")

[ "$count" -gt 0 ] || {
    printf '%s\n' "No suitable frames found" >&2
    exit 1
}

printf '[1/2] Analysis complete: %s frames selected.\n' "$count" >&2

# Build a balanced expression tree. This avoids FFmpeg parser-depth
# problems from a long flat eq()+eq()+eq()+... expression. If requested,
# scale to cover the target with square pixels, then crop exactly at centre.
awk -v width="$width" -v height="$height" '
    {
        a[NR] = "eq(pts\\," $1 ")"
    }

    END {
        n = NR

        while (n > 1) {
            m = 0

            for (i = 1; i <= n; i += 2) {
                m++

                if (i < n)
                    b[m] = "(" a[i] "+" a[i + 1] ")"
                else
                    b[m] = a[i]
            }

            for (i = 1; i <= m; i++)
                a[i] = b[i]

            n = m
        }

        filter = "setpts=PTS-STARTPTS,select=" a[1]

        if (width != "")
            filter = filter ",scale=w=" width ":h=" height \
                ":force_original_aspect_ratio=increase" \
                ":flags=lanczos+accurate_rnd+full_chroma_int:reset_sar=1" \
                ",crop=w=" width ":h=" height ":exact=1"

        print filter
    }
' "$tmp/picks" >"$tmp/filter"

mkdir "$out" || {
    printf 'Could not create output directory: %s\n' "$out" >&2
    exit 1
}

printf '[2/2] Extracting %s lossless PNG frames...\n' "$count" >&2

if ! ffmpeg -nostdin -hide_banner -loglevel error \
    -stats -stats_period 2 -n \
    -i "$input" \
    -map 0:v:0 \
    -/filter:v "$tmp/filter" \
    -fps_mode passthrough \
    -start_number 0 \
    "$out/frame_%06d.png"
then
    printf 'Extraction failed; partial output may remain in: %s\n' "$out" >&2
    exit 1
fi

actual=0
for file in "$out"/frame_*.png; do
    [ -f "$file" ] || continue
    actual=$((actual + 1))
done

[ "$actual" -eq "$count" ] || {
    printf 'Expected %s frames but wrote %s; output left in: %s\n' \
        "$count" "$actual" "$out" >&2
    exit 1
}

printf 'Done: %s frames written to %s/\n' "$count" "$out" >&2
