#!/usr/local/bin/bash
#
# Copyright (c) 2008, Christopher Cowart and contributors
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions 
# are met:
# * Redistributions of source code must retain the above copyright 
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright 
#   notice, this list of conditions and the following disclaimer in the 
#   documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# $Id: confman.in 589 2011-12-23 23:44:42Z blee $
#
# confman help
# 	Print help and usage information

if [ -r /usr/local/share/confman/confmancommon.sh ] ; then
    . /usr/local/share/confman/confmancommon.sh
else
    echo "Can't find confmancommon.sh. Exiting." >&2
    exit 17
fi

# Source the documentation library
if [ -f $REPO_DOCS_CONFMAN ] ; then
	. $REPO_DOCS_CONFMAN
else
    echo "Couldn't source the confman shell documentation library" >&2
	exit 1
fi

# Set a default editor
if [ -z ${EDITOR} ] ; then
    EDITOR="vi"
fi

# Let's make sure that only you can read your working copy
# Have to make this less restrictive due to NFS home dirs.
${NFS_HACK:-false} && umask 022 || umask 027

# This function implements the setup subcommand. It is intended to be called
# with no arguments, and will error if that's not the case.
function setup {
    local response
    local wcopy_lock

    if conf_wcopy_is_locked; then
        echo "Your working copy is locked." >&2
        exit 1
    else
        conf_debug "Running setup"
        if [ -n "$*" ] ; then
            print_usage 1
        elif [ -d ${WORK_PATH} ] ; then
            echo "Looks like ${WORK_PATH} already exists." 
            echo "Start over by removing it? (y/N)"
            read response
            if [[ $response =~ ^[yY]([es]|[ES])? ]] ; then
                rm -rf ${WORK_PATH}
                setup
            else
                echo "Setup failed." >&4
                return 1
            fi
        else
            mkdir -p ${WORK_PATH} 
            wcopy_lock=$(conf_lock_wcopy)
            conf_checkout_tree
            conf_unlock_wcopy $wcopy_lock
        fi
    fi
}

function create {
    local wcopy_lock
    if [ -z "$*" ] ; then
        print_usage 1
    else
        wcopy_lock=$(conf_lock_wcopy)
        conf_create_modules "$@" || exit 1
        conf_unlock_wcopy $wcopy_lock
    fi
}

function update {
    local wcopy_lock
    local opt OPTARG OPTIND
    local revision="HEAD" recipe update_all="false"
    while getopts ":r:a" opt ; do
        case $opt in
            r)  revision="$OPTARG" ;;
            a)  update_all="true" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))
    recipe="$1"
    wcopy_lock=$(conf_lock_wcopy)
    $UPDATE_RELEVANT_ONLY && [ -z $recipe ] && recipe="$RECIPE_NAME"
    if [ ! -r "$(conf_recipe_dir)/$recipe" ]; then
        echo "Recipe \"$recipe\" does not exist or is not readable." >&2
        exit 1
    fi
    if $update_all || [ -z $recipe ]; then
        conf_update_tree -r $revision ${WORK_PATH} || exit 1
    else
        conf_update_relevant_tree $recipe -r $revision || exit 1
    fi
    conf_unlock_wcopy $wcopy_lock
}

function revert {
    local wcopy_lock
    if [ -z "$*" ] ; then
        print_usage 1
    else
        wcopy_lock=$(conf_lock_wcopy)
        conf_revert "$@"
        conf_unlock_wcopy $wcopy_lock
    fi
}

# Either by prompting or respecting -m/-F, returns a filename on stdout
# with a log message in it
# $1 contains a file with the output of svn status to display in interactive
# log messages
#   Returns 1 if an abort was requested by the user
#   Returns 0 if the returned filename is good for a commit
function get_log {
    local status="$1"
    local finished=false
    local ignoreline="--This line, and those below, will be ignored--"
    local orig_log logfile

    # local seems to return true, overwriting the return of the nested
    # call for && evaluation
    logfile=$(conf_log_message) && finished=true

    if $finished ; then
        echo "$logfile"
        return 0
    fi

    orig_log=$(conf_tmp_file)
    printf "\n$ignoreline\n" >> "$logfile"
    cat "$status" >> "$logfile"
    cp "$logfile" "$orig_log"

    # I apologize for the redirect, but stdout is being written into a
    # variable, and at least vim (probably other editors too) gets cranky when
    # stdout doesn't go to the user. Luckily, the fork in the common section
    # leaves a copy of the original stdout in fd 3.
    ${EDITOR} "$logfile" >&3

    while diff "$logfile" "$orig_log" >/dev/null 2>&1 ; do
        echo "Log message unchanged. (E)dit / (a)bort / (c)ontinue?" >&3
        read response
        case "$response" in
            [cC]*)      break;;
            [aA]*)      return 1;;
            ""|[eE]*)   ${EDITOR} "$logfile" >&3;;
        esac
    done
    rm -f "$orig_log"
    sed_i_cmd -e "/${ignoreline}/,\$d" "$logfile" 2>/dev/null
    echo "$logfile"
    return 0
}

# Arguments are full paths to files within $WORK_PATH to check via svn status.
#   Returns 1 if the files are unchanged
#   Returns 0 if the files are changed and prints a filename to stdout
#     with status info suitable for log message footers
function get_status {
    local status=$(conf_tmp_file)
    local ABS_WORK=$(${readlink_cmd} -m "$WORK_PATH")

    # Using a subshell to preserve CWD
    (
        cd "$WORK_PATH"
        until [ -z "$1" ] ; do
            /usr/local/bin/svn status "${1#${ABS_WORK}/}" | egrep -v "^\?" >> "$status"
            shift
        done
    )

    if [ $(wc -l < "$status") -eq 0 ] ; then
        rm -f "$status"
        return 1
    fi

    echo "$status"
    return 0
}

function commit_repo_only {
    local msg status abswork
    local opt OPTIND OPTARG
    local wcopy_lock

    wcopy_lock=$(conf_lock_wcopy)
    abswork=$(conf_abswork)

    while getopts ":m:F:" opt ; do
        case $opt in
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    [ -z "$1" ] || print_usage 1

    if status=$(get_status "${abswork}"); then
        nocommit=false
        msg=$(get_log "$status") || conf_cleanExit
        rm -f "$status"
    fi

    echo "Commit operation started" >&2

    $nocommit || conf_commit_file $msg "${abswork}" || return $?

    echo "Commit operation finished successfully" >&2

    rm -f $msg
    conf_unlock_wcopy $wcopy_lock
}

function commit_full {
    local msg status modules module symlink abswork files statefile
    local nocommit=true
    local opt OPTIND OPTARG
    local wcopy_lock system_lock

    while getopts ":m:F:" opt ; do
        case $opt in
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    [ -z "$1" ] || print_usage 1

    conf_require_recipe 
    system_lock=$(conf_lock_system)
    wcopy_lock=$(conf_lock_wcopy)
    abswork=$(conf_abswork)
    
    for module in $(conf_get_reverse_recipe); do
        modules=${modules:+${modules} }${module}
        for symlink in $(find ${WORK_PATH}/${module} \
            -type l -exec ${readlink_cmd} -m {} \; -print)
        do
            symlink=$(conf_rel_path "${symlink}")
            files="${files:+${files} }${abswork}/${symlink}"
        done
        files="${files:+${files} }${abswork}/${module}"
    done
    
    $SUDO ${SUDO:+-v}

    if status=$(get_status $modules $symlinks); then
        nocommit=false
        msg=$(get_log "$status") || conf_cleanExit
        rm -f "$status"
    fi
    
    $SUDO ${SUDO:+-v}
    
    echo "Commit operation started" >&2
    $nocommit || conf_commit_file $msg $files || return $?
    update

    statefile=$(conf_tmp_file)
    for layer in $(conf_get_reverse_recipe) ; do
        echo "Rolling on $layer..."
        conf_rollout $layer $statefile || return $?
    done
    for file in $SINGULARITIES ; do
        conf_assemble_sing $file || return $?
    done
    conf_recordAction commit
    conf_markclean
    echo "Commit operation finished successfully" >&2
    rm -f $msg
    conf_unlock_wcopy $wcopy_lock
    conf_unlock_system $system_lock
}

function commit {
    if ${CONF_COMMIT_REPO_ONLY}; then
        commit_repo_only "$@" || return $?
    else
        commit_full "$@" || return $?
    fi
}

# Short name intentional, don't want collision with real install.
function inst {
    local file livefile
    local msg status files
    local nocommit=true
    local system_lock wcopy_lock
    local opt OPTIND OPTARG
    local module
    local statefile

    while getopts ":m:F:" opt ; do
        case $opt in
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    [ -n "$1" ] || print_usage 1

    system_lock=$(conf_lock_system)
    wcopy_lock=$(conf_lock_wcopy)
    conf_require_recipe 
    
    for file in "$@"; do
        files="${files:+${files} }$(${readlink_cmd} -m "$file")"
    done
    
    $SUDO ${SUDO:+-v}

    if status=$(get_status $files) ; then
        nocommit=false
        msg=$(get_log "$status") || conf_cleanExit
        rm -f "$status"
    fi
    echo "Installation operation started." >&2
    $nocommit || conf_commit_file "$msg" "$files" || return $?

    update
    
    statefile=$(conf_tmp_file)
    for layer in $(conf_get_reverse_recipe) ; do
        for file in "$@"; do
            module=$(conf_wfile_module $file)
            if conf_wfile_is_singularity $file; then
                conf_install $layer $statefile "${file%-$module}-${layer}"
            else
                conf_install $layer $statefile "$file"
            fi
        done
    done
    for file in $SINGULARITIES ; do
        conf_assemble_sing $file || conf_cleanExit
    done
    conf_recordAction install
    if ! conf_isclean ; then
        echo "WARNING: Recent 'install' operations prevented a 'sync'" >&2
        echo "Running a 'commit' is highly recommended." >&2
    fi
    echo "Installation operation succeeded." >&2
    conf_unlock_wcopy $wcopy_lock
    conf_unlock_system $system_lock
}
	

function import {
    local wcopy_lock

    local item OPTIND flags
    local force=false
    while getopts "f" OPT; do
        case "$OPT" in
            f)
                force=true
                flags="-f"
                ;;
            *)
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    conf_require_recipe 
    local module=$1
    local response usefile suffix file layer symlink morefiles i
    local mode=$DEFAULT_MODE_FILE
    local owner=$DEFAULT_OWNER
    local group=$DEFAULT_GROUP
    local comment=$DEFAULT_COMMENT
    shift

    wcopy_lock=$(conf_lock_wcopy)

    if [ -z "$module" ] || [ -z "$1" ] ; then
        if ! conf_lock_is_recursive $wcopy_lock ; then
            print_usage 1
            return 1
        fi
        return 0
    fi

    if ! [ -d "${WORK_PATH}/${module}" ] ; then
        echo "No such module: $module" >&2
        return 1
    fi

    $SUDO ${SUDO:+-v}
    if [ -r $1 ] ; then
        file=`$ABSPATH $1`
    else
        # If we can't enter the parent directory, this will help us
        # get the info we need.
        file=`$SUDO $ABSPATH $1`
    fi
    shift

    # See if we're importing a singularity
    if [[ "$SINGULARITIES" =~ "$file" ]] ; then
        suffix="-$module"
    fi

    if [ -f ${WORK_PATH}/${module}${file}${suffix} ] ; then
        echo "$file already exists in your working copy of $module." \
            "Skipping." >&4
        import $flags $module "$@"
        conf_unlock_wcopy $wcopy_lock
        return
    fi

    if ! $force ; then
        for layer in $(conf_get_recipe) ; do
            if [ -f ${WORK_PATH}/${layer}${file}${suffix} ] ; then
                echo "$file already exists in the ${layer}" \
                    "module. Skipping." >&4
                echo "Did you mean -f ?" >&4
                import $flags $module "$@"
                conf_unlock_wcopy $wcopy_lock
                return
            fi
        done
    fi

    if $SUDO [ -L $file ] ; then
        eval `$SUDO ${stat_cmd} "${stat_opts}" ${file}`
        symlink=`${readlink_cmd} -m $file`
        usefile=$(conf_tmp_file)
        echo "# This is a symlink, please do not modify" > $usefile

    elif $SUDO [ -f $file ] ; then
        eval `$SUDO ${stat_cmd} "${stat_opts}" ${file}`
        # Let's make our best guess on the comment character:
        local tmpfile=$(conf_tmp_file)
        local biggestcount=0
        local count

        # Let's see if we can read the file as ourself:
        usefile=$(conf_tmp_file)
        if [ ! -r $file ] ; then
            $SUDO cat $file > $usefile
        else
            cat $file > $usefile
        fi

        # Put all non-alphanumerics into a file
        awk '{print $1}' $usefile | egrep -o \
            "^[^-_A-Za-z0-9]" > $tmpfile

        for char in `cat $tmpfile | sort | uniq` ; do
            count=`egrep -o "\\$char" $tmpfile | wc -l`
            if [ $count -gt $biggestcount ] ; then
                biggestcount=$count
                comment="$char"
            fi
        done
        rm $tmpfile

        # Convert mode string to base 10:
        mode=`echo "obase=10;ibase=8;$mode" | bc`
        # And use a bitmask to subtract off all write-access
        # 3949d = 7555o
        mode=$(($mode & 3949))
        # And back to an octet:
        mode=`printf '%04o\n' $mode`
    elif $SUDO [ -d "$file" ] ; then
        # Create the directory in our working copy
        if ! [ -d "${WORK_PATH}/${module}${file}" ] ; then
            newdir "${WORK_PATH}/${module}${file}"
        fi

        # Now glob every possible file in the directory
        morefiles=(${file}/* ${file}/.[^.]* ${file}/..?*)

        # But filter out the ones that aren't files (e.g., ${file}/*,
        # because there were no files and the glob was interpreted literally)
        i=0
        until [ $i -eq ${#morefiles[@]} ] ; do
            if [ -f "${morefiles[$i]}" ] ; then
                import $flags $module "${morefiles[$i]}"
            fi
            i=$(($i + 1))
        done

        import $flags $module "$@" 
        conf_unlock_wcopy $wcopy_lock
        return
    else
        # Prompt for file owner
        echo "Who should be the file's owner? [ $owner ]"
        read response
        if [ ! -z $response ] ; then
            owner=$response
        fi

        # And file's group
        echo "Who should be the file's group? [ $group ]"
        read response
        if [ ! -z $response ] ; then
            group=$response
        fi

        # now, the permissions
        echo "What should the file's permissions be? [ $mode ]"
        read response
        if [ ! -z $response ] ; then
            mode=$response
        fi

        # now, the comment character
        echo "What string starts comment lines? [ $comment ]"
        read response
        if [ ! -z $response ] ; then
            comment=$response
        fi
    fi

    if [ ! -d "${WORK_PATH}/${module}`dirname $file`" ] ; then
        newdir "${WORK_PATH}/${module}`dirname $file`"
    fi

    # Time to generate the file
    conf_gen_file $module "${file}${suffix}" $owner $group \
        $mode "$comment" $usefile "$symlink"

    # Removing temporary files
    rm -f $usefile

    # Are there more files to import?
    if [ ! -z $1 ] ; then
        import $flags $module "$@"
    fi

    conf_unlock_wcopy $wcopy_lock
}

function remove {
    local wcopy_lock
    if [ -z $1 ]; then
        print_usage 1 
    fi

    wcopy_lock=$(conf_lock_wcopy)
    conf_rm_file "$@"
    conf_unlock_wcopy $wcopy_lock
}

function newdir {
    local dir="$1"
    local response module realpath
    local mode=$DEFAULT_MODE_DIRECTORY
    local owner=$DEFAULT_OWNER
    local group=$DEFAULT_GROUP
    local comment="dir"
    local workdir=`$ABSPATH .`
    local wcopy_lock

    if [ -z "$dir" ] ; then
        print_usage 1
    fi
    
    wcopy_lock=$(conf_lock_wcopy)
    # Find the "real" directory's path if not already specified
    if [[ ! $dir =~ ^/ ]] ; then
        dir="${workdir}/${dir}"
    fi
    module=`echo ${dir#$WORK_PATH} | ${sed_cmd} -e 's:/([^/]+)/.*:\1:'`
    realpath=${dir#${WORK_PATH}/${module}}
    
    local directories=`echo "$realpath" | ${sed_cmd} -e 's:/: :g'`
    local fulldir=""
    
    for dir_name in $directories; do
       fulldir="${fulldir}/${dir_name}"
    
       # If it exists on the filesystem, get its real attributes
       if [ -d $fulldir ] ; then
           eval `$SUDO ${stat_cmd} "${stat_opts}" ${fulldir}`
           echo "Making directory $fulldir with ${owner}:${group}, $mode"
           conf_mkdir "$WORK_PATH/$module/$fulldir" $owner $group $mode
    
       # Otherwise, prompt for attributes
       else
           # Prompt for file owner
           echo "Who should be the directory's owner? [ $owner ]"
           read response
           if [ ! -z $response ] ; then
               owner=$response
           fi
    
           # And file's group
           echo "Who should be the directory's group? [ $group ]"
           read response
           if [ ! -z $response ] ; then
               group=$response
           fi
    
           # now, the permissions
           echo "What should the directory's permissions be? [ $mode ]"
           read response
           if [ ! -z $response ] ; then
               mode=$response
           fi
           echo "Making directory $fulldir with ${owner}:${group}, $mode"
           conf_mkdir "$WORK_PATH/$module/$fulldir" $owner $group $mode
       fi
    done

    conf_unlock_wcopy $wcopy_lock
}

function cnf_touch {
    local file="$1"

    local owner="${DEFAULT_OWNER}"
    local group="${DEFAULT_GROUP}"
    local mode="${DEFAULT_MODE_FILE}"
    local comment="${DEFAULT_COMMENT}"
    local wcopy_lock
    local livefile

    if [ -z "${file}" ]; then
        print_usage 1
    fi
    
    wcopy_lock=$(conf_lock_wcopy)

    # Resolve the absolute path to the file
    file="$("${ABSPATH}" .)/${file}"

    # If the filename doesn't begin with WORK_PATH, we can't operate here
    if [ "${file#${WORK_PATH}}" = "${file}" ]; then
        echo "Cannot operate on non-working-copy file: ${file}" >&4
    fi

    module=${file#${WORK_PATH}/}
    module=${module%%/*}
    livefile="/${file#${WORK_PATH}/*/}"

    # If it exists on the live filesystem, pull the attributes from there
    if [ -f "${livefile}" ]; then
        eval `$SUDO ${stat_cmd} "${stat_opts}" "${livefile}"`

        # TODO: Guess the comment character too

        # Convert mode string to base 10:
        mode=`echo "obase=10;ibase=8;${mode}" | bc`
        # And use a bitmask to subtract off all write-access
        # 3949d = 7555o
        mode=$((${mode} & 3949))
        # And back to an octet:
        mode=`printf '%04o\n' ${mode}`

        echo "Touching file ${livefile} with ${owner}:${group}, ${mode}, comment string ${comment}"
        conf_gen_file "${module}" "${livefile}" "${owner}" "${group}" "${mode}" "${comment}" "" ""

    # Otherwise, prompt for attributes
    else
        # Prompt for file owner
        echo "Who should be the file's owner? [ ${owner} ]"
        read response
        if [ -n "${response}" ]; then
            owner="${response}"
        fi

        # Prompt for file group
        echo "Who should be the file's group? [ ${group} ]"
        read response
        if [ -n "${response}" ]; then
            group="${response}"
        fi

        # Prompt for file mode
        echo "What should the file's permissions be? [ ${mode} ]"
        read response
        if [ -n "${response}" ]; then
            mode="${response}"
        fi

        # Prompt for file comment
        echo "What string starts comment lines? [ ${comment} ]"
        read response
        if [ -n "${response}" ]; then
            comment="${response}"
        fi

        echo "Touching file ${livefile} with ${owner}:${group}, ${mode}, comment string ${comment}"
        conf_gen_file "${module}" "${livefile}" "${owner}" "${group}" "${mode}" "${comment}" "" ""
    fi

    conf_unlock_wcopy "${wcopy_lock}"
}

function list {
	local file item

	if [ -z $1 ] ; then 
		file=`$ABSPATH .`
	else
		file=`$ABSPATH $1`
	fi
	echo -e "--------------------------------------------------------"
	echo -e "Mode\tOwner\tGroup\tComment\t\tFilename"
	echo -e "--------------------------------------------------------"
	if [ -f "$file" ] ; then
		conf_list $file
	elif [ -d "$file" ] ; then
		for item in `ls -A $file | grep -v .svn` ; do
			conf_list "$file/$item"
		done
	fi
}

function status {
	conf_status "$@"
}

function chowner {
    local owner file wcopy_lock

    local recursive item OPTIND
    while getopts "R" opt ; do
        case $opt in
            R)
            recursive=1
            shift
            ;;
            *)
            print_usage 1
            ;;
        esac
    done

    owner=$1
    file=$2

    wcopy_lock=$(conf_lock_wcopy)
    conf_set_prop $file owner $owner

    if [ ! -z $recursive ] && [ -d $file ] ; then
        for item in $file/* ; do
            chowner -R $owner $item
        done
    fi

    conf_unlock_wcopy $wcopy_lock
}

function chgroup {
    local group file wcopy_lock

    local recursive item OPTIND
    while getopts "R" opt ; do
        case $opt in
            R)
            recursive=1
            shift
            ;;
            *)
            print_usage 1
            ;;
        esac
    done

    group=$1
    file=$2

    wcopy_lock=$(conf_lock_wcopy)
    conf_set_prop $file group $group

    if [ ! -z $recursive ] && [ -d $file ] ; then
        for item in $file/* ; do
            chgroup -R $group $item
        done
    fi

    conf_unlock_wcopy $wcopy_lock
}

function chmode {
    local recursive item OPTIND mode file wcopy_lock

    while getopts "R" opt ; do
        case $opt in
            R)
            recursive=1
            shift
            ;;
            *)
            print_usage 1
            ;;
        esac
    done

    mode=$1
    file=$2

    wcopy_lock=$(conf_lock_wcopy)
    conf_set_prop $file mode $mode
    if [ ! -z $recursive ] && [ -d $file ] ; then
        for item in $file/* ; do
            chmode -R $mode $item
        done
    fi

    conf_unlock_wcopy $wcopy_lock
}

function chcom {
    local wcopy_lock
    local comment="$1"
    local file=$2
    wcopy_lock=$(conf_lock_wcopy)
    conf_set_prop $file comment "$comment"
    conf_unlock_wcopy $wcopy_lock
}

function chln {
    local symlink="$1"
    local file="$2"
    local wcopy_lock

    if [ $# -lt 2 ] ; then
        print_usage 1
    fi

    wcopy_lock=$(conf_lock_wcopy)
    conf_set_prop "$file" symlink "$symlink"
    conf_unlock_wcopy $wcopy_lock
}

function checklook {
	local module=$1
	local chkdir=${WORK_PATH}/${REPO_CHECKPTS}/${module}
	if [ -z $1 ] ; then
		print_usage 1
	elif [ -d $chkdir ] ; then
		ls $chkdir
	else
		echo "There are no checkpoints for ${module}."
	fi
}

function checknew {
    local module=$1
    local checkpoint=$2
    local wcopy_lock
    if [ -z $2 ] ; then
        print_usage 1
    else      
        wcopy_lock=$(conf_lock_wcopy)
        conf_new_checkpoint $module $checkpoint
        conf_unlock_wcopy $wcopy_lock
    fi
}

function checkclear {
    local module=$1
    local checkpoint=$2
    local wcopy_lock
    if [ -z $2 ] ; then
        print_usage 1
    else
        wcopy_lock=$(conf_lock_wcopy)
        conf_rm_checkpoint $module $checkpoint
        conf_unlock_wcopy $wcopy_lock
    fi
}

function rollback {
    local module=$1
    local checkpoint=$2
    local clock=$3
    local wcopy_lock system_lock statefile

    if [ -z "$2" ] ; then
        print_usage 1
    else
        conf_require_recipe 
        wcopy_lock=$(conf_lock_wcopy)
        system_lock=$(conf_lock_system)
        echo "Rolling $module back to $checkpoint $clock" >&2
        conf_rollback $module $checkpoint $clock || conf_cleanExit
        statefile=$(conf_tmp_file)
        for layer in $(conf_get_reverse_recipe) ; do
            echo "Rolling on $layer..."
            conf_rollout $layer $statefile || conf_cleanExit
        done

        for file in $SINGULARITIES ; do
            conf_assemble_sing $file || conf_cleanExit
        done
        echo "Rollback succeeded." >&2
        conf_unlock_wcopy $wcopy_lock
        conf_unlock_system $system_lock
    fi
}

function rmmod {
    local wcopy_lock
    if [ -z "$*" ] ; then
        print_usage 1
    else
        wcopy_lock=$(conf_lock_wcopy)
        conf_remove_modules "$@" || exit 1
        conf_unlock_wcopy $wcopy_lock
    fi
}

function rename {
    local oldmod=$1
    local newmod=$2
    local dir
    local wcopy_lock
    if [ -z "$oldmod" -o -z "$newmod" ]; then
        print_usage 1
    fi
    wcopy_lock=$(conf_lock_wcopy)
    conf_update_tree || exit 1
    for dir in ${WORK_PATH}/{,${REPO_CHECKPTS}/}$oldmod; do
        if ! [ -d "$dir" ]; then
            echo "No such directory: $dir" >&4
            exit 1
        fi
    done
    for dir in ${WORK_PATH}/{,${REPO_CHECKPTS}/}$newmod; do
        if [ -e "$dir" ]; then
            echo "Directory already exists: $dir" >&4
            exit 1
        fi
    done
    conf_rename $oldmod $newmod || exit 1
    conf_unlock_wcopy $wcopy_lock
}

function cnf_diff {
	conf_diff "$@"
}

function cnf_log {
	conf_log "$@"
}

function copy {
    local src=$1
    local dest=$2
    local wcopy_lock
    if [ -z $2 ]; then 
        print_usage 1   
    fi
    wcopy_lock=$(conf_lock_wcopy)
    conf_cp $src $dest
    conf_unlock_wcopy $wcopy_lock
}

function move {
    local src=$1
    local dest=$2
    local wcopy_lock
    if [ -z $2 ] 
    then
        print_usage 1 
    fi
    wcopy_lock=$(conf_lock_wcopy)
    conf_mv $src $dest
    conf_unlock_wcopy $wcopy_lock
}

function mklink {
    if [ $# -lt 2 ] ; then
        print_usage 1
    fi

    local forced
    if [ $1 == "-f" ] ; then
        forced=true
        shift
        if [ $# -lt 2 ] ; then
            print_usage 1
        fi
    fi

    local target=$1
    local link=$2
    local wcopy_lock
    wcopy_lock=$(conf_lock_wcopy)
    conf_ln $target $link $forced
    conf_unlock_wcopy $wcopy_lock
}

function lsattr {
    if [ -z "$1" ] ; then
        conf_lsattr *
    else
        conf_lsattr "$@"
    fi
}

function chattr {
    local attr="$1"
    shift
    local value="${1:-true}"
    shift
    if [ -z "$1" ] ; then print_usage 1 ; fi
    local wcopy_lock

    wcopy_lock=$(conf_lock_wcopy)
    conf_chattr "$attr" "$value" "$@"
    conf_unlock_wcopy $wcopy_lock
}

function rmattr {
    local attr="$1"
    shift
    local wcopy_lock
    if [ -z "$1" ] ; then print_usage 1 ; fi

    wcopy_lock=$(conf_lock_wcopy)
    conf_rmattr "$attr" "$@"
    conf_unlock_wcopy $wcopy_lock
}

function state {
    if conf_isclean ; then
        echo "This host is clean."
    else
        echo "This host may be dirty. Consider running a 'commit' operation"
    fi
}

function audit {
    local wcopy_lock system_lock
    if [ -z "$*" ] ; then
        conf_require_recipe 
        system_lock=$(conf_lock_system)
        wcopy_lock=$(conf_lock_wcopy)
        local tmproot=$(conf_tmp_dir)
        local live_root="${LIVE_ROOT}"
        local statefile

        # Override LIVE_ROOT to commit into our tmporary directory.
        LIVE_ROOT="$tmproot"

        conf_update_tree || conf_cleanExit

        echo "Audit operation started" >&2
        statefile=$(conf_tmp_file)
        for layer in $(conf_get_reverse_recipe) ; do
            echo "Rolling on $layer..."
            conf_rollout $layer $statefile || return $?
        done >/dev/null
        for file in $SINGULARITIES ; do
            conf_assemble_sing $file || return $?
        done >/dev/null
        $SUDO diff -ru ${live_root}/ $tmproot | grep -v "^Only in"
        echo "Audit operation finished successfully" >&2
        conf_unlock_wcopy $wcopy_lock
        conf_unlock_system $system_lock
        $SUDO rm -rf $tmproot
    else
        print_usage 1
    fi
}

function recipe_edit {
    local recipe wcopy_lock
    local new_recipe_file recipe_file
    local repeat msg status
    local opt OPTIND OPTARG

    while getopts ":m:F:" opt ; do
        case $opt in
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    if [ -z "$1" ] ; then 
        conf_require_recipe
        recipe=$(conf_get_recipe_name)
    else
        recipe="$1"
    fi

    wcopy_lock=$(conf_lock_wcopy)
    recipe_file=$(conf_recipe_dir)/${recipe}

    if ! [ -f "$recipe_file" ] ; then
        echo "Recipe $recipe does not exist." >&4
        exit 1
    fi

    new_recipe_file=$(conf_tmp_file)

    cat "$recipe_file" > "$new_recipe_file"
    $EDITOR "$new_recipe_file"
    until conf_recipe_verify "$new_recipe_file" ; do
        echo -n "(E)dit / (c)ancel? "
        read repeat
        case "$repeat" in
            [Cc]*)  return 1;; 
            *)      $EDITOR "$new_recipe_file";;
        esac
    done

    cat "$new_recipe_file" > "$recipe_file"

    if status=$(get_status "${recipe_file#${WORK_PATH}/}") ; then
        msg=$(get_log "$status") || return 1
        conf_commit_recipes "$msg" "$recipe" 
    fi
    rm -f "$new_recipe_file"
    conf_unlock_wcopy $wcopy_lock
}

function recipe_create {
    local recipe recipe_file opt OPTIND msg status wcopy_lock
    while getopts ":m:F:R:" opt ; do
        case $opt in
            R)  new_recipe_file="$OPTARG" ;;
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}' for recipe create." >&4
                print_usage 1 
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    eval recipe=$1

    if [ -z "$recipe" ] ; then 
        print_usage 1 
    fi

    wcopy_lock=$(conf_lock_wcopy)
    recipe_file=$(conf_recipe_dir)/${recipe}
    conf_recipe_create "$recipe" || return 1

    if [ -z "$new_recipe_file" ] ; then
        if ! recipe_edit "$recipe" ; then
            conf_rm_file --force "${recipe_file}"
            conf_unlock_wcopy $wcopy_lock
            return 1
        fi
        conf_unlock_wcopy $wcopy_lock
        return 0
    fi

    if conf_recipe_verify "$new_recipe_file" ; then
        cp "$new_recipe_file" "$recipe_file"
        if status=$(get_status "${recipe_file#${WORK_PATH}/}") ; then
            msg=$(get_log "$status") || return 1
            conf_commit_recipes "$msg" "$recipe" 
        fi
        conf_unlock_wcopy $wcopy_lock
        return 0
    else
        conf_rm_file --force "${recipe_file}"
        conf_unlock_wcopy $wcopy_lock
        return 1
    fi
}

function recipe_remove {
    local opt OPTIND OPTARG
    local msg status recipes recipe wcopy_lock
    local nocommit=true
    while getopts ":m:F:" opt ; do
        case $opt in
            m)  LOG_MESSAGE="$OPTARG"; LOG_MESSAGE_SET="true" ;;
            F)  LOG_FILE="$OPTARG" ;;
            *)  echo "Invalid option '-${OPTARG}'." >&4
                print_usage 1
                ;;
        esac
    done
    shift $(($OPTIND - 1))

    wcopy_lock=$(conf_lock_wcopy)

    conf_remove_recipes "$@" || exit 1

    for recipe in "$@" ; do
        recipes="${recipes:+${recipes} }$(conf_recipe_dir)/${recipe}"
    done

    if status=$(get_status $recipes) ; then
        nocommit=false
        msg=$(get_log "$status") || conf_cleanExit
        rm -f "$status"
    fi

    $nocommit || conf_commit_recipes "$msg" "$@"

    conf_unlock_wcopy $wcopy_lock
}

function recipe_get {
    if ! conf_get_recipe_name ; then
        echo "This host's recipe is unset" >&2
        return 1
    fi
}

function recipe_set {
    local wcopy_lock system_lock
    if [ -z "$1" ] ; then print_usage 1 ; fi
    local recipe="$1"
    wcopy_lock=$(conf_lock_wcopy)
    system_lock=$(conf_lock_system)
    if ! [ -f "$(conf_recipe_dir)/${recipe}" ] ; then
        echo "Error: recipe $recipe does not exist. Create it first!" >&2
        exit 1
    fi
    conf_set_recipe "$recipe"
    conf_unlock_system $system_lock
    conf_unlock_wcopy $wcopy_lock
}

function recipe_print {
    local recipe="$1"

    if [ -z "$recipe" ] ; then
        recipe=$(conf_get_recipe_name)
    fi

    if ! [ -f "$(conf_recipe_dir)/${recipe}" ] ; then
        echo "Error: recipe $recipe does not exist." >&2
        exit 1
    fi

    cat "$(conf_recipe_dir)/${recipe}"
}
    
function recipe_list {
    (
        cd $(conf_recipe_dir)
        ls
    )
}

function recipe {
    local subcommand="$1"
    shift
    case "$subcommand" in
        edit)               recipe_edit "$@"    ;;
        create)             recipe_create "$@"  ;;
        remove|rem*|rm)     recipe_remove "$@"  ;;
        get)                recipe_get "$@"     ;;
        set)                recipe_set "$@"     ;;
        print)              recipe_print "$@"   ;;
        list)               recipe_list "$@"    ;;
        *)                  print_usage 1       ;;
    esac
}

function print_version {
    echo "confman-${VERSION}"
}

# Debug mode?
while getopts ":h:dv-" opt ; do
	case $opt in
		d) DEBUG="true" ;;
        h) print_help "$OPTARG" ; conf_cleanExit 0 ;;
        v) print_version ; conf_cleanExit 0 ;;
		*) echo "Invalid options '-${OPTARG}'." >&4
           print_usage 1 
           ;;
	esac
done

# Argument checking
if [ -n "$LOG_FILE" ] && ! [ -r "$LOG_FILE" ] ; then
    echo "Log file $LOG_FILE does not exist or is not readable." >&4
    conf_cleanExit 1
fi

if $LOG_MESSAGE_SET && [ -n "$LOG_FILE" ] ; then
    echo "Cannot specify both a message with -m and a file with -F." >&4
    conf_cleanExit 1
fi

# Dispatch the subcommand. This must happen last and in the global context.
eval subcommand=\$$OPTIND
shift $OPTIND

trap "conf_interrupt_trap" HUP INT QUIT TERM
case $subcommand in
    help )                          print_help "$@" ; conf_cleanExit 0 ;;
    version )                       print_version; conf_cleanExit 0;;
    setup|se* )                     setup "$@" ;;
    status )                        status "$@" ;;
    state )                         state ;;
    create|cr* )                    create "$@" ;;
    update|u* )                     update "$@" ;;
    commit|com* )                   commit "$@" ;;
    import|im* )                    import "$@" ;;
    install|in* )                   inst "$@" ;;
    recipe|rec* )                   recipe "$@" ;;
    remove|rem*|rm )                remove "$@" ;;
    rev* )                          revert "$@" ;;
    ren* )                          rename "$@" ;;
    rmmod )                         rmmod "$@" ;;
    mkdir|mkd* )                    newdir "$@" ;;
    touch )                         cnf_touch "$@" ;;
    list|ls )                       list "$@" ;;
    link|ln )                       mklink "$@" ;;
    lsattr )                        lsattr "$@" ;;
    chattr )                        chattr "$@" ;;
    rmattr )                        rmattr "$@" ;;
    move|mv )                       move "$@" ;;
    copy|cp )                       copy "$@" ;;
    diff )                          cnf_diff "$@" ;;
    log )                           cnf_log "$@" ;;
    chmod )                         chmode "$@" ;;
    chown )                         chowner "$@" ;;
    chcom )                         chcom "$@" ;;
    chgrp )                         chgroup "$@" ;;
    chln )                          chln "$@" ;;
    checklook|checklist|chls|chlk ) checklook "$@" ;;
    checknew|chnw )                 checknew "$@" ;;
    checkclear|chcl|chrm )          checkclear "$@" ;;
    rollback|ro* )                  rollback "$@" ;;
    audit )                         audit "$@" ;;
    * )                             print_usage 1 ;;
esac 

conf_cleanExit 0

# vim:ts=4:ft=sh
