#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only

# "spp" preprocessor script.

#  Copyright (c) 2010-2013 Wind River Systems, Inc.

#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License version 2 as
#  published by the Free Software Foundation.

#  This program 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 General Public License for more details.

version="0.8"

usage()
{
cat << EOF

 spp [--help] [--force] [-i] [--fuzz] [-w] [-o outfile] [-D<var>=<value>] [-I<path>] [-v] infiles

      --help:      This message
      --force:     force overwrite output file if it already exists
      --find:      Find the .scc file that matches the passed defines (-D)
      --fuzz:      Use fuzzy logic to find the target file (off by default). Only for
                   advanced use cases, since partial/close matches will be returned
      -D:          define <var> to <value> which will be available to sub scripts
      -I:          include path <path> will be searched for files
      -i:          leave intermediate files on failure
      -w:          only warn on missing files
      -v:          verbose output
      -o:          outfile file for processed results. stdout is used if not passed

      infiles      files to preprocess


EOF
}

if [ -z "$1" ]; then
	usage
	exit 1
fi

while [ $# -gt 0 ]; do
	case "$1" in
	    --help)
		usage
		exit
		;;
            --find)
                do_find=t
                ;;
	    -D*|--D*)
		if [ "$1" == "-D" ] || [ "$1" == "--D" ]; then
		    x=$2; shift;
		else
		    x=`echo $1 | sed s%^-*D%%`
		fi
		defines="$defines $x"
		;;
	    -I*|--I*)
		if [ "$1" == "-I" ] || [ "$1" == "--I" ]; then
		    x=$2; shift;
		else
		    x=`echo $1 | sed s%^-*I%%`
		fi
		if [ -n "$x" ] && [ -d "$x" ]; then
		    include_paths="$include_paths $(readlink -f $x)"
		fi
		;;
	    --force|-f)
		force=t
		;;
            --fuzz)
		fuzz=t
		;;
	    -i)
		intermediate=t
		;;
	    -w)
		warn_on_missing=t
		;;
	    -o)
		outfile=$2
		shift
		;;
	    -v) verbose=t
		;;
	    *) break
		;;
	esac
	shift
done

err()
{
    echo "$@" 1>&2
}

warn()
{
    err "$@"
}


# removes any common prefixes from a name (path, patch, etc). With
# these removed, the resulting name is now relative to a set of
# search paths, and can be found later.
strip_common_prefix()
{
    in_name=$1

    # this takes an input name and searches all known paths.
    # the relocation that removes the MOST from the original is
    # the one we want, since it is the best match by definition
    out_len=${#in_name}
    relocated_name=$in_name
    for r in $include_paths; do
	t=${in_name/$r}
	this_len=${#t}
	if [ $this_len -lt $out_len ]; then
	    relocated_name=$t
	    out_len=$this_len
	    break
	fi
	# add a trailing slash to get corner cases where one may
	# have been added or not dropped
	t=$in_name/
	t=${t/$r}
	this_len=${#t}
	if [ $this_len -lt $out_len ]; then
	    relocated_name=$t
	    out_len=$this_len
	    break
	fi
    done

    if [ ! -f ${relocated_name} ]; then
        # remove any leading slashes
        relocated_name=$(echo ${relocated_name} | sed 's%^//*%%g')
    fi
    echo "$relocated_name"
}


# looks for a .scc file that matches a set of defines (keywords)
search_includes_for_defines()
{
    defines=$1
    found_scc=

    declare -A define_map
    if [ -n "$defines" ]; then
        # this could totally just be a loop .. but we only need three, so
        # keep it simple. The array allows us to easily weed out duplicate
        # defines
        define_tgt=$(echo $defines | cut -d: -f1)
        if [ -n "${define_tgt}" ]; then
            define_map[${define_tgt}]=t
        fi
        define_tgt=$(echo $defines | cut -d: -f2)
        if [ -n "${define_tgt}" ]; then
            define_map[${define_tgt}]=t
        fi
        define_tgt=$(echo $defines | cut -d: -f3)
        if [ -n "${define_tgt}" ]; then
            define_map[${define_tgt}]=t
        fi
    fi

    max_score=0
    sccs_that_define=`find $include_paths -name '*.scc' \
                                | xargs grep -l -e '^[[:space:]]*define.*' | sort | uniq`
    for scc in $sccs_that_define; do
        score=0
        for tgt in ${!define_map[@]}; do
            if [ -n "$tgt" ]; then
                f=`grep -l -e '^[[:space:]]*define.*'$tgt'[[:space:]]*\$' $scc`
                if [ -n "$f" ]; then
                    score=`expr $score + 1`
                fi
            fi
        done

        if [ -n "${verbose}" ]; then
            >&2 echo "checking $scc ($score)" >&2
        fi

        if [ $score -gt $max_score ]; then
            found_scc="$scc"
            max_score=$score
        elif [ $score -eq $max_score ]; then
            found_scc="$found_scc $scc"
        fi
    done

    if [ -n "${verbose}" ]; then
        >&2 echo "score: $max_score"
    fi

    if [ $max_score -gt 0 ]; then
        # return the first target found (among equals)
        rstring="$(echo $found_scc | cut -f2 -d' ')"

        # .. and add the score
        echo "$rstring:$max_score"
    fi
}

search_include_paths()
{
    local tgt=$1
    shift
    local includes="$@"
    local exclude_files=""
    local base
    local feature_ext=".scc"
    local possible=""

    if [ -n "$verbose" ]; then
        echo "search_includes: $tgt" >&2
        echo "include paths: "  >&2
        for i in $includes; do
            echo "   $i"  >&2
        done
    fi

    # if the path is absolute, we are done
    if [ -f "${tgt}" ]; then
        echo ${tgt}
        return
    fi

    case $tgt in
        *.patch)
            feature_ext=".patch"
            ;;
        *.cfg)
            feature_ext=".cfg"
            ;;
        *.scc)
            feature_ext=".scc"
            ;;
        *defconfig)
            feature_ext=""
            ;;
    esac

    # remove the feature extension (if present) from the input name
    tgt=${tgt%$feature_ext}
    base=`basename $tgt`

    if [ -z "$exclude_files" ]; then
        exclude_files="/dev/null"
    fi

    for p in "." $includes; do
        # target file + default feature extension
        if [ -f $p/$tgt$feature_ext ]; then
            possible=`readlink -f "$p/$tgt$feature_ext"`
        # raw target file
        elif [ -f $p/$tgt ]; then
            possible=`readlink -f "$p/$tgt"`
        # special processing test the include directory +
        # the name of the feature. This saves us doing a massive
        # set of includes for sub categories includes
        elif [ -f $p/$tgt/$tgt$feature_ext ]; then
            possible=`readlink -f "$p/$tgt/$tgt$feature_ext"`
        elif [ -f $p/$tgt/$tgt ]; then
            possible=`readlink -f "$p/$tgt/$tgt"`
            # more special processing. test if the included
            # feature is actually just the name of a directory
            # AND there is not file with the same name present.
            # if that is true, then test for:
            #     <tgt>/<tgt>.extension
            # in that directory
        elif [ -f $p/$tgt/$base$feature_ext ]; then
            possible=`readlink -f "$p/$tgt/$base$feature_ext"`
        elif [ -f $p/$tgt/$base ]; then
            possible=`readlink -f "$p/$tgt/$base$feature_ext"`
        fi

        if [ -n "$possible" ]; then
            echo "$exclude_files" | grep -q "$possible"
            if [ $? -ne 0 ]; then
                echo $possible
                return
            fi
            possible=
        fi
    done
}

# args are the input files
infiles=$@

processed_files=""

# this function also removes duplicated lines by `sort -u`
sort_by_len_dec()
{
        for i in $@; do
                echo $i
        done | sort -u | awk '{ print length($0) " " $0; }' | sort -nr | cut -d ' ' -f 2-
}

include_paths=$(sort_by_len_dec $include_paths)

##
## create variables for use in scripts
##
if [ -n "$defines" ]; then
    vars=$(echo $defines | sed 's/,/ /g')
    for v in "$vars"; do
	# eval makes it available for this script
	eval $v
    done
fi

# we are just trying to locate a file on the search path
if [ -n "${do_find}" ]; then
    if [ -z "${KMACHINE}" ] && [ -z "${KTYPE}" ]; then
        if [ -z "${infiles}" ]; then
            echo "[ERROR]: No files provided, and no KMACHINE and KTYPE defined"
            exit 1
        fi
    fi

    # are we looking for a machine/kernel match ?
    if [ -n "${KMACHINE}" ] && [ -n "${KTYPE}" ]; then
        found=$(search_includes_for_defines ${KMACHINE})
        found_file=$(echo $found | cut -d: -f1)
        found_score=$(echo $found | cut -d: -f2)

        # now do the same search with the ktype AND the machine. If we
        # find a different file with a score of "2" (both), we'll use it
        # instead, otherwise, stick with the one that matched the machine
        found=$(search_includes_for_defines ${KMACHINE}:${KTYPE})
        found_file2=$(echo $found | cut -d: -f1)
        found_score2=$(echo $found | cut -d: -f2)

        if [ "${found_score2}" == "2" ]; then
            found_file=${found_file2}
            found_score=${found_score2}
        else
            # if --fuzz is passed, we'll continue with a score of 1, otherwise
            # we exit
            if [ -z "${fuzz}" ]; then
                exit 1
            fi
        fi

        if [ -n "${found_file}" ]; then
            echo "${found_file}"
        else
            exit 1
        fi
    fi

    # or are we looking for a file in particular ?
    if [ -n "${infiles}" ]; then
        for f in ${infiles}; do
            if [ -n "${verbose}" ]; then
                echo "[INFO]: searching for $f"
            fi
            search_include_paths $f $include_paths
        done
    fi

    exit 0
fi

if [ -z "$infiles" ]; then
    err "ERROR: at least one input file must be supplied"
    exit 1
else
    for f in $infiles; do
        expanded_file=$(search_include_paths $f $include_paths)
        if [ -z "${expanded_file}" ]; then
	    err "ERROR. input file \"$f\" does not exist"
	    exit 1
        fi
    done
fi

if [ -n "$outfile" ] && [ -f "$outfile" ]; then
    if [ -z "$force" ]; then
	err "ERROR: output file \"$outfile\" exists, and --force was not passed"
	exit 1
    fi
fi


# used by preprocessor directives to define values, very
# similar to the block above which processes command line
# values. These could be unified, but it is easier to keep the
# separate for now
define()
{
    var=$1
    value="$2"

    # make the variable real
    eval $var="\"$value\""
}


header()
{
    echo "#"
    echo "# spp v$version"
    echo "# processed: `date`"
    echo "#"
    echo "# This is a preprocessor output file, do not edit"
    echo "#"

    for i in $include_paths; do
	# strip a trailing /
	abs_dir=`readlink -f $i`
	abs_dir2=`cd $i; pwd`
	if [ x"$abs_dir" != x"$abs_dir2" ]; then
	    # there is some sort of symlink trickery going on.
	    # add both dirs to the relocation list
	    abs_dir="$abs_dir $abs_dir2"
	fi

	for d in $abs_dir; do
	    one_less_dir=${d%/}
	    # strip last path component
	    one_less_dir=${one_less_dir%/*}

	    echo "reloc_dir $one_less_dir"
	done
    done

    echo "#"
}

# arg1: duration
# remaining: the processed files
footer()
{
    local duration=$1
    shift
    local infiles=$@

    echo "# run time: $duration seconds"
    echo "# processed files:"
    for f in $infiles; do
	echo "# _cfg $f"
    done
}

process_file()
{
    local in=$1
    local containing_file=$2
    shift
    shift
    local flags=$@
    local inherited_inhibit_cfg=""
    local inherited_inhibit_patch=""
    local ret=0
    local done=""
    local kconf_type
    local kconf_name
    local arg1
    local fline
    local include_name
    local inhibit_cfg
    local inhibit_patch
    local working_dir=$(dirname ${containing_file})

    if [ -z "$in" ]; then
        return
    fi

    if [ -n "${verbose}" ]; then
        echo "[INFO]: processing $in"
    fi

    case $in in
        *.scc)
            # do nothing, the rest of this function handles things
            ;;
        *.patch)
            patch_name=$in
            patch_name_new=$(search_include_paths $patch_name ${working_dir} ${include_paths})
            if [ -z "${patch_name_new}" ]; then
                echo "[ERROR]: could not find patch: $in"
                exit 1
            fi

            # output the name, and exit
            relative_patch=$(strip_common_prefix $(readlink -f $patch_name))
            containing_dir=${patch_name/$relative_patch}
            echo "prefix ${containing_dir}"
            echo "patch \"${patch_name_new}\""
            return
            ;;
        *.cfg)
            # output the name, and exit
            kconf_name=$in
            kconf_name_new=$(search_include_paths $kconf_name ${working_dir} ${include_paths})
            if [ -z "${kconf_name_new}" ]; then
                echo "[ERROR]: could not find config fragment: $in"
                exit 1
            fi
            relative_kconf=$(strip_common_prefix $(readlink -f $kconf_name_new))
            containing_dir=${kconf_name_new/$relative_kconf}
            echo "prefix ${containing_dir}"
            echo "kconf non-hardware ${kconf_name_new}"
            return
            ;;
        *defconfig)
            # output the name, and exit
            kconf_name=$in
            kconf_name_new=$(readlink -f $(search_include_paths $kconf_name ${working_dir} ${include_paths}))
            if [ -z "${kconf_name_new}" ]; then
                echo "[ERROR]: could not find defconfig: $in"
                exit 1
            fi
	    relative_kconf=$(strip_common_prefix $(readlink -f $kconf_name_new))
            containing_dir=${kconf_name_new/$relative_kconf}
            echo "prefix ${containing_dir}"
            echo "kconf non-hardware ${kconf_name_new}"
            return
            ;;
    esac

    # process the flags to this file processing
    for flag in $flags; do
        case $flag in
            nocfg) inherited_inhibit_cfg=nocfg
                ;;
            nopatch) inherited_inhibit_patch=nopatch
                ;;
        esac
    done

    if [ ! -f "$in" ]; then
        local_includes=`dirname $containing_file`
        new_in=`search_include_paths $in $include_paths $local_includes`
        if [ ! -f "$new_in" ]; then
            err "ERROR: could not find file $in, included from $containing_file"
            return 1
        fi
        in=$new_in
    else
        in=`readlink -f $in`
    fi

    if [[ "$processed_files" =~ $in ]]; then
        echo "# NOTE: feature `basename $in` has already been processed"
    fi
    processed_files="$processed_files $in"


    echo "# --> file: $in"
    echo "# flags: $flags"
    echo "mark `basename $in` start"

    while read line || [ -n "$line" ];
    do
        fline=$line

        [[ $fline ]] || continue

        # include
        if [[ $fline =~ ^[[:space:]]*include ]]; then
            set $fline
            include_name=$2

            # if we were called with inhibit flags, passing them along is
            # the default
            inhibit_cfg=$inherited_inhibit_cfg
            inhibit_patch=$inherited_inhibit_patch

            # if we have a "nocfg" or "nopatch" on the include directive,
            # then we need to set the variables so they'll be passed down to
            # the nested process call
            if [[ $fline =~ [[:space:]]+nocfg ]]; then
                inhibit_cfg=nocfg
            fi
            if [[ $fline =~ [[:space:]]+nopatch ]]; then
                inhibit_patch=nopatch
            fi

            process_file $include_name $in $inhibit_cfg $inhibit_patch
            ret=$?
            if [ $ret -eq 1 ]; then
                return $ret
            fi

            # we can clear the inhbit flag, only if it wasn't passed into
            # us from above. This allows local .cfg files to be processed.
            if [ -z "$inherited_inhibit_cfg" ]; then
                inhibit_cfg=""
            fi
            if [ -z "$inherited_inhibit_patch" ]; then
                inhibit_patch=""
            fi
            continue
        fi

        ## preprocessor define
        if [[ $fline =~ ^[[:space:]]*#define ]]; then
            set $fline
            local define_name=$2
            local define_value=$3
            if [ -z "$define_value" ]; then
                define_value=t
            fi

            define $define_name $define_value

            continue
        fi

        ## patch
        if [[ $fline =~ ^[[:space:]]*patch ]]; then
            set $fline
            local patch_name=$2

            if [ -n "$inherited_inhibit_patch" ]; then
                echo "# inhibited patch"
                echo "true \"`dirname $in`/$patch_name\""
            else
                if [ ! -f `dirname $in`/$patch_name ]; then
                    local patch_name_new=`search_include_paths $patch_name $include_paths`
                    if [ ! -f "$patch_name_new" ]; then
                        err "ERROR: could not find patch $patch_name, included from $containing_file"
                        return 1
                    fi
                    patch_name=$patch_name_new
                else
                    patch_name="`dirname $in`/$patch_name"
                fi

                # output the patch
                relative_patch=$(strip_common_prefix $patch_name)
                containing_dir=${patch_name/$relative_patch}
                echo "prefix ${containing_dir}"
                echo "patch \"${patch_name}\""
            fi

            continue
        fi

        ## kconf
        if [[ $fline =~ ^[[:space:]]*kconf || $fline =~ ^force[[:space:]]*kconf ]]; then
            set $fline
            arg1=$1
            if [ "$arg1" = "force" ]; then
                # "force" will insist that its config be processed
                kconf_type=$3
                kconf_name=$4
            else
                arg1=""
                kconf_type=$2
                kconf_name=$3
            fi

            if [ -n "$inherited_inhibit_cfg" ] && [ -z "$arg1" ]; then
                echo "# inhibited kconf"
                echo "true \"`dirname $in`/$kconf_name\""
            else
                if [ ! -f `dirname $in`/$kconf_name ]; then
                    local kconf_name_new=`search_include_paths $kconf_name $include_paths`
                    if [ ! -f "$kconf_name_new" ]; then
                        if [ -n "$lazy_filenames" ]; then
                            warn "WARNING: could not find kconf $kconf_name, included from $containing_file"
                            kconf_name_new=$kconf_name
                        else
                            err "ERROR: could not find kconf $kconf_name, included from $containing_file"
                            return 1
                        fi
                    fi
                    kconf_name="${kconf_name_new}"
                else
                    kconf_name="`dirname $in`/$kconf_name"
                fi

                # output the kconfig
                relative_kconf=$(strip_common_prefix $kconf_name)
                containing_dir=${kconf_name/$relative_kconf}
                echo "prefix ${containing_dir}"
                echo "kconf $kconf_type ${kconf_name} # $arg1"
            fi

            continue
        fi

        echo "$fline"

    done < $in

    echo "mark `basename $in` end"
    echo "# <-- done file: $in"
    echo "#"

    return 0
}


start_time=`date +"%s"`

if [ -n "$outfile" ]; then
    header > $outfile
else
    header
fi

for f in $infiles; do
    if [ -z "$fail" ]; then
	if [ -n "$outfile" ]; then
	    process_file $f $f >> $outfile
	    ret=$?
	    if [ $ret -eq 1 ]; then
		fail=t
	    fi
	else
	    process_file $f $f
	    ret=$?
	    if [ $ret -eq 1 ]; then
		fail=t
	    fi
	fi
    fi
done

if [ -n "$fail" ]; then
    if [ -z "$intermediate" ]; then
	rm -f $outfile
    fi
    exit $ret
fi

stop_time=`date +"%s"`
duration=`expr $stop_time - $start_time`

if [ -n "$outfile" ]; then
    footer $duration $infiles >> $outfile
else
    footer $duration $infiles
fi

exit $ret
