#!/bin/bash

# LICENSE {{{1
# This file is part of Mkat
#
# Copyright (C) 2004, 2005, 2013 Dmitry Maksyoma <ledestin at gmail.com>.
#
# Mkat is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
# USA.

# ALGORITHM {{{1
#1. Make iso
#2. Burn iso
#3. Add the burned disc to the catalog
#4. Compare files on the disc with the original files

# Sensible defaults for overridable constants {{{1
CD_OPTS=('OVERBURN' '-overburn -dao' 'BLANK_MEDIA' 'blank=fast' 'SPEED' 'speed=$SPEED' 'MULTISESSION' '-multi')
DVD_OPTS=('OVERBURN' '-overburn' 'SPEED' '-speed=$SPEED' 'MULTISESSION' '-M' 'SINGLESESSION' '-Z')
ISO_OPTS=('QUIET' '-quiet')

# FUNCTIONS {{{1
# Complain and exit if $LOCKFILE exists.
function check_lock() {
  debug "checking for lockfile $LOCKFILE"
  if [ `dotlockfile -c $LOCKFILE && echo 0 || echo 1` -eq 0 ]; then
    error "$ISO_IMAGE: image locked ($LOCKFILE)"
    exit 1
  fi
}

# getopt(array_name, var_name)
# arran_name - name of variable containing array of NAME, VALUE pairs.
# var_name   - NAME part of the sought NAME, VALUE pair.
# Returns VALUE for specified var_name from array array_name.
function getopt() {
  name=$1; var=$2
  eval arr=("\"\${$name[@]}\"")
  i=0
  for o in "${arr[@]}"; do
    if [ "$o" == "$var" ]; then
      echo ${arr[$(($i+1))]}
      return
    fi
    i=$(($i+1))
  done
}

# Create lock file to protect $ISO_IMAGE file
function lock {
  debug "creating lock file..."
  dotlockfile $LOCKFILE && trap "dotlockfile -u $LOCKFILE" EXIT
}

# print_mb(blocks)
# Convert blocks to megabytes and print the result.
function print_mb() {
  echo $(($1*2048/1024**2))Mb
}

# Remove lock file
function unlock {
  debug "releasing lock file..."
  dotlockfile -u $LOCKFILE && trap - EXIT
}

# LOAD CONFIGURATION {{{1
RCFILE=mkatrc 
[ -f /etc/$RCFILE ] && source /etc/$RCFILE
[ -f ~/.$RCFILE ] && source ~/.$RCFILE
[ -z "$MKAT_LIBPATH" ] && \
  { echo >&2 "MKAT_LIBPATH not set (edit /etc/$RCFILE)"; exit 1; }
. "$MKAT_LIBPATH/helpers.sh" || exit 1
. "$MKAT_LIBPATH/common.sh" || exit 1

# CHECK CONFIGURATION (CONFIG-LOADED) {{{1
# The essential variables must be defined.
fail_unless_defined "DRIVE CD_BURN_CMD DVD_BURN_CMD CD LISTDIR TMP
  MKIMAGE_OPTS MKIMAGE_CMD ISO_IMAGE"

# DEFINE CONSTANTS {{{1
CD_FREE_BLOCKS=358400
DVD_FREE_BLOCKS=2295104
# Blu-ray sizes after 256MB were allocated to defect management.
BD25_FREE_BLOCKS=12088320
BD50_FREE_BLOCKS=24307712
BD_DEFECT_MANAGEMENT_BLOCKS=131072
# List of options/variables, which, if set, will be translated as parameters
# to CD or DVD/BD burning program. Parameter dictionaries are defined in CD_OPTS
# and DVD_OPTS
SUPPORTED_OPTIONS="OVERBURN BLANK_MEDIA SPEED MULTISESSION SINGLESESSION"

# COMMAND LINE PARAMETER PROCESSING {{{1
BANNERS=("[options] <label> <file>...")
OPTIONS=(\
  -n "print estimated image size, exit" 'PRINT_ONLY=1' \
  -s=SPEED "set burning speed" 'SPEED=$2' \
  -c "blank rewritable media" 'BLANK_MEDIA=1' \
  -o "overburn" 'OVERBURN=1' \
  '-p|--pipe' "pipe image directly to a burning program" \
    'PIPE_MKIMAGE=1; NO_QUESTIONS=1' \
  -r=REC_OPTS "pass parameters to burning program" 'REC_OPTS="$REC_OPTS $2"' \
  -i=MKIMAGE_OPTS "pass parameters to image making program" \
    'MKIMAGE_OPTS="$MKIMAGE_OPTS $2"' \
  -m=MKAT_OPTS "pass parameters to mkat" 'MKAT_OPTS=$2' \
  --media=TYPE 'specify media type (one of cd, dvd, bd, bd50)' 'BURN_MEDIA=$2' \
  --noimg "don't create image, just burn" 'SKIP_MKIMAGE=1' \
  --nocat "skip catalog" 'SKIP_CATALOG=1' \
  --nocheck "skip afterburn check" 'SKIP_CHECK=1' \
  --dbcheck "check if the files are in mkat db" 'DB_CHECK=1' \
  '-y|--yes' "don't ask questions" 'NO_QUESTIONS=1' \
  --simulate "print only, --debug implied" 'SIMULATE=1; DEBUG=1' \
  --debug "print debug output" 'DEBUG=1' \
  '-h|--help' "print available options" 'usage 0')
process_options "$@"

# CHECK PARAMETERS {{{1
interpolate_envvars

# Complain and exit if `-o file' option is present in MKIMAGE_OPTS or MKIMAGE_CMD.
for v in MKIMAGE_OPTS MKIMAGE_CMD; do
  eval "var=\"\$$v\""
  if [[ "$var" =~ -o ]]; then
    echo >&2 "$v: please remove -o option from the variable definition. Having -o option is incompatible with this release."
    exit 1
  fi
done;

# If --noimg is specified, ignore --pipe option.
[ $PIPE_MKIMAGE ] && [ $SKIP_MKIMAGE ] && PIPE_MKIMAGE=

[ "$MKISO_OPTS" ] && MKIMAGE_OPTS="$MKIMAGE_OPTS $MKISO_OPTS"

# Append `-o' option to MKIMAGE_OPTS where necessary.
if [ -z $PRINT_ONLY ]; then
  if [ -z $PIPE_MKIMAGE ]; then
    MKIMAGE_OPTS="$MKIMAGE_OPTS -o $ISO_IMAGE"
  else
    opt=`getopt ISO_OPTS QUIET`
    [ -n "$opt" ] && MKIMAGE_OPTS="$MKIMAGE_OPTS $opt"
    # Image will be piped directly to a burning program. So it should read its
    # stdin.
    ISO_IMAGE=/dev/stdin
  fi
fi
[ -z $MULTISESSION ] && SINGLESESSION=1

# TRANSLATE PARAMETERS TO REC_OPTS {{{1
# e.g. -o => -overburn
if [ -z "$BURN_MEDIA" ]; then
  BURN_MEDIA='auto'
  FREE_BLOCKS=`dvd+rw-mediainfo \
    2> >(grep -v 'READ BD SPARE INFORMATION failed') "$DRIVE" | \
    sed -ne 's/^ unformatted:\s\+//p' | sed -ne 's/*.\+//p'`
  [ -z "$FREE_BLOCKS" ] && \
    { echo >&2 "can't get media block info, insert media or specify --media option"; exit 1; }

  if [ "$FREE_BLOCKS" -ge "$DVD_FREE_BLOCKS" ]; then
    OPTS='DVD_OPTS'
    [ "$FREE_BLOCKS" -ge "$BD25_FREE_BLOCKS" ] && \
      let "FREE_BLOCKS -= $BD_DEFECT_MANAGEMENT_BLOCKS"
  else
    OPTS='CD_OPTS'
  fi
else
  case "$BURN_MEDIA" in
    cd) OPTS='CD_OPTS' ;;
    dvd|bd*) OPTS='DVD_OPTS' ;;
    *) error "\`$BURN_MEDIA': unknown media type" ;;
  esac
fi

for o in $SUPPORTED_OPTIONS; do
  eval OPTION_CHECKED="\$$o"
  if [ "$OPTION_CHECKED" ]; then
    val=`getopt $OPTS $o`
    [ "$val" ] && eval val="\"$val\"" && REC_OPTS="$REC_OPTS $val"
  fi
done

# Label and files arguments must be provided.
if [ -z $SKIP_MKIMAGE ]; then
  [ ${#ARGUMENTS[@]} -lt 2 ] && {
    echo >&2 "not enough parameters"
    usage 1
  }
  # Vars needed for $MKIMAGE_CMD.
  LABEL="${ARGUMENTS[0]}"; unset 'ARGUMENTS[0]'; FILES=("${ARGUMENTS[@]}")
  listfile="$LISTDIR/$LABEL.list"
  [ -f "$listfile" ] && \
    { echo >&2 "listfile already exists: $listfile";
      echo >&2 'Hint: use a different label';
      exit 1;
    }
fi

# REAL WORK {{{1
# PREPARE {{{2
# Give choice to quit if files you're about to write are already archived
# (exist in mkat database).
if [ $DB_CHECK ]; then
  echo "Checking if the files are in mkat db"
  for f in "${FILES[@]}"; do
    mkat -m "$f"
    [ $? -eq 0 ] && ALREADY_IN_DB=1
  done
  if [ $ALREADY_IN_DB ]; then
    echo "Some files you are about to write are already in the mkat database." 
    read -p "Press Enter to write anyway or Control-C to abort"
    [ $? -ne 0 ] && exit
  fi
fi

set -e
trap '[ "$OUT" ] && error $OUT' ERR

# Construct lockfile name following this template: ".$FILE.lock".
ISO_DIR="${ISO_IMAGE%/*}"
[ "$ISO_IMAGE" = "$ISO_DIR" ] && ISO_DIR=
LOCKFILE="${ISO_DIR:+$ISO_DIR/}.${ISO_IMAGE##*/}.lock"


# MAKE ISO IMAGE {{{2
if [ $PRINT_ONLY ]; then
  cmd="genisoimage -print-size $MKIMAGE_OPTS"
  debug $cmd "${FILES[@]}"
  OUT=`$cmd "${FILES[@]}" 2>&1`
  OUT=`echo "$OUT" | grep ^Total`; echo "$OUT"
  size=${OUT##* }; unset OUT
  print_mb $size
  case "$BURN_MEDIA" in
    cd) FREE_BLOCKS="$CD_FREE_BLOCKS" ;;
    dvd) FREE_BLOCKS="$DVD_FREE_BLOCKS" ;;
    bd) FREE_BLOCKS="$BD25_FREE_BLOCKS" ;;
    bd50) FREE_BLOCKS="$BD50_FREE_BLOCKS" ;;
  esac
  #complain if doesn't fit on media
  if [ $size -gt $FREE_BLOCKS ]; then 
    echo "won't fit: media size is $FREE_BLOCKS blocks, but $size blocks are to be written."
    diff=$(($size-$FREE_BLOCKS)) 
    echo "diff: $diff blocks or `print_mb $diff`"
  #suggest how much can be added to fill space
  elif [ $size -lt $FREE_BLOCKS ]; then
    diff=$(($FREE_BLOCKS-$size))
    echo "$diff more blocks or `print_mb $diff` could be added to fill space"
  fi
  exit
elif [ -z $SKIP_MKIMAGE ] && [ -z $PIPE_MKIMAGE ]; then
  echo "making ISO image"
  eval "debug $MKIMAGE_CMD"
  if [ -z $SIMULATE ]; then
    [ -d "$TMP" ] || mkdir -p "$TMP"
    check_lock

    #possible security issue when using existing directory
    #I check that current UID is the owner and the directory isn't world
    #writable
    if [ -d "$TMP" ]; then
      stat=(`stat -c '%u %A' "$TMP"`)
      [ ${stat[0]} = $EUID ] || \
	{ echo >&2 "error: $TMP isn't owned by current UID: $UID"; exit 1;}
      [ ${stat[1]:$((-2)):1} = "-" ] || \
        { echo >&2 "error: $TMP is world writable"; exit 1;}
    else
      #set umask=0022 only if $TMP resides in /tmp
      saved_umask=`umask`
      expr match "$TMP" /tmp
      [ $? -eq 0 ] && umask 0022
      mkdir "$TMP"
      umask $saved_umask
    fi
    lock
    eval "$MKIMAGE_CMD"
  fi
fi

# BURN {{{2
if [ -z $NO_QUESTIONS ] && [ -z $SKIP_MKIMAGE ]; then
  read -p "Press Enter to proceed to burn or Control-C to abort"
fi
echo "BURN!"
unset IFS

case "$BURN_MEDIA" in
  cd) BURN_CMD="$CD_BURN_CMD" ;;
  dvd|bd*) BURN_CMD="$DVD_BURN_CMD" ;;
esac

if [ $PIPE_MKIMAGE ]; then
  cmd="$MKIMAGE_CMD | $BURN_CMD"
else
  cmd="$BURN_CMD"
fi

eval "cmd_debug=\"$cmd\""
debug "$cmd_debug"

if [ -z $SIMULATE ]; then
  # Lock if image file is used for burning and if image wasn't made (and
  # locked) at this time run.
  if [ -z $PIPE_MKIMAGE ] && [ $SKIP_MKIMAGE ]; then
    # If the $LOCKFILE exists lock() will block. As a result, burn(1) will
    # just hang doing nothing. To mitigate it, I check if lockfile exists
    # before calling lock(). There is a race condition here, but it's better
    # than nothing.
    check_lock
    lock
  fi
  eval $cmd
  [ -z $PIPE_MKIMAGE ] && unlock
fi

# CATALOG {{{2
if [ ! $SKIP_CATALOG ] && [ $CD ]; then
  echo "Catalog..."
  if [ $SIMULATE ]; then
    echo "mkat $MKAT_OPTS"
  else
    mkat $MKAT_OPTS
  fi
fi

# AFTERBURN CHECK {{{2
if [ -z "$SKIP_CHECK" -a -n "$CD" ]; then
  set +e
  echo 'Afterburn check...'
  if [ -z "$SIMULATE" ]; then
    for f in "${FILES[@]}"; do
      if [ -d "$f" ]; then
	find "$f" -type f -printf "%P\0" | xargs -0 -ifile -n1 cmp file "$CD"/file
      else
	cmp "$f" "$CD/`basename "$f"`"
      fi
    done
  fi
fi

# EJECT {{{2
debug "going to sleep for $AUTOFS_DELAY seconds and eject"
[ -z $SIMULATE ] && sleep $AUTOFS_DELAY && eject
