#!/bin/bash

#  The rcd_autogen script scans source files of the target project and generates
#  source files needed to implement extended rcode functionality in the target
#  project.
#  The files generated by rcd_autogen have to be linked with the target project,
#  and their contents comes from template files distributed along with the rcode
#  project.
#  Therefore, all the files generated by rcd_autogen are licensed under the same
#  terms as the rcode project.
#
#  This file is part of the rcode project.
#
#  Copyright (C) 2019-2020 Tomasz Pawlak
#  e-mail: tomasz.pawlak@wp.eu
#
declare -r version='1.3' # 2019.12.30
#
#  The rcd_autogen script 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 3 of the License, or (at your
#  option) any later version.

#  The rcd_autogen script 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 the rcd_autogen script; see the file COPYING. If not,
#  see <http://www.gnu.org/licenses/>.



#check bash version
if (( ${BASH_VERSINFO[0]}*10+${BASH_VERSINFO[1]} < 44 ));then
	echo "[E!] rcd_autogen: required bash v4.4.0 or better"; exit 1
fi

#------------------| CONFIG |----------------
shopt -s expand_aliases
shopt -u checkwinsize #don't check terminal size

#keywords from rcode.h:
#PP keywords used to expand RCD macros in target files
declare -r kwd_autogen='-D_USE_RCD_AUTOGEN -D_RCD_AUTOGEN'
#rcode magic kwd used to recognize tokens in PP mode.
declare -r kwd_magic='_RCD_MAGIC_'
#rcode kwd for unit-id definitions
declare -r kwd_unit='UNIT'
#rcode kwd for messages with source line numbers
declare -r kwd_msg='LN_N'
#field separator
declare -r kwd_ifs='%RCD%'

#global vars
declare    tmps       #temp var
declare    rcode_dir  #returncode directory
declare    root_dir   #base dir: all other dirs are relative to root_dir, except tmp dir.
declare    tmp_dir    #dir for temp files
declare    out_dir    #dir for output files
declare    include_dir  #relative dir for including rcode.h
declare    arg_scan_tgt #arg: list of dirs/files to scan or a path/to/file.$arg_fext
declare    lang       #language: C/CPP
declare    src_fext   #src file extension: .c/.cpp
declare    pproc      #lang PP (e.g. 'gcc')
declare    pp_args    #PP args
declare    base_name  #base name for output files
declare    out_fn_hdr  #output rcd fn header file
declare    out_fn_src  #output rcd fn source file
declare    out_scp_hdr #output scope header file
declare    out_scp_src #output scope source file
declare    out_scp_ptr_hdr #output scope pointer header file
declare    rcdgen_md  #full/basic/dummy
declare -i max_jobs   #num of parallel jobs
declare -i verbose    #verbose mode
declare -i dbg_md     #debug mode

#defaults
rcode_dir=$(dirname $(realpath -s $0))
tmp_dir='rcd_temp'
rcdgen_md='full'
max_jobs=0
verb_md=0
dbg_md=0

#global arrays
declare -a target_file_ar
declare -a msg_struct_ar
declare -a log_file_ar

#constants
declare -r autogen_hdr='/* File generated with rcd_autogen v'$version' */'
declare -r out_prefix='rcd'
declare -r out_fn_prefix='func'
declare -r out_scp_prefix='scope'
declare -r out_scp_hdr_suffix='rcd_scope_ptr.h'
declare -r arg_fext='.rcdgen_cfg'
declare -r tmp_fext='.rcdgen_tmp'
declare -r tgt_fext='.rcdgen_target'
declare -r log_fext='.rcdgen_log'
declare -r end_list='__end__list__'
#template files
declare -r tmpl_func_hdr='rcd_fn.tmp.h'
declare -r tmpl_func='rcd_fn.tmp.c'
declare -r tmpl_func_bm='rcd_fn_bm.tmp.c'
declare -r tmpl_scp_hdr='rcd_scp.tmp.h'
declare -r tmpl_scp_fn='rcd_scp.tmp.c'
declare -r tmpl_scp_fn_dm='rcd_scp_dm.tmp.c'
declare -r tmpl_getscp_hdr='rcd_scp_ptr.tmp.h'
#units
declare    tmp_scp_file="scope_struct$tmp_fext"

#---<| Helper Functions |>---

F_CALL_STACK() {
	local -i depth=0
	local -i lvl=${#FUNCNAME[@]}
	echo -n "[Call stack]"
	while (( lvl > 1 )); do #lvl=0 -> this function!
		(( lvl-=1 ))
		echo -en "\n[$depth] ${BASH_LINENO[$lvl]} > ${FUNCNAME[$lvl]}()"
		(( depth+=1 ))
	done
	echo ".${BASH_LINENO[0]}"
}

fn_msg() { #trick: bash is not expanding variables in aliases
	echo -n "[$1] ${FUNCNAME[1]}(): "
}

# INFO / WARNING / ERR aliases
alias INFO="echo -en "'[i] '
alias WARN="echo -en "'[W] '
alias ERR="echo -en "'[E!] '
alias ERR_CS="echo '[E!]';F_CALL_STACK; echo -en '::'"

f_split_path() { #$1=full path: replaced with path without file name, $2=out file name
	local -n pth=$1
	local -n fnm=$2
	local -a path_ar
	local -i idxfn
	IFS='/' read -a path_ar <<< $pth; unset IFS
	idxfn=$(( ${#path_ar[@]} -1 ))
	fnm=${path_ar[idxfn]}
	pth=${pth/"$fnm"/}
}

f_clear_str() { #$1=the string
	local -n out=$1
	#replace non-alphanumeric characters with underscores
	out=$(echo $out | sed -n 's/[^a-zA-Z0-9]/_/g;P' )
}

f_chk_str_alpha() { #$1=the string
	local    schk=$1
	f_clear_str schk;
	if [ "$1" != "$schk" ]; then return 0; fi
	return 1
}

f_info_help() {
	echo -en \
"Usage:\n"\
" -st= --scan-target= list of dirs/files to scan or a list file\n"\
" -l=  --lang= C or CPP\n"\
" -pp= --preproc= lang preprocessor\n"\
" -pa= --pp-args= preprocessor args\n"\
" -bn= --base-name= base name for output files and vars\n"\
" -rd= --root-dir= base dir for all other settings, except temp. dir\n"\
" -td= --temp-dir= abs or relative path: dir for temp. files\n"\
" -od= --out-dir= relative path for output files\n"\
" -md= --run-mode= 0=full, 1=basic, 2=dummy.\n"\
" -j=  --jobs= num of parallel jobs\n"\
" -D=  --debug-level= 0=off, 1, 2, 3=don't delete tmp files\n"\
" -v   verbose mode\n\n"
}

f_print_settings() {
	echo -en \
"Environmental variables:\n"\
"  RCDGEN_BASIC=$RCDGEN_BASIC\n"\
"  RCDGEN_DUMMY=$RCDGEN_DUMMY\n"\
"  RCDGEN_CLEAN=$RCDGEN_CLEAN\n"\
"  RCDGEN_CPP=$RCDGEN_CPP\n"\
"  RCDGEN_CXXCPP=$RCDGEN_CXXCPP\n"\
"  RCDGEN_PP_ARGS=$RCDGEN_PP_ARGS\n\n"\
"RCD Autogen settings:\n"\
"  rcode_dir='$rcode_dir'\n"\
"  root_dir='$root_dir'\n"\
"  temp_dir='$tmp_dir'\n"\
"  include_dir='$include_dir'\n"\
"  out_dir='$out_dir'\n"\
"  out_scp_src='$out_scp_src'\n"\
"  out_scp_hdr='$out_scp_hdr'\n"\
"  out_fn_src='$out_fn_src'\n"\
"  out_fn_hdr='$out_fn_hdr'\n"\
"  scan_target='$arg_scan_tgt'\n"\
"  lang='$lang'\n"\
"  src_fext='$src_fext'\n"\
"  preproc='$pproc'\n"\
"  pp_args='$pp_args'\n"\
"  base_name='$base_name'\n"\
"  run_mode=$rcdgen_md\n"\
"  jobs=$max_jobs\n"\
"  verbose=$verb_md\n"\
"  dbg_level=$dbg_md\n\n"
}

F_CMD_LINE_ARGS() { #$1=arguments ar
local -n arg_ar=$1
local -i idx=0
for (( idx=0; idx<${#arg_ar[@]}; idx++ ));do
	parm="${arg_ar[idx]}"
	parmtype="${parm/\=*/}"
	parmval=${parm#$parmtype"="}
	case $parmtype in
		--scan-target|-st)
			#list of colon-separated files/dirs to be scanned (PATH format)
			#Target List files (*.$tgt_fext) can be provided as well.
			arg_scan_tgt=$parmval;;
		--root-dir|-rd)
			#root dir: all other dirs are relative to base dir, except temp. dir
			if [ -d $parmval ];then
				root_dir=$parmval
			else
				ERR "Path not found: --work-dir='$parmval'\n"; exit 1
			fi;;
		--temp-dir|-td)
			#dir for temp. files
			tmp_dir=$parmval;;
		--out-dir|-od)
			#dir for temp. files
			out_dir=$parmval;;
		--lang|-l)
			case $parmval in #C or CPP
				C|c)
					src_fext='.c'
					lang="c";;
				CPP|cpp)
					src_fext='.cpp'
					lang="cpp";;
				*)
					ERR "Unsupported language: --lang='$parmval'\n"; f_info_help; exit 1;;
			esac;;
		--preproc|-pp)
				pproc=$parmval;;
		--pp-args|-pa)
			pp_args=$parmval
			pp_args=${parmval/[\"|\']/}; pp_args=${pp_args/[\"|\']/};;
		--base-name|-bn)
			base_name=$parmval;; #prefix for output files
		--jobs|-j)
			max_jobs=$parmval
			if (( max_jobs < 1 ));then
				ERR "Invalid --jobs= number: '$parmval'\n"; exit 1
			fi;;
		--verbose|-v)
			verb_md=1;;
		--run-mode|-md)
			case $parmval in #C or CPP
				full|basic|dummy)
					rcdgen_md=$parmval;;
				*)
					ERR "Unsupported value: --run-mode='$parmval'\n"; f_info_help; exit 1;;
			esac;;
		--debug-level|-D)
			dbg_md=$parmval
			if (( dbg_md < 0 || dbg_md > 3 ));then
				ERR "Incorrect --debug-level='$parmval'\n"; f_info_help; exit 1
			fi;;
		--version)
			echo "rcd_autogen v$version"; exit 0;;
		--help)
			f_info_help; exit 0;;
		*)
			ERR "Unknown argument: "$parm"\n" f_info_help; exit 1;;
	esac
done
}

#---<| Startup |>---

if (( 0 == $# ));then ERR "No arguments provided\n"; f_info_help; exit 1; fi

#---<| environment variables |>---

INFO "Starting rcd_autogen v$version ...\n"

#env. var: basic mode (configure script)
if [ "$(printenv RCDGEN_BASIC)" = "1" ]; then
	rcdgen_md='basic'
fi
#env. var: dummy mode (configure script)
if [ "$(printenv RCDGEN_DUMMY)" = "1" ]; then
	rcdgen_md='dummy'
fi
#NOTE: Above env. vars are overridden by cmdline/config file args.

#---<| cmd line arguments |>---

#1st argument: read argumets from file *.$arg_fext: one arg per line
#copy arg array
args=("$@");
tmps=${args[0]};
if [ "${tmps: -${#arg_fext}}" == $arg_fext ];then
	if [ ! -e "$tmps" ]; then ERR "'$tmps': File does not exist!\n"; exit 1; fi
	INFO "Reading arguments from file: '$tmps'\n"
	unset args
	while read arg_ln ; do
		if [ -z "$arg_ln" ]; then continue; fi
		args+=("$arg_ln")
	done < <(sed -n 's|#.*||g;P' $tmps) #filter shell comments
fi
F_CMD_LINE_ARGS args
unset args

#force debug level from env. var.
tmps="$(printenv RCDGEN_DBG_LEVEL)"
if [ "d$tmps" != "d" ]; then
	INFO "env: RCDGEN_DBG_LEVEL=$tmps\n"
	if ! [[ $tmps =~ ^[0-9]+$ ]]; then dbg_md=-1; fi
	if (( dbg_md < 0 || tmps > 3 ));then ERR "RCDGEN_DBG_LEVEL: bad value:'$tmps'\n"; exit 1; fi
	dbg_md=$tmps
fi

#---<| Check settings |>---
if [ "x$arg_scan_tgt" = "x" ];then ERR "Missing scan target\n";exit 1;fi
if [ "x$lang" = "x" ];then ERR "Language not set\n";exit 1;fi
if [ "x$root_dir" = "x" ];then ERR "Working directory not set\n";exit 1;fi
if [ "x$base_name" = "x" ];then ERR "Base name not set\n";exit 1;fi
if (( 0 == ${#out_dir} ));then out_dir=$root_dir; fi

#---<| debug and verbose mode |>---
if (( $dbg_md > 0 ));then
	alias DBG1="echo -en '[D1]' "
	alias DBG1_FN="fn_msg D1; echo -en"
else
	alias DBG1=#
	alias DBG1_FN=#
fi
if (( dbg_md > 1 ));then
	alias DBG2="echo -en '[D2]' "
	alias DBG2_FN="fn_msg D2; echo -en"
else
	alias DBG2=#
	alias DBG2_FN=#
fi
if (( dbg_md > 2 ));then
	alias DBG3="echo -en '[D3]' "
	alias DBG3_FN="fn_msg D3; echo -en"
else
	alias DBG3=#
	alias DBG3_FN=#
fi
if (( verb_md != 0 || dbg_md > 0 ));then #verbose mode allways on in dbg mode
	alias VRB="echo -en '[i]' "
	alias VRB_FN="fn_msg i; echo -en"
else
	alias VRB=#
	alias VRB_FN=#
fi

#preprocessor setting:
#configure script overrides preprocessor name from --preproc|-pp arg.
tmps="$(printenv RCDGEN_CPP)"
if [ "pp$tmps" != "pp" ]; then
	if [ "$lang" = "c" ]; then
	pproc=$tmps; fi
fi
tmps="$(printenv RCDGEN_CXXCPP)"
if [ "pp$tmps" != "pp" ]; then
	if [ "$lang" = "cpp" ]; then
	pproc=$tmps; fi
fi
if [ "x$pproc" = "x" ];then ERR "PP not set\n";exit 1;fi

#append additional (external) PP args:
tmps="$(printenv RCDGEN_PP_ARGS)"
if [ "pp$tmps" != "pp" ]; then
	pp_args+=" $tmps";
fi

#prefix for output file names, functions and structures
if $(f_chk_str_alpha $base_name); then ERR "--base-name='$base_name' invalid string!\n"; exit 1; fi

#relative #include path to rcode.h: same dir as for $0
IFS='/' read -a path_ar <<< $out_dir; unset IFS
for (( idx=0; idx<${#path_ar[@]}; idx++ ));do
	include_dir+='../'
done
path=$0
f_split_path path file_name
include_dir+=${path/"./"/}
unset file_name path path_ar

#working directory: get abs. path
cd $root_dir
root_dir=$(pwd -P);

#check for nproc: def. max_jobs=2*numof_CPU
if (( max_jobs == 0 )); then
	hash nproc 2>/dev/null
	if (( 0 == $? ));then
		(( max_jobs=2*$(nproc) ))
		VRB "Using 'nproc' for setting jobs limit=$max_jobs\n"
	else
		max_jobs=1
	fi
fi
if (( $max_jobs < 2 )) ;then WARN "Parallel processing NOT enabled.\n"; fi

#output files
out_scp_hdr="$out_dir/"
out_scp_hdr+=$base_name'_'$out_prefix'_'$out_scp_prefix'.h'

out_scp_src="$out_dir/"
out_scp_src+=$base_name'_'$out_prefix'_'$out_scp_prefix$src_fext

out_fn_hdr="$out_dir/"
out_fn_hdr+=$base_name'_'$out_prefix'_'$out_fn_prefix'.h'

out_fn_src="$out_dir/"
out_fn_src+=$base_name'_'$out_prefix'_'$out_fn_prefix$src_fext

out_scp_ptr_hdr="$out_dir/"
out_scp_ptr_hdr+=$base_name"_"$out_scp_hdr_suffix

tmp_scp_file="$tmp_dir/$tmp_scp_file"

#---<| Autogen functions |>---

F_GEN_MSG_FILE() { #$1=file, $2=prefix, $3=msg_ar
	local    m_file=$1
	local    m_prefix=$2
	local -n m_ar=$3
	local -i midx
	local    m_ln
	#begin message struct for the unit
	m_ln="static const rcd_msg_t msg_"$m_prefix"[] = {\n"
	for (( midx=0; midx<(${#m_ar[@]}-1); midx++ )); do
		m_ln+="\t${m_ar[$midx]},\n"
	done
	#last line + end mesage struct:
	m_ln+="\t${m_ar[$midx]}\n};\n\n"
	echo -en $m_ln >> $m_file
}

F_SCAN_FILE() { #$1=target index
	local    tgt_idx=$1
	local    src_file #relative path
	local    msg_file
	local    log_file
	local -a rcd_line_ar
	local -a msg_ar
	local    rcd_magic_ln
	local    tmps
	local    scp_dir
	local    scp_unit
	local    scp_prefix
	local    scp_struct
	local    scp_pmsg
	local    num
	local -i str_len
	local -i rcd_unit_id=0
	local -i cnt_msg=0
	local -i max_mlen=0
	local -r sedcmd='s|^.*'"$kwd_magic"'|'"$kwd_magic"'|g;s|'"$kwd_ifs"'|^|g;/'"$kwd_magic"'/p'

	#path is prepended here, to reduce size of array of msg/log files
	src_file="${target_file_ar[$tgt_idx]}"
	msg_file="$tmp_dir/${msg_struct_ar[$tgt_idx]}"
	log_file="$tmp_dir/${log_file_ar[$tgt_idx]}"

	VRB "Scanning file: [idx=$tgt_idx] $src_file\n"
	DBG1_FN "   msg: '$msg_file'\n   log: '$log_file'\n"
	DBG2_FN "PID=$BASHPID\n"

	while read -r rcd_magic_ln ; do
		#the ^ field separator is generated from $kwd_ifs by sed
		IFS='^'; read -a rcd_line_ar <<< $rcd_magic_ln ; unset IFS; DBG3_FN "input line: '$rcd_magic_ln'\n"

		#pre-read fields
		tmps="${rcd_line_ar[1]}";
		num="${rcd_line_ar[2]}"

		#--> msg definition
		if [ $kwd_msg == "$tmps" ];then

			tmps=${rcd_line_ar[3]}
			DBG3 "RCD_RETURN_FAULT_MSG: scope=$rcd_unit_id, ln=$num, msg=$tmps\n"
			str_len=${#tmps}; (( str_len -= 2 )) #exclude double-quotes
			if (( str_len <= 0 )); then ERR_CS "$src_file: ln=$num : zero-length message!\n"; exit 1;fi
			#check rcode.h::RCD_VMSG_MAX_SZ
			if (( str_len > 512 )); then ERR_CS "$src_file: ln=$num : message length=$str_len > max=512\n"; exit 1;fi

			#max msg len
			if (( str_len > max_mlen )); then max_mlen=$str_len; fi

			#check message line range: RCD_DATA_MAX=0xFFFF
			if (( $num > 0xFFFF ));then
				ERR_CS "$src_file: ln=$num is Out Of Range!\n"; exit 1
			fi

			#append message struct
			msg_ar+=("{$tmps, $str_len, $num}")
			(( cnt_msg += 1 ))
			continue
		fi

		#--> unit definition
		if [ $kwd_unit == "$tmps" ];then

			if (( 0 != $rcd_unit_id ));then
				ERR_CS "$src_file: Multiple RCD_UNIT defs detected! (have scp=$scp_unit)\n"; exit 1
			fi

			if ! [[ "$num" =~ ^[0-9]+$ ]]; then ERR_CS "$src_file: RCD_UNIT: bad value:'$num'\n"; exit 1; fi
			rcd_unit_id=$num #unit number

			#check unit ID range: RCD_SCOPE_MAX=0x3FFF
			if (( $rcd_unit_id > 0x3FFF ));then
				ERR_CS "$src_file: RCD_UNIT=$rcd_unit_id is Out Of Range!\n"; exit 1
			fi

			#scope prefix
			scp_prefix=$src_file
			f_clear_str scp_prefix
			DBG2 "RCD_AUTOGEN_DEFINE_UNIT: scope=$rcd_unit_id, scp=$scp_unit, prefix=$scp_prefix\n"

			#spit path & file name
			scp_dir=$src_file
			f_split_path scp_dir scp_unit

			#predefined name of msg struct (pointer)
			#scp_pmsg=$(printf "msg_%s" $scp_prefix)
			scp_pmsg="msg_$scp_prefix"
		fi

		#unknown string from pproc here (ignored)
	done < <($pproc $pp_args $kwd_autogen $src_file 2>$log_file | sed -n "$sedcmd")

	if (( 0 == $rcd_unit_id ));then
		if (( 0 != cnt_msg ));then
			ERR_CS "$src_file: missing unit number definition\n"; exit 1
		fi
		DBG1_FN "$src_file: RCD_UNIT undefined -> clear msg tmp file.\n"
		echo -n '' > $msg_file
		return 0;
	fi

	if (( 0 != cnt_msg ));then
		#if no msg were registered, insert NULL ptr to msg struct
		F_GEN_MSG_FILE $msg_file $scp_prefix msg_ar

		scp_struct="{$scp_pmsg"
	else
		DBG1_FN "'$src_file' No massages found -> NULL msg struct ptr\n"
		scp_struct="{NULL"
		echo -n '' > $msg_file
	fi
	scp_struct+=", \"$scp_dir\", \"$scp_unit\", ${#scp_dir}, ${#scp_unit}, $rcd_unit_id, $cnt_msg}"
	(( max_mlen= max_mlen + ${#scp_dir} + ${#scp_unit} )) #min buff for this scope
	#inject $rcd_unit_id as the first field: for sorting units by number
	#append min buff size, for calc. overall min buff size
	scp_struct="$rcd_unit_id\t$scp_struct$max_mlen\n"; DBG2_FN "scp_struct line: '$scp_struct'\n"
	echo -en $scp_struct >> "$tmp_scp_file" # *atomic write*
}

F_CHK_FILE() { #$1=file $2=error info $3=check type
	local  Afile=$1
	local  e_info=$2
	local  chk_type=$3

	DBG2_FN "$e_info '$Afile' chk_type='$chk_type'\n"
	#check if file exist
	if [ ! -e "$Afile" ];then
		ERR_CS "$e_info:'$Afile' File does not exist.\n"; exit 1
	fi
	if [ "$chk_type" == 'IGN_TYPE' ];then return 0; fi
	#check extension
	if [ "${Afile: -${#src_fext}}" != $src_fext ];then
		ERR_CS "$e_info: '$Afile' wrong file extension!\n"; exit 1
	fi
}

F_SCAN_DIR() { #$1=dir
	local -n fdir=$1
	local    fname
	local -i fcnt=0
	#filter-files by extension
	while read fname; do
		#check extension
		if [ "${fname: -${#src_fext}}" != $src_fext ];then continue; fi

		fname=$fdir/$fname
		fname=${fname/'./'}; fname=${fname%'/'}
		F_CHK_FILE $fname ${FUNCNAME[0]} "IGN_TYPE"

		target_file_ar+=($fname); DBG2_FN "adding file: '$fname'\n"
		#fcnt++
		(( fcnt += 1 ))
	done < <(ls $fdir)
	DBG2_FN "'$fdir' : $fcnt files found.\n"
	if [ 0 == fcnt ];then
		VRB_FN "No matching files found in $fdir/ -> Skip\n"
	fi
}

F_READ_TGDEF_FILE() { #$1=list file
	local -n lstfile=$1
	local    line
	local    rel_dir='.'

	if [ ! -e $lstfile ];then
		ERR_CS "'$lstfile' File does not exist.\n"
		exit 1
	fi

	VRB "Reading list of targets to scan from file: '$lstfile'\n"
	idxA=0
	while read line; do
		#skip empty lines
		if [ 0 == ${#line} ];then continue; fi
		DBG3_FN "Rd line: '$line'\n"
		#.SETDIR: set relative dir for following files
		if [ "${line: 0:7}" == '.SETDIR' ];then
			line=${line: 7}; line=${line#' '}; line=${line%'/'};
			rel_dir=$line; DBG3_FN "SETDIR '$rel_dir'\n"
			continue
		fi
		#.SCANDIR: scan for files in a given dir, append them to global file array
		if [ "${line: 0:8}" == '.SCANDIR' ];then
			line=${line: 8}; line=${line#' '}; DBG3_FN "SCANDIR '$line'\n"
			F_SCAN_DIR line
			continue
		fi
		#line defines a file
		line=$rel_dir/$line;
		line=${line#' '}; line=${line/'./'}; DBG3_FN "file: '$line'\n"
		F_CHK_FILE $line $lstfile "CHK_TYPE"
		target_file_ar+=($line)
	done < <(sed -n 's|#.*||;s|\t| |g;s|[ ][ ]*| |g;P' $lstfile) #filter shell comments, single space only
}

F_ARG_TARGET_LIST() { #$1=array of args
	local -n arg_ar=$1
	local    arg

	for arg in ${arg_ar[@]}; do
		arg=${arg%'/'}; DBG2_FN "arg: '$arg'\n"
		#dir to scan
		if [ -d $arg ];then DBG3_FN "dir: '$arg'\n"
			F_SCAN_DIR arg
		else
			#check file extension
			if [ "${arg: -${#tgt_fext}}" == $tgt_fext ];then
				#read target list from a file
				F_READ_TGDEF_FILE arg
				continue
			fi
			#single file
			F_CHK_FILE $arg "--scan-target=" "CHK_TYPE"
			target_file_ar+=($arg); DBG3_FN "file: '$arg'\n"
		fi
	done
}

F_CHK_DUP_AR() { #$1=target array $2=message
	local -n file_ar=$1
	local    msg=$2
	local -i Aidx
	local -i Bidx
	local -i ARsz=${#file_ar[@]}
	local -i err=0
	local    FA
	local    FB
	for (( Aidx=0; Aidx<(ARsz-1); Aidx++ )); do
		FA=${file_ar[Aidx]}; DBG3_FN "\n"

		for (( Bidx=Aidx+1; Bidx<ARsz; Bidx++ )); do
			FB=${file_ar[Bidx]}; DBG3_FN "[$Aidx|$Bidx]'$FA' vs '$FB'\n"
			if [ "$FA" == "$FB" ];then
				(( err+=1 ))
				ERR_CS "$msg: Duplicate found: '$FA'\n";
			fi
		done
	done
	if (( err != 0 ));then
		ERR_CS "$msg: Found $err duplicates(s)\n"; exit 1
	fi
	VRB "$msg: OK\n"
}

F_GEN_TMP_NAMES() {
	local -i idxf
	local    base
	local    fname
	#tmp file name format: main.c -> msg_main_c.$tmp_fext
	for idxf in ${!target_file_ar[@]}; do
		base=${target_file_ar[idxf]}
		f_clear_str base
		printf -v fname "msg_%s$tmp_fext" $base
		msg_struct_ar[idxf]=$fname; DBG3_FN "tmp: [${#msg_struct_ar[@]}]: $fname\n"
		base+="$log_fext"
		log_file_ar[idxf]=$base; DBG3_FN "log: [${#log_file_ar[@]}]: $base\n"
	done
	DBG1_FN "done.\n"
}

F_CHECK_LOGS() {
	local    fname
	local    sz_str
	local -i fsize
	#check log files: size > 0
	for fname in ${log_file_ar[@]}; do
		fname="$tmp_dir/$fname"
		if [ -e $fname ];then
			DBG2_FN "checking log: '$fname'\n"
			sz_str=$(wc -c "$fname")
			fsize=${sz_str/" $fname"/}
			if (( fsize > 0 ));then
				ERR "PP error! Log file: $fname\n"; cat $fname; exit 1
			fi
		else
			ERR_CS "Missing log file: '$fname'\n"; exit 1
		fi
	done
	VRB "Checking PP log files: OK\n"
}

F_PARALLEL() {
	local -r n_jobs=${#target_file_ar[@]}
	local -i cnt
	local -i job_limit=$max_jobs
	local -a

	DBG2_FN "PID=$BASHPID\n"

	cnt=0
	while (( cnt < n_jobs )); do
		while (( cnt < job_limit )); do
			F_SCAN_FILE $cnt &
			(( cnt++ ))
			if (( cnt >= n_jobs ));then break; fi
		done

		DBG2_FN "WAIT: job count=$cnt of $n_jobs, max_jobs=$max_jobs\n"
		wait
		((job_limit += max_jobs))
	done
	DBG2_FN "DONE.\n"
}

F_SCAN_TARGET() {
	local -a arg_target_ar

	#--scan-target= get targets (colon-separated list)
	IFS=':'
	read -a arg_target_ar <<< $arg_scan_tgt
	unset IFS

	DBG1_FN "arg list (cnt=${#arg_target_ar[@]}):\n"${arg_target_ar[@]}"\n$end_list\n"
	F_CHK_DUP_AR arg_target_ar "--scan-target=: Checking for duplicate targets"

	#scan elements from arg
	F_ARG_TARGET_LIST arg_target_ar

	VRB "List of files to scan (count=${#target_file_ar[@]}):\n${target_file_ar[@]}\n$end_list\n"
	if (( 0 == ${#target_file_ar[@]} ));then ERR "No files to scan!\n"; exit 1
	fi

	F_CHK_DUP_AR target_file_ar "Checking for duplicate target files"
	F_GEN_TMP_NAMES

	#Parallel scanning of target files
	F_PARALLEL

	F_CHECK_LOGS
}

F_APPEND_MSG_FILES() {
	local    fname
	#append message structs from temp. files
	for fname in ${msg_struct_ar[@]}; do
		fname="$tmp_dir/$fname"
		if [ -e $fname ];then DBG3_FN "file: '$fname'\n"
			cat "$fname" >> $out_scp_src
		else
			ERR_CS "Missing temp file: '$fname'\n"; exit 1
		fi
	done
	DBG1_FN "done.\n"
}

F_CHK_UNIT_ID() { #$1=sorted unit array
	local -n ar_scp=$1
	local -i idxs
	local -i scpA
	local -i scpB
	local    scp_ln

	scp_ln=${ar_scp[0]}
	scpA="${scp_ln%'{'*}";
	for (( idxs=1; idxs<(${#ar_scp[@]}); idxs++ ));do
		scp_ln=${ar_scp[$idxs]}
		scpB="${scp_ln%'{'*}";
		if (( scpA == scpB )); then ERR_CS "Found duplicate unit ID=$scpA\n"; exit 1; fi
		scpA=$scpB
	done
   VRB "Checking for duplicate unit IDs: OK\n"
}

F_APPEND_SORT_UNITS() {
	local -a unit_ar
	local    scp_ln
	local    fout
	local    units
	local -i idxs
	local -i bufsz
	local -i min_buff_sz=0

	readarray -t unit_ar < <(cat $tmp_scp_file | sort)
	if (( 0 == ${#unit_ar[@]} )); then ERR_CS "No defined units found!\n"; exit 1; fi

	F_CHK_UNIT_ID unit_ar

	DBG3_FN "SORTED unit def. array: [${#unit_ar[@]}]\n${unit_ar[@]}\n"
	for (( idxs=0; idxs<(${#unit_ar[@]}-1); idxs++ ));do
		scp_ln=${unit_ar[$idxs]}; DBG3_FN "unit line: $scp_ln\n"
		bufsz="${scp_ln/*'}'/}"; DBG3_FN "bufsz=$bufsz\n" #check min buff size appended by F_SCAN_FILE
		if (( bufsz > min_buff_sz ));then min_buff_sz=$bufsz; fi
		scp_ln="${scp_ln/'}'*/'}'}"; DBG2_FN "appending unit: $scp_ln\n" #cut injected bufsz
		units+="\t\t${scp_ln/*'{'/'{'},\n" #cut scp numbers injected by F_SCAN_FILE
	done
	#last unit line: no comma
	scp_ln=${unit_ar[$idxs]}; DBG3_FN "unit line: $scp_ln\n"
	bufsz="${scp_ln/*'}'/}"; DBG3_FN "bufsz=$bufsz\n"
	if (( bufsz > min_buff_sz ));then min_buff_sz=$bufsz; fi
	scp_ln="${scp_ln/'}'*/'}'}"; DBG2_FN "appending unit: $scp_ln\n"
	units+="\t\t${scp_ln/*'{'/'{'}\n"
	#24= ($base_name) +7(65535: ) <min_buff::path/file> +1(dot) + 5(ln_num) +1(space) +6(' [-2] ') <msg here> + +1(NULL) +1
	(( min_buff_sz+=24 ))
	(( min_buff_sz+=${#base_name} ))
	fout="static rcd_scope_t "$base_name"_units = {\n"
	fout+='\t'".vmsg = NULL,\n"
	fout+='\t'".base_name = __base_name,\n"
	fout+='\t'".bname_slen = ${#base_name},\n"
	fout+='\t'".un_cnt = ${#unit_ar[@]},\n"
	fout+='\t'".min_bufsz = $min_buff_sz,\n"
	fout+='\t'".reserve = 0,\n"
	fout+='\t'".unit_ar = {\n"
	fout+=$units
	fout+='\t'"}\n};\n"
	echo -en $fout >> $out_scp_src
	DBG2_FN "min buff size: $min_buff_sz\n"
}

F_GEN_SOURCES() {
	local    ftmp
	local -r sedcmd='s|%rcd_bname%|'"$base_name"'|g;s|%path%|'"$include_dir"'|g;s|[ ]*\/\/|\n|g;/^[ ]*[\n]/d;/^[ ]*$/d;P'

	VRB "Generating source files:\n '$out_fn_src'\n '$out_scp_src'\n"
	if [ "$rcdgen_md" = "full" ];then
		ftmp=$tmpl_func
	else
		ftmp=$tmpl_func_bm #basic mode
	fi
	#autogen info string
	echo -en "\n$autogen_hdr\n\n" > $out_fn_src
	sed -n "$sedcmd" $rcode_dir/$ftmp >> $out_fn_src

	if [ "$rcdgen_md" = "dummy" ];then
		ftmp=$tmpl_scp_fn_dm #dummy mode
	else
		ftmp=$tmpl_scp_fn
		#autogen info string
		echo -en "\n$autogen_hdr\n\n" > $out_scp_src
		#include rcode.h
		echo -en "#include \""$include_dir"rcode.h\"\n\n" >> $out_scp_src
		#static base name for units
		echo -en "static const char __base_name[]=\""$base_name"\";\n\n" >> $out_scp_src
		#append static messages discovered by F_SCAN_FILE()
		F_APPEND_MSG_FILES
		#append sorted struct of units
		F_APPEND_SORT_UNITS
	fi
	#append scope accessors code
	sed -n "$sedcmd" $rcode_dir/$ftmp >> $out_scp_src
}

F_GEN_HEADERS() {
	#header: replace %s% with $base_name prefix, inject include path: %p%
	local -r sedcmd='s|%rcd_bname%|'"$base_name"'|g;s|%path%|'"$include_dir"'|;P'

	VRB "Generating header files:\n '$out_fn_hdr'\n '$out_scp_hdr'\n '$out_scp_ptr_hdr'\n"
	echo -en "\n$autogen_hdr\n" > $out_fn_hdr
	sed -n "$sedcmd" $rcode_dir/$tmpl_func_hdr >> $out_fn_hdr
	echo -en "\n$autogen_hdr\n" > $out_scp_hdr
	sed -n "$sedcmd" $rcode_dir/$tmpl_scp_hdr >> $out_scp_hdr
	echo -en "\n$autogen_hdr\n" > $out_scp_ptr_hdr
	sed -n "$sedcmd" $rcode_dir/$tmpl_getscp_hdr >> $out_scp_ptr_hdr
}

F_RM_TMP_FILES() { #$1=rmdir
	DBG1_FN "list of files:\n"$(ls $tmp_dir | sed -n "/[$tmp_fext$|$log_fext$]/p")"\n$end_list\n"
	if [ "$1" == "RM_DIR" ];then
		if (( dbg_md == 3 )); then return; fi
		rm -rf "$tmp_dir"; return;
	fi
	rm -f $tmp_dir/*$tmp_fext
	rm -f $tmp_dir/*$log_fext
}

F_RM_OUT_FILES() {
	#delete old/orphaned output files.
	if [ -e "$out_scp_src" ];then
		VRB_FN "'$out_scp_src'\n"
		rm -f "$out_scp_src"
	fi
	if [ -e "$out_fn_src" ];then
		VRB_FN "'$out_fn_src'\n"
		rm -f "$out_fn_src"
	fi
	if [ -e "$out_scp_hdr" ];then
		VRB_FN "'$out_scp_hdr'\n"
		rm -f "$out_scp_hdr"
	fi
	if [ -e "$out_fn_hdr" ];then
		VRB_FN "'$out_fn_hdr'\n"
		rm -f "$out_fn_hdr"
	fi
	if [ -e "$out_scp_ptr_hdr" ];then
		VRB_FN "'$out_scp_ptr_hdr'\n"
		rm -f "$out_scp_ptr_hdr"
	fi
}

#---<| Begin Autogen |>---

#env. var: clean mode (configure script): overrides all other settings.
if [ "$(printenv RCDGEN_CLEAN)" = "1" ]; then
	F_RM_OUT_FILES
	F_RM_TMP_FILES 'RM_DIR'
	INFO "rcd_autogen: CLEAN mode, finished.\n"
	exit 0;
fi

if [ ! -d $tmp_dir ];then
	VRB "Creating temp. directory: '$tmp_dir'\n"
	mkdir "$tmp_dir"
	if (( 0 != $? ));then ERR "Failed creating temp. directory: '$tmp_dir'\n"; exit 1; fi
fi

#print settings in verbose mode
if (( verb_md != 0 ));then f_print_settings; fi

#delete old/orphaned output files.
F_RM_OUT_FILES

#remove orphaned temp. files
F_RM_TMP_FILES

#generate header files: (required for PP stage)
F_GEN_HEADERS

#check for dummy mode
if [ "$rcdgen_md" != "dummy" ];then
	#create list of target files to scan, perform the scan, generate temp. files
	F_SCAN_TARGET
fi

#generate source files
F_GEN_SOURCES

#remove temp. dir
F_RM_TMP_FILES 'RM_DIR'

INFO "rcd_autogen finished.\n"
