# vim: ft=zsh noet ts=4 sts=4 sw=4

#
# confz functions for file hierarchies and fileset miniformat
#

zmodload -F zsh/stat b:zstat
zmodload -m -F zsh/files b:zf_\*

typeset -g -A fileset_stat_cache fileset_stat_cur fileset_ftypes fileset_typenames
typeset -g fileset_stat_cur fileset_stat_next_id fileset_stat_cur_type fileset_stat_cur_perm fileset_stat_cur_major fileset_stat_cur_minor

fileset_ftypes=(  # convert hex type to a word
	C  S  # unix socket
	c  S
	A  L  # symlink, could also be 'h'
	a  L
	8  f  # plain file
	6  b  # block device file
	4  d  # directory
	2  c  # character device file
	1  p  # FIFO pipe
)

fileset_typenames=(
	S "unix socket"
	L "symbolic link"
	f "plain file"
	b "block device"
	c "character device"
	p "named pipe"
)

fileset_reset_cache(){
	# omit unsetting used variables for now
	# probably no particular gain in freeing them
	fileset_stat_cache=()
	fileset_stat_cur=()
	fileset_stat_next_id=1
}
fileset_reset_cache

# runs zstat on $1 if not already in cache
# the result is available as $fileset_stat_cur associative array
fileset_stat() {
	local id ret ftype fperm
	id=${fileset_stat_cache[$1]:-missing}
	if [[ $id == missing ]]; then
		typeset -gA fileset_stat_$fileset_stat_next_id 
		zstat -L -H fileset_stat_$fileset_stat_next_id $1 &>/dev/null
		ret=$?
		if (($ret == 0)); then
			fileset_stat_cache[$1]=$fileset_stat_next_id
			fileset_stat_set_cur $fileset_stat_next_id
			fileset_stat_next_id=$[ $fileset_stat_next_id + 1 ]
		else
			fileset_stat_cache[$1]=-$ret
		fi
		return $ret
	elif (( $id >= 0 )); then
		fileset_stat_set_cur $id
		return 0
	else
		return $[ -($id) ]
	fi
}

fileset_stat_set_cur(){
	local param ftype
	param=fileset_stat_$1
	fileset_stat_cur=( "${(@kvP)param}" )
	ftype=$[ [##16] $fileset_stat_cur[mode] >> 12 ]
	fileset_stat_cur_type=$fileset_ftypes[$ftype]
	fileset_stat_cur_perm=$[ [##8] $fileset_stat_cur[mode] & 4095 ]
	fileset_stat_cur_major=$[ $fileset_stat_cur[rdev] >> 8 ]
	fileset_stat_cur_minor=$[ $fileset_stat_cur[rdev] & 255 ]
}

fileset_resetcmd() {
	fileset_reset_cache
	"$@"
}

confz_fs_p_check() {
	checkvars filename
	local parent
	parent=${${vars[filename]}:h}
	do_command=( fileset_resetcmd mkdir -p $parent )
	fileset_stat $parent && [[ $fileset_stat_cur_type == d ]]
}


confz_fs_type_or_missing_check() {
	checkvars filename flags filetype
	if fileset_stat $vars[filename]; then
		if [[ $fileset_stat_cur_type == $vars[filetype] ]]; then
			return 0
		elif [[ $vars[flags] == *'!'* ]]; then
			fail_reason="expected ${fileset_typenames[${vars[filetype]}]}, got ${fileset_typenames[$fileset_stat_cur_type]}: ${(qqq)vars[filename]}"
			return 1
		else
			die "$0: expected ${fileset_typenames[${vars[filetype]}]}, got ${fileset_typenames[$fileset_stat_cur_type]}: ${(qqq)vars[filename]}"
		fi
	else
		return 0
	fi
}

confz_fs_type_or_missing_do() {
	local is_dir
	if fileset_stat $vars[filename] && [[ $fileset_stat_cur_type == d ]]; then
		is_dir=1
	else
		is_dir=0
	fi

	fileset_reset_cache
	if [[ $vars[flags] == *r* ]]; then
		if [[ $vars[flags] == *f* ]]; then
			zf_rm -rf $vars[filename]
		else
			zf_rm -r $vars[filename]
		fi
	elif (($is_dir)); then
		zf_rmdir $vars[filename]
	else
		if [[ $vars[flags] == *f* ]]; then
			zf_rm -f $vars[filename]
		else
			zf_rm $vars[filename]
		fi
	fi
}


confz_fs_l_check() {
	checkvars filename destination
	defvar flags ''

	if [[ $vars[flags] == *p* ]]; then
		require fs_p :filename
	fi

	require fs_type_or_missing :filename :flags filetype=L

	fileset_stat $vars[filename] && \
		[[ $fileset_stat_cur_type == L ]] && \
		[[ $fileset_stat_cur[link] == $vars[destination] ]]
}

confz_fs_l_do() {
	local missing
	fileset_stat $vars[filename]
	missing=$?
	fileset_reset_cache
	if ! (($missing)); then
		rm $vars[filename] || return $?
	fi
	zf_ln -sh $vars[destination] $vars[filename]
}


confz_fs_device_check() {
	checkvars filename device_type major minor
	defvar flags ''

	if [[ $vars[flags] == *p* ]]; then
		require fs_p :filename
	fi

	case $vars[device_type] in
		(c) require fs_type_or_missing :filename :flags filetype=c ;;
		(b) require fs_type_or_missing :filename :flags filetype=b ;;
		(*) die "Incorrect device type: ${(qqq)vars[device_type]}" ;;
	esac

	fileset_stat $vars[filename] && \
		[[ $fileset_stat_cur_type == $vars[device_type] ]] && \
		[[ $fileset_stat_cur_major == $vars[major] ]] && \
		[[ $fileset_stat_cur_minor == $vars[minor] ]]
}

confz_fs_device_do() {
	local missing
	fileset_stat $vars[filename]
	missing=$?
	fileset_reset_cache
	if ! (($missing)); then
		rm $vars[filename] || return $?
	fi
	mknod $vars[filename] $vars[device_type] $vars[major] $vars[minor]
}


confz_fs_pipe_check() {
	checkvars filename
	defvar flags ''

	if [[ $vars[flags] == *p* ]]; then
		require fs_p :filename
	fi

	require fs_type_or_missing :filename :flags filetype=p

	do_command=( fileset_resetcmd mkfifo $vars[filename] )
	fileset_stat $vars[filename] && [[ $fileset_stat_cur_type == p ]]
}


confz_fs_r_check() {
	checkvars filename
	defvar flags ''
	do_command=( confz_fs_type_or_missing_do )
	! fileset_stat $vars[filename]
}


confz_fs_m_check() {
	checkvars filename mode
	do_command=( fileset_resetcmd chmod $vars[mode] $vars[filename] )
	fileset_stat $vars[filename] && (( $fileset_stat_cur_perm == $vars[mode] ))
}


confz_fs_o_check() {
	checkvars filename owner
	do_command=( fileset_resetcmd zf_chown -h $vars[owner] $vars[filename] )
	fileset_stat $vars[filename] || \
		die "fs_o: could not access file ${(qqq)vars[filename]}"

	if [[ $vars[owner] =~ '^[0-9]+:[0-9]+$' ]]; then
		(( $fileset_stat_cur[uid] == ${${vars[owner]}%:*} && \
			$fileset_stat_cur[gid] == ${${vars[owner]}#*:} ))
	elif [[ $vars[owner] =~ '^[0-9]+$' ]]; then
		(( $fileset_stat_cur[uid] == ${${vars[owner]}%:*} ))
	else
		die "fs_o does not support non-numeric user/group: ${(qqq)vars[owner]}"
	fi
}


confz_fs_f_check() {
	checkvars filename
	defvar flags ''

	if [[ $vars[flags] == *p* ]]; then
		require fs_p :filename
	fi

	require fs_type_or_missing :filename :flags filetype=f

	fileset_stat $vars[filename] && [[ $fileset_stat_cur_type == f ]]
}

confz_fs_f_do() {
	fileset_reset_cache
	printf '' >> $vars[filename]
}


confz_fs_d_check() {
	checkvars filename
	defvar flags ''

	require fs_type_or_missing :filename :flags filetype=d

	if [[ $vars[flags] == *p* ]]; then
		do_command=( fileset_resetcmd mkdir -p $vars[filename] )
	else
		do_command=( fileset_resetcmd mkdir $vars[filename] )
	fi

	fileset_stat $vars[filename] && [[ $fileset_stat_cur_type == d ]]
}


confz_fs_c_check() {
	checkvars filename content_call
	defvar flags ''
	require fs_f :filename :flags
	"${(Q@)${(z)vars[content_call]}}" | cmp -s - $vars[filename]
}

confz_fs_c_do() {
	"${(Q@)${(z)vars[content_call]}}" > $vars[filename]
}

confz_fs_contentnl_check() {
	checkvars filename content
	defvar flags ''
	require fs_f :filename :flags
	printf '%s\n' "$vars[content]" | cmp -s - $vars[filename]
}
confz_fs_contentnl_do() {
	printf '%s\n' "$vars[content]" > $vars[filename]
}