#!/usr/bin/env bash

if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
    echo "Your BASH shell version (${BASH_VERSION}) is too old." >&2
    echo "Run bootstrap on a machine with BASH 4.x" >&2
    exit 1
fi

# see #849
unset CDPATH

########################################
# Common meta-language implementation. Syntax:
#
# The template file is processed line by line, with @@VAR@@ placeholders
# being replaced with a value of the VAR variable.
# Special lines start with '#!' and a keyword:
#
# #!//
#     Comment, the rest of the line is ignored
# #!if COND
#     Conditional: the lines until the matching #!end-if are processed
#     only if the conditional COND evaluates to true.
# #!foreach NAME
#     Iterate over NAME entities (the iterator must be set up first
#     using the set_iter function), processing the lines until the matching
#     #!end-foreach line.
#
# Also, several forms of @@VAR@@ expansions are possible:
#
# @@VAR@@
#     Just the value of the variable VAR
# @@VAR|@@
#     The value of VAR made into Kconfig-name: all non-alphanumeric character
#     replaced with underscores; upper-cased.
# @@VAR?val@@
#     If VAR is non-empty, insert value "val". Otherwise, insert nothing.
# @@*ITER@@
#     Expands to a space separated list of values for the iterator ITER.
#     Postprocess operations, if any, are applied to each value.

declare -A info

debug()
{
    if [ -n "${DEBUG}" ]; then
        echo "DEBUG :: $@" >&2
    fi
}

msg()
{
    if [ -z "${QUIET}" ]; then
        echo "INFO  :: $@" >&2
    fi
}

warn()
{
    echo "WARN  :: $@" >&2
}

error()
{
    echo "ERROR :: $@" >&2
    exit 1
}

find_end()
{
    local token="${1}"
    local count=1

    # Skip first line, we know it has the proper '#!' command on it
    endline=$[l + 1]
    while [ "${endline}" -le "${end}" ]; do
        case "${tlines[${endline}]}" in
            "#!${token} "*)
                count=$[count + 1]
                ;;
            "#!end-${token}")
                count=$[count - 1]
                ;;
        esac
        if [ "${count}" = 0 ]; then
            return
        fi
        endline=$[endline + 1]
    done
    error "${template}:${l}: '${token}' token is unpaired"
}

set_iter()
{
    local name="${1}"
    local -a temp

    if [ "${info[iter_${name}]+set}" = "set" ]; then
        error "Iterator over '${name}' is already set up"
    fi
    shift
    debug "Setting iterator over '${name}' to '$*'"
    temp=("$@")
    info[iter_${name}]="$*"
    info[#${name}]=${#temp[@]}
}

run_if()
{
    local cond="$*"
    local endline

    find_end "if"
    if eval "${cond}"; then
        debug "True conditional '${cond}' at lines ${l}..${endline}"
        run_lines $[l + 1] $[endline - 1]
    else
        debug "False conditional '${cond}' at lines ${l}..${endline}"
    fi
    lnext=$[endline + 1]
    debug "Continue at line ${lnext}"
}

do_foreach()
{
    local var="${1}"
    local -A saveinfo
    local v k

    shift
    if [ "`type -t enter_${var}`" != "function" ]; then
        error "No parameter setup routine for iterator over '${var}'"
    fi
    if [ "x${info[iter_${var}]+set}" != "xset" ]; then
        error "Iterator over '${var}' not configured"
    fi
    for v in ${info[iter_${var}]}; do
        # This works in bash 4.4, but not in bash 4.3:
        # local saveinfo=`declare -p info`
        # ...
        # eval "${saveinfo}"
        # Therefore, need to save key-by-key
        saveinfo=()
        for k in "${!info[@]}"; do
            saveinfo["${k}"]=${info["${k}"]}
        done
        if eval "enter_${var} ${v}"; then
            eval "$@"
        fi
        info=()
        for k in "${!saveinfo[@]}"; do
            info["${k}"]=${saveinfo["${k}"]}
        done
    done
}

run_foreach()
{
    local endline
    local var="${1}"
    shift

    if [ "${info[iter_${var}]+set}" != "set" ]; then
        error "${template}:${l}: iterator over '${var}' is not defined"
    fi
    find_end "foreach"
    debug "Loop over '${var}', lines ${l}..${endline}"
    do_foreach ${var} run_lines_if $[l + 1] $[endline - 1] "$*"
    lnext=$[endline + 1]
    debug "Continue at line ${lnext}"
}

run_lines_if()
{
    local start="${1}"
    local end="${2}"
    shift 2
    local cond="$*"
    local a prev

    for a in ${cond}; do
        if [ -n "${prev}" ]; then
            case "${prev}" in
                if-differs)
                    if [ "${info[${a}]}" = "${saveinfo[${a}]}" ]; then
                        return
                    fi
                    ;;
                *)
                    error "${template}:${l}: unknown condition '${prev}' for loop"
                    ;;
            esac
            prev=
        else
            prev=${a}
        fi
    done
    run_lines "${start}" "${end}"
}

run_lines()
{
    local start="${1}"
    local end="${2}"
    local l lnext s s1 v vn vp pp p val val0 is_iter pp_saved
    local -a vpl

    debug "Running lines ${start}..${end}"
    l=${start}
    while [ "${l}" -le "${end}" ]; do
        lnext=$[l+1]
        s="${tlines[${l}]}"
        # Expand @@foo@@ to ${info[foo]}. First escape variables/backslashes for evals below.
        s="${s//\\/\\\\}"
        s="${s//\$/\\\$}"
        s1=
        while [ -n "${s}" ]; do
            case "${s}" in
                *@@*@@*)
                    v="${s#*@@}"
                    v="${v%%@@*}"
                    # $v now has the complete reference. First check if it is cached already.
                    if [ "${info[${v}]+set}" != "set" ]; then
                        case "${v}" in
                            "*"*) is_iter=y; vn="${v#\*}";;
                            *) is_iter=n; vn="${v}";;
                        esac
                        # $vn is now the reference without the preceding iterator
                        vp="${vn%%[|?]*}"
                        pp="${vn#${vp}}"
                        # $vp is name of the variable proper, $pp is any postprocessing
                        if [ "${is_iter}" = "n" ]; then
                            if [ "${info[${vp}]+set}" != "set" ]; then
                                error "${template}:${l}: reference to undefined variable '${vp}'"
                            fi
                            vpl=( "${info[${vp}]}" )
                        else
                            if [ "${info[iter_${vp}]+set}" != "set" ]; then
                                error "${template}:${l}: iterator over '${vp} is not defined"
                            fi
                            vpl=( ${info[iter_${vp}]} )
                        fi
                        # ${vpl[@]} now is an array of values to be transformed.
                        val=
                        pp_saved="${pp}"
                        for val0 in "${vpl[@]}"; do
                            debug "val0 [${val0}]"
                            pp="${pp_saved}"
                            # Apply postprocessing(s)
                            while [ -n "${pp}" ]; do
                                case "${pp}" in
                                    "|"*)
                                        # Kconfigize
                                        pp="${pp#|}"
                                        val0=${val0//[^0-9A-Za-z_]/_}
                                        val0=${val0^^}
                                        ;;
                                    "?"*)
                                        pp="${pp#?}"
                                        p="${pp%%[|?]*}"
                                        pp="${pp#${p}}"
                                        val0="${val0:+${p}}"
                                        ;;
                                esac
                            done
                            val="${val:+${val} }${val0}"
                        done
                        # Cache for future references.
                        info[${v}]="${val}"
                    fi
                    s1="${s1}${s%%@@*}\${info[${v}]}"
                    s="${s#*@@*@@}"
                    ;;
                *@@*)
                    error "${template}:${l}: non-paired @@ markers"
                    ;;
                *)
                    s1="${s1}${s}"
                    break
                    ;;
            esac
        done
        s=${s1}

        debug "Evaluate: ${s}"
        case "${s}" in
            "#!if "*)
                run_if ${s#* }
                ;;
            "#!foreach "*)
                run_foreach ${s#* }
                ;;
            "#!//"*)
                # Comment, do nothing
                ;;
            "#!"*)
                error "${template}:${l}: unrecognized command"
                ;;
            *)
                # Not a special command
                eval "echo \"${s//\"/\\\"}\""
                ;;
        esac
        l=${lnext}
    done
}

run_template()
{
    local -a tlines
    local src="${1}"

    if [ ! -r "${src}" ]; then
        error "Template '${src}' not found"
    fi
    template="${src}"
    debug "Running template ${src}"
    mapfile -O 1 -t tlines < "${src}"
    run_lines 1 ${#tlines[@]}
}

########################################

# Leave only relevant portion of the string
relevantize()
{
    local p pb pa vx
    local v="${1}"
    shift

    # Find the first match and contract to the matching portion.
    for p in "$@"; do
        pb=${p%|*}
        pa=${p#*|}
        eval "vx=\${v#${pb}${pa}}"
        if [ "${v%${pa}${vx}}" != "${v}" ]; then
            v=${v%${pa}${vx}}
            break
        fi
    done
    echo "${v}"
}

# Helper for cmp_versions: compare an upstream/debian portion of
# a version. Returns 0 if equal, otherwise echoes "-1" or "1" and
# returns 1.
equal_versions()
{
    local v1="${1}"
    local v2="${2}"
    local p1 p2

    # Compare alternating non-numerical/numerical portions, until
    # non-equal portion is found or either string is exhausted.
    while [ -n "${v1}" -a -n "${v2}" ]; do
        # Find non-numerical portions and compare lexicographically
        p1="${v1%%[0-9]*}"
        p2="${v2%%[0-9]*}"
        v1="${v1#${p1}}"
        v2="${v2#${p2}}"
        #debug "lex [${p1}] v [${p2}]"
        if [ "${p1}" \< "${p2}" ]; then
            echo "-1"
            return 1
        elif [ "${p1}" \> "${p2}" ]; then
            echo "1"
            return 1
        fi
        #debug "rem [${v1}] v [${v2}]"
        # Find numerical portions and compare numerically
        p1="${v1%%[^0-9]*}"
        p2="${v2%%[^0-9]*}"
        v1="${v1#${p1}}"
        v2="${v2#${p2}}"
        #debug "num [${p1}] v [${p2}]"
        if [ "${p1:-0}" -lt "${p2:-0}" ]; then
            echo "-1"
            return 1
        elif [ "${p1:-0}" -gt "${p2:-0}" ]; then
            echo "1"
            return 1
        fi
        #debug "rem [${v1}] v [${v2}]"
    done
    if [ -n "${v1}" ]; then
        echo "1"
        return 1
    elif [ -n "${v2}" ]; then
        echo "-1"
        return 1
    fi
    return 0
}

# Compare two version strings, similar to sort -V. But we don't
# want to depend on GNU sort availability on the host.
# See http://www.debian.org/doc/debian-policy/ch-controlfields.html
# for description of what the version is expected to be.
# Returns "-1", "0" or "1" if first version is earlier, same or
# later than the second.
cmp_versions()
{
    local v1="${1}"
    local v2="${2}"
    local e1=0 e2=0 d1=0 d2=0

    # Case-insensitive comparison
    v1="${v1^^}"
    v2="${v2^^}"

    # Find if the versions contain epoch part
    case "${v1}" in
        *:*)
            e1="${v1%%:*}"
            v1="${v1#*:}"
            ;;
    esac
    case "${v2}" in
        *:*)
            e2="${v2%%:*}"
            v2="${v2#*:}"
            ;;
    esac

    # Compare epochs numerically
    if [ "${e1}" -lt "${e2}" ]; then
        echo "-1"
        return
    elif [ "${e1}" -gt "${e2}" ]; then
        echo "1"
        return
    fi

    # Find if the version contains a "debian" part.
    # v1/v2 will now contain "upstream" part.
    case "${v1}" in
        *-*)
            d1=${v1##*-}
            v1=${v1%-*}
            ;;
    esac
    case "${v2}" in
        *-*)
            d2=${v2##*-}
            v2=${v2%-*}
            ;;
    esac

    # Compare upstream
    if equal_versions "${v1}" "${v2}" && equal_versions "${d1}" "${d2}"; then
        echo "0"
    fi
}

# Sort versions, descending
sort_versions()
{
    local sorted
    local remains="$*"
    local next_remains
    local v vx found

    while [ -n "${remains}" ]; do
        #debug "Sorting [${remains}]"
        for v in ${remains}; do
            found=yes
            next_remains=
            #debug "Candidate ${v}"
            for vx in ${remains}; do
                #debug "${v} vs ${vx} :: `cmp_versions ${v} ${vx}`"
                case `cmp_versions ${v} ${vx}` in
                    1)
			    next_remains+=" ${vx}"
			    ;;
                    0)
			    ;;
                    -1)
			    found=no
			    #debug "Bad: earlier than ${vx}"
			    break
			    ;;
                esac
            done
            if [ "${found}" = "yes" ]; then
                # $v is less than all other members in next_remains
                sorted+=" ${v}"
                remains="${next_remains}"
                #debug "Good candidate ${v} sorted [${sorted}] remains [${remains}]"
                break
            fi
        done
    done
    echo "${sorted}"
}

read_file()
{
    local l p

    while read l; do
        l="${p}${l}"
        p=
        case "${l}" in
            "")
                continue
                ;;
            *\\)
                p="${l%\\}"
                continue
                ;;
            "#"*)
                continue
                ;;
            *=*)
                echo "info[${l%%=*}]=${l#*=}"
                ;;
            *)
                error "syntax error in '${1}': '${l}'"
                ;;
        esac
    done < "${1}"
}

read_package_desc()
{
    read_file "packages/${1}/package.desc"
}

read_version_desc()
{
    read_file "packages/${1}/${2}/version.desc"
}

find_forks()
{
    local -A info

    info[preferred]=${1}
    eval `read_package_desc ${1}`

    if [ -n "${info[master]}" ]; then
        pkg_nforks[${info[master]}]=$[pkg_nforks[${info[master]}]+1]
        pkg_forks[${info[master]}]+=" ${1} "
    else
        pkg_preferred[${1}]=${info[preferred]}
        pkg_nforks[${1}]=$[pkg_nforks[${1}]+1]
        pkg_forks[${1}]+=" ${1} "
        pkg_milestones[${1}]=`sort_versions ${info[milestones]}`
        pkg_relevantpattern[${1}]=${info[relevantpattern]}
        pkg_masters+=( "${1}" )
    fi
    # Keep sorting so that preferred fork is first
    if [ -n "${pkg_preferred[${1}]}" ]; then
        pkg_forks[${1}]="${pkg_preferred[${1}]} ${pkg_forks[${1}]##* ${pkg_preferred[${1}]} } ${pkg_forks[${1}]%% ${pkg_preferred[${1}]} *}"
    fi
}

enter_fork()
{
    local fork="${1}"
    local versions
    local only_obsolete only_experimental
    local -A seen_selectors

    # Set defaults
    info[obsolete]=
    info[experimental]=
    info[repository]=
    info[repository_branch]=
    info[repository_cset]=
    info[repository_subdir]=
    info[bootstrap]=
    info[fork]=${fork}
    info[pkg_name]=${fork}
    info[pkg_label]=${fork}
    info[mirrors]=
    info[src_release]=
    info[src_devel]=
    info[src_custom]=
    info[archive_filename]='@{pkg_name}-@{version}'
    info[archive_dirname]='@{pkg_name}-@{version}'
    info[versionlocked]=
    info[origin]=
    info[signature_format]=

    eval `read_package_desc ${fork}`

    if [ -r "packages/${info[origin]}.help" ]; then
        info[originhelp]=`sed 's/^/      /' "packages/${info[origin]}.help"`
    else
        info[originhelp]="      ${info[master]} from ${info[origin]}."
    fi

    if [ -n "${info[repository]}" ]; then
        info[vcs]=${info[repository]%% *}
        info[repository_url]=${info[repository]#* }
    fi

    info[mirrors]=${info[mirrors]//$\(/\\$\(}

    versions=`cd packages/${fork} && \
        for f in */version.desc; do [ -r "${f}" ] && echo "${f%/version.desc}"; done`
    versions=`sort_versions ${versions}`

    set_iter version ${versions}
    info[all_versions]=${versions}

    check_relevant_pattern()
    {
        if [ "x${seen_selectors[${info[ver_sel]}]+set}" = "xset" ]; then
            error "${info[pkg_name]}: version ${info[ver]} conflicts with version ${seen_selectors[${info[ver_sel]}]} (${info[ver_sel]} selector)"
        else
            seen_selectors[${info[ver_sel]}]=${info[ver]}
        fi
    }
    do_foreach version check_relevant_pattern

    # If a fork does not define any versions at all ("rolling release"), do not
    # consider it obsolete/experimental unless it is so marked in the fork's
    # description.
    if [ -n "${versions}" ]; then
        only_obsolete=yes
        only_experimental=yes

        check_obsolete_experimental()
        {
            [ -z "${info[obsolete]}" ] && only_obsolete=
            [ -z "${info[experimental]}" ] && only_experimental=
        }
        do_foreach version check_obsolete_experimental
        info[only_obsolete]=${only_obsolete}
        info[only_experimental]=${only_experimental}
    else
        info[only_obsolete]=${info[obsolete]}
        info[only_experimental]=${info[experimental]}
    fi
}

enter_version()
{
    local version="${1}"

    eval `read_version_desc ${info[fork]} ${version}`
    info[ver]=${version}
    info[ver_sel]=`relevantize ${version} ${info[relevantpattern]}`
}

enter_milestone()
{
    local ms="${1}"

    info[ms]=${ms}
    if [ -n "${info[ver]}" ]; then
        if [ -n "${info[version_number]}" ]; then
            info[version_cmp_milestone]=`cmp_versions ${info[version_number]} ${info[ms]}`
        else
            info[version_cmp_milestone]=`cmp_versions ${info[ver]} ${info[ms]}`
        fi
    fi
}

gen_packages()
{
    local -A pkg_forks pkg_milestones pkg_nforks pkg_relevantpattern
    local -a pkg_masters pkg_all pkg_preferred

    pkg_all=( `cd packages && \
        ls */package.desc 2>/dev/null | \
        while read f; do [ -r "${f}" ] && echo "${f%/package.desc}"; done | \
        xargs echo` )

    debug "Packages: ${pkg_all[@]}"

    # We need to group forks of the same package into the same
    # config file. Discover such relationships and only iterate
    # over "master" packages at the top.
    for p in "${pkg_all[@]}"; do
        find_forks "${p}"
    done
    msg "Master packages: ${pkg_masters[@]}"

    # Now for each master, create its kconfig file with version
    # definitions. As a byproduct, generate a list of all package
    # versions for maintenance purposes.
    exec 3>"maintainer/package-versions"
    for p in "${pkg_masters[@]}"; do
        msg "Generating '${config_versions_dir}/${p}.in'"
        exec >"${config_versions_dir}/${p}.in"
        # Base definitions for the whole config file
        info=( \
            [master]=${p} \
            [nforks]=${pkg_nforks[${p}]} \
            [relevantpattern]=${pkg_relevantpattern[${p}]} \
            )
        set_iter fork ${pkg_forks[${p}]}
        set_iter milestone ${pkg_milestones[${p}]}

        run_template "maintainer/kconfig-versions.template"
        run_template "maintainer/package-versions.template" >&3
    done
}

msg "*** Generating package version descriptions"
config_versions_dir=config/versions
rm -rf "${config_versions_dir}"
mkdir -p "${config_versions_dir}"
gen_packages

get_components()
{
    local dir="${1}"
    local f b

    for f in ${dir}/*.in; do
        b=${f#${dir}/}
        echo ${b%.in}
    done
}

enter_choice()
{
    local choice="${1}"
    local input="config/${info[dir]}/${choice}.in"
    local l ln

    info[choice]="${choice}"
    info[pkg]="${choice}"

    # Not local, we need these arrays be set in enter_dependency/enter_help
    deplines=( )
    helplines=( )
    ln=0
    while read l; do
        ln=$[ln+1]
        case "${l}" in
        "## help "*)
            helplines+=( "${l#\#\# help }" )
            ;;
        "## depends "*|"## select "*|"## default "*)
            deplines+=( "${l#\#\# }" )
            ;;
        "## no-package")
            info[pkg]=
            ;;
        "## package "*)
            info[pkg]=${l#\#\# package }
            ;;
        "##"|"## help")
            # accept empty, for formatting
            ;;
        "##"*)
            error "${input}:${ln}: unrecognized command"
            ;;
        esac
    done < "${input}"
    set_iter dependency "${!deplines[@]}"
    set_iter help "${!helplines[@]}"
}

enter_dependency()
{
    info[depline]="${deplines[${1}]}"
}

enter_help()
{
    info[helpline]="${helplines[${1}]}"
}

gen_selection()
{
    local type="${1}"
    local dir="${2}"
    local label="${3}"

    msg "Generating ${dir}.in (${type})"
    exec >"${config_gen_dir}/${dir}.in"
    info=( \
        [dir]=${dir} \
        [label]="${label}" \
        )
    set_iter choice `get_components config/${dir}`
    run_template "maintainer/kconfig-${type}.template"
}

msg "*** Generating menu/choice selections"
config_gen_dir=config/gen
rm -rf "${config_gen_dir}"
mkdir -p "${config_gen_dir}"

gen_selection choice arch "Target Architecture"
gen_selection choice kernel "Target OS"
gen_selection choice cc "Compiler"
gen_selection choice binutils "Binutils"
gen_selection choice libc "C library"
gen_selection menu debug "Debug facilities"
gen_selection menu comp_tools "Companion tools"
gen_selection menu comp_libs "Companion libraries"

msg "*** Gathering the list of data files to install"
{
    declare -A seen_files
    echo -n "verbatim_data ="
    find COPYING config contrib licenses.d packages samples scripts -type f | LANG=C sort | while read f; do
        # Implement some kind of .installignore for these files?
        case "${f}" in
            # Avoid temp files
            *.sw[po])
                continue
                ;;
            # And, some files automake insists we must have
            scripts/compile | scripts/missing | scripts/depcomp | scripts/ltmain.sh | scripts/install-sh)
                continue
                ;;
            #
            # will produce. FIXME: create this file at the time of 'ct-ng build'.
            config/configure.in.in | config/configure.in)
                continue
                ;;
        esac
        # Checks & substitutions above may result in duplicate files
        if [ -n "${seen_files[${f}]}" ]; then
            continue
        fi
        echo " \\"
        echo -n "	${f}"
        seen_files[${f}]=y
    done
} > verbatim-data.mk

msg "*** Running autoreconf"
autoreconf -Wall --force -I m4

msg "*** Done!"
