#!/usr/bin/env bash

# Make an MP4/H.264 file.
#
# Copyright (c) 2006-2026 by Reto Kromer <https://reto.ch/>
#
# This Bash script is released under a 3-Clause BSD License and is provided
# "as is" without warranty or support of any kind.


# initialise constants
VERSION='2026-01-02'
SCRIPT_NAME="$(basename "$0")"
CONFIG_FILE="${HOME}/.config/AVpres/Bash_AVpres/${SCRIPT_NAME}.txt"
RED='\033[1;31m'
BLUE='\033[1;34m'
NC='\033[0m'

# load configuration file if any and initialise default values
[[ -f "${CONFIG_FILE}" ]] && . "${CONFIG_FILE}"
ffmpeg_bin="${ffmpeg_bin:-$(which ffmpeg)}"
ffprobe_bin="${ffprobe_bin:-$(which ffprobe)}"
ff_glo_opt="${ff_glo_opt:--y}"
f="${f:-image2}"
framerate="${framerate:-24}"
filter_v="${filter_v}"
c_v="${c_v:-libx264}"
preset="${preset:-veryslow}"
crf="${crf:-21}"
qp="${qp:-21}"
pix_fmt="${pix_fmt:-yuv420p}"
filter_a="${filter_a}"
c_a="${c_a:-aac}"
movflags="${movflags:-+faststart+write_colr}"
suffix="${suffix:-_H264}"
extension="${extension:-mp4}"

# initialise other default values
regex_c_v='^\(libx264|libx264rgb|h264\$)'
regex_pix_fmt='^\(yuv420p|yuv420p10le\$)'

# initialise variables
unset input_path
unset output_path
unset ff_in_opt
unset ff_out_opt
unset first_file
unset start_number
unset input_file_regex


# get date-and-time stamp
date_time() {
  TZ='UTC' date +'[%F %T %Z]'
}


# start log file
[[ -d '/tmp/AVpres' ]] || mkdir -p '/tmp/AVpres'
LOG_FILE="$(mktemp "/tmp/AVpres/${SCRIPT_NAME}.XXXXXXXXXX")"
echo "$(date_time) ${SCRIPT_NAME} ${VERSION}" > "${LOG_FILE}"
echo "$(date_time) $0 $*" >> "${LOG_FILE}"
echo "$(date_time) START" >> "${LOG_FILE}"

# check that Bash 3.2 or later is running
bash_version="$(bash -c 'echo ${BASH_VERSION}')"
echo "$(date_time) running bash version = '${bash_version}'" >> "${LOG_FILE}"
if ! printf '%s\n%s\n' "${bash_version}" "3.2" | sort -rVC; then
  echo -e "${BLUE}Warning: This 'bash' binary is very old."
  echo -e "We strongly recommend that you use the current version 5.3.${NC}"
fi


# print an error message and exit with status 1
abort() {
  echo -e "${RED}${1:-An unknown error occurred.\a}${NC}"
  echo "$(date_time) ${1:-An unknown error occurred.}" >> "${LOG_FILE}"
  echo "$(date_time) END" >> "${LOG_FILE}"
  exit 1
}


# print a minimal help message and exit with status 1
print_prompt() {
  echo "$(date_time) print prompt" >> "${LOG_FILE}"
  cat << EOF
Help:
  ${SCRIPT_NAME} -h
EOF
  echo "$(date_time) END" >> "${LOG_FILE}"
  exit 1
}


# print the help message and exit with status 0
print_help() {
  local tmp
  if [[ -f "${CONFIG_FILE}" ]]; then
    tmp="local configuration file found and loaded"
  else
    tmp="no local configuration file found on this computer"
  fi

  echo "$(date_time) print help" >> "${LOG_FILE}"
  cat << EOF

Usage:
  ${SCRIPT_NAME} -i <input_path> -o <output_file>
  ${SCRIPT_NAME} -h | -x
Options:
  -i  input file (stream based) or folder (single-image based)
  -o  output file
  -h  this help
  -x  advanced options with their default arguments
      (${tmp})
Dependencies:
  ffmpeg and ffprobe
See also:
  man ${SCRIPT_NAME}
  https://avpres.net/Bash_AVpres/
About:
  Abstract: Make an MP4/H.264 file
  Version:  ${VERSION}

EOF
  echo "$(date_time) END" >> "${LOG_FILE}"
  exit 0
}


# display advanced options with their default arguments and exit with status 0
print_options() {
  echo "$(date_time) print options" >> "${LOG_FILE}"
  if [[ -f "${CONFIG_FILE}" ]]; then
    cat << EOF

Local configuration file
  '${CONFIG_FILE}'
found and loaded.
EOF
  else
    cat << EOF

No local configuration file for '${SCRIPT_NAME}' found on this computer.
EOF
  fi
  cat << EOF

FFmpeg default binaries:
  --ffmpeg='${ffmpeg_bin}'
  --ffprobe='${ffprobe_bin}'

Global ffmpeg option:
  --ff_glo_opt='${ff_glo_opt}'

Input file default parameters (used only for single-image based input files):
  --f='${f}'
  --framerate='${framerate}'

Video codec default parameters:
  --filter_v='${filter_v}'
  --c_v='${c_v}'
  --preset='${preset}'
  --crf='${crf}'
  --qp='${qp}'
  --pix_fmt='${pix_fmt}'

Audio codec default parameters (only used when audio stream is present):
  --filter_a='${filter_a}'
  --c_a='${c_a}'

Output file default parameters:
  --movflags='${movflags}'
  --suffix='${suffix}'
  --extension='${extension}'

EOF
  echo "$(date_time) END" >> "${LOG_FILE}"
  exit 0
}


# check if external ffmpeg and ffprove commands are running
check_ffmpeg() {
  if command -v "${ffmpeg_bin}" &> /dev/null; then
    echo "$(date_time) '${ffmpeg_bin}' found" >> "${LOG_FILE}"
  else
    abort "Error: 'ffmpeg' binary not found."
  fi
  if command -v "${ffprobe_bin}" &> /dev/null; then
    echo "$(date_time) '${ffprobe_bin}' found" >> "${LOG_FILE}"
  else
    abort "Error: 'ffprobe' binary not found."
  fi
}


# verify if the input file is valid
verify_input() {
  local in_file="${1}"

  echo "$(date_time) verify input" >> "${LOG_FILE}"
  if [[ ! "${in_file}" || "${in_file}" == "_." ]]; then
    abort "Error: No input file or folder provided via '-i' or '--input'."
  elif [[ ! -f "${in_file}" ]]; then
    abort "Error: '${input_path}' is not a valid input."
  elif "${ffprobe_bin}" "${in_file}" 2>&1 | grep "Invalid data found" > /dev/null; then
    abort "Error: '$(basename ${input_path})' is not an AV file."
  fi

  return 0
}


# verify if the output file is valid
verify_output() {
  local out_file="${1}"

  echo "$(date_time) verify output" >> "${LOG_FILE}"
  if [[ ! "${out_file}" ]]; then
    abort "Error: No output file provided via '-o' or '--output'."
  elif ! : > "${out_file}.txt" &> /dev/null; then
    abort "Error: Cannot create an output file '${out_file}'."
  else
    rm "${out_file}.txt"
  fi
  if [[ "${input_path}" == "${output_path}" ]]; then
    abort "Error: '${input_path}' cannot be both input and output."
  fi

  return 0
}


# prepare the parameters for the ffmpeg command
prepare_command() {
  if [[ -d "${input_path}" ]]; then
    if [[ "${start_number}" == $(basename "${first_file%.*}") ]]; then
      input_file_regex="%0${#start_number}d.${first_file##*.}"
    else
      input_file_regex="${first_file%_*}_%0${#start_number}d.${first_file##*.}"
    fi
    ff_in_opt+=" -f ${f} -framerate ${framerate}"
    if (( $(echo "${start_number}" | bc -l) > 1 )); then
      ff_in_opt+=" -start_number ${start_number}"
    fi
    ff_out_opt+=" -an"
  elif [[ $("${ffprobe_bin}" "${input_path}" -show_streams -select_streams a -loglevel quiet) == '' ]]; then
    ff_out_opt+=" -an"
  else
    if [[ "${filter_a}" ]]; then
      ff_out_opt+=" -filter:a ${filter_a}"
    fi
    ff_out_opt+=" -c:a ${c_a}"
  fi
  if [[ "${filter_v}" ]]; then
    ff_out_opt+=" -filter:v ${filter_v}"
   fi
  ff_out_opt+=" -c:v ${c_v} -preset ${preset} -pix_fmt ${pix_fmt}"
  if [[ "${crf}" != '#' ]]; then
    ff_out_opt+=" -crf ${crf}"
  elif [[ "${qp}" ]]; then
    ff_out_opt+=" -qp ${qp}"
  fi
  if [[ "${movflags}" != '#' ]]; then
    ff_out_opt+=" -movflags ${movflags}"
  fi
  if [[ -d "${input_path}" ]]; then
    input_file="${input_path}/${input_file_regex}"
  else
    input_file="${input_path}"
  fi
  if [[ "${suffix}" == '#' ]]; then
    if [[ "${output_path}" == "${output_path##*.}" ]]; then
      output_file="${output_path%.*}.${extension}"
    else
      output_file="${output_path}"
    fi
  else
    if [[ "${output_path}" == "${output_path##*.}" ]]; then
      output_file="${output_path%.*}${suffix}.${extension}"
    else
      output_file="${output_path%.*}${suffix}.${output_path##*.}"
    fi
  fi
}


# transcode to MP4/H.264
transcode() {
  local in_file="${1}"
  local out_file="${2}"

  echo "$(date_time) Generating '${out_file}'" >> "${LOG_FILE}"
  echo -e "${BLUE}Please wait while generating '${out_file}'...${NC}"
  [[ "${ff_glo_opt}" == '#' ]] && ff_glo_opt=''
  echo "$(date_time) ${ffmpeg_bin} ${ff_glo_opt} ${ff_in_opt} -i ${in_file} \
    ${ff_out_opt} ${out_file}" >> "${LOG_FILE}"
  if ! "${ffmpeg_bin}" ${ff_glo_opt} ${ff_in_opt} -i "${in_file}" \
    ${ff_out_opt} "${out_file}" >> "${LOG_FILE}" 2>&1
  then
    abort "Fatal error, see '${LOG_FILE}'."
  fi
  echo -e "${BLUE}... done.${NC}"

  return 0
}


# parse and process provided input
(( $# == 0 )) && print_prompt
while getopts ":i:o:-:hx" opt; do
  case "${opt}" in
    i) if [[ "${OPTARG:0:1}" == '-' ]]; then
         abort "Error: The option '-i' requires an argument."
       else
         input_path="${OPTARG}"
       fi ;;
    o) if [[ "${OPTARG:0:1}" == '-' ]]; then
         abort "Error: The option '-o' requires an argument."
       else
         output_path="${OPTARG}"
       fi ;;
    -) case "${OPTARG}" in
         input=?*) input_path="${OPTARG#*=}" ;;
         output=?*) output_path="${OPTARG#*=}" ;;
         ffmpeg=?*) ffmpeg_bin="${OPTARG#*=}" ;;
         ffprobe=?*) ffprobe_bin="${OPTARG#*=}" ;;
         ff_glo_opt=?*) ff_glo_opt="${OPTARG#*=}" ;;
         f=?*) f="${OPTARG#*=}" ;;
         framerate=?*) framerate="${OPTARG#*=}" ;;
         filter_v=?*) filter_v="${OPTARG#*=}" ;;
         c_v=?*) c_v="${OPTARG#*=}" ;;
         preset=?*) preset="${OPTARG#*=}" ;;
         crf=?*) crf="${OPTARG#*=}" ;;
         qp=?*) qp="${OPTARG#*=}" ;;
         pix_fmt=?*) fix_fmt="${OPTARG#*=}" ;;
         filter_a=?*) filter_a="${OPTARG#*=}" ;;
         c_a=?*) c_a="${OPTARG#*=}" ;;
         movflags=?*) movflags="${OPTARG#*=}" ;;
         suffix=?*) suffix="${OPTARG#*=}" ;;
         extension=?*) extension="${OPTARG#*=}" ;;
         help) print_help ;;
         options) print_options ;;
         *) abort "Error: The option '--${OPTARG}' is not valid." ;;
       esac ;;
    h) print_help ;;
    x) print_options ;;
    :) abort "Error: The option '-${OPTARG}' requires an argument." ;;
    *) abort "Error: The option '-${OPTARG}' is not valid." ;;
  esac
done

# check if FFmpeg is running
check_ffmpeg

# check if provided input file is valid
if [[ -d "${input_path}" ]]; then
  first_file=$(ls "${input_path}" | sort | head -n 1)
  start_number=$(echo "${first_file}" | grep -oE '_[0-9]+\.' | grep -oE '[0-9]+')
  if [[ ! "${start_number}" ]]; then
    start_number=$(echo "${first_file}" | grep -oE '[0-9]+')
  fi
  if [[ "${start_number}" == $(basename "${first_file%.*}") ]]; then
    verify_input "${input_path}/${start_number}.${first_file##*.}"
  else
    verify_input "${input_path}/${first_file%_*}_${start_number}.${first_file##*.}"
  fi
else
  verify_input "${input_path}"
fi

# check if provided output file is valid
verify_output "${output_path}"

# transcode from input file to output file
prepare_command
transcode "${input_file}" "${output_file}"

# end log file
echo "$(date_time) END" >> "${LOG_FILE}"
