# -*-shell-script-*-

# Dependency management code

###
#
# This code 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright 2002-2003 Mike Hearn (mike@navi.cx)
#
###


##
# _forgeDependency <ROOTNAME1> <ROOTNAME2>
# ROOTNAME1: versioned or unversioned package root name.
# ROOTNAME2: unversioned package root name. Function can accept either a versioned or unversioned root name.
#
# Places dependency information in the ROOTNAME1 package's database location.
# Dependencies are stored in a text file - this may change in future.
# ROOTNAME1 will depend on ROOTNAME2.
# <p>
# Places supports information in the unversioned ROOTNAME2 database location.
# Supports are stored in a text file and reference both versioned and
# unversioned root names.
#
function _forgeDependency() {

    trace called with $1, $2
    local root_name2=`justRootName $2`

    if [[ ! -e "${autopackage_db_location}/${1}" ]]; then
        mkdirs "${autopackage_db_location}/${1}"
    fi
    echo "$2" >> "${autopackage_db_location}/${1}/dependencies"
    logCommand --session "removeLine `escapeFilename \"${autopackage_db_location}/${1}/dependencies\"` $2"

    # now link $2 to $1 so we can show the user warnings
    if [[ ! -e "$autopackage_db_location/$root_name2" ]]; then
        mkdirs "$autopackage_db_location/$root_name2"
    fi

    trace writing supports entry
    echo "$1" >> "${autopackage_db_location}/${root_name2}/supports"
    logCommand --session "removeLine `escapeFilename \"${autopackage_db_location}/${root_name2}/supports\"` $1"

}


# removes a dependency from a package. $1 is the package, $2 is the dependency
function _unforgeDependency() {

    local root_name2=`justRootName $2`

    trace called with $1, $2
    # Remove the dependency
    removeLine "$autopackage_db_location/$1/dependencies" "$2"
    # now remove the supports entry
    removeLine "$autopackage_db_location/$root_name2/supports" "$1"
}

# eliminates all links to dependencies of package $1 (usually in preparation for uninstall)
function _unforgeDependencies() {
    trace called with $1
    if [ ! -e "$autopackage_db_location/$1/dependencies" ]; then trace "no dependencies file found"; return 0; fi
    local deps=`cat "$autopackage_db_location/$1/dependencies" | sort | uniq`
    if [[ "$deps" == "" ]]; then return 0; fi;
    local c=$( countStringLines "$deps" )
    local i=1
    while (( i <= c )); do
	local dep=`getLine "$deps" $i`
	_unforgeDependency "$1" "$dep"
	(( i++ ));
    done
}


##
# _locateSkeleton <ROOTNAME>
# ROOTNAME: unversioned package root name
# Returns: 0 on success
# Returns: 1 if the skeleton was not found
# Returns: 2 if found but not an ASCII text file
# Outputs: the absolute path to the skeleton file.
#
# Given a root name, will search through the skeleton path until the highest numbered skeleton for that name is found.
# It will then output the absolute filename.
#
# Example:
# _locateSkeleton "@gnome.org/libxml" --> /usr/local/share/autopackage/skeletons/\@gnome.org/libxml/skeleton.2
function _locateSkeleton() {
    # first things first, locate the highest matching directory for the given root name
    local s
    local r=`justRootName "$1"`
    trace locating $r in "$meta_dir" "$_apspec_dir" "${_skel_dirs[@]}"
    
    for s in "$meta_dir" "$_apspec_dir" "${_skel_dirs[@]}"; do
        [[ "$s" == "" ]] && continue || true
        trace "looking for: $s/$r"
	if [ -e "$s/$r" ]; then
	    # find the highest versioned skeleton
	    local skel=$( ls -1 -B "$s/$r" | sort -g -t. -k2 -r | head -n1 )
	    skel="$s/$r/$skel"
	    trace located at $skel
	    #if ! file "$skel" | awk -F": " ' { print $2 } ' | grep ASCII >/dev/null; then
		#warn "skeleton is not ASCII text?"
		#return 0;
	    #fi
	    echo $skel
	    return 0;
	fi
    done
    return 1;
}


##
# checkForPackage [-i REQUIRED VERSION] [-e] <ROOTNAME> [SKELETON ARGUMENTS]
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# Calls the test section of a skeleton file for the given root name. The results
# are available in the $INTERFACE_VERSIONS and $SOFTWARE_VERSIONS variables. If the
# package wasn't found, these variables are empty, otherwise they contain a space
# delimited list of version numbers that were detected. The ones you're probably
# interested in are the interface numbers, these identify a logical software interface. <p>
#
# If the -i parameter is specified, this function will match against the given interface
# version. It will return 0 on a match, 1 if otherwise. If -i is not specified and the
# rootname is unversioned, the function will always returns 0. Used with require(). <p>
#
# If the -e parameter is specified, this function will match against the given software
# version. It will return 0 on a match, 1 if otherwise. Used with requireExact(). <p>
#
# If the rootname is versioned, this function will match for the given software
# version. Used with requireAtLeast(). <p>
#
# Optional arguments given after the root name are passed to the skeleton test script
# in $@, so can be accessed using the standard $1, $2, $3 etc variables
#
# Example:
# checkForPackage -i 2 "@gnome.org/libxml"     <-- require "@gnome.org/libxml" "2"
# checkForPackage "@gnome.org/libxml:1"        <-- requireAtLeast "@gnome.org/libxml:1"
# checkForPackage -e "@gnome.org/libxml:1.2"   <-- requireExact "@gnome.org/libxml:1.2"
#
function checkForPackage() {
    pushOptE; set +e
    
    trace called with $@
    local root_name
    local required_atleast_version
    local required_exact_version    
    local required_interface
    local INTERFACE_VERSIONS
    local SOFTWARE_VERSIONS

    # Determine mode of operation from arguments and set variables
    if [[ "$1" == "-i" ]]; then
        shift
        required_interface="$1"
        shift
        root_name="$1"
        if versionFromRootName "$root_name" >/dev/null; then
            err The rootname $root_name needs to be unversioned if using the -i argument
            popOptE
            return 1
        fi
        # Normalize the required_interface variable to be A.0 if necessary
        if [[ $( getMinor "$required_interface" ) == "" ]]; then
            required_interface="$required_interface.0"
        fi
    elif [[ "$1" == "-e" ]]; then
        shift
        root_name=`justRootName "$1"`
        if ! required_exact_version=$( versionFromRootName "$1" ); then
            err The rootname $root_name needs to be versioned if using the -e argument
            popOptE
            return 1
        fi
        # Normalize the required_exact_version variable to be A.0 if necessary
        if [[ $( getMinor "$required_exact_version" ) == "" ]]; then
            required_exact_version="$required_exact_version.0"
        fi
    elif required_atleast_version=$( versionFromRootName "$1" ); then
        root_name=`justRootName "$1"`
        # Normalize the required_atleast_version variable to be A.0 if necessary
        if [[ $( getMinor "$required_atleast_version" ) == "" ]]; then
            required_atleast_version="$required_atleast_version.0"
        fi
    else
        root_name="$1"
    fi

    trace root_name=$root_name, interface=$required_interface, exact=$required_exact_version, atleast=$required_atleast_version

    local _skelpath=$( _locateSkeleton "$root_name" )
    trace skeleton path is $_skelpath
    if [[ "$_skelpath" == "" ]]; then
        err "No skeleton found for $root_name"
        popOptE
        return 1
    fi
    local _test=$( getSection "$_skelpath" Test )

    if [[ "$_test" == "" ]]; then
        err "No test section for $root_name?"
        popOptE
        return 1
    fi

    # process the [Test] script
    eval "function t(){
      $_test
    }"
    t;
    unset t

    trace INTERFACE_VERSIONS after executing test script is \"$INTERFACE_VERSIONS\"
    trace SOFTWARE_VERSIONS after executing test script is \"$SOFTWARE_VERSIONS\"
    # check for versions and remove duplicates and countDownVersion if not requiring an exact match
    if [[ "$required_exact_version" != "" ]]; then
        INTERFACE_VERSIONS=$( echo $INTERFACE_VERSIONS | stripDupedItems )
        SOFTWARE_VERSIONS=$( echo $SOFTWARE_VERSIONS | stripDupedItems )
    else
        INTERFACE_VERSIONS=$( countDownVersions $INTERFACE_VERSIONS | stripDupedItems )
        SOFTWARE_VERSIONS=$( countDownVersions $SOFTWARE_VERSIONS | stripDupedItems )
    fi
    trace "INTERFACE_VERSIONS (after stripping)=$INTERFACE_VERSIONS"
    trace "SOFTWARE_VERSIONS (after stripping)=$SOFTWARE_VERSIONS"

    local found
    if [[ "$required_interface" != "" ]]; then
        trace "looking for interface $required_interface"
        found="false"
        for v in $INTERFACE_VERSIONS; do
            [[ "$v" == "$required_interface" ]] && found="true"
            [[ `getMajor $v` == "$required_interface" ]] && found="true"
        done
        trace found=$found
        popOptE
        $found && return 0
        return 1
    elif [[ "$required_atleast_version" != "" ]]; then
        trace "looking for at least $required_atleast_version"
        found="false"
        for v in $SOFTWARE_VERSIONS; do
            [[ "$v" == "$required_atleast_version" ]] && found="true"
            [[ `getMajor $v` == "$required_atleast_version" ]] && found="true"
        done
        trace found=$found
        popOptE
        $found && return 0
        return 1
    elif [[ "$required_exact_version" != "" ]]; then
        trace "looking for exactly $required_exact_version"
        for v in $SOFTWARE_VERSIONS; do
            if [[ "$v" == "$required_exact_version" ]]; then
                popOptE
                return 0
            fi
        done
        popOptE
        return 1
    else
        popOptE
        return 0
    fi

}


##
# _retrieveLocalPackage <DIRECTORY> <ROOTNAME> <REQUIRED VERSION>
# DIRECTORY: file system location to search for ROOTNAME.
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# Returns: 0 on success.
# Returns: 1 on failure.
#
# Retrieve the package ROOTNAME from DIRECTORY. Similar to retrieve(), but only
# for local files.
function _retrieveLocalPackage() {

    local f;
    local r;
    # directory not found so return package not found
    [ ! -d "$1" ] && return 1

    local oIFS="$IFS"
    IFS=$'\n'

    local best_candidate_pv
    local best_candidate_if
    local best_candidate

    local _rn=$2
    trace DIRECTORY=$1, ROOTNAME=$_rn, VER=$3

    # Scan all .package files in DIRECTORY.
    for f in `ls -1 "$1" | grep '\.package$'`; do
	trace scanning $f

	# Verify that this really is an Autopackage
	if ! head -n 6 "$1/$f" | grep -q '# autopackage '; then
		continue
	fi

	# grab and set header
	local header=`head -n 7 "$1/$f"`

	# Verify package matches machine cpu architecture
	local c=$( echo "$header" | grep CPUArchitecture | sed 's/# CPUArchitecture //' )
	trace package cpuarchitecture is \"$c\"
	if [[ "$cpu_architecture" != "$c" ]]; then
		continue
	fi

	r=$( echo "$header" | grep RootName | sed 's/# RootName //' )
	r=$( justRootName "$r" ) # strip any version info
	trace root name is \"$r\"

	if [[ "$_rn" == "$r" ]]; then
	    # ok, the root name matches. did we specify a versioned rootname?
	    trace .. matched
	    if [[ $( versionFromRootName "$_rn" ) != "" ]]; then
		# yes, so we want to match exactly, as only the given version is good enough
		local v=$( echo "$header" | grep SoftwareVersion | sed 's/# SoftwareVersion //' )
		if [[ "$v" == $( versionFromRootName "$rn" ) ]]; then
		    # found so let's process it
		    echo "$1/$f"
		    trace "... and found [ $1/$f ]"
		    IFS="$oIFS"
		    return 0
		fi
		trace but the version was not correct
	    else
		# we need to match against the interface version

		# normalise $3 into A.B form
		if [[ `getMinor $3` == "" ]]; then
			requiredver="$3.0"
		else
			requiredver="$3"
		fi

		trace we need $requiredver
		local if_version=$( echo "$header" | grep InterfaceVersion | sed 's/# InterfaceVersion //' )
		trace if_version=${if_version}
		if_version=$( countDownVersions $if_version )
		trace interfaces this package fulfils is $if_version

		local v
		IFS=" "
		for v in $if_version; do
		    trace v is \"$v\"
		    if [[ "$v" == "$requiredver" ]]; then

			trace matched against \"$1/$f\"
			local pv=$( echo "$header" | grep 'PackageVersion' | sed 's/# PackageVersion //' )
			if [[ "$pv" == "" ]]; then pv=1; fi
			
			if [[ "$best_candidate" == "" ]] || compareVersionsSimple $v $best_candidate_if || (( pv > best_candidate_pv )); then
			    best_candidate="$1/$f"
			    best_candidate_if="$v"
			    best_candidate_pv="$pv"
			    trace best candidate is now $best_candidate / $best_candidate_if / $best_candidate_pv
			fi
		    fi
		done
		IFS=$'\n'
	    fi
	fi
    done

    if [[ "$best_candidate" ]]; then
	trace chosen package was $best_candidate / $best_candidate_if / $best_candidate_pv
	chmod +x "$best_candidate" || {	err "Could not set execute permission on $best_candidate"; return 1; }
	
	# what is the versioned root name of this package?
	export RETRIEVAL_VERSIONED_ROOTNAME=$( head -n 7 "$best_candidate" | grep "RootName" | sed 's/# RootName //' )
	trace versioned rootname is $RETRIEVAL_VERSIONED_ROOTNAME

	IFS="$oIFS"

	# delay extract it (this will extract the .package file into the working dir, but not start the install)
	trace delay extracting, executing: \"$best_candidate\" -w \"$working_dir\" --delay
	"$best_candidate" -w "$working_dir" --delay
	return $?
    fi

    # Package not found
    warn package not found locally
    IFS="$oIFS"
    return 1
}

##
# _retrieveFromNet <ROOTNAME> [REQUIRED VERSION]
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package if ROOTNAME is unversioned
#
# Attempts to locate a package from the network, and stores the metadata locally.
# Returns 0 if successful, 1 if there was a failure retrieving the metadata, 2 if the subprep failed
function _retrieveFromNet() {
    trace called with $@

    #
    # We need to call into luau-downloader with the following pieces of information:
    #   * package description XML file URL
    #   * If $1 is unversioned, take the interface version from $2, otherwise get the version and download that


    # api usage checks
    if [[ "$1" == "" ]]; then
	err must be called with at least one argument
	return 1
    fi

    versionFromRootName $1 >/dev/null;
    if [[ $? == 1 ]] && [[ "$2" == "" ]]; then
	err must provide an interface version when the ROOTNAME argument is unversioned
	return 1
    fi

    if [ ! -e "$working_dir/meta/`justRootName $1`/repositories" ]; then
	warn no repositories were registered for $1
	return 1
    fi

    local repositories=$( cat "$working_dir/meta/`justRootName $1`/repositories" )

    for repository in $repositories; do
	    
        # get the metadata
	local before
	local after

	trace trying $repository
	before=$( date +%s ) # gnu extension

        
        	
	local v
	v=$( versionFromRootName $1 )
	if [[ $? == 1 ]]; then
        
            trace `which luau-downloader` -P "/dev/zero" -m -u "$repository" -i $2 -o "$working_dir/download.meta"
            
	    if ! luau-downloader -P "/dev/zero" -m -u "$repository" -i $2 -o "$working_dir/download.meta"; then
		warn $1 not found in $repository
		continue;
	    fi
	elif [[ $? == 0 ]]; then

            trace `which luau-downloader`  -P "/dev/zero" -m -u "$repository" --version $2 -o "$working_dir/download.meta"
        
	    if ! luau-downloader -P "/dev/zero" -m -u "$repository" --version $2 -o "$working_dir/download.meta"; then
		warn $1 not found in $repository
		continue;
	    fi
	fi
	after=$( date +%s )
	trace "$[ after - before ] seconds to get metadata"

	[ ! -d "$working_dir/holding" ] && mkdir "$working_dir/holding"
	tar xzf "$working_dir/download.meta" -C "$working_dir/holding" || { err "Could not extract metadata to holding area"; return 1; }
	rm "$working_dir/download.meta" || { err "Could not delete temporary meta download file"; return 1; }

	export RETRIEVAL_VERSIONED_ROOTNAME=$( _loadPackageEnvironment "$working_dir/holding"; echo $ROOTNAME; )
        
	[ ! -d `dirname "$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME"` ] && mkdir `dirname "$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME"`
	mv "$working_dir/holding" "$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME" || { err "Could not moving holding area to $working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME"; return 1; }

        # put the repository location in the metadata of the package about to be downloaded. we
        # can't access the skeleton again later, because meta_dir is changed so stuff works properly
        # in the install script.
	echo "$repository" >"$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME/repository-location"

	trace "download done"
	return 0
    done

    warn "no packages were found that matched the given criteria"
    warn "the following repositories were tried:"
    local r
    for r in $repositories; do
	warn $r
    done

    return 1
}

##
# _retrievePayloadFromNet <ROOTNAME>
# ROOTNAME: versioned or unversioned package root name.
#
# Downloads the actual payload.
function _retrievePayloadFromNet() {
    trace called with $@

    local v # unversioned rootname
    v=$( versionFromRootName $1 )

    local before
    local after
    before=$( date +%s )
    mkdir -p "$working_dir/payload/$1/"

    # we need to get the repository location from the metadata
    local r
    trace meta_dir=$meta_dir
    r=$( cat "$meta_dir/repository-location" )
    if [[ "$r" == "" ]]; then
	err "repository-location is empty"
	return 1
    fi

    trace luau-downloader -P "$autopackage_pipe" -u "$r" -v $v -o "$working_dir/payload/$1/payload.tar.bz2"

    if ! luau-downloader -P "$autopackage_pipe" -u "$r" -v $v -o "$working_dir/payload/$1/payload.tar.bz2"; then
	# this should be an outputFail
	outputFail "Download of payload for $1 failed - check your internet connection?"
	return 1
    fi

    after=$( date +%s )
    trace "$[ after - before ] seconds to get payload"
    return 0
}

##
# retrieve [--install] <ROOTNAME> [REQUIRED VERSION]
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# Returns: 0 on success.
# Returns: 1 if the package could not be retrieved, 2 if it was retrieved but prepare/install failed
#
# Will attempt to locate and optionally install a package either from the local computer or a remote
# network given either a versioned rootname (for example @foo.org/bar:6.4b) or an unversioned root
# name with an interface number. <p>
#
# Default behaviour is to only download the downloaded and extract the package, unless the --install
# option is specified in which case the package will also be installed.  <p>
#
# Note that when invoked from inside other installers, the package will
# be prepared, but not fully installed. Instead, the payload will be registered, and
# later all the install sections of the packages that were prepared will be run at once. <p>
#
# This function is primarily used by require(), however it's available for your own
# package scripts as well. <p>
#
# If you specify a versioned root name (should not contain a release number) then
# you should not give an interface number, if you do it will be ignored. <p>
#
# If you specify an unversioned root name, then the system will select the best
# package that satisfies the given interface. For example, requesting an interface
# 1.2 will allow a match against any package that advertises fulfilment of interface
# 1.2, 1.3, 1.4, 1.5 and so on (but not 0.x nor 2.x) <p>
#
# Example:
# retrieve @foo.org/bar 2.2   --> will install any package that can fulfil interface 2.2
# retrieve @bar.org/foo:2003  --> will attempt to install Foo 2003, no other version will do
function retrieve() {
    # Search packages in the same directory that the package is in
    local packageDir=$( dirname "$package_filename" )
    local install=false
    if [[ "$1" == "--install" ]]; then
	install=true
	shift
    fi

    # check we have been given the correct arguments
    if [[ $( justRootName $1 ) == "$1" ]] && [[ "$2" == "" ]]; then
	err "retrieve must be given an interface version in the case of an unversioned rootname"
	return 1;
    fi

    local res

    trace calling _retrieveLocalPackage with $@
    _retrieveLocalPackage "$packageDir" $@
    res=$?
    
    if [[ $res == 2 ]]; then
	return 1
    elif [[ $res == 1 ]]; then
	# If that didn't work, search packages in the sealed installer directory
	# that the package was started with
	
	trace searching sealed installer directory $meta_dir/packages
	_retrieveLocalPackage "$meta_dir/packages" "$@"
	res=$?
        if [[ $res == 2 ]]; then
	    return 1
        elif [[ $res == 1 ]]; then
		# If that didn't work, search packages in the working directory
		# that the package was started with
	
		trace searching $executed_from_directory
		_retrieveLocalPackage "$executed_from_directory" "$@"
		res=$?

		if [[ $res == 2 ]]; then
		    return 1
		elif [[ $res == 1 ]]; then
		    # we could not find it locally. we need to scan the network for potential matches
	
		    trace calling _retrieveFromNet with $@
		    _retrieveFromNet "$@"
		    res=$?
	    
		    if [[ $res != 0 ]]; then
			warn no package found
			return 1
		    fi
	    
		fi
	fi
    fi

    # we might want to stop here
    if ! $install; then return 0; fi

    if [[ "$working_dir" != "" ]]; then

	if [[ "$RETRIEVAL_VERSIONED_ROOTNAME" == "" ]]; then
	    err "RETRIEVAL_VERSIONED_ROOTNAME is unset"
	    return 1
	fi
    
	if ! "$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME/apkg-installer"; then
	    warn "installer for $RETRIEVAL_VERSIONED_ROOTNAME failed to prepare"
	    return 1
	fi

        # that script will not have actually run the [Install] section, it's delayed.
	# FIXME: should this code be here, or in the prepare script for the subpackage itself?
	_registerPayload "$RETRIEVAL_VERSIONED_ROOTNAME" # ... so register it for later installation, assuming we have a working dir (ie are being run from an installer)
    fi

    return 0
}


# _addSkeletonFile <ROOTNAME>
# ROOTNAME: unversioned package root name. Function can accept either a versioned or unversioned root name.
#
# Places the given skeleton file into the database, and places shortname symlinks
# this is used for verifying packages later.
#
function _addSkeletonFile() {

    local root_name=`justRootName "$1"`
    local short_name
    local skeleton_dirs_backup
    local skeleton_file

    # i really need to write a stack for bash
    skeleton_dirs_backup="$skel_dirs"
    skeldirs="$autopackage_db_location"

    skeleton_file=`_locateSkeleton "${root_name}"`
    copyFiles --silent --nolog --nobackup "$skeleton_file" "$autopackage_db_location/${root_name}"

    skeldirs="$skeleton_dirs_backup";

}


# registers a root name for later installation, part of building the "installation tree"
function _registerPayload() {
    if [[ "$1" == "" ]]; then
	err must have at least one argument
	return 1
    fi
    trace "Registering $1 as a payload in $meta_dir/payloads"
    echo "$1" >>"$meta_dir/payloads"
    echo "$1" >>"$working_dir/global-payloads" # so we can figure out how many packages we'll be installing later
    return 0
}


##
# _requireCommons <REQUIRE MODE> <ROOTNAME> [REQUIRED VERSION] [SKELETON ARGUMENTS]
# REQUIRE MODE: mode to use for this require call. Argument can be require, requireAtLeast, or requireExact.
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# Example:
# _requireCommons require "@gnome.org/libxml" 1
# _requireCommons requireAtLeast "@gnome.org/libxml:1" --with-xslt
# _requireCommons requireExact "@gnome.org/libxml:1.2" --with-xslt
# _requireCommons recommend "@gnome.org/libxml" 1
# _requireCommons recommendAtLeast "@gnome.org/libxml:1" --with-xslt
# _requireCommons recommendExact "@gnome.org/libxml:1.2" --with-xslt
#
function _requireCommons() {
    pushOptE; set +e;
    trace context is $_context

    if [[ "$_context" != "prep" ]] && [[ "$_context" != "verify" ]]; then
        err "require cannot be called outside of prep or verify context"
        return 1
    fi

    local verify_command
    local checkForPackage_arguments
    local required_version
    local mode="$1"; shift
    local required_name="$1"; shift

    if [[ "$mode" == "require" ]] || [[ "$mode" == "recommend" ]]; then
        required_version="$1"; shift
    fi

    trace mode=$mode, required_name=$required_name, required_version=$required_version

    # check - have we already prepared this package? if so then a previous call to require failed and sorted it out so we can skip this one
    if [ -e "$working_dir/prepared-rootnames" ] && grep "$required_name" "$working_dir/prepared-rootnames" >/dev/null; then
        trace $required_name is prepared already ... skipping dependency check
        popOptE
        return 0
    fi

    local skelfile=`_locateSkeleton "$required_name"`
    if [[ "$skelfile" == "" ]]; then
        err could not locate required skeleton file in package metadata for \"$required_name\"
        return 1
    fi
    export r_displayname=`getSectionKey "$skelfile" "Meta" "DisplayName"`
    local found="n"

    # output the fact that we are doing a dependency check
    outputTest "$r_displayname"

    # if verify context then do not write/create any verification files
    if [[ $_context != "verify" ]]; then
        # record how require was invoked, so we can rerun it in verify later
        verify_command="$mode \"$required_name\" $required_version $@"
        echo "$verify_command" >> "$meta_dir/verification"
    fi

    if [[ "$mode" == "require" ]] || [[ "$mode" == "recommend" ]]; then
        checkForPackage -i "$required_version" "$required_name" "$@"
    elif [[ "$mode" == "requireExact" ]] || [[ "$mode" == "recommendExact" ]]; then
        checkForPackage -e "$required_name" "$@"
    elif [[ "$mode" == "requireAtLeast" ]] || [[ "$mode" == "recommendAtLeast" ]]; then
        checkForPackage "$required_name" "$@"
    else
        assertNotReached
    fi

    if [[ "$?" == "0" ]]; then
        outputTestPass;
        # have we already got the skeleton info
        if [[ ! -e "$autopackage_global_db_location/$required_name" ]] && [[ ! -e "$autopackage_user_db_location/$required_name" ]]; then
            _addSkeletonFile "$required_name"
            _forgeDependency "$ROOTNAME" "$required_name" # this will make a useless supports file, but oh well, FIXME later
        fi
        popOptE
        return 0
    else
        # if we fail, then call retrieve (ie fail for now)
        outputTestSearching
 
        # at this point, attempt to locate and install the dependency.
        # this can be from the net, or from the same directory (or from the net cache)

	# if we have a Repository key, register it
        trace checking for Repository key
	local skelrepo=$( getSection "$skelfile" Meta | getKey - "Repository" )
	if [[ "$skelrepo" != "" ]]; then
	    registerRepository $( justRootName $required_name ) $skelrepo
	fi

        # get the [retrieval] section of the skeleton, if there is one
        retrieval=$( getSection "$skelfile" Retrieval )
        if [[ "$retrieval" == "" ]]; then
            retrieval="retrieve $required_name $required_version" # default entry
            trace "using default retrieval command: $retrieval"
        else
            retrieval=$( getSection "$skelfile" Retrieval )
        fi

	# so we can use return...
        function __retrieval() { eval "$retrieval"; return $?; }

        # retrieval of package exports RETRIEVAL_VERSIONED_ROOTNAME variable
        if __retrieval; then
	    outputTestPass

	    if [[ "$RETRIEVAL_VERSIONED_ROOTNAME" == "" ]]; then
		err RETRIEVAL_VERSIONED_ROOTNAME is unset
		popOptE
		return 1
	    fi

	    # prepare the package
	    trace about to run \"$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME/apkg-installer\" as subprep
	    if ! "$working_dir/meta/$RETRIEVAL_VERSIONED_ROOTNAME/apkg-installer"; then
		warn "subprep for $RETRIEVAL_VERSIONED_ROOTNAME / $required_name failed"
		
		unset RETRIEVAL_VERSIONED_ROOTNAME
		unset REPOSITORY_LOCATION
		unset __retrieval
		popOptE
		return 1
	    else
		_registerPayload "$RETRIEVAL_VERSIONED_ROOTNAME"
	    
		_addSkeletonFile "$required_name"
		_forgeDependency "$ROOTNAME" "$RETRIEVAL_VERSIONED_ROOTNAME"
		
		unset RETRIEVAL_VERSIONED_ROOTNAME
		unset REPOSITORY_LOCATION
		unset __retrieval

                identify "$DISPLAYNAME" "$_cur_series_pos" "prep"

		popOptE
		return 0
	    fi
        else
            local retres=$?
            unset REPOSITORY_LOCATION
            unset RETRIEVAL_VERSIONED_ROOTNAME
            unset __retrieval
    
            # recommend* modes do not output failure message
            if [[ "$mode" == "recommend" ]] || [[ "$mode" == "recommendAtLeast" ]] || [[ "$mode" == "recommendExact" ]]; then
                outputTestRecommend
                # I don't think we care about short names here
                # r_shortname=`getSectionKey "$skelfile" "Meta" "ShortName"`
                [[ $_context != "verify" ]] && echo "$r_displayname" >> "$working_dir/failed-recommends"
                # have we already got the skeleton info
                if [[ ! -e "$autopackage_global_db_location/$required_name" ]] && [[ ! -e "$autopackage_user_db_location/$required_name" ]]; then
                    _addSkeletonFile "$required_name"
                    _forgeDependency "$ROOTNAME" "$required_name"
                fi
                popOptE
                return 0
            # require* modes output failure message 
            else
                r_shortname=`getSectionKey "$skelfile" "Meta" "ShortName"`
                outputTestFail
                identify "$DISPLAYNAME" "$_cur_series_pos" "prep"
                local msg=$( out "$intl_PACKAGE_COULD_NOT_FIND" "$r_displayname" "$r_shortname" )
                outputFail "$msg"

                popOptE
                return 1
            fi

        fi
    fi
}


##
# require <ROOTNAME> <REQUIRED VERSION> [SKELETON ARGUMENTS]
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# Require allows you to depend upon an implementation of a particular interface being present on the system.
# The interface you wish to depend on is specified by a combination of the root name and the major.minor code.
# The root name should not have a version number in it - it identifies a particular skeleton to use, not a package.
# The best package to implement that interface will automatically be located and retrieved if it's not found,
# assuming the skeleton supports that.
# <p>
# Optional arguments can be added to the end in any format, it's up to the skeleton file to figure out what to do with
# them, so refer to the notes for that skeleton.
# <p>
# This function can only be called from inside prepare scripts. Calling it from any other context is an error.
#
# Example:
# require "@gnome.org/libxml" 1
# require "@gnome.org/libxml" 1.2 --with-xslt
#
function require() {

    if [[ $1 == "" ]] || [[ $2 == "" ]]; then
        err "require needs at least two arguments."
        return 1
    fi

    _requireCommons "require" $@
    return $?
}


##
# requireAtLeast <ROOTNAME> [SKELETON ARGUMENTS]
# ROOTNAME: versioned package root name.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# requireAtLeast allows you to depend upon an implementation of a specific piece of software being
# present on the system. It works with the SOFTWARE_VERSIONS output of the skeleton file.
# <p>
#
# The root name should have a version number in it - any version equal
# to or higher than the version in the root name will satisfy this
# function.  <p>
#
# See require() for more information.
#
# Example:
# requireAtLeast "@gnome.org/libxml:1"
# requireAtLeast "@gnome.org/libxml:1.2" --with-xslt
function requireAtLeast() {

    if [[ `versionFromRootName $1` == "" ]]; then
        err "requireAtLeast needs the first argument to be a versioned rootname."
        return 1
    fi

    _requireCommons "requireAtLeast" "$@"
    return $?
}


##
# requireExact <ROOTNAME> [SKELETON ARGUMENTS]
# ROOTNAME: versioned package root name.
# SKELETON ARGUMENTS: option arguments that are passed to the package skeleton.
#
# This function will check for the version specified in the root name and will fail if that exact
# version is not found on the system. For instance, if you require version 1.2 exactly, and version
# 1.1 or 1.3 won't do, you can use this function. <p>
#
# See require() for more information.
#
# Example:
# requireExact "@gnome.org/libxml:1"
# requireExact "@gnome.org/libxml:1.2"
#
function requireExact() {

    if [[ `versionFromRootName $1` == "" ]]; then
        err "requireExact needs the first argument to be a versioned rootname."
        return 1
    fi

    _requireCommons "requireExact" "$@"
    return $?
}

##
# recommend <ROOTNAME> <INTERFACE-VERSION> [SKELETON ARGUMENTS]
# ROOTNAME: versioned or unversioned package root name.
# REQUIRED VERSION: MAJOR[.MINOR] formatted numbers to validate requirements on ROOTNAME's package.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# Recommend allows you to check if an interface is present, and if not inform the user that this
# package would have enhanced functionality if that interface was present. It functions virtually
# identically to require() except that if the check fails, it will still return 0 (success). Rather
# than abort the script therefore, it will simply display the dependency as "recommended" in the UI.
# <p>
# For more information see require()
# 
# Example:
# recommend "@gnome.org/libxml" 2
function recommend() {

    if [[ $1 == "" ]] || [[ $2 == "" ]]; then
        err "recommend needs at least two arguments."
        return 1
    fi

    _requireCommons "recommend" $@
    return $?

}

##
# recommendExact <ROOTNAME> [SKELETON ARGUMENTS]
# ROOTNAME: versioned package root name.
# SKELETON ARGUMENTS: option arguments that are passed to the package skeleton.
#
# See the documentation for requireExact() and recommend() to understand this function
function recommendExact() {

    if [[ `versionFromRootName $1` == "" ]]; then
        err "recommendExact needs the first argument to be a versioned rootname."
        return 1
    fi

    _requireCommons "recommendExact" "$@"
    return $?

}

##
# recommendAtLeast <ROOTNAME> [SKELETON ARGUMENTS]
# ROOTNAME: versioned package root name.
# SKELETON ARGUMENTS: optional arguments that are passed to the package skeleton.
#
# See recommend() and requireAtLeast() to understand this function.
function recommendAtLeast() {

    if [[ `versionFromRootName $1` == "" ]]; then
        err "recommendAtLeast needs the first argument to be a versioned rootname."
        return 1
    fi

    _requireCommons "recommendAtLeast" $@
    return $?

}



##
# requireFile <FILE-PATH>
# FILE-PATH: An absolute path to the file that is required
#
# This function should be called from prep scripts when you depend on a file
# existing in a well known location. Examples of where this is useful would be
# for scripts that have an absolute path in their shebang line.
# <p>
# This function outputs the test, and returns 1 if the file is not present. Otherwise it returns 0.
# This function will NOT attempt to resolve dependenices if the test fails, so be careful in its use.
#
# Example:
# requireFile /bin/bash
# requireFile /lib/libm.so.6 
function requireFile() {
    pushOptE; set +e;
    outputTest "existance of $1"
    if [ -e "$1" ]; then
       outputTestPass;
       popOptE;
       return 0;
    else
       outputTestFail;
       popOptE;
       return 1;
    fi
}

##
# registerRepository <ROOTNAME> <URL>
# ROOTNAME: the unversioned root name of the package you wish to registry a repository for
# URL: the absolute URL of the XML repository description. In autopackage 1.0, this must be an HTTP URL
#
# Each package (identified by an unversioned root name) has a list of remote repositories that
# should be attempted in order, in the case that the package is not present and must be retrieved
# from the network. <p>
#
# A repository registration is scoped local to this autopackage session, ie once the
# install/removal/upgrade has finished, the registrations are lost. <p>
#
# When a package must be retrieved from the network, each repository in the list is tried in order
# from first registered to last registered. The first that provides a successful match is used. <p>
#
# If a skeleton includes a "Repository" key in its meta section, then this URL will registered just
# before the Retrieval section is run, so you will usually not need to use this function.  It's
# useful when for whatever reason you feel compelled to break the Golden Rule and have packaged the
# dependencies of your package yourself. Placing the URL to your collection in the skeleton file
# would be the wrong approach as the skeleton is supposed to be independent of any one persons
# particular packaging. Instead, you should register the dependencies in your packages prepare
# script so your repository will be tried first.
#
# Example:
# registerRepository @foo.org/bar http://myproject.sourceforge.net/myproject-packages.xml
# registerRepository @fee.org/fifofum http://myproject.sourceforge.net/myproject-packages.xml
function registerRepository() {
    trace "called with: $1, $2"

    # check arguments
    if [[ "$1" == "" ]] || [[ "$2" == "" ]]; then
        err must provide at least two arguments
        return 1
    fi
    
    if [ ! -d "$working_dir/meta/$1" ]; then
        trace "making directory $working_dir/meta/$1"
        mkdir -p "$working_dir/meta/$1/"
        # allow a non-root user to delete a possible root generated temporary file
        #chmod 777 "$working_dir/meta/$1/"
    fi
    
    echo $2 >>"$working_dir/meta/$1/repositories"
    # allow a non-root user to delete a possible root generated temporary file
    #chmod 777 "$working_dir/meta/$1/repositories"

}
