#!/usr/bin/env bash

# set exit on error and tracing
# see https://gist.github.com/wolfeidau/6761104#file-pi-build-deb-sh-L3-L4
set -e
set -o errtrace

# initialise settings
BASHINATE_VERSION=0.7.1

# initialise and resolve BASHINATE_HOME
BASHINATE_HOME=$(readlink -f ${BASHINATE_HOME-~/.bashinate})

# initialise the JSON_PARSER
JSON_PARSER=$BASHINATE_HOME/JSON.sh

# initialise the os name and version
# at this stage only ubuntu is supported
OS_NAME=ubuntu
OS_VERSION=$(lsb_release --release | sed -E s/[^0-9\.]//g)

# define the versions hash
declare -A APP_VERSIONS

# define the bashinate permissions hash
declare -A BASHINATE_PERMISSIONS

# initialise locations
if [ ! $BASHINATE_SOURCE ]; then
	BASHINATE_SOURCE=$BASHINATE_HOME/src
fi

# initialise remote location
if [ ! $BASHINATE_REMOTE ]; then
	BASHINATE_REMOTE=https://bitbucket.org/DamonOehlman/bashinate/raw/master
fi

# initialise elements remote and local
SCRIPTLETS_REMOTE=$BASHINATE_REMOTE/scriptlets
SCRIPTLETS_LOCAL=$BASHINATE_HOME/scriptlets

# initialise templates remote and local
TEMPLATES_REMOTE=$BASHINATE_REMOTE/templates
TEMPLATES_LOCAL=$BASHINATE_HOME/templates

# determine where we are running as root (rememeber 0 == true in bash)
[[ -w "/root" ]] && IS_ROOT=0

# initialise the install base (if not previously specified)
if [ ! $BASHINATE_INSTALL ]; then
	if [ $IS_ROOT ]; then
		BASHINATE_INSTALL=/opt/local
	else
		BASHINATE_INSTALL=$BASHINATE_HOME/install
	fi
fi

# initialise the target bin folder
if [ ! $BASHINATE_BIN ]; then
	if [ $IS_ROOT ]; then
		BASHINATE_BIN=/usr/local/bin
	else
		BASHINATE_BIN=$BASHINATE_HOME/bin
	fi
fi

# general utility functions

# indexof test array
# return the 0-based index of the test item within the array
indexof() {
	local target=$1; shift
	local ii=0

	while [ ! -z "$1" ] && [ "$target" != "$1" ]; do
		((ii++)); shift
	done

	([[ "$target" == "$1" ]] && echo $ii) || echo -1
}

# colors
DEFAULT_FGCOLOR=white
DEFAULT_BGCOLOR=none

colorize() {
	local colors=( black red green yellow blue magenta cyan white none )
	local fgidx=$(indexof ${1-$DEFAULT_FGCOLOR} ${colors[@]})
	local bgidx=$(indexof ${2-$DEFAULT_BGCOLOR} ${colors[@]})

	if [ $fgidx -lt 0 ]; then
		fgidx=$(indexof $DEFAULT_FGCOLOR ${colors[@]})
	fi

	if [ $bgidx -lt 0 ]; then
		bgidx=$(indexof $DEFAULT_BGCOLOR ${colors[@]})
	fi

	# rebase the fg index and bg index correctly
	((fgidx += 30)); ((bgidx += 40))

	echo "\E[$fgidx;${bgidx}m"
}

acceptVersion() {
	local package=$1
	local version=$2
	local currentVersion=${APP_VERSIONS[$package]}

	if [ $version ] && [ ! $currentVersion ]; then
		APP_VERSIONS[$package]=$version
	fi
}

# ## abort msg
# 
# The abort function is used to exit the script with an error code and report 
# the error on the console.
abort() {
	echo $1; exit 1
}

# ## bootstrap
#
# Let's ensure that we've got what we need to be able to run bashinate
bootstrap() {

	# ensure we have an ~/.bashinate scriptlets directory to download into
	mkdir -p $BASHINATE_SOURCE $BASHINATE_INSTALL $BASHINATE_BIN

	# ensure we have json.sh available
	if [ ! -e $JSON_PARSER ]; then
		JSON_PARSER=$(fetch JSON.sh https://raw.github.com/dominictarr/JSON.sh/master/JSON.sh)
		chmod a+x $JSON_PARSER
	fi

	if [ ! $BASHINATE_USE ]; then
		if [ $IS_ROOT ]; then
			log "- running as root"
		fi

		log "- bootstrapped bashinate (running on Ubuntu $UBUNTU_RELEASE)"
		log "- install base is: $BASHINATE_INSTALL"
	fi
}

# ## buildFromSource
#
# A typical build from source process
# 
# - download source tar.gz
# - extract file
# - configure
# - make and make install
buildFromSource() {
	local PACKAGE_NAME=$1
	local PACKAGE_VERSION=${APP_VERSIONS[$PACKAGE_NAME]}
	local PACKAGE_SOURCE=`echo $2 | sed -e "s#%%VERSION%%#$PACKAGE_VERSION#g"`

	# check that a node version is specified
	[[ ! $PACKAGE_VERSION ]] && abort "$PACKAGE_NAME version not set"

	hash g++ 2> /dev/null || require packages build-essential

	# if we don't have the latest node version, then install
	log "Checking $PACKAGE_NAME version $PACKAGE_VERSION installed";
	if [ ! -e $BASHINATE_INSTALL/$PACKAGE_NAME/$PACKAGE_VERSION ]
	then
	  log "Installing $PACKAGE_NAME version $PACKAGE_VERSION"

	  fetchSource $PACKAGE_SOURCE
	  cd $BASHINATE_SOURCE/$PACKAGE_NAME-$PACKAGE_VERSION

	  # configure 
	  log "- configuring"
	  ./configure --prefix=$BASHINATE_INSTALL/$PACKAGE_NAME/$PACKAGE_VERSION ||
	    abort "!!! Unable to configure $PACKAGE_NAME build"

	  # make and install
	  log "- compiling and installing"
	  make && make install ||
	    abort "!!! Unable to make $PACKAGE_VERSION $PACKAGE_VERSION"
	fi
}

# ## detectPackageInstaller
# 
# Used to detect the appropriate command (plus appproriate arguments) for the
# installation of system packages
detectPackageInstaller() {
	local pkgmgr=`which apt-get` # start with apt

	# check for rhel
	if [ -f /etc/redhat-release ]; then
		pkgmgr=`which yum`
	fi

	echo "sudo $pkgmgr -y install"
}

# ## fetch resource
# 
# Attempt to dowload the scriptlet from the remote repository into the local 
fetch() {
	# initialise the location of local scriptlet path
	local el=$BASHINATE_HOME/$1
	local remote=${2-$BASHINATE_REMOTE/$1}

	if [ ! -f $el ]; then
		log "fetching ${remote}" 1>&2

		# make sure the target path exists
		mkdir -p $(dirname $el)

		# TODO: check cache first: http://blog.yjl.im/2012/03/downloading-only-when-modified-using.html
		wget -q --spider $remote && wget -q $remote -O - > $el
	fi

	# return the element name
	echo $el
}

# ## fetchSource
# 
# Get the package source from the specified url, Once the source has been 
# downloaded, unpack it using the appropriate decompressor
# 
# TODO: support things other than tar
fetchSource() {
	local filename=`echo $1 | sed -E 's/^.*\/(.*)$/\1/'`
	[[ ! -z "$3" ]] && filename=$3

	log "downloading $1"

	# ensure the source directory exists
	mkdir -p $BASHINATE_SOURCE

	# get the source
	if [ ! -f $BASHINATE_SOURCE/$filename ]; then
		wget --quiet $1 -O $BASHINATE_SOURCE/$filename || abort "!!! Unable to download source: $1"
	fi

	# unpack the source
	case $2 in
		unzip)
			unzip -qo $BASHINATE_SOURCE/$filename -d $BASHINATE_SOURCE
			;;

		*)
			tar xf $BASHINATE_SOURCE/$filename --directory $BASHINATE_SOURCE
	esac
}

log() {
	echo -en "$(colorize yellow)#>" 1>&2;
	tput sgr 0 1>&2;
	echo " $@" 1>&2;
}

# ## includePath
# This function is used to add the requested path to the global $PATH
# variable.  The function should also seek to remove any previously
# set values of path for the specified bashinate installed tool from
# the path
includePath() {
	# local pathMembers
	# local appParts
	# local basePath

	# # split into arrays
	# IFS=':' read -a pathMembers <<< $PATH
	# IFS='/' read -a appParts <<< $1

	# # create the basePath for the currently specified app
	# basePath="$BASHINATE_INSTALL/$appParts[0]"

	# log "adding to PATH: $1"
	# appParts=( ${pathMembers[@]} $BASHINATE_INSTALL/$1 )
	# echo $(printf ":%s" ${appParts[@]})
 	log "adding to PATH: $1"

 	# add to the path variable for internal usage
 	export PATH=$PATH:$BASHINATE_INSTALL/$1

 	# echo the path for usage in .profile commands
 	# TODO: detect usage mode and only echo in cases where it's useful...
	echo $BASHINATE_INSTALL/$1
}

linkBinaries() {
	# grab the basepath from the args
	local basePath=$1; shift

	if [ $BASHINATE_LINK ] || [ $BASHINATE_USE ]; then
		# if the basepath does not exist, then abort
		if [ ! -d $BASHINATE_INSTALL/$basePath ]; then
			abort "Could not link binaries in $BASHINATE_INSTALL/$basePath"
		fi

		log "linking $basePath ( $@ )"
	  for binary in $@; do
	    ln -fs $BASHINATE_INSTALL/$basePath/$binary $BASHINATE_BIN/$binary
	  done
	fi
}

listVersions() {
	ls $BASHINATE_INSTALL/$1 | sort -V
}

# ## parseLine
# 
# This function is used to parse a line from a bashinate specification script
# and take action on that line.
parseLine() {
	# initialise the line, stripping comments
	local line=`echo $@ | sed -E 's/^\s*\#.*$//'`

	if [ "$line" != "" ]; then
		require $line
	fi
}

# ## require scriptlet *args
#
# The require function is used to include a particular element in the provisioning process.  For instance,
# if you wanted to orchestrate the installation of node then you would use a statement similar to the one
# below:
#
# ```bash
# require node
# ```
require() {
	# get the name of the scriptlet
	local scriptlet=$1
	shift

	# change directory back to the $BASHINATE_HOME directory
	cd $BASHINATE_HOME

	# first look in an scriptlets directory relative to the current working directory
	local el=$SCRIPTLETS_LOCAL/${scriptlet}

	# if we don't have a cwd element override, fetch the scriptlet
	[[ -f $el ]] || el=$(fetch scriptlets/${scriptlet})

	# if we don't have the required element, then abort
	if [[ ! -f $el ]]; then abort "Unable to require \"${scriptlet}\" scriptlet"; fi

	# include the source file
	source $el $@
}

# return 0 if the target is installed, 1 if not
targetInstalled() {
	local testTarget=$BASHINATE_INSTALL/$1

	if [ $2 ]; then
		testTarget=$testTarget/$2
	fi

	if [ -d $testTarget ]; then
		echo 1
	fi
}

userExists() {
	echo $(id -u $1 2>/dev/null)
}

# determine the platform appropriate package manager
# PACKAGE_INSTALLER=$(detectPackageManager)

bootstrap || abort "Could not bootstrap bashinate"

# if we have arguments and the first argument is not a file, then process
if [ ! -f $1 ]; then
	case $1 in
		update)
			# if in a .git repo abort, we don't want to hurt ourselves
			if [ -d .git ]; then abort "Attempting update in git managed folder, danger"; fi

			wget $BASHINATE_REMOTE/bashinate -O $(pwd)/bashinate
			exit 0
			;;

		clean)
			# if we bashinate home is a git repo, then bail
			if [ -d $BASHINATE_HOME/.git ]; then abort "Cannot clean in a git managed environment"; fi

			log "removing cached scriptlets"
			rm -rf $SCRIPTLETS_LOCAL
			rm -rf $TEMPLATES_LOCAL
			exit 0
			;;

		# list versions of requested software
		ls)
			shift; listVersions $@; exit 0
			;;

		# install is basically the same as default
		install)
			shift; parseLine $@; exit 0
			;;

		use)
			# set use mode, but only if the specified package and version exist
			shift; BASHINATE_USE=$(targetInstalled $@)

			if [ ! $BASHINATE_USE ]; then
				log "$@ not detected, running install"
			fi

			# run the specified script
			parseLine $@
			exit 0
			;;

		--version)
			echo "v$BASHINATE_VERSION"
			exit 0
			;;

		*)
			parseLine $@
			exit
	esac
fi

# if we have been provided a scriptlet then run it
while read line
do
	parseLine ${line}
done < "${1:-/proc/${$}/fd/0}"

# parse the last line
parseLine ${line}