#!/bin/bash
# MODIFIED by wdef:
# many lines:  array appending, for compatability with older bash syntax where newer =+ fails
# Ditto lines  563, 569 for string concat
# notation array= ( ) to declare an empty array is also bash version specific and causes problems - removed
# ln 568: print zero hours in stamp - minutes were getting printed as hours
# removed millseconds in stamp - since mplayer manual refers only to three fields hrs:mins:secs
# ln 962: capture 5 frames and discard first 4 -  required for my mplayer-1.0pre8cvs or broken: only get first frame every vidcap
# this is probably why "mplayer seems to fail on mpeg or wmv9 files" (below)
# http://lists.mplayerhq.hu/pipermail/mencoder-users/2006-August/003843.html

# ffmeg (2004 cvs) simply cannot do filenames containg spaces, whatever anyone says about quotes etc.
# which suggests default (line 145) should  be mplayer with fix as above.


# $Rev: 968 $ $Date: 2007-11-08 15:52:44 +0100 (dj, 08 nov 2007) $
#
# vcs
# Video Contact Sheet *NIX: Generates contact sheets (previews) of videos
#
# Copyright (C) 2007 Toni Corvera
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
# Author: Toni Corvera <outlyer@gmail.com>
#
# References:
# Pages I've taken snippets from or wrote code based on them.
# I refer to them in comments as e.g. [[R1]]. [[R1#3]] Means section 3 in R1.
# I also use internal references in the form [x1] (anchor -where to point-)
# and [[x1]] (x-reference -point to what-).
# [R0] getopt-parse.bash example, on Debian systems:
#      /usr/share/doc/util-linux/examples/getopt-parse.bash.gz
# [R1] Bash (and other shells) tips
#      <http://wooledge.org/mywiki/BashFaq>
# [R2] List of officially registered FOURCCs and WAVE Formats (aka wFormatTag)
#      <http://msdn2.microsoft.com/en-us/library/ms867195.aspx>
# [R3] Unofficial list of FOURCCs
#      <http://www.fourcc.org/>
# [R4] A php module with a list of FOURCCs and wFormatTag's mappings
#      <http://webcvs.freedesktop.org/clipart/experimental/rejon/getid3/getid3/module.audio-video.riff.php>
#

declare -r VERSION="1.0.10-wdef"
# {{{ # CHANGELOG
# History (The full changelog was moved to a separate file and can be found
# at <http://p.outlyer.net/vcs/files/CHANGELOG>).
#
# 1.0.10: (2007-11-08)
#    * BUGFIX: Corrected aspect guessing bug: would fail if width
#              was standard but height not
#    * FEATURE: Allow explicitly disabling timestamps (-dt or
#               --disable timestamps)
#    * FEATURE: Allow explicitly disabling shadows (-ds or --disable shadows)
#    * Added HD resolution guessed aspect ratio (defaults to 16/9)
#    * OTHER: Changed e-mail address in the comments to gmail's, would probably
#             get a quicker response.
# }}} # CHANGELOG

set -e

# {{{ # TODO

# TODO / FIXME:
# * [[R1#22]] states that not all bc versions understand '<', more info required
# * [[x1]] Find out why the order of ffmpeg arguments breaks some files.
# * [[x2]] Find out if egrep is safe to use or grep -E is more commonplace.
#

# }}} # TODO

# {{{ # Constants

# Configuration file, please, use this file to modify the behaviour of the
# script. Using this allows overriding some variables (see below)
# to your liking. Only lines with a variable assignment are evaluated,
# it should follow bash syntax, note though that ';' can't be used
# currently in the variable values; e.g.:
#
#	# Sample configuration for vcs
#	user=myname		# Sign all compositions as myname
#	bg_heading=gray	# Make the heading gray
#
# There is a total of three configuration files than are loaded if the exist:
# * /etc/vcs.conf: System wide conf, least precedence
# * $CFGFILE (by default ~/.vcs.conf): Per-user conf, second least precedence
# * ./vcs.conf: Per-dir confif, most precedence
#
# The variables that can be overriden are below the block of constants ahead.
declare -r CFGFILE=~/.vcs.conf

# see $decoder
declare -ri DEC_MPLAYER=1 DEC_FFMPEG=3
# See $timecode_from
declare -ri TC_INTERVAL=4 TC_NUMCAPS=8
# These can't be overriden, modify this line if you feel the need
declare -r PROGRAM_SIGNATURE="Video Contact Sheet *NIX ${VERSION} <http://p.outlyer.net/vcs/>"
# see $safe_rename_pattern
declare -r DEFAULT_SAFE_REN_PATT="%b-%N.%e"
# see $extended_factor
declare -ri DEFAULT_EXT_FACTOR=4
# see $verbosity
declare -ri V_ALL=5 V_NONE=-1 V_ERROR=1 V_WARN=2 V_INFO=3
# see $font_filename and $FONT_MINCHO
declare -ri FF_DEFAULT=5 FF_MINCHO=7
# Indexes in $VID
declare -ri W=0 H=1 FPS=2 LEN=3 VCODEC=4 ACODEC=5 VDEC=6 CHANS=7
# Exit codes, same numbers as /usr/include/sysexits.h
declare -r EX_OK=0   EX_USAGE=64  EX_UNAVAILABLE=69  \
           EX_NOINPUT=66   EX_SOFTWARE=70   EX_CANTCREAT=73 \
           EX_INTERRUPTED=79 # This one is not on sysexits.h
# The context allows the creator to identify which contact sheet it is creating
# (CTX_*) HL: Highlight (-l), STD: Normal, EXT: Extended (-e)
declare -ri CTX_HL=1 CTX_STD=2 CTX_EXT=3

# }}} # End of constants

# {{{ # Override-able variables

declare -i DEFAULT_INTERVAL=300
declare -i DEFAULT_NUMCAPS=16
declare -i DEFAULT_COLS=2
# Text before the user name in the signature
declare user_signature="Preview created by"
# By default sign as the system's username (see -u, -U)
declare user=$(id -un)
# Which of the two methods should be used to guess the number of thumbnails
declare -i timecode_from=$TC_INTERVAL
# Which of the two vidcappers should be used (see -F, -M)
# mplayer seems to fail for mpeg or WMV9 files, at least on my system
# also, ffmpeg allows better seeking: ffmpeg allows exact second.fraction
# seeking while mplayer apparently only seeks to nearest keyframe
declare -i decoder=$DEC_MPLAYER
# Options used in imagemagick, these options set the final aspect
# of the contact sheet
declare output_format=png     # ImageMagick decides the type from the extension
declare -i output_quality=92  # Output image quality (only affects the final
                              # image and obviously only in lossy formats)
# Colours, see convert -list color to get the list
declare bg_heading=YellowGreen  # Background for meta info (size, codec...)
declare bg_sign=SlateGray       # Background for signature
declare bg_title=White          # Background for the title (see -T)
declare bg_contact=White        # Background of the thumbnails
declare fg_heading=black        # Font colour for meta info box
declare fg_sign=black           # Font colour for signature
declare fg_tstamps=white        # Font colour for timestamps
declare fg_title=Black          # Font colour fot the title
# Fonts, see convert -list type to get the list
declare font_tstamps=courier    # Used for timestamps over the thumbnails
declare font_heading=helvetica  # Used for the heading (meta info box)
declare font_sign=$font_heading # Used for the signature box
# Unlike other font_ variables this doesn't take a font name directly
# but is restricted to the $FF_ values. This is to allow overrides
# from the command line to be placed anywhere, i.e. in
#  $ vcs -I file.avi -O 'FONT_MINCHO=whatever'
# as the font is overridden is after requesting its use, it wouldn't be
# affected 
# The other font_ variables are only affected by overrides and not command
# line options that's why this one is special.
declare font_filename=$FF_DEFAULT # Used to print only the filename in the heading
declare font_title=$font_heading # Used fot the title (see -T)
# Font sizes, in points
declare -i pts_tstamps=18          # Used for the timestamps
declare -i pts_meta=16             # Used for the meta info box
declare -i pts_sign=11             # Used for the signature
declare -i pts_title=36            # Used for the title (see -T)
# See --shoehorn
declare shoehorned=
# See -E / $end_offset
declare -i DEFAULT_END_OFFSET=60
# This can only be changed in the configuration file
# Change it to change the safe renanimg:
# When writing the output file, the input name + output extension is
# used (e.g.: "some video.avi.png"), if it already exists, though,
# a number if appended to the name. This variable dictates where the number is
# placed.
# By default "%b-%N.%e" where:
# %b is the basename (file name without extension)
# %N is the appended number
# %e is the extension
# The default creates outputs like "output.avi-1.png"
#
# If overridden with an incorrect value it will be silently set to the default
declare safe_rename_pattern="$DEFAULT_SAFE_REN_PATT"
# Controls how many extra captures will be created in the extended mode
# (see -e), 0 is the same as disabling the extended mode
# This number is multiplied by the total number of captures to get
# the number of extra captures. So, e.g. -n2 -e2 leads to 4 extra captures.
declare extended_factor=0
# Options added always to the ones in the command line
# (command line options override them).
# Note using this is a bit tricky :P mostly because I've no clue of how this
# should be done.
# As an example: you want to set always the title to "My Title" and output
# to jpeg: default_options="-T'My Title' -j"
#declare default_options=
# Verbosity level so far from the command line can only be muted (see -q)
# it can be overridden, though
declare -i verbosity=$V_ALL
# When set to 0 the status messages printed by vcs while running
# are coloured if the terminal supports it. Set to 1 if this annoys you.
declare -i plain_messages=0
# Experimental in 1.0.7b:
# Experiment to get international font support
# I'll need to get some help here, so if you use anything beyond a latin
# alphabet, please help me choosing the correct fonts
# To my understanding Ming/Minchō fonts should cover most of Japanse,
# Chinese and Korean
# Apparently Kochi Mincho should include Hangul *and* Cyrillic... which would be
# great :) Although it couldn't write my hangul test, and also the default font
# (helvetica) in my system seems to include cyrillic too, or at least a subset of
# it.
declare FONT_MINCHO=/usr/share/fonts/truetype/kochi/kochi-mincho.ttf
# Output of capturing programs is redirected here
declare stdout=/dev/null stderr=/dev/null

# }}} # End of override-able variables

# {{{ # Variables

# Options and other internal usage variables, no need to mess with this!
declare interval=$DEFAULT_INTERVAL     # Interval of captures (=numsecs/numcaps)
declare -i numcaps=$DEFAULT_NUMCAPS    # Number of captures (=numsecs/interval)
declare title=""
declare fromtime=0                    # Starting second (see -f)
declare totime=-1                     # Ending second (see -t)
declare -a initial_stamps      # Manually added stamps (see -S)
declare -i th_height= # Height of the thumbnails, by default use same as input
declare -i cols=$DEFAULT_COLS         # Number of output columns
declare -i manual_mode=0 # if 1, only command line timestamps will be used
declare aspect_ratio=0                # If 0 no transformations done (see -a)
                                      # If -1 try to guess (see -A)

declare -a TEMPSTUFF             # Temporal files
declare -a TIMECODES             # Timestamps of the video captures
declare -a HLTIMECODES          # Timestamps of the highlights (see -l)

declare VCSTEMPDIR=                   # Temporal directory, all temporal files
                                      # go there
# This holds the output of mplayer -identify on the current video
declare MPLAYER_CACHE=
# This holds the parsed values of MPLAYER_CACHE, see also the Indexes in VID
# (defined in the constants block)
declare -a VID=

# Workarounds:
# Argument order in FFmpeg is important -ss before or after -i will make
# the capture work or not depending on the file. See -Wo.
# TODO: [x1].
# Admittedly the workaraound is abit obscure: those variables will be added to
# the ffmpeg arguments, before and after -i, replacing spaces by the timestamp.
# e.g.: for second 50 '-ss ' will become '-ss 50' while '' will stay empty
# By default -ss goes before -i.
declare wa_ss_af="" wa_ss_be="-ss "
# This number of seconds is *not* captured from the end of the video
declare -i end_offset=$DEFAULT_END_OFFSET

# Experimental in 1.0.7b: transformations/filters
# Operations are decomposed into independent optional steps, this will allow
# to add some intermediate steps (e.g. polaroid mode)
# Filters in this context are functions.
# There're two kinds of filters and a delegate:
#  * individual filters are run over each vidcap
#  * global filters are run over all vidcaps at once
#  * The contact sheet creator delegates on some function to create the actual
#    contact sheet
#
# Individual filters take the form:
#  filt_name( vidcapfile, timestamp in seconds.milliseconds, width, height )
# They're executed in order by filter_vidcap()
declare -a FILTERS_IND=( 'filt_resize' 'filt_apply_stamp' )
# Global filters take the form
#  filtall_name( vidcapfile1, vidcapfile2, ... )
# They're executed in order by filter_all_vidcaps
declare -a FILTERS_CS
# The contact sheet creators take the form
#  csheet_name( number of columns, context, width, height, vidcapfile1,
#               vidcapfile2, ... ) : outputfile
# Context is one of the CTX_* constants (see below)
# The width and height are those of an individual capture
# It is executed by create_contact_sheet()
declare CSHEET_DELEGATE=csheet_montage

# Gravity of the timestamp (will be override-able in the future)
declare grav_timestamp=SouthEast

# When set to 1 the signature won't contain the "Preview created by..." line
declare -i anonymous_mode=0

# See chseet_montage for more details
declare -i DISABLE_SHADOWS=0

# }}} # Variables

# {{{ # Configuration handling

# These are the variables allowed to be overriden in the config file,
# please.
# They're REGEXes, they'll be concatenated to form a regex like
# (override1|override2|...).
# Don't mess with this unless you're pretty sure of what you're doing.
# All this extra complexity is done to avoid including the config
# file directly for security reasons.
declare -ra ALLOWED_OVERRIDES=(
	'user'
	'user_signature'
	'bg_.*'
	'font_.*'
	'pts_.*'
	'fg_.*'
	'output_quality'
	'DEFAULT_INTERVAL'
	'DEFAULT_NUMCAPS'
	'DEFAULT_COLS'
	'decoder'
	'output_format'
	'shoehorned'
	'timecode_from'
	'safe_rename_pattern'
#	'default_options'
	'extended_factor'
	'verbosity'
	'plain_messages'
	'FONT_MINCHO'
	'stdout'
	'stderr'
	'DEFAULT_END_OFFSET'
)

# This is only used to exit when -DD is used
declare -i DEBUGGED=0 # It will be 1 after using -D

# Loads the configuration files if present
# load_config()
load_config() {
	local CONFIGS=( /etc/vcs.conf $CFGFILE ./vcs.conf)

	for cfgfile in ${CONFIGS[*]} ;do
		if [ ! -f "$cfgfile" ]; then continue; fi

		while read line ; do # auto variable $line
			override "$line" "file $cfgfile" # Feeding it comments should be harmless
		done <$cfgfile
	done

	# Override-able hack, this won't work with command line overrides, though
	end_offset=$DEFAULT_END_OFFSET
	interval=$DEFAULT_INTERVAL
	numcaps=$DEAFULT_NUMCAPS
}

# Do an override
# It takes basically an assignment (in the same format as bash)
# to one of the override-able variables (see $ALLOWED_OVERRIDES).
# There are some restrictions though. Currently ';' is not allowed to
# be in the assignment.
# override($1 = bash variable assignment, $2 = source)
override() {
	local o="$1"
	local src="$2"

	local compregex=$( sed 's/ /|/g' <<<${ALLOWED_OVERRIDES[*]} )

	# Don't allow ';', FIXME: dunno how secure that really is...
	# FIXME: ...it doesn't really works anyway
	if ! egrep -q '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*=[^;]*' <<<"$o" ; then
		return
	fi
	if ! egrep -q "^($compregex)=" <<<"$o" ; then
		return
	fi

	local varname=$(sed -r 's/^[[:space:]]*([a-zA-Z0-9_]*)=.*/\1/'<<<"$o")
	local varval=$(sed -r 's/[^=]*=(.*)/\1/'<<<"$o")
	# FIXME: Security!
	local curvarval=
	eval curvarval='$'"$varname"
	if [ "$curvarval" == "$varval" ]; then
		warn "Ignored override '$varname' (already had same value)"
	else
		eval "$varname=\"$varval\""
		# FIXME: Only for really overridden ones
		warn "Overridden variable '$varname' from $src"
	fi
}

# }}} # Configuration handling

# {{{ # Convenience functions

# Returns true if input is composed only of numbers
# is_number($1 = input)
is_number() {
	egrep -q '^[0-9]+$' <<<"$1"
}

# Returns true if input can be parsed as a floating point number
# Accepted: XX.YY XX. .YY (.24=0.24
# is_float($1 = input)
is_float() {
	egrep -q '^([0-9]+\.?([0-9])?+|(\.[0-9]+))$'<<<"$1"
}

# Returns true if input is a fraction (*strictly*, i.e. "1" is not a fraction)
# Only accepts XX/YY
# is_fraction($1 = input)
is_fraction() {
	egrep -q '^[0-9]+/[0-9]+$'<<<"$1"
}

# Makes a string lowercase
# tolower($1 = string)
tolower() {
	tr '[A-Z]' '[a-z]' <<<"$1"
}


# Rounded product
# multiplies parameters and prints the result, rounded to the closest int
# parameters can be separated by commas or spaces
# e.g.: rmultiply 4/3,576 OR 4/3 576 = 4/3 * 576 = 768
# rmultiply($1 = operator1, [$2 = operator2, ...])
# rmultiply($1 = "operator1,operator2,...")
rmultiply() {
	local exp=$(sed 's/[ ,]/*/g'<<<"$@") # bc expression
	#local f=$(bc -lq<<<"$exp") # exact float value
	# division is integer by default (without -l) so it's the smae
	# as rounding to the lower int
	#bc -q <<<"( $f + 0.5 ) / 1"
	bc -q <<<"scale=5; v=( ($exp) + 0.5 ) ; scale=0 ; v/1"
}

# Like rmultiply() but always rounded upwards
ceilmultiply() {
	local exp=$(sed 's/[ ,]/*/g'<<<"$@") # bc expression
	local f=$(bc -lq<<<"$exp") # exact float value
	bc -q <<<"( $f + 0.999999999 ) / 1"
}

# Round to a multiple
# Rounds a number ($1) to a multiple of ($2)
# rtomult($1 = number, $2 = divisor)
rtomult() {
	local n=$1 d=$2
	local r=$(( $n % $d ))
	if [ $r -ne 0 ]; then
		let 'n += ( d - r )'
	fi
	echo $n
}

# numeric test eqivalent for floating point
# fptest($1 = op1, $2 = operator, $3 = op2)
fptest() {
	local op=
	case $2 in
		-gt) op='>' ;;
		-lt) op='<' ;;
		-ge) op='>=' ;;
		-le) op='<=' ;;
		-eq) op='==' ;;
		-ne) op='!=' ;;
		*) error "Internal error" && return $EX_SOFTWARE
	esac
	[ '1' == $(bc -q <<<"$1 $op $3") ]
}

# Applies the Pythagorean Theorem
# pyth_th($1 = cathetus1, $2 = cathetus2)
pyth_th() {
	bc -ql <<<"sqrt( $1^2 + $2^2)"
}

# Prints the width correspoding to the input height and the variable
# aspect ratio
# compute_width($1 = height) (=AR*height) (rounded)
compute_width() {
	rmultiply $aspect_ratio,$1
}

# Parse an interval and print the corresponding value in seconds
# returns something not 0 if the interval is not recognized.
#
# The current code is a tad permissive, it allows e.g. things like
# 10m1h (equivalent to 1h10m)
# 1m1m  (equivalent to 2m)
# I don't see reason to make it more anal, though.
# get_interval($1 = interval)
get_interval() {
	if is_number "$1" ; then echo $1 ; return 0 ; fi

	local s=$(tolower "$1") t r

	# Only allowed characters
	if ! egrep -qi '^[0-9smh.]+$' <<<"$s"; then
		return $EX_USAGE;
	fi

	# New parsing code: replaces units by a product
	# and feeds the resulting string to bc
	t=$s
	t=$(sed -r 's/([0-9]+)h/ ( \1 * 3600 ) + /g' <<<$t)
	t=$(sed -r 's/([0-9]+)m/ ( \1 * 60 ) + /g' <<<$t)
	t=$(sed 's/s/ + /g' <<<$t)
	t=$(sed -r 's/\.\.+/./g'<<<$t)
	t=$(sed -r 's/(\.[0-9]+)/0\1 + /g' <<<$t)
	t=$(sed -r 's/\+ ?$//g' <<<$t)

	r=$(bc -lq <<<$t 2>/dev/null) # bc parsing fails with correct return code
	if [ -z "$r" ]; then
		return $EX_USAGE
	fi
	# Negative interval
	if [ "-" == ${r:0:1} ]; then
		return $EX_USAGE
	fi

	echo $r
}

# Pads a string with zeroes on the left until it is at least
# the indicated length
# pad($1 = minimum length, $2 = string)
pad() {
	# printf "%0${1}d\n" "$2" # [[R1#18]] # Can't be used with non-numbers
	local str=$2
	while [ "${#str}" -lt $1 ]; do
		str="0$str"
	done
	echo $str
}

# Get Image Width
# imw($1 = file)
imw() {
	identify "$1" | cut -d' ' -f3 | cut -d'x' -f1
}

# Get Image Height
# imh($1 = file)
imh() {
	identify "$1" | cut -d' ' -f3 | cut -d'x' -f2
}

# Prints a number of seconds in a more human readable form
# e.g.: 3600 becomes 1:00:00
# pretty_stamp($1 = seconds)
pretty_stamp() {
	if ! is_float "$1" ; then return $EX_USAGE ; fi

	local t=$1
	#local h=$(( $t / 3600 ))
	# bc's modulus seems to *require* not using the math lib (-l)
	local h=$( bc -q <<<"$t / 3600")
	echo "h is $h" >/dev/tty
	t=$(bc -q <<<"$t % 3600")
	local m=$( bc -q <<<"$t / 60")
	t=$(bc -q <<<"$t % 60")
	local s=$(cut -d'.' -f1 <<<$t)
	local ms=$(cut -d'.' -f2 <<<$t)

	local R=""

	if [ $h -gt 0 ]; then
		R="$h:"
	else
	# added by wdef
	        R="00:"
	fi
	#~ # Right pad of decimal seconds
	#~ if [ ${#ms} -lt 2 ]; then
		#~ ms="${ms}0"
	#~ fi
	#~ R="$R$(pad 2 "$m"):$(pad 2 $s).$ms"
	
	# Removed mS - wdef
	R="$R$(pad 2 "$m"):$(pad 2 $s)"

	# Trim (most) decimals
	sed -r 's/\.([0-9][0-9]).*/.\1/'<<<$R
}

# Prints the size of a file in a human friendly form
# The units are in the IEC/IEEE/binary format (e.g. MiB -for mebibytes-
# instead of MB -for megabytes-)
# get_pretty_size($1 = file)
get_pretty_size() {
	local f="$1"

	#~ local bytes=$(du -DL --bytes "$f" | cut -f1)
	local kbytes=$(du -L "$f" | cut -f1)  # option -D breaks Busybox du,  redundant anyway?
	local bytes
	(( bytes = kbytes * 1024 ))  # no --bytes option to Busybox du
	local size=""

	if [ "$bytes" -gt $(( 1024**3 )) ]; then
		local gibs=$(( $bytes / 1024**3 ))
		local mibs=$(( ( $bytes % 1024**3 ) / 1024**2 ))
		size="${gibs}.${mibs:0:2} GiB"
	elif [ "$bytes" -gt $(( 1024**2)) ]; then
		local mibs=$(( $bytes / 1024**2 ))
		local kibs=$(( ( $bytes % 1024**2 ) / 1024 ))
		size="${mibs}.${kibs:0:2} MiB"
	elif [ "$bytes" -gt 1024 ]; then
		local kibs=$(( $bytes / 1024 ))
		bytes=$(( $bytes % 1024 ))
		size="${kibs}.${bytes:0:2} KiB"
	else
		size="${bytes} B"
	fi

	echo $size
}

# Rename a file, if the target exists, try with appending numbers to the name
# And print the output name to stdout
# See $safe_rename_pattern
# safe_rename($1 = original file, $2 = target file)
# XXX: Note it fails if target has no extension
safe_rename() {
	local from="$1"
	local to="$2"

	# Output extension
	local ext=$(sed -r 's/.*\.(.*)/\1/' <<<$to)
	# Input extension
	local iext=$(sed -r 's/.*\.(.*)/\1/' <<<$to)
	# Input filename without extension
	local b=${to%.$iext}

	# safe_rename_pattern is override-able, ensure it has a valid value:
	if  ! grep -q '%e' <<<"$safe_rename_pattern" ||
		! grep -q '%N' <<<"$safe_rename_pattern" ||
		! grep -q '%b' <<<"$safe_rename_pattern" ; then
		safe_rename_pattern=$DEFAULT_SAFE_REN_PATT
	fi

	local n=1
	while [ -f "$to" ]; do # Only executes if $2 exists
		to=$(sed "s#%b#$b#g" <<<"$safe_rename_pattern")
		to=$(sed "s#%N#$n#g" <<<"$to")
		to=$(sed "s#%e#$ext#g" <<<"$to")

		let 'n++';
	done

	mv -- "$from" "$to"
	echo "$to"
}

# Tests the presence of all required programs
# test_programs()
test_programs() {
	local retval=0 last=0
	for prog in getopt mplayer convert montage identify bc \
				ffmpeg mktemp sed grep egrep cut; do
		type -pf "$prog" >/dev/null
		if [ $? -ne 0 ] ; then
			error "Required program $prog not found!"
			let 'retval++'
		fi
	done
	# TODO: [x2]

	return $retval
}

# Remove any temporal files
# Does nothing if none has been created so far
# cleanup()
cleanup() {
	if [ -z $TEMPSTUFF ]; then return 0 ; fi
	inf "Cleaning up..."
	rm -rf ${TEMPSTUFF[*]}
	unset TEMPSTUFF
}

# Exit callback. This function is executed on exit (correct, failed or
# interrupted)
# exithdlr()
exithdlr() {
	cleanup
}

# Feedback handling, these functions are use to print messages respecting
# the verbosity level
# Optional color usage added from explanation found in
# <http://wooledge.org/mywiki/BashFaq>
#
# error($1 = text)
error() {
	if [ $verbosity -ge $V_ERROR ]; then
		if [ $plain_messages -eq 0 ]; then
			tput bold ; tput setaf 1;
		fi
		# sgr0 is always used, this way if
		# a) something prints inbetween messages it isn't affected
		# b) if plain_messages is overridden colour stops after the override
		echo "$1" ; tput sgr0
	fi >&2
	# It is important to redirect both tput and echo to stderr. Otherwise
	# n=$(something) wouldn't be coloured
}
#
# Print a non-fatal error or warning
# warning($1 = text)
warn() {
	if [ $verbosity -ge $V_WARN ]; then
		if [ $plain_messages -eq 0 ]; then
			tput bold ; tput setaf 3;
		fi
		echo "$1" ; tput sgr0
	fi >&2
}
#
# Print an informational message
# inf($1 = text)
inf() {
	if [ $verbosity -ge $V_INFO ]; then
		if [ $plain_messages -eq 0 ]; then
			tput bold ; tput setaf 2;
		fi
		echo "$1" ; tput sgr0
	fi >&2
}
#
# Same as inf but with no colour ever.
# infplain($1 = text)
infplain() {
	if [ $verbosity -ge $V_INFO ]; then
		echo "$1" >&2
	fi
}

# }}} # Convenience functions

# {{{ # Core functionality

# Creates a new temporary directory
# create_temp_dir()
create_temp_dir() {
	# Try to use /dev/shm if available, this provided a very small
	# benefit on my system but me of help for huge files. Or maybe won't.
	if [ -d /dev/shm ] && [ -w /dev/shm ]; then
		VCSTEMPDIR=$(mktemp -d -p /dev/shm vcs.XXXXXX)
	else
		VCSTEMPDIR=$(mktemp -d -t vcs.XXXXXX)
	fi
	if [ ! -d "$VCSTEMPDIR" ]; then
		error "Error creating temporary directory"
		return $EX_CANTCREAT
	fi
	#~ TEMPSTUFF+=( "$VCSTEMPDIR" )
	TEMPSTUFF[${#TEMPSTUFF[@]}]="$VCSTEMPDIR"
}

# Create a new temporal file and print its filename
# new_temp_file($1 = suffix)
new_temp_file() {
	local r=$(mktemp -p "$VCSTEMPDIR" "vcs-XXXXXX")
	if [ ! -f "$r" ]; then
		error "Failed to create temporary file"
		return $EX_CANTCREAT
	fi
	r=$(safe_rename "$r" "$r$1") || {
		error "Failed to create temporary file"
		return $EX_CANTCREAT
	}
	#~ TEMPSTUFF+=( "$r" )
	TEMPSTUFF[${#TEMPSTUFF[@]}]="$r"
	echo "$r"
}

# Randomizes the colours and fonts. The result won't be of much use
# in most cases but it might be a good way to discover some colour/font
# or colour combination you like.
# randomize_look()
randomize_look() {

	local mode=f lineno

	if [ "f" == $mode ]; then # Random mode
		# There're 5 rows of extra info printed
		local ncolours=$(( $(convert -list color | wc -l) - 5 ))
		randcolour() {
		lineno=$(( 5 + ( $RANDOM % $ncolours ) ))
		convert -list color | sed -n "${lineno}p" | cut -d' ' -f1 # [[R1#19]]
		}
	else # Pseudo-random mode, WIP!
		randccomp() {
			# colours are in the 0..65535 range, while RANDOM in 0..32767
			echo $(( $RANDOM + $RANDOM + ($RANDOM % 1) ))
		}
		randcolour() {
			echo "rgb($(randccomp),$(randccomp),$(randccomp))"
		}
	fi

	local nfonts=$(( $(convert -list type | wc -l) - 5 ))
	randfont() {
		lineno=$(( 5 + ( $RANDOM % $nfonts )))
		convert -list type | sed -n "${lineno}p" | cut -d' ' -f1 # [[R1#19]]
	}

	bg_heading=$(randcolour)
	bg_sign=$(randcolour)
	bg_title=$(randcolour)
	bg_contact=$(randcolour)
	fg_heading=$(randcolour)
	fg_sign=$(randcolour)
	fg_tstamps=$(randcolour)
	fg_title=$(randcolour)
	font_tstamps=$(randfont)
	font_heading=$(randfont)
	font_sign=$(randfont)
	font_title=$(randfont)
	inf "Randomization result:
 Chosen backgrounds:
   '$bg_heading' for the heading
   '$bg_sign' for the signature
   '$bg_title' for the title
   '$bg_contact' for the contact sheet
 Chosen font colours:
   '$fg_heading' for the heading
   '$fg_sign' for the signature
   '$fg_title' for the title
   '$fg_tstamps' for the timestamps,
 Chosen fonts:
   '$font_heading' for the heading
   '$font_sign' for the signature
   '$font_title' for the title
   '$font_tstamps' for the timestamps"

   unset -f randcolour randfound randccomp
}

# Add to $TIMECODES the timecodes at which a capture should be taken
# from the current video
# compute_timecodes($1 = timecode_from, $2 = interval, $3 = numcaps)
compute_timecodes() {
	local st=0 end=${VID[$LEN]} tcfrom=$1 tcint=$2 tcnumcaps=$3 eo=0
	# globals: fromtime, totime, timecode_from, TIMECODES, end_offset
	if fptest $st -lt $fromtime ; then
		st=$fromtime
	fi
	if fptest $totime -gt 0 && fptest $end -gt $totime ; then
		end=$totime
	fi
	if fptest $totime -le 0 ; then # If no totime is set, use end_offset
		eo=$end_offset

		local runlen=$( bc <<<"$end - $st" )

		if fptest "($end-$eo-$st)" -le 0 ; then
			if fptest "$eo" -gt 0 && fptest "$eo" -eq "$DEFAULT_END_OFFSET" ; then
				warn "Default end offset was too high, ignoring it."
				eo=0
			else
				error "End offset too high, use e.g. '-E0'."
				return $EX_UNAVAILABLE
			fi
		fi
	fi

	local inc=
	if [ "$tcfrom" -eq $TC_INTERVAL ]; then
		inc=$tcint
	elif [ "$tcfrom" -eq $TC_NUMCAPS ]; then
		# Numcaps mandates: timecodes are obtained dividing the length
		# by the number of captures
		if [ $tcnumcaps -eq 1 ]; then # Special case, just one capture, center it
			inc=$( bc -lq <<< "scale=3; ($end-$st)/2 + 1" )
		else
			#inc=$(( ($end-$st) / $tcnumcaps ))
			# FIXME: The last second is avoided (-1) to get the correct caps number
			inc=$( bc -lq <<< "scale=3; ($end-$eo-$st)/$tcnumcaps" )
		fi
	else
		error "Internal error"
		return $EX_SOFTWARE
	fi

	if fptest $inc -gt ${VID[$LEN]}; then
		error "Interval is longer than video length, skipping $f"
		return $EX_USAGE
	fi

	local stamp=$st
	unset LTC
	while fptest $stamp -le $(bc -q <<<"$end-$eo"); do
		if fptest $stamp -lt 0 ; then
			error "Internal error, negative timestamp calculated!"
			return $EX_SOFTWARE
		fi
		#~ LTC+=( $stamp )
		LTC[${#LTC[@]}]="$stamp"
		
		stamp=$(bc -q <<<"$stamp+$inc")
	done
	unset LTC[0] # Discard initial cap (=$st)
	TIMECODES=( ${TIMECODES[@]} ${LTC[@]} )
}

# Tries to guess an aspect ratio comparing width and height to some
# known values (e.g. VCD resolution turns into 4/3)
# guess_aspect($1 = width, $2 = height)
guess_aspect() {
	# mplayer's ID_ASPECT seems to be always 0 ¿?
	local w=$1 h=$2 ar

	case "$w" in
		352)
			if [ $h -eq 288 ] || [ $h -eq 240 ]; then
				# Ambiguous, could perfectly be 16/9
				# VCD / DVD @ VCD Res. / Half-D1 / CVD
				ar=4/3
			elif [ $h -eq 576 ] || [ $h -eq 480 ]; then
				# Ambiguous, could perfectly be 16/9
				# Half-D1 / CVD
				ar=4/3
			fi
			;;
		704|720)
			if [ $h -eq 576 ] || [ $h -eq 480 ]; then # DVD / DVB
				# Ambiguous, could perfectly be 16/9
				ar=4/3
			fi
			;;
		480)
			if [ $h -eq 576 ] || [ $h -eq 480 ]; then # SVCD
				ar=4/3
			fi
			;;
	esac

	if [ -z "$ar" ]; then
		if [ $h -eq 720 ] || [ $h -eq 1080 ]; then # HD
			# TODO: Is there a standard for PAL yet?
			ar=16/9
		fi
	fi

	if [ -z "$ar" ]; then
		warn "Couldn't guess aspect ratio."
		ar="$w/$h" # Don't calculate it yet
	fi

	echo $ar
}

# Capture a frame
# capture($1 = filename, $2 = second)
capture() {
	local f=$1 stamp=$2
	local VIDCAPFILE=00000001.png
	# globals: $shoehorned $decoder

	
	if [ $decoder -eq $DEC_MPLAYER ]; then
		# wdef: need to take 5 frames and discard first four, else only uses first frame of movie each time on my system
		# http://lists.mplayerhq.hu/pipermail/mencoder-users/2006-August/003843.html
		{
			mplayer -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \
			-frames 5 -ss $stamp $shoehorned "$f"
		} >"$stdout" 2>"$stderr"
		
		rm -f 00000001.png 00000002.png 00000003.png 00000004.png
		mv -f 00000005.png 00000001.png # rest of script expects vidcap to be called this
	elif [ $decoder -eq $DEC_FFMPEG ]; then
		# XXX: It would be nice to show a message if it takes too long
		{
			# See wa_ss_* declarations at the start of the file for details

			ffmpeg -y ${wa_ss_be/ / $stamp} -i '"$f"' ${wa_ss_af/ / $stamp} -an \
				-dframes 1 -vframes 1 -vcodec png \
				-f rawvideo $shoehorned $VIDCAPFILE
			
			# Used to test bogus files (e.g. to test codec ids)
			#convert -size 1x xc:black $VIDCAPFILE
		} >"$stdout" 2>"$stderr"
	else
		error "Internal error!"
		return $EX_SOFTWARE
	fi || {
		local retval=$?
		error "The capturing program failed!"
		return $retval
	}
	if [ ! -f "$VIDCAPFILE" ] || [ "0" == "$(du "$VIDCAPFILE" | cut -f1)" ]; then
		error "Failed to capture frame (at second $stamp)"
		return $EX_SOFTWARE
	fi

	return 0
}

# Applies all individual vidcap filters
# filter_vidcap($1 = filename, $2 = timestamp, $3 = width, $4 = height)
filter_vidcap() {
	# For performance purposes each filter simply prints a set of options
	# to 'convert'. That's less flexible but enough right now for the current
	# filters.
	local cmdopts=
	for filter in ${FILTERS_IND[@]}; do
		cmdopts="$cmdopts $( $filter "$1" "$2" "$3" "$4" ) "
	done
	local t=$(new_temp_file .png)
	eval "convert '$1' $cmdopts '$t'"
	# If $t doesn't exist returns non-zero
	[ -f "$t" ] && mv "$t" "$1"
}

# Applies all global vidcap filters
#filter_all_vidcaps() {
#	# TODO: Do something with "$@"
#	true
#}

filt_resize() {
	local f="$1" t=$2 w=$3 h=$4

	# Note the '!', required to change the aspect ratio
	echo " \( -geometry ${w}x${h}! \) "
}

# Draw a timestamp in the file
# filt_apply_stamp($1 = filename, $2 = timestamp, $3 = width, $4 = height)
filt_apply_stamp() {
	local filename=$1 timestamp=$2 width=$3 height=$4

	echo -n " \( -box '#000000aa' -fill '$fg_tstamps' -pointsize '$pts_tstamps' "
	echo -n "    -gravity '$grav_timestamp' -stroke none -strokewidth 3 -annotate +5+5 "
	echo "    ' $(pretty_stamp $stamp) ' \) -flatten "
}

# Apply a Polaroid-like effect
# Taken from <http://www.imagemagick.org/Usage/thumbnails/#polaroid>
# filt_photoframe($1 = filename, $2 = timestamp, $3 = width, $4 = height)
filt_photoframe() {
#	local file="$1" ts=$2 w=$3 h=$4
# Tweaking the size gives a nice effect too
# w=$(( $w - ( $RANDOM % ( $w / 3 ) ) ))
# TODO: Split softshadow in a filter
	echo -n "-bordercolor white -border 6 -bordercolor grey60 -border 1 "
	echo -n "-background black \( +clone -shadow 60x4+4+4 \) +swap "
	echo "-background none -flatten -trim +repage"
}

# Applies a random rotation
# Taken from <http://www.imagemagick.org/Usage/thumbnails/#polaroid>
# filt_randrot($1 = filename, $2 = timestamp, $3 = width, $4 = height)
filt_randrot() {
	# Rotation angle [-18..18]
	local angle=$(( ($RANDOM % 37) - 18 ))
	echo "-background none -rotate $angle "
}

# This one requires much more work, the results are pretty rough, but ok as
# a starting point / proof of concept
filt_film() {
	local file="$1" ts=$2 w=$3 h=$4
	# Base reel dimensions
	local rw=$(rmultiply $w,0.08) # 8% width
	local rh=$(( $rw / 2 ))
	# Ellipse center
	local ecx=$(( $rw / 2 )) ecy=0
	# Ellipse x, y radius
	local erx=$(( (rw/2)*60/100 )) # 60% halt rect width
	local ery=$(( $erx / 2))

	local base_reel=$(new_temp_file .png) reel_strip=$(new_temp_file .png)

	# Create the reel pattern...
	convert -size ${rw}x${rh} 'xc:black' \
		-fill white -draw "ellipse $ecx,$ecy $erx,$ery 0,360" -flatten \
		\( +clone -flip \) -append \
		-fuzz '40%' -transparent white \
		"$base_reel"
	# FIXME: Error handling

	# Repeat it until the height is reached and crop to the exact height
	local sh=$(imh "$base_reel") in=
	local repeat=$( ceilmultiply $h/$sh)
	while [ $repeat -gt 1 ]; do
		in+=" '$base_reel' "
		let 'repeat--'
	done
	eval convert "$base_reel" $in -append -crop $(imw "$base_reel")x${h}+0+0 \
		"$reel_strip"

	# As this options will be appended to the commandline we cannot
	# order the arguments optimally (eg: reel.png image.png reel.png +append)
	# A bit of trickery must be done flipping the image. Note also that the
	# second strip will be appended flipped, which is intended.
	echo -n "'$reel_strip' +append -flop '$reel_strip' +append -flop "
}

# Creates a contact sheet by calling the delegate
# create_contact_sheet($1 = columns, $2 = context, $3 = width, $4 = height,
#                      $5...$# = vidcaps) : output
create_contact_sheet() {
	$CSHEET_DELEGATE "$@"
}

# This is the standard contact sheet creator
# csheet_montage($1 = columns, $2 = context, $3 = width, $4 = height,
#                $5... = vidcaps) : output
csheet_montage() {
	local cols=$1 ctx=$2 width=$3 height=$4 output=$(new_temp_file .png)
	shift 4
	case $ctx in
		$CTX_STD|$CTX_HL) hpad=10 vpad=5 ;;
		$CTX_EXT) hpad=5 vpad=2 ;;
		*) error "Internal error" && return $EX_SOFTWARE ;;
	esac
	# Using transparent seems to make -shadow futile
	montage -background Transparent "$@" -geometry +$hpad+$vpad -tile "$cols"x "$output"

	# This should actually be moved to a filter but with the current
	# architecture I'm unable to come up with the correct convert options
	if [ $DISABLE_SHADOWS -eq 0 ]; then
		# This produces soft-shadows, which look much better than the montage ones
		convert \( -shadow 50x2+10+10 "$output" \) "$output" -composite "$output"
	fi
	#convert \( -shadow 50x2+10+10 "$output" \) "$output" -composite "$output"

	# FIXME: Error handling
	echo $output
}

# Polaroid contact sheet creator: it overlaps vidcaps with some randomness
# csheet_overlap($1 = columns, $2 = context, $3 = width, $4 = height,
#                 $5... = $vidcaps) : output
csheet_overlap() {
	local cols=$1 ctx=$2 width=$3 height=$4
	# globals: $VID
	shift 4

	# TBD: Handle context

	# Explanation of how this works:
	# On the first loop we do what the "montage" command would do (arrange the
	# images in a grid) but overlapping each image to the one on their left,
	# creating the output row by row, each row in a file.
	# On the second loop we append the rows, again overlapping each one to the
	# one before (above) it.
	# XXX: Compositing over huge images is quite slow, there's probably a
	# better way to do it

	# Offset bounds, this controls how much of each snap will be over the
	# previous one. Note it is important to work over $width and not $VID[$W]
	# to cover all possibilities (extended mode and -H change the vidcap size)
	local maxoffset=$(( $width / 3 ))
	local minoffset=$(( $width / 6 ))

	# Holds the files that will form the full contact sheet
	# each file is a row on the final composition
	local -a rowfiles

	# Dimensions of the canvas for each row, it should be big enough
	# to hold all snaps.
	# My trigonometry is pretty rusty but considering we restrict the angle a lot
	# I believe no image should ever be wider/taller than the diagonal (note the
	# ceilmultiply is there to simply round the result)
	local diagonal=$(ceilmultiply $(pyth_th $width $height) 1)
	# XXX: The width, though isn't guaranteed (e.g. using filt_film it ends wider)
	#      adding 3% to the diagonal *should* be enough to compensate
	local canvasw=$(( ( $diagonal + $(rmultiply $diagonal,0.3) ) * $cols ))
	local canvash=$(( $diagonal ))

	# The number of rows required to hold all the snaps
	local numrows=$(ceilmultiply ${#@},1/$cols) # rounded division

	# Variables inside the loop
	local col       # Current column
	local rowfile   # Holds the row we're working on
	local offset    # Random offset of the current snap [$minoffset..$maxoffset]
	local accoffset # The absolute (horizontal) offset used on the next iteration
	local cmdopts   # Holds the arguments passed to convert to compose the sheet
	local w         # Width of the current snap
	for row in $(seq 1 $numrows) ; do
		col=0
		rowfile=$(new_temp_file .png)
		#~ rowfiles+=( "$rowfile" )
		rowfiles[${#rowfiles[@]}]="$rowfile"
		accoffset=0
		cmdopts= # This command is pretty time-consuming, let's make it in a row

		# Base canvas
		inf "Creating polaroid base canvas $row/$numrows..."
		convert -size ${canvasw}x${canvash} xc:transparent "$rowfile"

		# Step through vidcaps (col=[0..cols-1])
		for col in $(seq 0 $(( $cols - 1 ))); do
			# More cols than files in the last iteration (e.g. -n10 -c4)
			if [ -z "$1" ]; then break; fi
			w=$(imw "$1")

			# Stick the vicap in the canvas
			#convert -geometry +${accoffset}+0 "$rowfile" "$1" -composite "$rowfile"
			cmdopts="$cmdopts -geometry +${accoffset}+0 '$1' -composite "

			offset=$(( $minoffset + ( $RANDOM % $maxoffset ) ))
			let 'accoffset=accoffset + w - offset'
			shift
		done
		inf "Composing polaroid row $row/$numrows..."
		eval convert "'$rowfile'" "$cmdopts" -trim +repage "'$rowfile'" >&2
	done

	inf "Merging polaroid rows..."
	output=$(new_temp_file .png)
	# Standard composition
	#convert -background Transparent "${rowfiles[@]}" -append polaroid.png
	# Overlapped composition
	convert -size ${canvasw}x$(( $canvash * $cols )) xc:transparent "$output"
	cmdopts=
	accoffset=0
	local h
	for row in "${rowfiles[@]}" ; do
		w=$(imw "$row")
		h=$(imh "$row")
		minoffset=$(( $h / 8 ))
		maxoffset=$(( $h / 4 ))
		offset=$(( $minoffset + ( $RANDOM % $maxoffset ) ))
		# The row is also offset horizontally
		cmdopts="$cmdopts -geometry +$(( $RANDOM % $maxoffset ))+$accoffset '$row' -composite "
		let 'accoffset=accoffset + h - offset'
	done
	# After the trim the top corners are too near the heading, we add some space
	# with -splce
	eval convert -background transparent "$output" $cmdopts -trim +repage \
		-bordercolor Transparent -splice 0x10 "$output" >&2

	# FIXME: Error handling
	echo $output
}

# Sorts timestamps and removes duplicates
# clean_timestamps($1 = space separated timestamps)
clean_timestamps() {
	# Note AFAIK sort only sorts lines, that's why y replace spaces by newlines
	local s=$1
	sed 's/ /\n/g'<<<"$s" | sort -n | uniq
}

# Fills the $MPLAYER_CACHE and $VID variables with the video data
# identify_video($1 = file)
identify_video() {
	local f=$1
	# Meta data extraction
	# Note to self: Don't change the -vc as it would affect $vdec
	MPLAYER_CACHE=$(mplayer -benchmark -ao null -vo null -identify -frames 0 -quiet "$f" 2>/dev/null | grep ^ID)
	VID[$VCODEC]=$(grep ID_VIDEO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # FourCC
	VID[$ACODEC]=$(grep ID_AUDIO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2)
	VID[$VDEC]=$(grep ID_VIDEO_CODEC <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # Decoder (!= Codec)
	VID[$W]=$(grep ID_VIDEO_WIDTH <<<"$MPLAYER_CACHE" | cut -d'=' -f2)
	VID[$H]=$(grep ID_VIDEO_HEIGHT <<<"$MPLAYER_CACHE" | cut -d'=' -f2)
	VID[$FPS]=$(grep ID_VIDEO_FPS <<<"$MPLAYER_CACHE" | cut -d'=' -f2)
	VID[$LEN]=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2)
	# For some reason my (one track) samples have two ..._NCH, first one 0
	VID[$CHANS]=$(grep ID_AUDIO_NCH <<<"$MPLAYER_CACHE"|cut -d'=' -f2|head -2|tail -1)

	# Upon consideration:
	#if grep -q '\.[0-9]*0$' <<<${VID[$FPS]} ; then
	#	# Remove trailing zeroes...
	#	VID[$FPS]=$(sed -r 's/(\.[1-9]*)0*$/\1/' <<<${VID[$FPS]})
	#	# ...And trailing decimal point
	#	VID[$FPS]=$(sed 's/\.$//'<<<${VID[$FPS]})
	#fi

	# Voodoo :P Remove (one) trailing zero
	if [ "${VID[$FPS]:$(( ${#VID[$FPS]} - 1 ))}" == "0" ]; then
		VID[$FPS]="${VID[$FPS]:0:$(( ${#VID[$FPS]} - 1 ))}"
	fi

	# Check sanity of the most important values
	is_number "${VID[$W]}" && is_number "${VID[$H]}" && is_float "${VID[$LEN]}"
}

# Main function.
# Creates the contact sheet.
# process($1 = file)
process() {
	local f=$1

	local numcols=

	if [ ! -f "$f" ]; then
		error "File \"$f\" doesn't exist"
		return $EX_NOINPUT
	fi
	inf "Processing $f..."

	identify_video "$f" || {
		error "Found unsupported value while identifying video. Can't continue."
		return $EX_SOFTWARE
	}

	# Vidcap/Thumbnail height
	local vidcap_height=$th_height
	if ! is_number "$vidcap_height" || [ "$vidcap_height" -eq 0 ]; then
		vidcap_height=${VID[$H]}
	fi
	if [ "0" == "$aspect_ratio" ]; then
		aspect_ratio=$(bc -lq <<< "${VID[$W]} / ${VID[$H]}")
	elif [ "-1" == "$aspect_ratio" ]; then
		aspect_ratio=$(guess_aspect ${VID[$W]} ${VID[$H]})
		inf "Aspect ratio set to $(sed -r 's/(\.[0-9]{2}).*/\1/g'<<<$aspect_ratio)"
	fi
	local vidcap_width=$(compute_width $vidcap_height)

	local numsecs=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2 | cut -d. -f1)
	
	local nc=$numcaps

	create_temp_dir

	# Compute the stamps (if in auto mode)...
	TIMECODES=${initial_stamps[*]}
	if [ $manual_mode -ne 1 ]; then
		compute_timecodes $timecode_from $interval $numcaps || {
			return $?
		}
	fi

	local base_montage_command="montage -font $font_tstamps -pointsize $pts_tstamps \
	                       -gravity SouthEast -fill white "
	local output=$(new_temp_file '-preview.png')
	local VIDCAPFILE=00000001.png

	# If the temporal vidcap already exists, abort
	if [ -f $VIDCAPFILE ]; then
		error "Temporal vidcap file ($VIDCAPFILE) exists, remove it before running!."
		return $EX_CANTCREAT
	fi

	#~ TEMPSTUFF+=( $VIDCAPFILE )
	TEMPSTUFF[${#TEMPSTUFF[@]}]="$VIDCAPFILE"

	# Highlights
	local hlfile n=1 # hlfile Must be outside the if!
	if [ "$HLTIMECODES" ]; then
		local hlcapfile= pretty=
		for stamp in $(clean_timestamps "${HLTIMECODES[*]}"); do
			if fptest $stamp -gt $numsecs ; then let 'n++' && continue ; fi
			pretty=$(pretty_stamp $stamp)
			inf "Generating highlight #${n}/${#HLTIMECODES[*]} ($pretty)..."

			capture "$f" $stamp || return $?
			filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || {
				local r=$?
				error "Failed to apply transformations to the capture."
				return $r
			}
	
			hlcapfile=$(new_temp_file "-hl-$(pad 6 $n).png")
			mv "$VIDCAPFILE" "$hlcapfile"
			#~ capfiles+=( "$hlcapfile" )
			capfiles[${#capfiles[@]}]="$hlcapfile"
			let 'n++'
		done

		let 'n--' # There's an extra inc
		if [ "$n" -lt "$cols" ]; then
			numcols=$n
		else
			numcols=$cols
		fi

		inf "Composing highlights contact sheet..."
		hlfile=$( create_contact_sheet $numcols $CTX_HL $vidcap_width $vidcap_height "${capfiles[@]}" )
		unset hlcapfile pretty n capfiles numcols
	fi
	unset n

	# Normal captures
	# TODO: Don't reference $VIDCAPFILE
	local capfile pretty n=1
	for stamp in $(clean_timestamps "${TIMECODES[*]}"); do
		pretty=$(pretty_stamp $stamp)
		inf "Generating capture #${n}/${#TIMECODES[*]} ($pretty)..."

		capture "$f" $stamp || return $?
		filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || return $?

		# identified by capture number, padded to 6 characters
		capfile=$(new_temp_file "-cap-$(pad 6 $n).png")
		mv "$VIDCAPFILE" "$capfile"
		#~ capfiles+=( "$capfile" )
		capfiles[${#capfiles[@]}]="$capfile"
		let 'n++' # $n++
	done
	#filter_all_vidcaps "${capfiles[@]}"

	let 'n--' # there's an extra inc
	if [ "$n" -lt "$cols" ]; then
		numcols=$n
	else
		numcols=$cols
	fi

	inf "Composing standard contact sheet..."
	echo "Using capfiles ${capfiles[@]}"
	output=$(create_contact_sheet $numcols $CTX_STD $vidcap_width $vidcap_height "${capfiles[@]}")
	unset capfile capfiles pretty n # must carry on to the extended caps: numcols

	# Extended mode
	local extoutput=
	if [ "$extended_factor" != 0 ]; then
		# Number of captures. Always rounded to a multiplier of *double* the
		# number of columns (the extended caps are half width, this way they
		# match approx with the standard caps width)
		local hlnc=$(rtomult "$(( ${#TIMECODES[@]} * $extended_factor ))" $((2*$numcols)))

		unset TIMECODES # required step to get the right count
		declare -a TIMECODES # Note the manual stamps aren't included anymore
		compute_timecodes $TC_NUMCAPS "" $hlnc
		unset hlnc

		local n=1 w= h= capfile= pretty=
		unset capfiles
		# The image size of the extra captures is 1/4
		let 'w=vidcap_width/2, h=vidcap_height/2'
		for stamp in $(clean_timestamps "${TIMECODES[*]}"); do
			pretty=$(pretty_stamp $stamp)
			inf "Generating capture from extended set: ${n}/${#TIMECODES[*]} ($pretty)..."
			capture "$f" $stamp || return $?
			filter_vidcap "$VIDCAPFILE" $pretty $w $h || return $?

			capfile=$(new_temp_file "-excap-$(pad 6 $n).png")
			mv "$VIDCAPFILE" "$capfile"
			#~ capfiles+=( "$capfile" )
			capfiles[${#capfiles[@]}]="$capfile"
			let 'n++'
		done

		let 'n--' # There's an extra inc
		if [ $n -lt $(( $cols * 2 )) ]; then
			numcols=$n
		else
			numcols=$(( $cols * 2 ))
		fi

		inf "Composing extended contact sheet..."
		extoutput=$( create_contact_sheet $numcols $CTX_EXT $w $h "${capfiles[@]}" )

		unset w h capfile pretty n numcols
	fi # Extended mode

	# Video codec "prettyfication", see [[R2]], [[R3]], [[R4]]
	local vcodec= acodec=
	case "${VID[$VCODEC]}" in
		0x10000001) vcodec="MPEG-1" ;;
		0x10000002) vcodec="MPEG-2" ;;
		0x00000000) vcodec="Raw RGB" ;; # How correct is this?
		avc1) vcodec="MPEG-4 AVC" ;;
		DIV3) vcodec="DivX ;-) Low-Motion" ;; # Technically same as mp43
		DX50) vcodec="DivX 5" ;;
		FMP4) vcodec="FFmpeg" ;; # XXX: Would LAVC be a better name?
		I420) vcodec="Raw I420 Video" ;; # XXX: Officially I420 is Indeo 4 but it is mapped to raw ¿?
		MJPG) vcodec="M-JPEG" ;; # XXX: Actually mJPG != MJPG
		MPG4) vcodec="MS MPEG-4 V1" ;;
		MP42) vcodec="MS MPEG-4 V2" ;;
		MP43) vcodec="MS MPEG-4 V3" ;;
		RV10) vcodec="RealVideo 1.0/5.0" ;;
		RV20) vcodec="RealVideo G2" ;;
		RV30) vcodec="RealVideo 8" ;;
		RV40) vcodec="RealVideo 9/10" ;;
		SVQ1) vcodec="Sorenson Video 1" ;;
		SVQ3) vcodec="Sorenson Video 3" ;;
		theo) vcodec="Ogg Theora" ;;
		tscc) vcodec="TechSmith Screen Capture Codec" ;;
		VP6[012]) vcodec="On2 Truemotion VP6" ;;
		WMV1) vcodec="WMV7" ;;
		WMV2) vcodec="WMV8" ;;
		WMV3) vcodec="WMV9" ;;
		WMVA) vcodec="WMV9 Advanced Profile" ;; # Not VC1 compliant. Unsupported.
		XVID) vcodec="Xvid" ;;

		# These are known FourCCs that I haven't tested against so far
		DIV4) vcodec="DivX ;-) Fast-Motion" ;;
		DIVX|divx) vcodec="DivX" ;; # OpenDivX / DivX 5(?) / Project Mayo
		IV4[0-9]) vcodec="Indeo Video 4" ;;
		IV50) vcodec="Indeo 5.0" ;;
		VP3[01]) vcodec="On2 VP3" ;;
		VP40) vcodec="On2 VP4" ;;
		VP50) vcodec="On2 VP5" ;;
		# Legacy(-er) codecs (haven't seen files in these formats in awhile)
		IV3[0-9]) vcodec="Indeo Video 3" ;;
		MSVC) vcodec="Microsoft Video 1" ;;
		MRLE) vcodec="Microsoft RLE" ;;
		*) # If not recognized show FOURCC
			vcodec=${VID[$VCODEC]}
			;;
	esac
	if [ "${VID[$VDEC]}" == "ffodivx" ]; then
		vcodec+=" (MPEG-4)"
	elif [ "${VID[$VDEC]}" == "ffh264" ]; then
		vcodec+=" (h.264)"
	fi

	# Audio codec "prettyfication", see [[R4]]
	case $(tolower ${VID[$ACODEC]} ) in
		85) acodec='MPEG Layer III (MP3)' ;;
		80) acodec='MPEG Layer I/II (MP1/MP2)' ;; # Apparently they use the same tag
		mp4a) acodec='MPEG-4 AAC' ;; # LC and HE, apparently
		352) acodec='WMA7' ;; # =WMA1
		353) acodec='WMA8' ;; # =WMA2 No idea if lossless can be detected
		354) acodec='WMA9' ;; # =WMA3
		8192) acodec='AC3' ;;
		1|65534)
			# 1 is standard PCM (apparently all sample sizes)
			# 65534 seems to be multichannel PCM
			acodec='Linear PCM' ;;
		vrbs|22127)
			# 22127 = Vorbis in AVI (with ffmpeg) DON'T!
			# vrbs = Vorbis in Matroska, probably other sane containers
			acodec='Vorbis'
			;;
		qdm2) acodec="QDesign" ;;
		"") acodec="no audio" ;;
		# Following not seen by me so far, don't even know if mplayer would
		# identify them
		#<http://lists.mplayerhq.hu/pipermail/ffmpeg-devel/2005-November/005054.html>
		355) acodec="WMA9 Lossless" ;;
		10) acodec="WMA9 Voice" ;;
		*) # If not recognized show audio id tag
			acodec=${VID[$ACODEC]}
			;;
	esac

	if [ "${VID[$CHANS]}" ] && is_number "${VID[$CHANS]}" &&[ ${VID[$CHANS]} -ne 2 ]; then
		if [ ${VID[$CHANS]} -eq 0 ]; then
			# This happens e.g. in non-i386 when playing WMA9 at the time of
			# this writing
			warn "Detected 0 audio channels."
			warn "    Does this version of mplayer support the audio codec ($acodec)?"
		elif [ ${VID[$CHANS]} -eq 1 ]; then
			acodec+=" (mono)"
		else
			acodec+=" (${VID[$CHANS]}ch)"
		fi
	fi


	if [ "$HLTIMECODES" ] || [ "$extended_factor" != "0" ]; then
		inf "Merging contact sheets..."
	fi
	# If there were highlights then mix them in
	if [ "$HLTIMECODES" ]; then
		#\( -geometry x2 xc:black -background black \) # This breaks it!
		convert \( -background LightGoldenRod "$hlfile" -flatten \) \
			\( "$output" \) -append "$output"
	fi
	# Extended captures
	if [ "$extended_factor" != 0 ]; then
		convert "$output" "$extoutput" -append "$output"
	fi
	# Add the background
	convert -background "$bg_contact" "$output" -flatten "$output"

	# Let's add meta inf and signature
	inf "Adding header and footer..."
	local meta2="Dimensions: ${VID[$W]}x${VID[$H]}
Format: $vcodec / $acodec
FPS: ${VID[$FPS]}"
	local signature
	if [ $anonymous_mode -eq 0 ]; then
		signature="$user_signature $user
with $PROGRAM_SIGNATURE"
	else
		signature="Created with $PROGRAM_SIGNATURE"
	fi
	local headwidth=$(identify "$output" | cut -d' ' -f3 | cut -d'x' -f1)
	# TODO: Use a better height calculation
	local headheight=$(($pts_meta * 4 ))
	local heading=$(new_temp_file .png)
	# Add the title if any
	if [ "$title" ]; then
		# TODO: Use a better height calculation
		convert \
			\( \
				-size ${headwidth}x$(( $pts_title + ($pts_title/2) )) "xc:$bg_title" \
				-font "$font_title" -pointsize "$pts_title" \
				-background "$bg_title" -fill "$fg_title" \
				-gravity Center -annotate 0 "$title" \
			\) \
			-flatten \
			"$output" -append "$output"
	fi
	local fn_font=	# see $font_filename
	case $font_filename in
		$FF_DEFAULT) fn_font="$font_heading" ;;
		$FF_MINCHO) fn_font="$FONT_MINCHO" ;;
		*)
			warn "\$font_filename was overridden with an incorrect value, using default."
			fn_font="$font_heading"
			;;
	esac
	# Talk about voodoo... feel the power of IM... let's try to explain what's this:
	# It might technically be wrong but it seems to work as I think it should
	# (hence the voodoo I was talking)
	# Parentheses restrict options inside them to only affect what's inside too
	# * Create a base canvas of the desired width and height 1. The width is tweaked
	#   because using "label:" later makes the text too close to the border, that
	#   will be compensated in the last step.
	# * Create independent intermediate images with each row of information, the
	#   filename row is split in two images to allow changing the font, and then
	#   they're horizontally appended (and the font reset)
	# * All rows are vertically appended and cropped to regain the width in case
	#   the filename is too long
	# * The appended rows are appended to the original canvas, the resulting image
	#   contains the left row of information with the full heading width and
	#   height, and this is the *new base canvas*
	# * Draw over the new canvas the right row with annotate in one
	#   operation, the offset compensates for the extra pixel from the original
	#   base canvas. XXX: Using -annotate allows setting alignment but it breaks
	#   vertical alignment with the other rows' labels.
	# * Finally add the border that was missing from the initial width, we have
	#   now the *complete header*
	# * Add the contact sheet and append it to what we had.
	# * Start a new image and annotate it with the signature, then append it too.
	convert \
		\( \
			-size $(( ${headwidth} -18 ))x1 "xc:$bg_heading" +size \
			-font "$font_heading" -pointsize "$pts_meta" \
			-background "$bg_heading" -fill "$fg_heading" \
			\( \
				-gravity West \
				\( label:"Filename:" \
					-font "$fn_font" label:"$(basename "$f")" +append \
				\) \
				-font "$font_heading" \
				label:"File size: $(get_pretty_size "$f")" \
				label:"Length: $(cut -d'.' -f1 <<<$(pretty_stamp ${VID[$LEN]}))" \
				-append -crop ${headwidth}x${headheight}+0+0 \
			\) \
			-append \
			\( \
				-size ${headwidth}x${headheight} \
				-gravity East -fill "$fg_heading" -annotate +0-1 "$meta2" \
			\) \
			-bordercolor "$bg_heading" -border 9 \
		\) \
		"$output" -append \
		\( \
			-size ${headwidth}x34 -gravity Center "xc:$bg_sign" \
			-font "$font_sign" -pointsize "$pts_sign" \
			-fill "$fg_sign" -annotate 0 "$signature" \
		\) \
		-append \
		"$output"
	unset signature meta2 headwidth headheight heading fn_font

	if [ $output_format != "png" ]; then
		local newout="$(dirname "$output")/$(basename "$output" .png).$output_format"
		convert -quality $output_quality "$output" "$newout"
		output="$newout"
	fi
	
	output_name=$( safe_rename "$output" "$(basename "$f").$output_format" ) || {
		error "Failed to write the output file!"
		return $EX_CANTCREAT
	}
	inf "Done. Output wrote to $output_name"

	cleanup
}

# }}} # Core functionality

# {{{ # Debugging helpers

# Tests integrity of some operations.
# Used to test internal changes for consistency.
# It helps me to identify incorrect optimizations.
# unit_test()
unit_test() {
	local t op val ret comm retval=0

	# Textual tests, compare output to expected output
	# Tests are in the form "operation arguments correct_result #Description"
	local TESTS=(
		"rmultiply 1,1 1 #Identity"
		"rmultiply 16/9,1 2 #Rounding" # 1.77 rounded 2
		"rmultiply 4/3 1 #Rounding"    # 1.33 rounded 1
		"rmultiply 1,16/9 2 #Commutative property"
		"rmultiply 1.7 2 #Alternate syntax"

		"ceilmultiply 1,1 1 #"
		"ceilmultiply 4/3 2 #" # 1.33 rounded 2
		"ceilmultiply 7,1/2 4 #" # 3.5 rounded 4
		"ceilmultiply 7/2 4 #Alternative syntax"
		"ceilmultiply 1/2,7 4 #Commutative property"

		"pad 10 0 0000000000 #Padding"
		"pad 1 20 20 #Unneeded padding"
		"pad 5 23.3 023.3 #Floating point padding"

		"guess_aspect 720 576 4/3 #DVD AR Guess"
		"guess_aspect 1024 576 1024/576 #Unsupported Wide AR Guess"

		"tolower ABC abc #lowercase conversion"

		"pyth_th 4 3 5 #Integer pythagorean theorem"
		"pyth_th 16 9 18.35755975068581929849 #FP pythagorean theorem"
	)
	
	for t in "${TESTS[@]}" ; do
		# Note the use of ! as separator, this is because # and / are used in
		# many of the inputs
		comm=$(sed 's!.* #!!g' <<<$t)
		# Expected value
		val=$(sed -r "s!.* (.*) #$comm\$!\1!g"<<<$t)
		op=$(sed "s! $val #$comm\$!!g" <<<$t)
		if [ -z "$comm" ]; then
			comm=unnamed
		fi
		ret=$($op) || true

		if [ "$ret" != "$val" ] && fptest "$ret" -ne "$val" ; then
			error "Failed test ($comm): '$op $val'. Got result '$ret'."
			let 'retval++,1' # The ,1 ensures let doesn't fail
		else
			inf "Passed test ($comm): '$op $val'."
		fi
	done

	# Returned value tests, compare return to expected return
	local TESTS=(
		# Don't use anything with a RE meaning

		# Floating point numeric "test"
		"fptest 3 -eq 3 0 #FP test" 
		"fptest 3.2 -gt 1 0 #FP test"
		"fptest 1/2 -le 2/3 0 #FP test"
		"fptest 6.34 -gt 6.34 1 #FP test"
		"fptest 1>0 -eq 1 0 #FP -logical- test"

		"is_number 3 0 #Numeric recognition"
		"is_number '3' 1 #Quoted numeric recognition"
		"is_number 3.3 1 #Non-numeric recognition"

		"is_float 3.33 0 #Float recognition"
		"is_float 3 0 #Float recognition"
		"is_float 1/3 1 #Non-float recognition"

		"is_fraction 1/1 0 #Fraction recognition"
		"is_fraction 1 1 #non-fraction recognition"
		"is_fraction 1.1 1 #Non-fraction recognition"
	)
	for t in "${TESTS[@]}"; do
		comm=$(sed 's!.* #!!g' <<<$t)
		# Expected value
		val=$(sed -r "s!.* (.*) #$comm\$!\1!g"<<<$t)
		op=$(sed "s! $val #$comm\$!!g" <<<$t)
		if [ -z "$comm" ]; then
			comm=unnamed
		fi
		ret=0
		$op || {
			ret=$?
		}

		if [ $val -eq $ret ]; then
			inf "Passed test ($comm): '$op; returns $val'."
		else
			error "Failed test ($comm): '$op; returns $val'. Returned '$ret'"
			let 'retval++,1'
		fi
	done

	return $retval
}


# }}} # Debugging helpers

# {{{ # Help / Info

# Prints the program identification to stderr
show_vcs_info() { # Won't be printed in quiet modes
	inf "Video Contact Sheet *NIX v${VERSION}, (c) 2007 Toni Corvera" "sgr0"
}

# Prints the list of options to stdout
show_help() {
    local P=$(basename $0)
	cat <<EOF
Usage: $P [options] <file>

Options:
  -i|--interval <arg>   Set the interval to arg. Units can be used
                        (case-insensitive), i.e.:
                            Seconds:      90 or 90s
                            Minutes:      3m
                            Hours:        1h
                            Combined:     1h3m90
                        Use either -i or -n.
  -n|--numcaps <arg>    Set the number of captured images to arg. Use either
                        -i or -n.
  -c|--columns <arg>    Arrange the output in 'arg' columns.
  -H|--height <arg>     Set the output (individual thumbnail) height. Width is
                        derived accordingly. Note width cannot be manually set.
  -a|--aspect <aspect>  Aspect ration. Accepts floating point number or
                        fractions.
  -f|--from <arg>       Set starting time. No caps before this. Same format
                        as -i.
  -t|--to <arg>         Set ending time. No caps beyond this. Same format
                        as -i.
  -E|--end_offset <arg> This time is ignored, from the end of the video. Same
                        format as -i. This value is not used when a explicit
                        ending time is set. By default it is $DEFAULT_END_OFFSET.
  -T|--title <arg>      Add a title above the vidcaps.
  -j|--jpeg             Output in jpeg (by default output is in png).
  -q|--quiet            Don't print progess messages just errors. Repeat to
                        mute completely even on error.
  -h|--help             Show this text.
  -Wo                   Workaround: Change ffmpeg's arguments order, might
                        work with some files that fail otherwise.
  -d|--disable <arg>    Disable some default functionality.
                        Features that can be disabled are:
                        * timestamps: use -dt or --disable timestamps
                        * shadows: use -ds or --disable shadows
  -A|--autoaspect       Try to guess aspect ratio from resolution.
  -e[num] | --extended=[num]
                        Enables extended mode and optionally sets the extended
                        factor. -e is the same as -e$DEFAULT_EXT_FACTOR.
  -l|--highlight <arg>  Add the image found at the timestamp "arg" as a
                        highlight. Same format as -i.
  -m|--manual           Manual mode: Only timestamps indicated by the user are
                        used (use in conjunction with -S), when using this
                        -i and -n are ignored.
  -O|--override <arg>   Use it to override a variable (see the homepage for
                        more details). Format accepted is 'variable=value' (can
                        also be quoted -variable="some value"- and can take an
                        internal variable too -variable="\$SOME_VAR"-).
  -S|--stamp <arg>      Add the image found at the timestamp "arg". Same format
                        as -i.

  -u|--user <arg>       Set the username found in the signature to this.
  -U|--fullname         Use user's full/real name (e.g. John Smith) as found in
                        /etc/passwd.
  -Ij|-Ik
  --mincho              Use the kana/kanji/hiragana font (EXPERIMENTAL) might
                        also work partially with Hangul and Cyrillic.
  -k <arg>
  --funky <arg>         Funky modes:
                        These are toy output modes in which the contact sheet
                        gets a more informal look.
                        Order *IS IMPORTANT*. A bad order gets a bad result :P
                        They're random in nature so using the same funky mode
                        twice will usually lead to quite different results.
                        Currently available "funky modes":
                        "overlap":  Use '-ko' or '--funky overlap'
                            Randomly overlap captures.
                        "rotate":     Use '-kr' or '--funky rotate'
                            Randomly rotate each image.
                        "photoframe": Use '-kf' or '--funky photoframe'
                            Adds a photo-like white frame to each image.
                        "polaroid": Use '-kp' or '--funky polaroid'
                            Combination of rotate, photoframe and overlap.
                            Same as -kr -ko -kf.
                        "film":     Use '-ki' or '--funky film'
                            Imitates filmstrip look.
                        "random":   Use '-kx' or '--funky random'
                            Randomizes colours and fonts.

  Options used for debugging purposes or to tweak the internal workings:
  -M|--mplayer          Force the usage of mplayer.
  -F|--ffmpeg           Force the usage of ffmpeg.
  --shoehorn <arg>      Pass "arg" to mplayer/ffmpeg. You shouldn't need it.
  -D                    Debug mode. Used to test features/integrity. It:
                          * Prints the input command line
                          * Sets the title to reflect the command line
                          * Does a basic test of consistency.

Examples:
    Create a contact sheet with default values (vidcaps at intervals of
    $DEFAULT_INTERVAL seconds), the resulting file will be called
    input.avi.png:
        \$ $P input.avi

    Create a sheet with vidcaps at intervals of 3 and a half minutes:
        \$ $P -i 3m30 input.avi

    Create a sheet with vidcaps starting at 3 mins and ending at 18 mins,
    add an extra vidcap at 2m and another one at 19m:
        \$ $P -f 3m -t 18m -S2m -S 19m input.avi

    See more examples at vcs' homepage <http://p.outlyer.net/vcs/>.

EOF
}

# }}} # Help / Info

#### Execution starts here ####

# If tput isn't found simply ignore tput commands
# (no colour support)
# Important to do it before any message can be thrown
if ! type -pf tput >/dev/null ; then
	tput() { cat >/dev/null <<<"$1"; }
	warn "tput wasn't found. Coloured feedback disabled."
fi

# Execute exithdlr on exit
trap exithdlr EXIT

show_vcs_info

load_config

# {{{ # Command line parsing

# TODO: Find how to do this correctly (this way the quoting of $@ gets messed):
#eval set -- "${default_options} ${@}"
ARGS="$@"

# [[R0]]
TEMP=$(getopt -s bash -o i:n:u:T:f:t:S:jhFMH:c:ma:l:De::U::qAO:I::k:W:E:d: \
       --long "interval:,numcaps:,username:,title:,from:,to:,stamp:,jpeg,help,"\
"shoehorn:,mplayer,ffmpeg,height:,columns:,manual,aspect:,highlight:,"\
"extended::,fullname,anonymous,quiet,autoaspect,override:,mincho,funky:,"\
"end_offset:,disable:" \
       -n $0 -- "$@")

eval set -- "$TEMP"

while true ; do
	case "$1" in
		-i|--interval)
			if ! interval=$(get_interval "$2") ; then
				error "Incorrect interval format. Got '$2'."
				exit $EX_USAGE
			fi
			if [ "$interval" == "0" ]; then
				error "Interval must be higher than 0, set to the default $DEFAULT_INTERVAL"
				interval=$DEFAULT_INTERVAL
			fi
			timecode_from=$TC_INTERVAL
			shift # Option arg
			;;
		-n|--numcaps)
			if ! is_number "$2" ; then
				error "Number of captures must be (positive) a number! Got '$2'."
				exit $EX_USAGE
			fi
			if [ $2 -eq 0 ]; then
				error "Number of captures must be greater than 0! Got '$2'."
				exit $EX_USAGE
			fi
			numcaps="$2"
			timecode_from=$TC_NUMCAPS
			shift # Option arg
			;;
		-u|--username) user="$2" ; shift ;;
		-U|--fullname)
			# -U accepts an optiona argument, 0, to make an anonymous signature
			# --fullname accepts no argument
			if [ "$2" ]; then # With argument, special handling
				if [ "$2" != "0" ]; then
					error "Use '-U0' to make an anonymous contact sheet or '-u \"My Name\"'"
					error "    to sign as My Name. Got -U$2"
					exit $EX_USAGE
				fi
				anonymous_mode=1
				shift
			else # No argument, default handling (try to guess real name)
				user=$(grep ^$(id -un): /etc/passwd | cut -d':' -f5 |sed 's/,.*//g')
				if [ -z "$user" ]; then
					user=$(id -un)
					error "No fullname found, falling back to default ($user)"
				fi
			fi
			;;
		--anonymous) anonymous_mode=1 ;; # Same as -U0
		-T|--title) title="$2" ; shift ;;
		-f|--from)
			if ! fromtime=$(get_interval "$2") ; then
				error "Starting timestamp must be a valid timecode. Got '$2'."
				exit $EX_USAGE
			fi
			shift
			;;
		-E|--end_offset)
			if ! end_offset=$(get_interval "$2") ; then
				error "End offset must be a valid timecode. Got '$2'."
				exit $EX_USAGE
			fi
			shift
			;;
		-t|--to)
			if ! totime=$(get_interval "$2") ; then
				error "Ending timestamp must be a valid timecode. Got '$2'."
				exit $EX_USAGE
			fi
			if [ "$totime" -eq 0 ]; then
				error "Ending timestamp was set to 0, set to movie length."
				totime=-1
			fi
			shift
			;;
		-S|--stamp)
			if ! temp=$(get_interval "$2") ; then
				error "Timestamps must be a valid timecode. Got '$2'."
				exit $EX_USAGE
			fi
			initial_stamps=( ${initial_stamps[*]} $temp )
			shift
			;;
		-l|--highlight)
			if ! temp=$(get_interval "$2"); then
				error "Timestamps must be a valid timecode. Got '$2'."
				exit $EX_USAGE
			fi
			#~ HLTIMECODES+=( $temp )
			HLTIMECODES[${#HLTIMECODES[@]}]="$temp"
			shift
			;;
		-j|--jpeg) output_format=jpg ;;
		-h|--help) show_help ; exit $EX_OK ;;
		--shoehorn)
			shoehorned="$2"
			shift
			;;
		-F) decoder=$DEC_FFMPEG ;;
		-M) decoder=$DEC_MPLAYER ;;
		-H|--height)
			if ! is_number "$2" ; then
				error "Height must be a (positive) number. Got '$2'."
				exit $EX_USAGE
			fi
			th_height="$2"
			shift
			;;
		-a|--aspect)
			if ! is_float "$2"  && ! is_fraction "$2" ; then
				error "Aspect ratio must be expressed as a (positive) floating "
				error "    point number or a fraction (ie: 1, 1.33, 4/3, 2.5). Got '$2'."
				exit $EX_USAGE
			fi
			aspect_ratio="$2"
			shift
			;;
		-A|--autoaspect) aspect_ratio=-1 ;;
		-c|--columns)
			if ! is_number "$2" ; then
				error "Columns must be a (positive) number. Got '$2'."
				exit $EX_USAGE
			fi
			cols="$2"
			shift
			;;
		-m|--manual) manual_mode=1 ;;
		-e|--extended)
			# Optional argument quirks: $2 is always present, set to '' if unused
			# from the commandline it MUST be directly after the -e (-e2 not -e 2)
			# the long format is --extended=VAL
			# XXX: For some reason parsing of floats gives an error, so for now
			# ints and only fractions are allowed
			if [ "$2" ] && ! is_float "$2" && ! is_fraction "$2" ; then
				error "Extended multiplier must be a (positive) number (integer, float "\
"or fraction)."
				error "    Got '$2'."
				exit $EX_USAGE
			fi
			if [ "$2" ]; then
				extended_factor="$2"
			else
				extended_factor=$DEFAULT_EXT_FACTOR
			fi
			shift
			;;
		--mincho) font_filename=$FF_MINCHO ;;
		-I) # -I technically takes an optional argument (for future alternative
			# fonts) although it is documented as a two-letter option
			# Don't relay on using -I though, if I ever add a new alternative font
			# I might not allow it anymore
			if [ "$2" ] ; then
				# If an argument is passed, test it is one of the known ones
				case "$2" in
					k|j) ;;
					*) error "-I must be followed by j or k!" && exit $EX_USAGE ;;
				esac
			fi
			# It isn't tested for existence because it could also be a font
			# which convert would understand without giving the full path
			font_filename=$FF_MINCHO;
			shift
			;;
		-O|--override)
			# Rough test
			if ! egrep -q '[a-zA-Z_]+=[^;]*' <<<"$2"; then
				error "Wrong override format, it should be variable=value. Got '$2'."
				exit $EX_USAGE
			fi
			override "$2" "command line"
			shift
			;;
		-W) # Workaround mode, see wa_ss_* declarations at the start for details
			if [ "$2" != "o" ]; then
				error "Wrong argument. Use -Wo instead of -W$2."
				exit $EX_USAGE
			fi
			wa_ss_af='-ss ' wa_ss_be=''
			shift
			;;
		-k|--funky) # Funky modes
			case $(tolower "$2") in
				p|polaroid) # Same as overlap + rotate + photoframe
					inf "Changed to polaroid funky mode."
					#~ FILTERS_IND+=( 'filt_photoframe' 'filt_randrot' )
					FILTERS_IND[${#FILTERS_IND[@]}]='filt_photoframe'
					FILTERS_IND[${#FILTERS_IND[@]}]='filt_randrot'
					CSHEET_DELEGATE='csheet_overlap'
					# The timestamp must change location to be visible
					grav_timestamp=NorthWest
					;;
				o|overlap) # Random overlap mode
					CSHEET_DELEGATE='csheet_overlap'
					grav_timestamp=NorthWest
					;;
				r|rotate) # Random rotation
					#~ FILTERS_IND+=( 'filt_randrot' )
					FILTERS_IND[${#FILTERS_IND[@]}]='filt_randrot'
					;;
				f|photoframe) # White photo frame
					#~ FILTERS_IND+=( 'filt_photoframe' )
					FILTERS_IND[${#FILTERS_IND[@]}]='filt_photoframe'
					;;
				i|film)
					inf "Enabled film mode."
					#~ FILTERS_IND+=( 'filt_film' )
					FILTERS_IND[${#FILTERS_IND[@]}]='filt_film'
					;;
				x|random) # Random colours/fonts
					inf "Enabled random colours and fonts."
					randomize_look
					;;
				*)
					error "Unknown funky mode. Got '$2'."
					exit $EX_USAGE
					;;
			esac
			shift
			;;
		-d|--disable) # Disable default features
			case $(tolower "$2") in
				# timestamp (no final s) is undocumented but will stay
				t|timestamps|timestamp)
					inf "Timestamps disabled."
					# TODO: Can array splicing be done in a saner way?
					declare -a tmp=${FILTERS_IND[@]}
					unset FILTERS_IND
					FILTERS_IND=${tmp[@]/filt_apply_stamp/}
					unset tmp
					;;
				s|shadows|shadow)
					if [ $DISABLE_SHADOWS -eq 0 ]; then
						inf "Shadows disabled."
						DISABLE_SHADOWS=1
					fi
					;;
				*)
					error "Requested disabling unknown feature. Got '$2'."
					exit $EX_USAGE
					;;
			esac
			shift
			;;
		-q|--quiet)
			# -q to only show errors
			# -qq to be completely quiet
			if [ $verbosity -gt $V_ERROR ]; then
				verbosity=$V_ERROR
			else
				verbosity=$V_NONE
			fi
			;;
		-D) # Repeat to just test consistency
			if [ $DEBUGGED -gt 0 ]; then exit ; fi
			inf "Testing internal consistency..."
			unit_test
			if [ $? -eq 0 ]; then
				warn "All tests passed"
			else
				error "Some tests failed!"
			fi
			DEBUGGED=1
			warn "Command line: $0 $ARGS"
			title="$(basename "$0") $ARGS"
			;;
		--) shift ; break ;;
		*) error "Internal error! (remaining opts: $@)" ; exit $EX_SOFTWARE ;
	esac
	shift
done

# Remaining arguments
if [ ! "$1" ]; then
	show_help
	exit $EX_USAGE
fi

# }}} # Command line parsing

# Test requirements
test_programs || exit $EX_UNAVAILABLE

# If -m is used then -S must be used
if [ $manual_mode -eq 1 ] && [ -z $initial_stamps ]; then
	error "You must provide timestamps (-S) when using manual mode (-m)"
	exit $EX_USAGE
fi

set +e # Don't fail automatically
for arg do process "$arg" ; done


# vim:set ts=4 ai foldmethod=marker: #
