#!/bin/sh

set -o errexit

VERSION='0.5.1'
DIRS=
FILENAMES=".html"
CONFIGFILE=


check_init()
{
    # analogue for ../../
    tmp_dir="$(which $0)"
    tmp_dir="${tmp_dir%/*}"
    tmp_dir="${tmp_dir%/*}"
    base_dir="$(cd $tmp_dir ; pwd -P )"

    if test -r ${base_dir}/etc/crossref.conf ; then
        CONFIGFILE=${base_dir}/etc/crossref.conf
    elif test -r /etc/crossref.conf ; then
        CONFIGFILE=/etc/crossref.conf
    fi

    TMP_EDIT=$(mktemp)
    TMP_MARKERS=$(mktemp)
    TMP_MARKERS_MAN="${TMP_MARKERS}.man"
    TMP_MARKERS_FILE="${TMP_MARKERS}.file"
    TMP_MARKERS_TEXTLINK="${TMP_MARKERS}.txtlink"
    TMP_MARKERS_TEXTTARGET="${TMP_MARKERS}.txttarg"

    INNER_MARKERS_MAN="${TMP_MARKERS}.man.inner"
    INNER_MARKERS_FILE="${TMP_MARKERS}.file.inner"
    INNER_MARKERS_TEXTLINK="${TMP_MARKERS}.txtlink.inner"
    INNER_MARKERS_TEXTTARGET="${TMP_MARKERS}.txttarg.inner"

    MARKERS_MAN=
    MARKERS_FILE=
    MARKERS_TEXTLINK=
    MARKERS_TEXTTARGET=
}

check_prepare()
{
    if test ! -f "${CONFIGFILE}" ; then
        error_code "
No correct config file was found.
Set it up with -c option or check the installation:
it should normally be in ../../etc/crossref.conf relatively to executable crossref.
"
        exit ${error_NOCONFIG}
    fi
}

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

print_help()
{
    echo "\
Usage:
crossref [-c configfile] [-f '.html'] [--] DIR1 [DIR2 ... DIRN]
crossref --help | --version

Edit html files in each given DIR, cross-referencing them.
Initial text:
<span class="hrefstyle">page</span>
will be transformed to:
<span class="hrefstyle"><a class="hrefstyle" href="path/to/page.html">page</a></span>

<span class="ref">nameintext</span>
will be transformed to:
<span class="ref"><a class="ref" href="#inner_name">nameintext</a></span>
where inner_name is extracted from objects:
<a name="inner_name">nameintext</a>

That variant is also possible:
<span class="commandname">page</span>(1)
<span class="commandname"><a class="commandname" href="path/to/page.1.html">page</a></span>(1)

Option -c sets up the config with text patterns to search for and cross-reference.
If no config given, default one is searched in the following places:
\${base_dir}/etc/crossref.conf
/etc/crossref.conf
executable file crossref is installed into: \${base_dir}/bin/crossref

-f option provides extension of files to search and edit. '.html' is the default.
"
    exit 0
}

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

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

error_NOCONFIG=2
error_BADCONFIG=3

parse_config()
{
    : > ${TMP_MARKERS_MAN}
    : > ${TMP_MARKERS_FILE}
    : > ${TMP_MARKERS_TEXTLINK}
    : > ${TMP_MARKERS_TEXTTARGET}
    cat ${CONFIGFILE} | while read string ; do
        case "${string}" in
            '#'*)
                # commented string
                continue
            ;;
            '')
                # empty string
                continue
            ;;
        esac
        type="${string%%@*}"
        case "${type}" in
            manfile|file|textlink|texttarget)
                textsolid="${string#${type}@}"
                textsolid="${textsolid%%@*}"
                marker_open="${textsolid%___TEXT___*}"
                if test "${marker_open}" = "${textsolid}" ; then
                    error_code "Incorrect string in config file:
${string}

Template section has no marker for user text: ___TEXT___
"
                    exit ${error_BADCONFIG}
                fi
                marker_close="${textsolid#${marker_open}___TEXT___}"
                if test "${type}" = "manfile" ; then
                    printf "%s\n" "${marker_open}[[:print:]]*${marker_close}([0-9]*)" >> ${TMP_MARKERS_MAN}
                    printf "%s\n" "${marker_open}@${marker_close}" >> ${INNER_MARKERS_MAN}
                elif test "${type}" = "file" ; then
                    printf "%s\n" "${marker_open}[[:print:]]*${marker_close}" >> ${TMP_MARKERS_FILE}
                    printf "%s\n" "${marker_open}@${marker_close}" >> ${INNER_MARKERS_FILE}
                elif test "${type}" = "textlink" ; then
                    printf "%s\n" "${marker_open}[[:print:]]*${marker_close}" >> ${TMP_MARKERS_TEXTLINK}
                    printf "%s\n" "${marker_open}@${marker_close}" >> ${INNER_MARKERS_TEXTLINK}
                elif test "${type}" = "texttarget" ; then
                    printf "%s\n" "${marker_open}[[:print:]]*${marker_close}" >> ${TMP_MARKERS_TEXTTARGET}
                    printf "%s\n" "${marker_open}@${marker_close}" >> ${INNER_MARKERS_TEXTTARGET}
                fi
                continue
            ;;
            *)
                error_code "Incorrect string in config file:
${string}

Allowed types are:
manfile
file
textlink
textobject

The resulting type is:
${type}
"
                exit ${error_BADCONFIG}
            ;;
        esac
    done

    MARKERS_MAN="$(cat ${INNER_MARKERS_MAN})"
    MARKERS_FILE="$(cat ${INNER_MARKERS_FILE})"
    MARKERS_TEXTLINK="$(cat ${INNER_MARKERS_TEXTLINK})"
    MARKERS_TEXTTARGET="$(cat ${INNER_MARKERS_TEXTTARGET})"
}


relativedir()
{
    dir_first="$1"
    dir_second="$2"

    # remove common part from both paths

    backup_IFS="${IFS}"
    IFS=/
    for first_element in ${dir_first} ; do
        if test -z "${first_element}" ; then
            continue
        fi
        temp_element="${dir_second#/${first_element}}"
        if test "${temp_element}" != "${dir_second}" ; then
            case "${temp_element}" in
                /*|'')
                    dir_second="${temp_element}"
                    delete_from_first="${delete_from_first}/${first_element}"
                ;;
                *)
                    break
                ;;
            esac
        else
            break
        fi
    done
    dir_first="${dir_first#${delete_from_first}}"

    # replace remaining part of first path with backward paths /..
    backed_path=
    for dir in ${dir_first} ; do
        if test -z "${dir}" ; then
            continue
        fi
        backed_path="${backed_path}/.."
    done
    IFS="${backup_IFS}"

    if test -n "${backed_path}" ; then
        backed_path=${backed_path#/}
    elif test -n "${dir_second}" ; then
        backed_path="."
    fi
    backed_path="${backed_path}${dir_second}"

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

get_open_marker()
{
    string="$1"

    open_marker="${string%%@*}"
    printf "%s\n" "${open_marker}"
}
get_close_marker()
{
    string="$1"

    close_marker="${string#*@}"
    close_marker="${close_marker%%@*}"
    printf "%s\n" "${close_marker}"
}

find_marker_generic()
{
    entry="$1"
    markers_array="$2"

    printf "%s\n" "${markers_array}" | while read try_string ; do
        open_marker="$(get_open_marker "${try_string}" )"
        close_marker="$(get_close_marker "${try_string}" )"
        case "${entry}" in
            "${open_marker}"*"${close_marker}"*)
                printf "%s\n" "${try_string}"
                break
            ;;
        esac
    done
}

find_marker_file()
{
    entry="$1"
    filetype="$2"

    if test -n "${filetype}" ; then
        markers_array="${MARKERS_FILE}"
    else
        markers_array="${MARKERS_MAN}"
    fi

    find_marker_generic "${entry}" "${markers_array}"
}

find_marker_text()
{
    entry="$1"
    texttype="$2"

    if test -n "${texttype}" ; then
        # markers to wrap target
        markers_array="${MARKERS_TEXTTARGET}"
    else
        # default type
        markers_array="${MARKERS_TEXTLINK}"
    fi

    find_marker_generic "${entry}" "${markers_array}"
}

get_link_destination()
{
    toextract="$1"

    linkcontent="${toextract##* href=\"}"
    if test "${toextract}" = "${linkcontent}" ; then
        printf "%s\n" "string does not have needed href content:
${toextract}" >& 2
        return 1
    fi
    linkcontent="${linkcontent%%\"*}"
    linkcontent="${linkcontent#\#}"
    printf "%s\n" "${linkcontent}"
}

prepare_edit_file()
{
    main_dir="$1"
    rest_dirs="$2"
    filetype="$3"

    if test -t 0 ; then
        error "method reads from stdin"
    fi
    while read entry <&0 ; do
        content="${entry%([0-9]*)}"
        num=
        if test "${content}" != "${entry}" ; then
            # extract chapter of manual
            num="${entry#${content}(}"
            num="${num%)}"
        fi

        # find matching marker from array of possible ones

        match_marker="$(find_marker_file "${entry}" "${filetype}" )"
        open_marker="$(get_open_marker "${match_marker}" )"
        close_marker="$(get_close_marker "${match_marker}" )"

        # extract content of the string
        content="${content%${close_marker}}"
        content="${content#${open_marker}}"
        check_already_tagged="${content#<[:space:]*a[:space:]}"
        if test "${check_already_tagged}" != "${content}" ; then
            # already has link tag inside
            continue
        fi

        num_string=
        if test -n "${num}" ; then
            num_string=".${num}"
        fi
        filename_candidate="${content}${num_string}${FILENAMES}"

        # search for real file location
        found_filename=
        if test -r "${main_dir}/${filename_candidate}" ; then
            found_filename="${filename_candidate}"
        else
            for path in ${rest_dirs} ; do
                if test -r "${main_dir}/${path}/${filename_candidate}" ; then
                    found_filename="${path}/${filename_candidate}"
                    break
                fi
            done
        fi
        if test -n "${found_filename}" ; then
            if test -n "${num}" ; then
                num_text="(${num})"
            fi
            # prepare sed template s/tag/tag_with_link/
            text="${open_marker}<a href=\"${found_filename}\">${content}</a>${close_marker}${num_text}"
            printf "%s\n" "s#${entry}#${text}#g"
        fi
    done
}

prepare_edit_text()
{
    main_dir="$1"

    if test -t 0 ; then
        error "method reads from stdin"
    fi
    inner_markers_searchtext="${TMP_MARKERS}.txtsearch.inner"
    while read entry <&0 ; do

        # find matching marker from array of possible ones
        match_marker="$(find_marker_text "${entry}" )"
        open_marker="$(get_open_marker "${match_marker}" )"
        close_marker="$(get_close_marker "${match_marker}" )"

        # extract content of the string
        content="${entry%${close_marker}}"
        content="${content#${open_marker}}"

        link_to_search="$( get_link_destination "${content}" )"

        # search for required object name in all other files
        # first variant: straightforward <a name="${link_to_search}">
        # second variant: <marker><a name="some_name">link_to_search</a></marker>,
        #   and for each texttarget marker

        : > "${inner_markers_searchtext}"
        # straightforward name
        printf "%s\n" "<a [[:print:]]*name=\"${link_to_search}\"" >> "${inner_markers_searchtext}"
        # each possible marker for target
        printf "%s\n" "${MARKERS_TEXTTARGET}" | while read single_marker ; do
            open_targetmarker="$(get_open_marker "${single_marker}" )"
            close_targetmarker="$(get_close_marker "${single_marker}" )"
            # tricky name by content of tag, not 'name' parameter
            # but parameter 'name' must be present inside <a> tag
            printf "%s\n" \
                "${open_targetmarker}<a [[:print:]]*name=\"[[:print:]]*\"[[:print:]]*>${link_to_search}</a>${close_targetmarker}" \
                >> "${inner_markers_searchtext}"
        done

        grep -r -H -o -f "${inner_markers_searchtext}" ${DIRS} | while read location ; do
            location_file="${location%:*}"
            # absolute path must be converted to relative
            location_base="${location_file%/*}"
            location_endfile="${location_file#${location_base}/}"
            if test "${location_base}" != "${main_dir}" ; then
                relative_base="$(relativedir "${main_dir}" "${location_base}")"
                relative_base="${relative_base}/"
            else
                relative_base=
            fi
            relative_file="${relative_base}${location_endfile}"

            location_text="${location#*:}"
            # extract the content of the 'name' parameter, which is final goal
            target_name="${location_text#* name=\"}"
            if test "${target_name}" = "${location_text}" ; then
                # error, should be no such string
                continue
            fi
            target_name="${target_name%\"*}"
            target_text="${relative_file}#${target_name}"

            # prepare sed template s|a href=|a href=actual_url|
            original_text=" href=\".*\""
            actual_text=" href=\"${target_text}\""
            printf "%s\n" "\|${entry}|s|${original_text}|${actual_text}|g"
        done
    done
}

check_init "${@}"

in_arg_cycle=1

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

    checked_arg="${arg#-}" || true
    if test "$checked_arg" != "${arg}" ; then
        in_arg_cycle=1
        shift

        case ${checked_arg} in
            -version)
                print_version
            ;;
            -help)
                print_help
            ;;
            c)
                CONFIGFILE="$1"
                shift || error "No argument for -c option"
            ;;
            f)
                FILENAMES="$1"
                shift || error "No argument for -f option"
            ;;
            -)
                in_arg_cycle=
            ;;
            *)
                error "Unknown option ${arg}"
            ;;
        esac
    fi
done

check_prepare "${@}"
parse_config

bad_arg=

while test -n "$1" ; do
    arg="${1}"
    shift
    if test -d "${arg}" ; then
        resolved_dir="$(cd ${arg} ; pwd -P)"
        DIRS="${DIRS} ${resolved_dir}"
    else
        bad_arg="${bad_arg}
${arg}"
    fi
done

if test -n "${bad_arg}" ; then
    error "The following args are not correct directories:
${bad_arg}"
fi
if test -z "${DIRS}" ; then
        error "Directory with doc pages is not given.
Provide it as argument.
"
fi


all_dirs="$(find ${DIRS} -type d)"

for dir in ${all_dirs} ; do
    relative_dirs=
    for subdir in ${all_dirs} ; do
        relative_dirs="${relative_dirs} $(relativedir ${dir} ${subdir})"
    done

    files_to_edit="$( find ${dir} -maxdepth 1 -type f -name "*${FILENAMES}" )"

    if test -z "${files_to_edit}" ; then
        continue
    fi

    : > "${TMP_EDIT}"
    grep -h -o -f "${TMP_MARKERS_MAN}" ${files_to_edit} | sort | uniq | \
        prepare_edit_file "${dir}" "${relative_dirs}" >> "${TMP_EDIT}"
    grep -h -o -f "${TMP_MARKERS_FILE}" ${files_to_edit} | sort | uniq | \
        prepare_edit_file "${dir}" "${relative_dirs}" 'files' >> "${TMP_EDIT}"
    grep -h -o -f "${TMP_MARKERS_TEXTLINK}" ${files_to_edit} | sort | uniq | \
        prepare_edit_text "${dir}" >> "${TMP_EDIT}"

    if test -s "${TMP_EDIT}" ; then
        printf "%s\n" "${files_to_edit}" | while read filename ; do
            sed -f ${TMP_EDIT} ${filename} > ${filename}.edit
            mv ${filename}.edit ${filename}
        done
    fi
done

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

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

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

