#!/usr/bin/env bash

# Verify fixity and a few parameters of a Matroska/FFV1 file.
#
# Copyright (c) 2012-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)}"

# initialise variable
unset input_file


# 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_file>
  ${SCRIPT_NAME} -h | -x
Options:
  -i  input 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: Verify fixity and a few parameters of a Matroska/FFV1 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}'

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


# verify input
verify_input() {
  local in_file="${1}"

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

  return 0
}


# verify validity
verify_validity() {
  local in_file="${1}"
  local codec
  local container
  local ffv1_vers
  local line
  local metadata

  echo "$(date_time) verify FFV1" >> "${LOG_FILE}"
  metadata="$(${ffprobe_bin} -show_format -show_streams -print_format flat -v quiet ${in_file})"

  # verity the Matroska or Audio Video Interleaved container
  if [[ "${in_file##*.}" == 'mkv' ]]; then
    container="$(echo "${metadata}" | grep 'format_name' | grep -o 'matroska')"
    if [[ "${container}" == 'matroska' ]]; then
      echo "> container: Matroska ('.mkv' file)"
    else
      abort "Error: The container is not Matroska, despite the file extension 'mkv'."
    fi
  elif [[ "${in_file##*.}" == 'avi' ]]; then
    container="$(echo "${metadata}" | grep 'format_name' | grep -o 'avi')"
      if [[ "${container}" == 'avi' ]]; then
      echo "> container: Audio Video Interleaved ('.avi' file)"
    else
      abort "Error: The container is not Audio Video Interleaved, despite the file extension 'avi'."
    fi
  elif [[ "${in_file##*.}" == 'mxf' ]]; then
    container="$(echo "${metadata}" | grep 'format_name' | grep -o 'mxf')"
      if [[ "${container}" == 'mxf' ]]; then
      echo "> container: Material eXchange Format ('.mxf' file)"
    else
      abort "Error: The container is not Material eXchange Format, despite the file extension 'mxf'."
    fi
  else
    echo "The container is '${in_file##*.}'." >> "${LOG_FILE}"
    echo -e "${BLUE}Warning: The container is '${in_file##*.}'.${NC}"
  fi

  # verify the FFV1 video codec
  codec="$(echo "${metadata}" | grep 'codec_name' | grep -o 'ffv1')"
  if [[ "${codec}" == 'ffv1' ]]; then
    echo "> video codec: FFV1"
  else
    abort "Error: The video codec is not FFV1."
  fi

  # FFV1 version
  line="$(${ffprobe_bin} -debug 1 ${input_file} 2>&1 | grep '\[ffv1 @ 0x' | head -n 1)"
  ffv1_vers="$(echo "${line}" | grep -oE 'ver:[0-9].?[0-9]?' | grep -oE '[0-3]\.?[0-4]?')"
  echo "$(date_time) FFV1 version ${ffv1_vers}" >> "${LOG_FILE}"
  if [[ "${ffv1_vers}" == '' ]]; then
    abort "Error: The file '${input_file}' is not valid."
  elif [[ "${ffv1_vers}" =~ 3.[0-4] ]]; then
    echo "> FFV1 version: ${ffv1_vers}"
  elif [[ "${ffv1_vers}" == '2.0' ]]; then
    echo -e "${BLUE}Warning: The FFV1 file is of version 2.0, which should not exist in the wild.${NC}"
    echo "Please report this file to <info@reto.ch>. Thank you very much indeed!"
  elif [[ "${ffv1_vers}" == '1' ]]; then
    echo -e "${BLUE}Warning: The FFV1 file is of version 1, which is old.${NC}"
  elif [[ "${ffv1_vers}" == '0' ]]; then
    echo -e "${BLUE}Warning: The FFV1 file is of version 0, which is very old.${NC}"
  else
    abort "Error: Unknown FFV1 version '${ffv1_vers}'."
  fi

  # check fixity
  echo "$(date_time) check fixity" >> "${LOG_FILE}"
  echo -e "${BLUE}Please wait while checking fixity...${NC}"
  echo "$(date_time) input='${in_file}'" >> "${LOG_FILE}"
  echo "$(date_time) ${ffmpeg_bin} -i ${in_file} -f null -" >> "${LOG_FILE}"
  if "${ffmpeg_bin}" -i "${in_file}" -f null - >> "${LOG_FILE}" 2>&1; then
    echo -e "${BLUE}... all is okay!${NC}"
  else
    abort "Error: '${in_file}' is not correct, see '${LOG_FILE}'."
  fi

  return 0
}


# parse and process provided input
(( $# == 0 )) && print_prompt
while getopts ":i:-:hx" opt; do
  case "${opt}" in
    i) if [[ "${OPTARG:0:1}" == '-' ]]; then
         abort "Error: The option '-i' requires an argument."
       else
         input_file="${OPTARG}"
       fi ;;
    -) case "${OPTARG}" in
         input=?*) input_file="${OPTARG#*=}" ;;
         ffmpeg=?*) ffmpeg_bin="${OPTARG#*=}" ;;
         ffprobe=?*) ffprobe_bin="${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 that FFmpeg is running
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

# check that provided input file is valid
verify_input "${input_file}"

# verify validity of the provided file
verify_validity "${input_file}"

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