#!/bin/zsh

setopt no_unset warn_create_global extendedglob

PS4='+%B%* %F{cyan}%N%f:%F{yellow}%i%f>%b '

zparseopts -D \
	h=ARGS_H -help=ARGS_H \
	x=ARGS_X -trace=ARGS_X \
	c:=ARGS_C -config:=ARGS_C \
	b:=ARGS_B -basedir:=ARGS_B \
	-continue=ARGS_CONTINUE \
	-pretend=ARGS_PRETEND \


if (($#ARGS_H)); then cat <<END
usage: ${0:t} [options] [target]

options:
	-h --help           this help
	-x --trace          enable debugging output
	-c --config <file>  source a configuration file
	-b --basedir <dir>  set this directory as basedir
	   --continue       don't erase existing build directories
	   --pretend        only show what build steps would be taken

description:
	Flexible Stage Builder
END
	exit 0
fi

(($#ARGS_X)) && set -x

for option_flag option_value in $ARGS_B; do
	FSB_BASE_DIR:=$option_value
done

FSB_CONFIG=${FSB_BASE_DIR:-${0:a:h:h}}/fsbrc
[[ -a $FSB_CONFIG ]] && source $FSB_CONFIG

for option_flag option_value in $ARGS_C; do
	source $option_value || exit 1
	: ${FSB_TARGET_NAME:=${option_value:t}}
done

(($#@)) && FSB_TARGET=$1

# parameter defaults

: ${FSB_SNAPSHOTS:=btrfs}
: ${FSB_BASE_DIR:=${0:a:h:h}}
: ${FSB_BUILD_DIR:=$FSB_BASE_DIR/build}
: ${FSB_FUNCTIONS_DIR:=$FSB_BASE_DIR/functions}
: ${FSB_DOWNLOADS_DIR:=$FSB_BASE_DIR/downloads}
: ${FSB_SCRIPTS_DIR:=$FSB_BASE_DIR/scripts}
: ${FSB_TEMPLATE_DIR:=$FSB_BASE_DIR/template}
: ${FSB_GENTOO_MIRROR:=ftp://ftp.fi.muni.cz/pub/linux/gentoo/}
: ${FSB_OVERLAY_BASE:=/home/ccx/bzr}
: ${FSB_PORTAGE_SNAPSHOTS:=/mnt/data/snapshots/portage}

# basic initialisation

path=( $FSB_SCRIPTS_DIR $path )

if ! [[ $terminfo[colors] -ge 8 ]]; then
	FSB_NO_COLORS=1
fi

if ! (($+commands[cowsay])); then
	FSB_NO_COWSAY=1
fi

setopt extendedglob warncreateglobal

# error reporting

if (($+FSB_NO_COWSAY)); then
	if (($+FSB_NO_COLORS)); then
		fsb_info()  { print -r '[I]' "$@" }
		fsb_warn()  { print -r '[W]' "$@" }
		fsb_error() { print -r '[E]' "$@" }
	else
		autoload colors
		colors
		fsb_info()  { print -r $fg[green]\*$reset_color "$@" }
		fsb_warn()  { print -r $fg[yellow]\*$reset_color "$@" }
		fsb_error() { print -r $fg[red]\*$reset_color "$@" }
	fi
else
	if (($+FSB_NO_COLORS)); then
		fsb_info()  { print -r - "$@" | cowsay -n}
		fsb_warn()  { print -r - "$@" | cowsay -w -W $COLUMNS }
		fsb_error() { print -r - "$@" | cowsay -d -W $COLUMNS }
	else
		autoload colors
		colors
		fsb_say() {
			local color=$1
			shift
			cowsay "$@" | sed "1{s/^/${fg[${color}]}/};/^ ---* \$/s/\$/${reset_color}/"
		}
		fsb_info()  { print -r - "$@" | fsb_say green  -n }
		fsb_warn()  { print -r - "$@" | fsb_say yellow -w -W $COLUMNS }
		fsb_error() { print -r - "$@" | fsb_say red    -d -W $COLUMNS }
	fi
fi

die() {
	(($#)) && fsb_error "$@"
	(($+build_dir)) && print -r 'Interrupted in: '$build_dir
	exit 1
}

# function autoloading

fpath+=$FSB_FUNCTIONS_DIR
typeset -gA fsb_targets

load_functions() {
	local ppath='' name func
	local -a match mbegin mend
	for func in $^fpath/fsb_*(N) ; do
		if ! [[ -r "$func" ]]; then
			fsb_warn "$func is not readable"
			continue
		fi
		autoload -Uz $func:t
		if [[ $func = */fsb_target_(#b)(*) ]]; then
			# remember all targets
			typeset -g "fsb_targets[${match[1]}]"=$func:t
		elif [[ $func = */fsb_stage_(#b)(*)_${FSB_SNAPSHOTS} ]]; then
			# alias stage functions according to snapshot method
			functions[fsb_stage_${match[1]}]=$func:t' "$@"'
		fi
	done
}

# build functions
typeset -g build_dir build_dir_base
typeset -ga build_stack

target_info() {
	local -a info_vars
	info_vars=(
		FSB_INFO_BASE # base build directory name passed up to calling stage
		FSB_INFO_DIR # directory for building this stage
		# FSB_INFO_KEEP # whether to keep build directory around
		FSB_INFO_SKIP # skip calling build stage of this script
		FSB_INFO_RESUME # if this stage can be resumed
		FSB_INFO_DEPEND # build this stage first and then clone it's directory
	)
	typeset -g $info_vars
	FSB_INFO_DEPEND='' # sanitize
	FSB_INFO_DIR='%s'
	FSB_INFO_SKIP=0
	FSB_INFO_RESUME=0

	(($+fsb_targets[$1])) || die "no such target: ${(q)1}"
	$fsb_targets[$1] info || die "$1 info failed"

	local param info_name
	info_name=TARGET_INFO_${1#fsb_target_}
	local -A info
	for param in $info_vars; do
		# [[ -n ${(P)param} ]] && \
		typeset "info[${param#FSB_INFO_}]"=${(P)param}
		unset $param
	done
	info[FUNC]=$1

	if (($info[SKIP])); then
		target_info $info[DEPEND]
		return
	fi

	build_stack+=$info_name

	if [[ $info[BASE] == *'%s'* ]]; then
		[[ -n $info[DEPEND] ]] || die "target $1 does use %s in BASE and does not depend on another target"
		target_info $info[DEPEND]
		build_dir_base=${${info[BASE]}//\%s/${build_dir_base}}
		info[DIR]=${${info[DIR]}//\%s/${build_dir_base}}
	else
		build_dir_base=${info[BASE]}
		if [[ $info[DIR] == *'%s'* ]]; then
			info[DIR]=${${info[DIR]}//\%s/${build_dir_base}}
		fi
		build_dir_check ${info[DIR]} ${info[FUNC]}
		local ret=$?
		if (( $ret == 100 )) ; then
			if [[ -n $info[DEPEND] ]]; then
				target_info $info[DEPEND] || die "target_info on ${(q)info[DEPEND]} failed"
			fi
		elif (( $ret )) ; then
			die "build_dir_check failed"
		fi
		build_dir_base=${info[BASE]}
	fi
	local k v
	typeset -gA ${info_name}
	for k v in "${(kv@)info}"; do
		typeset -g "${info_name}[${k}]"=$v
	done
}

target_stack_build() {
	local -A info
	local k v mark
	for k v in "${(kvP@)build_stack[${1:-1}]}"; do
		typeset "info[$k]"=$v
	done

	build_dir=$info[DIR]
	build_dir_check $info[DIR] $info[FUNC] && return 0
	[[ $? -eq 100 ]] || die "build_dir_check failed"

	if [[ -d $build_dir ]]; then
		fsb_info "resuming build in $build_dir"
	elif [[ -n $info[DEPEND] ]]; then
		target_stack_build $(( ${1:-1} + 1 ))
		fsb_stage_clone $build_dir $info[DIR]
	else
		fsb_stage_init $info[DIR]
	fi
	build_dir=$info[DIR]
	${fsb_targets[${info[FUNC]}]} build || die "${info[FUNC]} build failed"
	mark=${info[FUNC]#fsb_target_}
	grep -qFe $mark $build_dir/.fsb_done || print -r - $mark >> $build_dir/.fsb_done
}

build_target() {
	((${+fsb_targets[${1}]})) || die "no such target: $1"
	target_info $1
	if (($#ARGS_PRETEND)); then
		local n k v
		for n in $(seq $#build_stack -1 1); do
			printf "%d) %s:\n"  $[ $#build_stack + 1 - $n ] $build_stack[$n]
			for k v in "${(kvP@)build_stack[${n}]}"; do
				printf "   %s=%s\n" $k ${(qqq)v}
			done
		done
		exit 0
	fi
	target_stack_build
}
# typeset -ga build_dir_stack
typeset -g build_dir build_dir_done

build_dir_check() {
	# check whether there's stage ready inside a directory
	# $1 is directory, $2 is target name
	local name=${2#fsb_target_}
	[[ -a $1 ]] || return 100  # does not exist, proceed building
	[[ -d $1 ]] || die "$1 is not a directory, required by target $2"
	if [[ -f $1/.fsb_done ]] && grep -qFe $name $1/.fsb_done; then
		return 0  # target already completed in this directory
	fi
	# scrap incomplete build
	(($#ARGS_CONTINUE)) || fsb_stage_remove $1
	return 100
}

# build_dir_push() {
# 	[[ -n $build_dir ]] && build_dir_stack+=$build_dir
# 	build_dir=$1
# 	[[ -d $1:h ]] || mkdir -p $1:h || die "could not mkdir $1:h for stage $1:t"
# 	# [[ -d $1 ]] || fsb_stage_init $1 || die "failed to create stage directory: $1"
# }

# build_target() {
# 	(($+fsb_targets[${1}])) || die "no such target: $1"
# 	$fsb_targets[${1}] || die "build of target $1 failed"
# 	[[ -d $build_dir ]] || die "'$build_dir' is not valid stage directory, target $1 failed"
# 	grep -qFe $1 $build_dir/.fsb_done || print -r - $1 >> $build_dir/.fsb_done
# 	if (( $#build_dir_stack )); then
# 		fsb_stage_clone $build_dir $build_dir_stack[-1]
# 		# pop from stack
# 		build_dir=$build_dir_stack[-1]
# 		build_dir_stack=( $build_dir_stack[1,-2] )
# 	fi
# }

# run!
load_functions
if [[ -z $FSB_TARGET ]]; then
	fsb_info "usage: $0:h <target name>"$'\ntargets:\n'${(kF)fsb_targets/#/ - }
	exit
fi
mkdir -p $FSB_BUILD_DIR
build_target ${1:-$FSB_TARGET}
fsb_info "build completed into: $build_dir"
if [[ -d $build_dir:h:h/done/$build_dir:t ]]; then
	btrfs sub del $build_dir:h:h/done/$build_dir:t || \
		die "could not remove done snapshot"
fi
btrfs sub snap $build_dir $build_dir:h:h/done/$build_dir:t
if (($+FSB_TARGET_NAME)); then
	local done_link
	done_link=$build_dir:h:h/done/$FSB_TARGET_NAME
	if [[ -e $done_link ]]; then
		rm $done_link || die "could create done-link ${(q)done_link}: already present"
	fi
	ln -s $build_dir:t $done_link || die "could create done-link ${(q)done_link}"
	tar -czpf ${done_link:a}.tar.gz.new -C $done_link . || die "could not create tarball"
	if [[ -e $FSB_BASE_DIR/signify/key.sec ]]; then
		signify -Sz -s $FSB_BASE_DIR/signify/key.sec -m ${done_link:a}.tar.gz.new -x ${done_link:a}.tar.gz.signed || die
		rm ${done_link:a}.tar.gz.new
		mv ${done_link:a}.tar.gz.signed ${done_link:a}.tar.gz || die
	else
		mv ${done_link:a}.tar.gz.new ${done_link:a}.tar.gz || die
	fi
	fsb_info "target available in: $done_link and ${done_link:a}.tar.gz"
fi