#!/bin/bash
#
# Copyright (c) 2009-2010 Haiku Inc. All rights reserved.
# Distributed under the terms of the MIT License.
#
# Authors:
#		Matt Madia, mattmadia@gmail.com
#
# Synopsis:
#	Provides a controlled mechanism for end-users to install certain pre-built
#	OptionalPackages. The script will determine the host information: the
#	default GCC, availability of secondary GCC libs, and revision. Using this
#	information, the user will be limited to the appropriate OptionalPackages
#	that were available for that specific revision.
#
# Disclaimer:
#   This is a temporary solution for installing OptionalPackages.
#   In time, there will be an official package manager.
#   See these URL's for info on the in-development package manager.
#     http://dev.haiku-os.org/wiki/PackageManagerIdeas
#     http://dev.haiku-os.org/wiki/PackageFormat
#
# Usage: ./installoptionalpackage [-l] [-a "<pkg> [<pkg> ...]"]
# -l      List installable packages
# -a      Add one or more packages and all dependencies


declare -A availablePackages
declare availablePackagesKeys=""
declare wantsToInstall=""
# Some Packages cannot be installed,
# as they require either the source code or compiled binaries
declare packageIgnoreList='Bluetooth Development DevelopmentMin \
DevelopmentBase UserlandFS Welcome WifiFirmwareScriptData'


function CreateInstallerScript()
{
	# This function will create a secondary script, containing all of the
	# information needed to install the optional package and its dependencies

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	cat << EOF > ${tmpDir}/install-optpkg.sh
#!/bin/bash

tmpDir=${tmpDir}
HAIKU_GCC_VERSION[1]=${HAIKU_GCC_VERSION[1]}
isHybridBuild=${isHybridBuild}
TARGET_ARCH=${TARGET_ARCH}
HAIKU_IMAGE_HOST_NAME=`uname -n`
#TODO: possibly add a CLI option to execute InstallSourceArchive
HAIKU_INCLUDE_SOURCES=0
$urlLine
$sslPkgLine
$sslUrlLine
declare -a functionArgs
expanderRulesFile=`finddir B_COMMON_DATA_DIRECTORY`/expander.rules
if [ -f \${expanderRulesFile} ] ; then
	expanderRulesFileExists=1
fi


function ParseFunctionArguments()
{
	# ParseFunctionArguments <args>
	# Parse arguments for Jam wrapper functions into an array.
	IN="\$@"
 	OIFS=\$IFS
 	IFS=":"

 	local count=0
	functionArgs=( )
	for x in \$IN
	do
		functionArgs[\${count}]="\${x}"
		((count++))
	done
 	IFS=\$OIFS
}


function TrimLeadingSpace()
{
	# TrimLeadingSpace <variable name>
	eval local text='\$'"\$1"
	local _outvar="\$1"

 	local length=\${#text}
 	((length--))
 	if [ "\${text:0:1}" == ' ' ] ; then
 		text=\${text#' '}
 	fi

	eval \$_outvar="'\$text'"
}


function TrimEndingSpace()
{
	# TrimEndingSpace <variable name>
	eval local text='\$'"\$1"
	local _outvar="\$1"

 	local length=\${#text}
 	((length--))
 	if [ "\${text:\$length}" == ' ' ] ; then
 		text=\${text%' '}
 	fi

	eval \$_outvar="'\$text'"
}


function Exit()
{
	# Exit <message>
	# Wrapper for Jam rule
	echo "\$@"
	exit 1
}


function InstallOptionalHaikuImagePackage()
{
	# InstallOptionalHaikuImagePackage package : url : dirTokens : isCDPackage
	# Wrapper for Jam rule
	echo "Installing \$1 ..."
	cd \$tmpDir

	archiveFile=\`echo \$3 | sed -s "s/http.*\///"\`
	if ! [ -f \$archiveFile ] ; then
		echo "Downloading \$3 ..."
		wget -nv \$3
	fi

	local dirTokens='/boot'
	local count=4
	local i=0
	for possibleToken in "\$@" ; do
		if [ \$i -lt \$count ] ; then
			((i++))
		else
			((i++))
			if [ "\$possibleToken" != ':' ] ; then
				dirTokens=\${dirTokens}/\$possibleToken
			else
				break
			fi
		fi
	done
	echo "Extracting \$archiveFile ..."
	extractDir="\${dirTokens}"

	case "\$archiveFile" in
		*.zip)
			unzip -q -o -d "\$extractDir" "\$archiveFile"
			;;
		*.tgz|*.tar.gz)
			tar -C "\$extractDir" -xf "\$archiveFile"
			;;
		*)
			echo "Unhandled archive extension in InstallOptionalHaikuImagePackage()"
			exit 1
			;;
	esac

	if [ -f '/boot/.OptionalPackageDescription' ] ; then
		rm '/boot/.OptionalPackageDescription'
	fi
	rm "\$archiveFile"
}


function InstallSourceArchive()
{
	if [ \$HAIKU_INCLUDE_SOURCES -gt 0 ]; then
		echo "InstallSourceArchive is not implemented."
	fi
}


function AddSymlinkToHaikuImage()
{
	# AddSymlinkToHaikuImage <dir tokens> : <link target> [ : <link name> ]
	# Wrapper for Jam rule
 	ParseFunctionArguments "\$@"

 	local dirTokens="/boot/\${functionArgs[0]}"
 	TrimLeadingSpace dirTokens
	TrimEndingSpace dirTokens
 	dirTokens=\${dirTokens//' '/\/}

 	local linkTarget="\${functionArgs[1]}"
 	TrimLeadingSpace linkTarget
	TrimEndingSpace linkTarget

 	local linkName="\${functionArgs[2]}"
 	TrimLeadingSpace linkName
	TrimEndingSpace linkName

	mkdir -p "\${dirTokens}"

	if [ "\${linkName}" == '' ] ; then
		ln -sf "\${linkTarget}" -t "\${dirTokens}"
	else
		ln -sf "\${linkTarget}" "\${dirTokens}/\${linkName}"
	fi
}


function AddUserToHaikuImage()
{
	# AddUserToHaikuImage user : uid : gid : home : shell : realName
	# Wrapper for Jam rule
	ParseFunctionArguments "\$@"

	local user=\${functionArgs[0]}
 	local uid=\${functionArgs[1]}
 	local gid=\${functionArgs[2]}
 	local home=\${functionArgs[3]}
 	local shell=\${functionArgs[4]}
 	local realName=\${functionArgs[5]}

 	passwdLine="\${user}:x:\${uid}:\${gid}:\${realName}:\${home}:\${shell}"
 	passwdLine=\${passwdLine//' :'/':'}
 	passwdLine=\${passwdLine//': '/':'}

 	local length=\${#passwdLine}
 	((length--))
 	if [ "\${passwdLine:\$length}" == ' ' ] ; then
 		passwdLine=\${passwdLine%' '}
 	fi

 	passwdFile="\`finddir B_COMMON_ETC_DIRECTORY\`/passwd"
 	touch \${passwdFile}

 	local userExists=1
 	while read line ; do
		if [ "\${passwdLine}" == "\${line}" ] ; then
			userExists=0
		fi
	done < \${passwdFile}

	if [ \$userExists -ge 1 ] ; then
		echo "\${passwdLine}" >> \${passwdFile}
	fi
}


function AddExpanderRuleToHaikuImage()
{
	# AddExpanderRuleToHaikuImage <mimetype> : <extension> : <list> : <extract>
	# Wrapper for Jam rule
	ParseFunctionArguments "\$@"

	local mimetype=\${functionArgs[0]}
	local extension=\${functionArgs[1]}
	local list=\${functionArgs[2]}
	local extract=\${functionArgs[3]}

	# clean up the variables
	TrimLeadingSpace mimetype
	TrimEndingSpace mimetype
	TrimLeadingSpace extension
	TrimEndingSpace extension
	TrimLeadingSpace list
	TrimEndingSpace list
	TrimLeadingSpace extract
	TrimEndingSpace extract
	local rule_raw="\${mimetype}\\t\${extension}\\t\${list}\\t\${extract}"

	# reset this at every invocation
	ruleFound=

	if [ \${expanderRulesFileExists} ] ; then
		# Check if a rule for the mimetype & extension exists.
		while read line ; do
			existing_rule=`echo \$line | awk '{ print \$1\$2 }'`
			if [ "\${mimetype}\${extension}" == "\${existing_rule}" ] ; then
				ruleFound=1
				break
			fi
		done < "\${expanderRulesFile}"
	fi
	if ! [ \${expanderRulesFileExists} ] || ! [ \${ruleFound} ] ; then
		# Either expander.rules does not exist or a rule for mimetype &
		# extension does not exist. Output the new rule directly to it.
		echo -e \${rule_raw} >> \${expanderRulesFile}
	fi
}


EOF
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	cat ${tmpDir}/optpkg.stage2 >> ${tmpDir}/install-optpkg.sh
	rm ${tmpDir}/optpkg.stage2
}


function ContainsSubstring()
{
	# ContainsSubstring <stringToLookIn> <stringToLookFor>
	local string="$1"
	local substring="$2"
	local newString=${string/${substring}/''}
	if [ ${#string} -eq `expr ${#newString} + ${#substring}` ] ; then
		return 0
	fi
	return 1
}


function ErrorExit()
{
	echo $1
	exit 1
}


function Init()
{

	# Set up some directory paths
	baseDir=`finddir B_COMMON_DATA_DIRECTORY`/optional-packages
	tmpDir=`finddir B_COMMON_TEMP_DIRECTORY`
	libDir=`finddir B_SYSTEM_LIB_DIRECTORY`

	# Make sure these files are empty.
	echo "" > ${tmpDir}/optpkg.jam
	echo "" > ${tmpDir}/optpkg.stage1

	if ! [ -d ${baseDir} ] ; then
		mkdir -p ${baseDir}
	fi

	DetectSystemConfiguration
	DownloadAllBuildFiles
	ReadPackageNamesIntoMemory
}


function DownloadAllBuildFiles()
{
	# DownloadAllBuildFiles
	# Retreive the necessary jam files from svn.
	local buildFiles="OptionalPackages OptionalPackageDependencies \
		OptionalBuildFeatures"
	for file in ${buildFiles} ; do
		GetBuildFile ${file}
	done

}


function GetBuildFile()
{
	# GetBuildFile <file>
	# Downloads files from Haiku's svn
	local buildfile="$1"
	if ! [ -f ${baseDir}/${buildfile} ] ; then
		echo "Fetching ${buildfile} ..."
		cd ${baseDir}
		local baseURL=http://dev.haiku-os.org/export/
		local revision=`uname -v | awk '{print $1}' | sed -e 's/r//'`
		local url="${baseURL}${revision}/haiku/trunk/build/jam/${buildfile}"
		wget -q ${url} || ErrorExit "...failed to download $buildfile"
	fi
}


function DetectSystemConfiguration()
{
	# Determine which GCC we're running and if we're a hybrid
	if [ -d "$libDir"/gcc4 ] ; then
		HAIKU_GCC_VERSION[1]=2
		isHybridBuild=1
	elif [ -d "$libDir"/gcc2 ] ; then
		HAIKU_GCC_VERSION[1]=4
		isHybridBuild=1
	elif [ -f "$libDir"/libsupc++.so ] ; then
		HAIKU_GCC_VERSION[1]=4
		isHybridBuild=""
	else
		HAIKU_GCC_VERSION[1]=2
		isHybridBuild=""
	fi

	# Determine the Architecture.
	if [ `uname -m` == "BePC" ] ; then
		TARGET_ARCH='x86'
	fi
}


function ReadPackageNamesIntoMemory()
{
	local file="${baseDir}/OptionalPackageNames"
	if ! [ -f ${file} ] ; then
		GeneratePackageNames
	fi

	# read list into associative array
	while read line ; do
		local pkg=`echo ${line} | awk '{print $1}'`
		local pkgDeps=${line/"${pkg} :"/}
		availablePackages[${pkg}]="${pkgDeps}"
		availablePackagesKeys="${availablePackagesKeys} ${pkg}"
	done < ${file}
}


function GeneratePackageNames()
{
	# GeneratePackageNames
	# Creates a file containing available package names
	# Each line shows a pakage and all of its recrusive dependencies
	# "<pkg> : <dep1> <dep2> ..."
	echo "Generating a list of Package Names ..."

	local file="${baseDir}/OptionalPackageNames"
	touch ${file}

	local regExp='/^if\ \[\ IsOptionalHaikuImagePackageAdded/p'
	sed -n -e "$regExp" ${baseDir}/OptionalPackages > ${file}.temp
	while read line ; do
		# in each non-filtered line, the 4th word is the optional package
		local pkg=`echo ${line} | awk '{print $4}'`

		nonRepeatingDeps=""
		GetPackageDependencies "$pkg"
		local lowerCasePkg=`echo ${pkg} | tr '[A-Z]' '[a-z]'`
		if IsPackageAndDepsOkToInstall ${pkg} ; then
			echo "${lowerCasePkg} : ${pkg} ${nonRepeatingDeps}"  >> ${file}
		fi

	done < ${file}.temp
	rm ${file}.temp
}


function GetPackageDependencies()
{
	# GetPackageDependencies <pkg>

	# parse OptionalPackageDependencies for the single line that defines
	# this optional package's dependencies.
	local regExp="^OptionalPackageDependencies\ ${1}\ \:"
	local inputFile="${baseDir}/OptionalPackageDependencies"

	# print that single line
	sed -n -e "/${regExp}\ /p" ${inputFile} > ${tmpDir}/optpkg.temp

	# strip out "OptionalPackageDependencies PackageName :"
	# this leaves "<dep1> .... ;"
	tempDeps=`sed -e "s/${regExp}\ //" ${tmpDir}/optpkg.temp`

	for foo in ${tempDeps%' ;'} ; do
		# Prevent duplicate entries of the same dependency package.
		if ! ContainsSubstring "${nonRepeatingDeps} " "${foo} " ; then
			nonRepeatingDeps="$foo $nonRepeatingDeps "
			nonRepeatingDeps="${nonRepeatingDeps//  / }"
		fi
	done

	# Recursively get the dependencies of these dependencies.
	for dep in ${tempDeps%' ;'} ; do
		GetPackageDependencies "$dep"
	done

}


function IsPackageAndDepsOkToInstall()
{
	# IsPackageAndDepsOkToInstall <pkg>
	if ContainsSubstring "${packageIgnoreList}" "${1}"; then
		echo "...warning: ${1} cannot be installed"
		return 1
	fi
	for foo in ${nonRepeatingDeps} ; do
		if ContainsSubstring "${packageIgnoreList}" "${foo}"; then
			echo "...warning: ${1} cannot be installed because of ${foo}"
			return 1
		fi
	done
	return 0
}


function BuildListOfRequestedPackages()
{
	if [ "$1" = '-a' ] || [ "$1" = '-s' ]; then
		shift
	fi
	while [ $# -gt 0 ]; do
		local lowerCase=`echo $1 | tr '[A-Z]' '[a-z]'`
		wantsToInstall="${wantsToInstall} $lowerCase"
		shift
	done
}


function AddPackages()
{
	# AddPackages

	# If one or more packages can be installed, do it.
	if BuildFinalListOfPackagesToInstall ; then

		for package in ${packagesToInstall} ; do
			# output the "if [ IsOptionalHaikuImagePackageAdded..." code block
			local regExp="if\ \[\ IsOptionalHaikuImagePackageAdded\ ${package}"
			local inputFile="${baseDir}/OptionalPackages"
			sed -n "/^$regExp/,/^\}/p" ${inputFile} >> ${tmpDir}/optpkg.jam
		done

		ConvertJamToBash "${tmpDir}/optpkg.jam"
		rm "${tmpDir}/optpkg.jam"
	CreateInstallerScript
	sh ${tmpDir}/install-optpkg.sh
	rm ${tmpDir}/install-optpkg.sh
	fi
}


function BuildFinalListOfPackagesToInstall()
{
	# BuildFinalListOfPackagesToInstall

	packagesToInstall=""
	proceedWithInstallation=false

	for desiredPackage in ${wantsToInstall}; do
		if IsPackageNameValid $desiredPackage  ; then
			for item in ${availablePackages[${desiredPackage}]} ; do
				if ! ContainsSubstring "${packagesToInstall}" "${item}" ; then
					packagesToInstall="${packagesToInstall} ${item}"
				fi
			done
			proceedWithInstallation=true
		fi
	done
	if [ $proceedWithInstallation ] ; then
		echo "To be installed: ${packagesToInstall}"
		return 0
	else
		return 1
	fi
}


function IsPackageNameValid()
{
	# IsPackageNameValid <name>
	for name in ${availablePackagesKeys} ; do
		if [ "$1" == "$name" ] ; then
			return 0
		fi
	done
	return 1
}


function ConvertJamToBash()
{
	# ConvertJamToBash <input file>
	# The main Jam-to-Bash conversion function.
	local inputFile=$1
	declare -a generatedBash
	countGenBashLine=0

	# Parse out some variable declarations
	local regExp='/^HAIKU_OPENSSL_PACKAGE/p'
	sslPkgLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures`
	ConvertVariableDeclarationLines "$regExp" 'sslPkgLine'

	local regExp='/^HAIKU_OPENSSL_URL/p'
	sslUrlLine=`sed -n -e "$regExp" ${baseDir}/OptionalBuildFeatures`
	ConvertVariableDeclarationLines "$regExp" 'sslUrlLine'

	local regExp='/^local\ baseURL/p'
	urlLine=`sed -n -e "$regExp" ${baseDir}/OptionalPackages`
	urlLine=${urlLine/local\ /''}
	ConvertVariableDeclarationLines "$regExp" 'urlLine'

	# Convert the easy bits.
	while read line ; do
		line=${line/'Echo'/'echo'}

		ConvertIfStatements "$line"
		ConvertVariables "$line"
		#ReplaceComparators "$line"

		line=${line/"IsOptionalHaikuImagePackageAdded"/'"SomeText" !='}
		generatedBash[$countGenBashLine]=${line}
		((countGenBashLine++))
	done < ${tmpDir}/optpkg.jam

	# output stage 1 generated code
	local i=0
	while [ $i -lt $countGenBashLine ] ; do
		echo ${generatedBash[$i]} >> ${tmpDir}/optpkg.stage1
		((i++))
	done

	# This converts multi-line jam statements into a single line.
	# --- Start awk ---
	awk '
		/InstallOptionalHaikuImagePackage/,/\;/{
			isRule=1;
			if($0~/\;/) ORS="\n";
			else ORS=" "; print
		}
		/AddSymlinkToHaikuImage/,/\;/{
			isRule=1;
			if($0~/\;/) ORS="\n";
			else ORS=" "; print
		}
		/AddUserToHaikuImage/,/\;/{
			isRule=1;
			if($0~/\;/) ORS="\n";
			else ORS=" "; print
		}
		/AddExpanderRuleToHaikuImage/,/\;/{
			isRule=1;
			if($0~/\;/) ORS="\n";
			else ORS=" "; print
		}
		/Exit/,/\;/{
			isRule=1;
			if($0~/\;/) ORS="\n";
			else ORS=" "; print
		}
		{
			if($1!='InstallOptionalHaikuImagePackage' && isRule!=1 && $1!="\;")
		 	print $0
		}
		{ isRule=0; }
		' ${tmpDir}/optpkg.stage1 > ${tmpDir}/optpkg.stage2 2>/dev/null
	# --- End awk ---
	rm ${tmpDir}/optpkg.stage1
}


function ConvertVariableDeclarationLines()
{
	# ConvertVariableDeclarationLines <regex> <variable>
	# One of the Jam-to-Bash conversion functions.
	# Jam lines that define variables need to be parsed differently.
	eval local input='$'"$2"
	local regex="$1"
	local _outvar="$2"

	input=${input/\ =\ /=}
	input=${input/\;/''}
	input=${input//\(/'{'}
	input=${input//\)/'}'}

	eval $_outvar="'$input'"
}


function ConvertIfStatements()
{
	# ConvertIfStatements <line>
	# One of the Jam-to-Bash conversion functions.
	line=${line//'} else {'/'else '}
	line=${line//'} else if '/'elif '}
	if ContainsSubstring "$line" "if " ; then
		if ! ContainsSubstring "$line" "if [" ; then
			line=${line/'if '/'if [ '}
		fi

		if ContainsSubstring "$line" '] {' ; then
			line=${line/'{'/' ; then'}
		elif ContainsSubstring "$line" '{' ; then
			line=${line/'{'/' ] ; then'}
		fi

		for compound in '&&' '||' ; do
			if ContainsSubstring "$line" "$compound" ; then
				line=${line/"$compound"/"] $compound ["}
			fi
		done
		ReplaceComparators "$line"
	fi
	# Assume all remaining closing braces are part of if statements
	line=${line/'}'/'fi'}
}


function ConvertVariables()
{
	# ConvertVariables
	# One of the Jam-to-Bash conversion functions.

	# NOTE: jam's variables are normally '$(VARIABLE)'. \n
	# 		The issue is with '(' and ')', so let's replace them globally.
	if ContainsSubstring "$line" '$(' ; then
		line=${line//'('/'{'}
		line=${line//')'/'}'}
	fi
}


function ReplaceComparators()
{
	# ReplaceComparators <line>
	# One of the Jam-to-Bash conversion functions.

	# Preserve string comparators for TARGET_ARCH.
	if ! ContainsSubstring "$line" 'TARGET_ARCH' ; then
		line=${line//'>='/'-ge'}
		line=${line//'<='/'-le'}
		line=${line//'>'/'-gt'}
		line=${line//'<'/'-lt'}
		line=${line//'!='/'-ne'}
		line=${line//'='/'-eq'}
	fi
}


function DisplayUsage()
{
	cat << EOF

Disclaimer:
  This is a temporary solution for installing OptionalPackages.
  In time, there will be an official package manager.
  See these URL's for information on the in-development package manager.
    http://dev.haiku-os.org/wiki/PackageManagerIdeas
    http://dev.haiku-os.org/wiki/PackageFormat

Usage: ./installoptionalpackage [<pkg> [<pkg> ...]]
  or   ./installoptionalpackage [-a|-s <pkg> [<pkg> ...]]
  or   ./installoptionalpackage [-f|-h|-l]

Options:
-a     Add one or more packages and all dependencies
-s     Show the final list of packages that would be installed
-f     Remove cached data and list installable packages
-h     Print this help.
-l     List installable packages

EOF
}


function RemoveCachedFiles()
{
	# RemoveCachedFiles
	echo "Removing cached files ..."
	if [ -d ${baseDir} ]; then
		rm -rf ${baseDir}
	fi

	# Unset variables, which prevents duplicate entries.
	declare -A availablePackages
	declare availablePackagesKeys=""

	# Reinitialize
	Init
}


function ListPackages()
{
	# ListPackages
	echo ""
	echo ""
	echo "Available Optional Packages:"

	# single line:
	echo ${availablePackagesKeys}

	# one per line:
	#for package in ${availablePackagesKeys} ; do
	#	echo ${package}
	#done
}


# If no arguments were passed to the script, display its usage and exit.
if [ "$#" -lt 1 ] ; then
	DisplayUsage
	exit 0
else
	Init
fi

# Support `installoptionalpackage <pkg> <pkg> ...`
if [ "$1" != '-f' ] && [ "$1" != '-l' ] && [ "$1" != '-h' ] \
	&& [ "$1" != '-s' ]; then
	BuildListOfRequestedPackages $@
	AddPackages
	exit 0
fi

# Parse the arguments given to the script.
while getopts "as:fhl" opt; do
	case $opt in
		a)
			BuildListOfRequestedPackages $@
			AddPackages
			exit 0
			;;
		f)
			RemoveCachedFiles
			ListPackages
			exit 0
			;;
		h)
			DisplayUsage
			exit 0
			;;
		l)
			ListPackages
			exit 0
			;;
		s)
			BuildListOfRequestedPackages $@
			BuildFinalListOfPackagesToInstall
			exit 0
			;;
		\?)
			echo "Invalid option: -$OPTARG" >&2
			exit 1
			;;
		:)
			echo "Option -$OPTARG requires an argument." >&2
			exit 1
			;;
	esac
done
