#!/bin/sh

VERSION="0.6.0"

check_init()
{

# we rely on directory structure:
# PREFIX/bin/build2markup
# PREFIX/share/build2markup/<filez>

# analogue for ../../
tmp_dir="$(local_which $0)"
tmp_dir="${tmp_dir%/*}"
tmp_dir="${tmp_dir%/*}"

base_dir="$(cd $tmp_dir ; pwd -P )"
bin_DIR="${base_dir}/bin"
data_DIR="${base_dir}/share/build2markup"

DATA_DIR="${DATA_DIR:-"${data_DIR}"}"

base_OUTPUT="$(pwd)/var"
OUTPUT="${base_OUTPUT}"
EXT=".tex"
OUTPUT_FILE="${OUTPUT}/build.tex"
MARKER=
CONDMARKER=
CONDMARKER_ELSE=
CONDMARKER_END=
FINAL_WORD='final'

DROP_FILES=
DROP_PATCHES=
DROP_GLOBAL_DATA=
WORDS=
GLOBAL_HEADER=

PATH_FILES=

}

local_which()
{
    arg="$1"

    if test -x "${arg}" ; then
        printf "%s\n" "${arg}"
        return 0
    fi

    IFS=:
    for dir in ${PATH} ; do
        if test -x "${dir}/${arg}" ; then
            printf "%s\n" "${dir}/${arg}"
            return 0
        fi
    done
    return 1
}


check_prepare()
{
    if test -z "${base_OUTPUT}" ; then
        error "No correct output directory was set"
    fi
    if test ! -d "${DATA_DIR}" ; then
        error "Data directory was not found.\nExpected path: ${DATA_DIR}"
    fi

    for data_name in \
        "${DATA_DIR}/build_header" \
        "${DATA_DIR}/build_footer" \
        "${DATA_DIR}/method_header" \
        "${DATA_DIR}/block_open" \
        "${DATA_DIR}/block_close" \
        "${DATA_DIR}/marker" \
        "${DATA_DIR}/condmarker" \
        "${DATA_DIR}/condmarker_else" \
        "${DATA_DIR}/condmarker_end" \
        "${DATA_DIR}/dictionary" \
        "${DATA_DIR}/fileblock_open" \
        "${DATA_DIR}/fileblock_close" \
        "${DATA_DIR}/fileblock_item" \
        "${DATA_DIR}/links_item" \
        ; do
        if test ! -f "${data_name}" ; then
            errornum 2 "Data file is absent: ${data_name}"
        fi
    done

    MARKER="$( cat "${DATA_DIR}/marker" )"
    CONDMARKER="$( cat "${DATA_DIR}/condmarker" )"
    CONDMARKER_ELSE="$( cat "${DATA_DIR}/condmarker_else" )"
    CONDMARKER_END="$( cat "${DATA_DIR}/condmarker_end" )"
    DICTIONARY="$( cat "${DATA_DIR}/dictionary" )"

    if test -n "${WORDS}" ; then
        # so that all words may be checked in the same pattern
        WORDS=" ${WORDS} "
    fi

    if test -n "${OUTPUT_FILE}" ; then

        if test -z "${DROP_GLOBAL_DATA}" ; then
            if test -z "${GLOBAL_HEADER}" ; then
                GLOBAL_HEADER="${DATA_DIR}/global_header"
            fi
            for data_name in \
                "${GLOBAL_HEADER}" \
                "${DATA_DIR}/global_footer" ; do
                if test ! -f "${data_name}" ; then
                    errornum 2 "Data file is absent: ${data_name}"
                fi
            done
        fi
        OUTPUT="$( dirname "${OUTPUT_FILE}" )"
        mkdir -p "${OUTPUT}"
        if test -n "${GLOBAL_HEADER}" ; then
            cat "${GLOBAL_HEADER}" > "${OUTPUT_FILE}"
        else
            : > "${OUTPUT_FILE}"
        fi
    else
        mkdir -p "${OUTPUT}"
    fi

    # we cannot check PATH_FILES on correctness, as it is actual only afterwords,
    # after generated html files are placed at their target location
}

print_version()
{
    echo "${VERSION}"
    exit 0
}

print_help()
{
    echo "\
Usage:
build2markup [options] [--] DIR
build2markup --help | --version

Convert contents of anybuild directory DIR to page in markup language (LaTeX by default).
"
    exit 0
}

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

errornum()
{
    num="$1"
    shift

    echo "Error:" 1>&2
    echo "$@" 1>&2
    exit "${num}"
}

cat_filelist()
{
    filelist="${1}"
    separator="${2}"
    # for potential pathes with space inside, instead of simple 'cat ${filelist}'

    printf "%s\n" "${filelist}" | while read listname ; do
        if test -n "${listname}" ; then
            cat "${listname}"
            if test -n "${separator}" ; then
                printf "%s\n" "${separator}"
            fi
        fi
    done
}

print_build_before_methods()
{
    alt_source="$1"

    ( printf "\n" ; cat "${alt_source}" ) | sed -n -e "1,/^${MARKER}/p" | sed -e "/^${MARKER}/d"
}

print_upper_method_content()
{
    method_name="$1"
    alt_source="$2"

    tmp_marker='%#%#%#%# '

    # first sed replaces first appearance of method_name with tmp_marker
    # second one prints block between method_name and the beginning of next block (starts with MARKER)
    # third sed deletes strings with markers
    sed \
        -e "1 s/^${MARKER}${method_name}/${tmp_marker}${method_name}/; t" \
        -e "1,/${MARKER}${method_name}/ s/${MARKER}${method_name}/${tmp_marker}${method_name}/" \
        "${alt_source}" | \
    sed -n "/^${tmp_marker}${method_name}/,/^${MARKER}/p" | sed -e "/^${MARKER}/d;/^${tmp_marker}/d"
}

rearrange_build()
{
    keys="$1"
    alt_source="$2"

    flag_top_add=
    top_alt_content="$(print_build_before_methods "${alt_source}" )"
    if test -n "$(printf "%s\n" "${top_alt_content}" | sed -e '/^$/d')" ; then
        flag_top_add=1
    fi
    flag_skip=
    keys_visited=
    while IFS='' read -r buildline <&0 ; do
        case "${buildline}" in
            *[a-z_A-Z]'()'*)
                method_name="${buildline%()*}"
                method_name="${method_name##* }"
                flag_skip=

                if test -n "${flag_top_add}" ; then
                    # add top from alternative module, only for the first time
                    printf "%s\n" "${top_alt_content}"
                    flag_top_add=
                fi

                case "${keys}" in
                    *" ${method_name} "*)
                        print_upper_method_content "${method_name}" "${alt_source}"
                        flag_skip=1
                        keys_visited="${keys_visited} ${method_name}"
                    ;;
                    *)
                        printf "%s\n" "${buildline}"
                    ;;
                esac
            ;;
            *)
                if test -z "${flag_skip}" ; then
                    printf "%s\n" "${buildline}"
                fi
            ;;
        esac
    done

    keys_visited=" ${keys_visited} "
    # methods, added by alt_source and absent in main build from stream
    for missed_name in ${keys} ; do
        case "${keys_visited}" in
            *" ${missed_name} "*)
                # do nothing
            ;;
            *)
                # found missed method
                print_upper_method_content "${missed_name}" "${alt_source}"
            ;;
        esac
    done
}

#set up finalised text with build code
#concerning multiple build files and datawords
finalize_build()
{
    content_dir="${1}"

    buildfiles="$( ls -1 "${content_dir}/"*.build )"
    if test -z "${buildfiles}" ; then
        errornum 3 "No build files in directory: ${content_dir}"
    fi
    multi_build=
    if test "${buildfiles}" != "${buildfiles%
*}" ; then
        multi_build=1
    fi

    if test -z "${multi_build}" ; then
        merged_build="${buildfiles}"
    else
        merged_build="${OUTPUT}/tmp.build"
        cat_filelist "${buildfiles}" > "${merged_build}"
    fi
    # search for matching *.sh modules
    list_loaded=
    for single_word in ${WORDS} ; do
        if test -f "${content_dir}/${single_word}.sh" ; then
            list_loaded="${content_dir}/${single_word}.sh
${list_loaded}"
        fi
    done

    # \([a-z_A-Z]*\) - template for 'name_function'
    # \(().*\) - brackets of function declaration name_function() and random stuff after
    # after all, replace 'name_function()' with:
    # MARKER name_function
    # name_function()
    cat_filelist "${list_loaded}" "${MARKER}" | sed -e "s/^\([a-z_A-Z]*\)\(().*\)/${MARKER}\1\n\1\2/" > "${OUTPUT}/tmp.modules"
    # array of functions, present in .sh modules
    key_names="$(sed -n -e "/^${MARKER}/p" "${OUTPUT}/tmp.modules" | sed -e "s/^${MARKER}//" | sort | uniq | tr '\n' ' ')"
    key_names=" ${key_names} "
    cat "${merged_build}" | rearrange_build "${key_names}" "${OUTPUT}/tmp.modules" > "${OUTPUT}/tmp.rearbuild"
    merged_build="${OUTPUT}/tmp.rearbuild"

    printf "%s\n" "${merged_build}"
}

# process conditional blocks in text and output single variant
condition_text()
{
    flag_skip=
    while IFS='' read -r line <&0 ; do
        case "${line}" in
            ${CONDMARKER}*)
                condition="${line#${CONDMARKER}}"
                case "${WORDS}" in
                    *" ${condition} "*)
                        # condition matched, leave the text in place
                    ;;
                    *)
                        #did not match, skip it
                        flag_skip=1
                    ;;
                esac
            ;;
            ${CONDMARKER_ELSE}*)
                # revert flag
                if test -n "${flag_skip}" ; then
                   flag_skip=
                else
                   flag_skip=1
                fi
            ;;
            ${CONDMARKER_END}*)
                flag_skip=
            ;;
            *)
                if test -z "${flag_skip}" ; then
                    printf "%s\n" "${line}"
                fi
            ;;
        esac
    done
}

descripted_method()
{
    method="${1}"

    descripted="${DICTIONARY#*
${method}:}"
    if test "${DICTIONARY}" != "${descripted}" ; then
        descripted="${descripted%%
*}"
        printf "%s\n" "${descripted}"
    else
        printf "%s\n" "${method}"
    fi
}

linked_files()
{
    content_dir="${1}"

    if test -z "${PATH_FILES}" ; then
        return
    fi

    package_name="$(basename "${content_dir}")"
    (
    cd "${content_dir}"
    : > "${OUTPUT}/tmp.file.patches"
    : > "${OUTPUT}/tmp.file.build"
    : > "${OUTPUT}/tmp.file.runscripts"
    : > "${OUTPUT}/tmp.file.other"
    find . -type f | sort | while read name ; do
        case "${name}" in
            './patch/'*)
                printf "%s\n" "${name}" >> "${OUTPUT}/tmp.file.patches"
            ;;
            *'.build'|*'.sh')
                printf "%s\n" "${name}" >> "${OUTPUT}/tmp.file.build"
            ;;
            *'.postinst'|*'/postinst'|*'.preinst'|*'/preinst'|*'.postrm'|*'/postrm'|*'.prerm'|*'/prerm')
                printf "%s\n" "${name}" >> "${OUTPUT}/tmp.file.runscripts"
            ;;
            *)
                printf "%s\n" "${name}" >> "${OUTPUT}/tmp.file.other"
            ;;
        esac
    done
    )

    if test -s "${OUTPUT}/tmp.file.patches" ; then
        sed -e "s/FILEBLOCK_NAME/Patches/" "${DATA_DIR}/fileblock_open"
        cat "${OUTPUT}/tmp.file.patches" | while read fileline ; do
            filename="${fileline#./patch/}"
            fileurl="${PATH_FILES}/${package_name}${fileline#\.}"
            sed -e "s%ITEM_URL%${fileurl}%;s%ITEM_NAME%${filename}%" "${DATA_DIR}/fileblock_item"
        done
        sed -e "s/FILEBLOCK_NAME/Patches/" "${DATA_DIR}/fileblock_close"
    fi

    if test -s "${OUTPUT}/tmp.file.runscripts" ; then
        sed -e "s/FILEBLOCK_NAME/Run time scripts/" "${DATA_DIR}/fileblock_open"
        cat "${OUTPUT}/tmp.file.runscripts" | while read fileline ; do
            filename="${fileline#./}"
            fileurl="${PATH_FILES}/${package_name}${fileline#\.}"
            sed -e "s%ITEM_URL%${fileurl}%;s%ITEM_NAME%${filename}%" "${DATA_DIR}/fileblock_item"
        done
        sed -e "s/FILEBLOCK_NAME/Run time scripts/" "${DATA_DIR}/fileblock_close"
    fi

    if test -s "${OUTPUT}/tmp.file.build" ; then
        sed -e "s/FILEBLOCK_NAME/Build code/" "${DATA_DIR}/fileblock_open"
        cat "${OUTPUT}/tmp.file.build" | while read fileline ; do
            filename="${fileline#./}"
            fileurl="${PATH_FILES}/${package_name}${fileline#\.}"
            sed -e "s%ITEM_URL%${fileurl}%;s%ITEM_NAME%${filename}%" "${DATA_DIR}/fileblock_item"
        done
        sed -e "s/FILEBLOCK_NAME/Build code/" "${DATA_DIR}/fileblock_close"
    fi

    if test -s "${OUTPUT}/tmp.file.other" ; then
        sed -e "s/FILEBLOCK_NAME/Other files/" "${DATA_DIR}/fileblock_open"
        cat "${OUTPUT}/tmp.file.other" | while read fileline ; do
            filename="${fileline#./}"
            fileurl="${PATH_FILES}/${package_name}${fileline#\.}"
            sed -e "s%ITEM_URL%${fileurl}%;s%ITEM_NAME%${filename}%" "${DATA_DIR}/fileblock_item"
        done
        sed -e "s/FILEBLOCK_NAME/Other files/" "${DATA_DIR}/fileblock_close"
    fi
}

meta_info()
{
    content_dir="${1}"

    if test -f "${content_dir}/meta.txt" ; then
        all_links="$( sed -n -e "/^url:/p;/^url\..*:/p" "${content_dir}/meta.txt" )"
        if test -n "${all_links}" ; then
            sed -e "s/FILEBLOCK_NAME/Links/" "${DATA_DIR}/fileblock_open"
            printf "%s\n" "${all_links}" | while read linkline ; do
                linkbody="${linkline#*:}"
                linkkey="${linkline%${linkbody}}"
                sed -e "s%LINK_DESC%${linkkey}%;s%LINK_URL%${linkbody}%" "${DATA_DIR}/links_item"
            done
            sed -e "s/FILEBLOCK_NAME/Links/" "${DATA_DIR}/fileblock_close"
        fi
    fi
}

process_dir()
{
    content_dir="${1}"

    markup_output=
    buildname="$( basename "${content_dir}" )"
    if test -n "${OUTPUT_FILE}" ; then
        markup_output="${OUTPUT_FILE}"
    else
        markup_output="${OUTPUT}/${buildname}${EXT}"
    fi

    merged_build="$(finalize_build "${content_dir}" )"

    TEXT=
    if test -f "${content_dir}/text" ; then
        TEXT="$(cat ${content_dir}/text)"
    fi
    # remove conditional parts
    TEXT="$( printf "%s\n" "${TEXT}" | condition_text )"

    # gather function names, which have comments on them in text
    keys="$(
    printf "%s\n" "${TEXT}" | sed -n -e "/^${MARKER}/p" | while IFS='' read -r markerline ; do
        printf "%s" " ${markerline#${MARKER}}"
    done
    )"
    keys="${keys} "
    # text above all functions
    upper_text="$(printf "%s\n" "${TEXT}" | sed -n -e "0,/^${MARKER}/p")"
    upper_text="${upper_text%
${MARKER}*}"

    sed -e "s/PACKAGENAME/${buildname}/g;s/_/\\\_/g" "${DATA_DIR}/build_header" >> "${markup_output}"
    if test -n "${upper_text}" ; then
        printf "%s\n" "${upper_text}" >> "${markup_output}"
    fi
    cat "${DATA_DIR}/block_open" >> "${markup_output}"
    bufer_empty_line=
    cat "${merged_build}" | while IFS='' read -r line ; do
        case "${line}" in
            '')
                bufer_empty_line="${bufer_empty_line}
"
            ;;
            *[a-z_A-Z]'()'*)
                bufer_empty_line=
                cat "${DATA_DIR}/block_close" >> "${markup_output}"

                method_name="${line%()*}"
                method_name="${method_name##* }"
                desc_text="$(descripted_method ${method_name})"
                sed -e "s/METHODNAME/${desc_text}/g;s/_/\\\_/g" "${DATA_DIR}/method_header" >> "${markup_output}"

                case "${keys}" in
                    *" ${method_name} "*)
                        printf "%s\n" "${TEXT}" | sed -n "/^${MARKER}${method_name}/,/^${MARKER}/p" | sed -e "/^${MARKER}/d" >> "${markup_output}"
                    ;;
                esac
                cat "${DATA_DIR}/block_open" >> "${markup_output}"
                printf "%s\n" "${line}" >> "${markup_output}"
            ;;
            *)
                printf "%s\n" "${bufer_empty_line}${line}" >> "${markup_output}"
                bufer_empty_line=
            ;;
        esac
    done
    cat "${DATA_DIR}/block_close" "${DATA_DIR}/build_footer" >> "${markup_output}"

    # block for final text after all functions
    case "${keys}" in
        *" ${FINAL_WORD} "*)
            desc_text="$(descripted_method ${FINAL_WORD})"
            sed -e "s/METHODNAME/${desc_text}/g;s/_/\\\_/g" "${DATA_DIR}/method_header" >> "${markup_output}"
            printf "%s\n" "${TEXT}" | sed -n "/^${MARKER}${FINAL_WORD}/,/^${MARKER}/p" | sed -e "/^${MARKER}/d" >> "${markup_output}"
        ;;
    esac

    linked_files "${content_dir}" >> "${markup_output}"
    meta_info "${content_dir}" >> "${markup_output}"

    if test -z "${OUTPUT_FILE}" ; then
        printf "%s\n" "${markup_output}"
    fi
}

# weird behaviour in bash-5.0.17 for check inside case:
# the string below:
# *[:lower:_:upper:]'()'*)
# matches src_compile(), src_install(), but does not match src_config or src_check
#
# from the other hand, the used string
# *[a-z_A-Z]'()'*)
# successfully matches everything

process_resource()
{
    while read arg <&0 ; do
        if test -d "${arg}" ; then
            # directory with anybuild
            process_dir "${arg}"

        elif test -f "${arg}" ; then
            # plain file with build
            :
        else
            # string name, need to find content directory on our own
            :
        fi
    done
}

check_final()
{
    if test -n "${OUTPUT_FILE}" ; then
        if test -z "${DROP_GLOBAL_DATA}" ; then
            cat "${DATA_DIR}/global_footer" >> "${OUTPUT_FILE}"
        fi
        printf "%s\n" "${OUTPUT_FILE}"
    fi
}

# -------------------------- execution -----------------------------------------

check_init "${@}"

in_opt_cycle=1
input_args=

while test -n "$1" ; do
    arg="$1"
    shift

    if test -n "${in_opt_cycle}" ; then
        checked_arg="${arg#-}" || true
        if test "$checked_arg" != "${arg}" ; then
            case ${checked_arg} in
                D)
                    DROP_FILES=1
                    DROP_PATCHES=1
                ;;
                d)
                    DROP_FILES=1
                ;;
                a)
                    WORDS="$1"
                    shift || error "No argument for option -a"
                ;;
                t)
                    DATA_DIR="$1"
                    shift || error "No argument for option -t"
                ;;
                o)
                    OUTPUT_FILE="$1"
                    shift || error "No argument for option -o"
                ;;
                O)
                    OUTPUT="$1"
                    shift || error "No argument for option -O"
                ;;
                e)
                    EXT="$1"
                    shift || error "No argument for option -e"
                ;;
                H)
                    GLOBAL_HEADER="$1"
                    shift || error "No argument for option -H"
                ;;
                B)
                    DROP_GLOBAL_DATA=1
                ;;
                p)
                    PATH_FILES="$1"
                    shift || error "No argument for option -p"
                ;;

                -version)
                    print_version
                ;;
                -help)
                    print_help
                ;;
                -)
                    in_opt_cycle=
                ;;
                *)
                    error "Unknown option ${arg}"
                ;;
            esac

            # if it was non-terminal option, do not add it to argument array
            continue
        fi
    fi

    # not option
    input_args="${input_args}
${arg}"

done

check_prepare "${@}"

printf "%s\n" "${input_args}" | process_resource

if [ ! -t 0 ] ; then
    while read resource <&0 ; do
        printf "%s\n" "${resource}" | process_resource
    done
fi

check_final "${@}"

# ---------------------------------------------------------

# Copyright (c) 2022 Random Crew.
#
# Available under ISC License.

# ---------------------------------------------------------
