#!/usr/bin/env bash
# Video encoding script using ffmpeg with H.264 and scaling to FHD.

set -euo pipefail

# Colors for log output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'

# Logging functions for user-friendly output
log_info() { echo -e "${BLUE}INFO:${NC} $1"; }
log_success() { echo -e "${GREEN}SUCCESS:${NC} $1"; }
log_warn() { echo -e "${YELLOW}WARN:${NC} $1"; }
log_error() { echo -e "${RED}ERROR:${NC} $1" >&2; }

# Gets the duration of a video file in seconds using ffprobe.
get_duration() {
  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$1" 2>/dev/null || echo "0"
}

# Default encoding parameters tuned for quality and compatibility.
readonly DEFAULT_CRF="23"
readonly DEFAULT_PRESET="medium"
readonly DEFAULT_MAXRATE="6M"
readonly DEFAULT_BUFSIZE="12M"
readonly DEFAULT_MAX_W="1920"
readonly DEFAULT_MAX_H="1080"

# Global state variables modified during argument parsing and validation.
INFILE=""
OUTFILE=""
CRF="${DEFAULT_CRF}"
PRESET="${DEFAULT_PRESET}"
MAXRATE="${DEFAULT_MAXRATE}"
BUFSIZE="${DEFAULT_BUFSIZE}"
EXTRA_ARGS=()
MAX_W="${DEFAULT_MAX_W}"
MAX_H="${DEFAULT_MAX_H}"
OVERWRITE_SAME=false
OVERWRITE_DIFFERENT=false
IS_BATCH=false
INPUT_DIR=""
INPUT_FILES=()

# Prints usage information to stdout.
usage() {
  cat <<EOF
Usage: $(basename "$0") -i input.mp4 [options]

  Options:
    -i, --input            Input file or directory (required)
    -o, --output           Output file or directory (default: <input>_videncode.mp4 or original name if different dir)
    -c, --crf              CRF value (default: ${DEFAULT_CRF})
    -p, --preset           x264 preset (default: ${DEFAULT_PRESET})
    -m, --maxrate          Maxrate constraint (default: ${DEFAULT_MAXRATE})
    -b, --bufsize          Bufsize constraint (default: ${DEFAULT_BUFSIZE})
    --extra                Extra ffmpeg args (wrap in quotes). May be specified multiple times.
    --max-w                Maximum width in pixels (default: ${DEFAULT_MAX_W})
    --max-h                Maximum height in pixels (default: ${DEFAULT_MAX_H})
    --overwrite-same       Overwrite output even if duration matches input
    --overwrite-different  Overwrite output even if duration differs from input
    -h, --help             Show this help
EOF
}

# Parses command-line arguments, supporting both short and long options.
parse_args() {
  if [[ $# -eq 0 ]]; then
    usage
    exit 1
  fi

  while [[ $# -gt 0 ]]; do
    case "$1" in
      -i|--input) INFILE="$2"; shift 2 ;;
      -o|--output) OUTFILE="$2"; shift 2 ;;
      -c|--crf) CRF="$2"; shift 2 ;;
      -p|--preset) PRESET="$2"; shift 2 ;;
      -m|--maxrate) MAXRATE="$2"; shift 2 ;;
      -b|--bufsize) BUFSIZE="$2"; shift 2 ;;
      --extra) EXTRA_ARGS+=("$2"); shift 2 ;;
      --max-w) MAX_W="$2"; shift 2 ;;
      --max-h) MAX_H="$2"; shift 2 ;;
      --overwrite-same) OVERWRITE_SAME=true; shift ;;
      --overwrite-different) OVERWRITE_DIFFERENT=true; shift ;;
      -h|--help) usage; exit 0 ;;
      --) shift; break ;;
      *) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
    esac
  done
}

# Validates input and determines output path based on rules to avoid overwriting.
validate_input() {
  if [[ -z "${INFILE:-}" ]]; then
    log_error "input (-i/--input) is required."
    usage
    exit 1
  fi

  if [[ -d "$INFILE" ]]; then
    IS_BATCH=true
    INPUT_DIR="$INFILE"
    log_info "Processing directory: $INFILE"
    # Find video files in the directory
    while IFS= read -r -d '' file; do
      INPUT_FILES+=("$file")
    done < <(find "$INPUT_DIR" -type f \( -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.mov" -o -iname "*.flv" -o -iname "*.wmv" -o -iname "*.webm" -o -iname "*.m4v" \) -print0)
    if [[ ${#INPUT_FILES[@]} -eq 0 ]]; then
      log_error "no video files found in directory: $INFILE"
      exit 1
    fi
    log_info "Found ${#INPUT_FILES[@]} video file(s) to process"
  elif [[ -f "$INFILE" ]]; then
    IS_BATCH=false
    INPUT_FILES=("$INFILE")
    log_info "Processing single file: $INFILE"
  else
    log_error "input not found or not a file/directory: $INFILE"
    exit 1
  fi
}

# Validates single input file and determines output path.
validate_input_single() {
  # Determine output path: use _videncode.mp4 suffix in same dir, original name in different dir.
  if [[ -n "$OUTFILE" && -d "$OUTFILE" ]]; then
    if [[ "$OUTFILE" == "$INPUT_DIR" ]]; then
      OUTFILE="${OUTFILE%/}/$(basename "${INFILE%.*}")_videncode.mp4"
    else
      OUTFILE="${OUTFILE%/}/$(basename "$INFILE")"
    fi
  elif [[ -z "$OUTFILE" ]]; then
    OUTFILE="${INFILE%.*}_videncode.mp4"
  fi

  log_info "Output will be: $OUTFILE"

  # Prevent accidental overwriting of the input file.
  if [[ "$OUTFILE" == "$INFILE" ]]; then
    log_error "output file would overwrite input file."
    exit 1
  fi
}

# Assembles the full ffmpeg command array with all configured options.
build_ffmpeg_cmd() {
  # Sets audio flags to copy streams unchanged for fidelity.
  local audio_flags=("-c:a" "copy")

  # Constructs the ffmpeg video scaling filter to fit within max dimensions while preserving aspect ratio.
  # Scale logic: for landscape (w>h), constrain width to MAX_W and height to MAX_H;
  # for portrait (h>w), constrain width to MAX_H and height to MAX_W.
  # Commas are escaped for safe shell passing; force_original_aspect_ratio=decrease avoids upscaling.
  local vf="scale='if(gt(iw\\,ih)\\,min(iw\\,${MAX_W})\\,min(iw\\,${MAX_H}))':'if(gt(iw\\,ih)\\,min(ih\\,${MAX_H})\\,min(ih\\,${MAX_W}))':force_original_aspect_ratio=decrease"

  CMD=(ffmpeg -y -i "$INFILE" -map_metadata 0 -c:v libx264 -preset "$PRESET" -crf "$CRF" -maxrate "$MAXRATE" -bufsize "$BUFSIZE" -vf "$vf" "${audio_flags[@]}" -movflags +faststart)

  # Safely append user-provided extra arguments by splitting on whitespace.
  if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then
    for ea in "${EXTRA_ARGS[@]}"; do
      read -r -a split <<< "$ea"
      CMD+=("${split[@]}")
    done
  fi

  CMD+=("$OUTFILE")
}

# Executes the constructed ffmpeg command after displaying it for transparency.
run_ffmpeg() {
  log_info "Processing: $INFILE -> $OUTFILE"
  if [[ -f "$OUTFILE" ]]; then
    input_duration=$(get_duration "$INFILE")
    output_duration=$(get_duration "$OUTFILE")
    if [[ "$input_duration" == "$output_duration" && "$output_duration" != "0" ]]; then
      if [[ "$OVERWRITE_SAME" == false ]]; then
        log_warn "Skipping existing file with matching duration: $OUTFILE"
        return
      fi
    elif [[ "$IS_BATCH" == false ]]; then
      if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
        read -p "Output file exists with different duration, overwrite? (y/N) " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
          log_warn "User chose not to overwrite: $OUTFILE"
          return
        fi
      fi
    else
      if [[ "$OVERWRITE_DIFFERENT" == false ]]; then
        log_warn "Skipping existing file with different duration: $OUTFILE"
        return
      fi
    fi
  fi
  log_info "Starting encoding..."
  log_info "Command:"
  printf ' %q' "${CMD[@]}"
  echo
  "${CMD[@]}"
  log_success "Encoding completed: $OUTFILE"
}

# Main entry point: orchestrates parsing, validation, command building, and execution.
main() {
  parse_args "$@"
  validate_input
  if [[ "$IS_BATCH" == true ]]; then
    # Save the output directory for batch processing
    local OUTPUT_DIR="$OUTFILE"
    local total=${#INPUT_FILES[@]}
    local count=1
    for file in "${INPUT_FILES[@]}"; do
      log_info "Processing file $count of $total: $(basename "$file")"
      INFILE="$file"
      OUTFILE="$OUTPUT_DIR"  # Reset OUTFILE for each iteration
      validate_input_single
      build_ffmpeg_cmd
      run_ffmpeg
      ((count++))
    done
    log_success "Batch processing completed: $total file(s) processed"
  else
    validate_input_single
    build_ffmpeg_cmd
    run_ffmpeg
  fi
}

main "$@"
