#!/bin/bash
# Desc: Generates Mediawiki subpage navigation links
# Input: file   text file with list of chapters
#        stdin  text with list of chapters
# Output: [[../Chapter 4|Next]], [[../Chapter 2|Previous]], [[../|Up]]
# Version: 0.1.0
# Attrib: Steven Baltakatei Sandoval. (2024-07-17). reboil.com
# License: GPLv3+

yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
read_input() {
    # Note: May accept specified file or stdin, but not both
    # Input:  arg     input text (newline delimited)
    #         stdin   input text (newline delimited)
    # Output: stdout  output text (newline delimited)
    # Depends: BK-2020-03 read_stdin (v0.1.1), yell(), die()

    # Parse args
    ## Only 1 argument.
    if [[ "$#" -gt 1 ]]; then die "FATAL:Too many arguments ($#):$*"; fi;
    ## File specified.
    if [[ "$#" -eq 1 ]] && [[ -f "$1" ]] && [[ ! -p /dev/stdin ]]; then
        while read -r line; do
            printf "%s\n" "$line";
        done < "$1" && return 0; fi;
    if [[ "$#" -eq 0 ]] && [[ -p /dev/stdin ]]; then
        read_stdin && return 0; fi;
    die "FATAL:Unknown error.";
};
read_stdin() {
    # Desc: Consumes stdin; outputs as stdout lines
    # Input: stdin (consumes)
    # Output: stdout  (newline delimited)
    #         return  0  stdin read
    #         return  1  stdin not present
    # Example: printf "foo\nbar\n" | read_stdin
    # Depends: GNU bash (version 5.1.16), GNU Coreutils 8.32 (cat)
    # Version: 0.1.1
    # Attrib: Steven Baltakatei Sandoval (2024-01-29). reboil.com
    local input_stdin output;

    # Store stdin
    if [[ -p /dev/stdin ]]; then
        input_stdin="$(cat -)" || {
            echo "FATAL:Error reading stdin." 1>&2; return 1; };
    else
        return 1;
    fi;

    # Store as output array elements
    ## Read in stdin
    if [[ -n $input_stdin ]]; then
        while read -r line; do
            output+=("$line");
        done < <(printf "%s\n" "$input_stdin") || {
            echo "FATAL:Error parsing stdin."; return 1; };
    fi;

    # Print to stdout
    printf "%s\n" "${output[@]}";

    return 0;
}; # read stdin to stdout lines
get_path_fork_level() {
    # Desc: Get fork level from two paths
    # Input:  arg1    str  path
    #         arg2    str  path
    # Output: stdout  int  fork level
    # Version: 0.0.1
    local path1="$1";
    local path2="$2";

    # Squeeze multiple slashes and remove trailing slashes
    path1="$(echo "$path1" | tr -s '/' | sed 's:/*$::' )";
    path2="$(echo "$path2" | tr -s '/' | sed 's:/*$::' )";
    
    # Check for mixed absolute/relative paths
    if [[ "$path1" =~ ^/ ]] && [[ "$path2" =~ ^/ ]]; then
        flag_root=true;
        # Remove initial /
        path1="$(echo "$path1" | sed -e 's:^/::' )";
        path2="$(echo "$path2" | sed -e 's:^/::' )";
    elif [[ ! "$path1" =~ ^/ ]] && [[ ! "$path2" =~ ^/ ]]; then
        flag_root=false;
    else
        declare -p path1 path2 flag_root;
        echo "FATAL:Mixed relative and absolute paths not supported." 1>&2;
        return 1;
    fi;

    # Save path as arrays with `/` as element delimiter
    local IFS='/';
    read -ra parts1 <<< "$path1";
    read -ra parts2 <<< "$path2";

    # Get fork level by counting identical path elements from rootside
    local fork_level=0;
    for (( i=0; i<${#parts1[@]} && i<${#parts2[@]}; i++ )); do
        if [[ "${parts1[i]}" != "${parts2[i]}" ]]; then break; fi;
        ((fork_level++));
    done;
    
    echo "$fork_level";
    #declare -p path1 path2 flag_root parts1 parts2 fork_level; # debug
    return 0;
}; # Get fork level int from two paths
prune_path_rootside() {
    # Desc: Prunes a path from the root-side to a specified prune level.
    # Input: arg1  str  path
    #        arg2  int  prune level (0-indexed)
    # Depends: GNU sed 4.8
    # Version: 0.0.1
    local path="$1";
    local prune_level="$2";

    # Check for absolute or relative path
    if [[ "$path" =~ ^/ ]]; then
        flag_root=true;
        # Remove initial /
        path="$(echo "$path" | sed -e 's:^/::' )";
    else
        flag_root=false;
    fi;
    
    # Save path as array with `/` as element delimiter
    local IFS='/';
    read -ra parts <<< "$path";

    # Assemble pruned path from prune_level
    local pruned_path="";
    for (( i=prune_level; i<${#parts[@]}; i++ )); do
        pruned_path+="${parts[i]}/";
    done;

    # Trim trailing `/` delimiter
    pruned_path=$(echo "$pruned_path" | sed 's:/*$::');

    # Restore initial / if appropriate
    if [[ "$flag_root" == "true" ]] && [[ "$prune_level" -eq 0 ]]; then
        pruned_path=/"$pruned_path";
    fi;

    # Output pruned path
    echo "$pruned_path";
    #declare -p path prune_level parts pruned_path && printf "========\n"; # debug
    return 0;
}; # prune path rootside to int specified level
get_path_hierarchy_level() {
    # Desc: Outputs hierarchy level of input paths
    # Example: $ cat lines.txt | get_path_hierarchy_level
    # Input: stdin    str  lines with /-delimited paths
    # Output: stdout  int  hierarchy level of each path
    # Version: 0.0.1

    local line level;
    local flag_root;
    local -a output;

    n=0;
    while read -r line; do
        # Check for mixed absolute/relative paths.
        if [[ $n -le 0 ]] && [[ "$line" =~ ^/ ]]; then
            flag_root=true;
        else
            flag_root=false;
        fi;
        if { [[ "$flag_root" == "true" ]] && [[ ! "$line" =~ ^/ ]]; } || \
           { [[ "$flag_root" == "false" ]] && [[ "$line" =~ ^/ ]]; } then
            echo "FATAL:Mixed relative and absolute paths not supported." 1>&2; return 1;
        fi;
        
        # Squeeze multiple slashes and remove trailing slashes
        line="$(echo "$line" | tr -s '/' | sed 's:/*$::' )";

        # Count the number of slashes to determine hierarchy level
        level="$(echo "$line" | awk -F'/' '{print NF-1}' )";
        if [[ "$flag_root" == "true" ]]; then ((level--)); fi;

        # Append to output
        output+=("$level");
        #declare -p flag_root level; # debug
        ((n++));
    done;
    # Print output
    printf "%s\n" "${output[@]}";
}; # return hierarchy level of lines as integers
validate_subpage_list() {
    # Desc: Check for illegal characters in subpage titles
    # Input:  stdin   unvalidated subpage list
    # Output: stdout  validated subpage list
    # Depends: BK-2020-03 read_stdin(), yell(), die()
    #          GNU sed v4.8
    while read -r line; do

        # Reject chars illegal in Mediawiki page titles.
        re_illegal='[][><|}{#_]'; #  match illegal page names chars #, <, >, [, ], _, {, |, }
        if [[ "$line" =~ $re_illegal ]]; then
            die "FATAL:Illegal char. Not allowed: #, <, >, [, ], _, {, |, }:$line";
        fi;

        # Reject trailing spaces.
        re_ts=' $';  # match trailing space
        if [[ "$line" =~ $re_ts ]]; then
            die "FATAL:Trailing spaces not allowed:$line";
        fi;

        # Replace some chars with HTML-style codes
        ## replace ampersand    & with &#38  # must be first
        ## replace double quote " with &#34
        ## replace single quote ' with &#39
        line="$(sed \
                  -e 's/&/\&#38;/g' \
                  -e 's/"/\&#34;/g' \
                  -e "s/'/\&#39;/g" \
                  <<< "$line" )" || { echo "FATAL:Error running sed."; };
        printf "%s\n" "$line";
    done || {
        echo "FATAL:Error reading stdin." 1>&2; return 1; };
};
generate_wikicode() {
    # Desc: Generates navigational link wikicode for subpages
    # Input:  stdin   validated subpage list
    # Output: stdout  wikicode
    # Depends: get_path_fork_level()
    #          prune_path_rootside()
    #          get_path_hierarchy_level()

    n=0;
    while read -r line; do
        #yell "$n:Processing line:$line";  # debug
        # Advance input lines
        lprev="$lcurr";
        lcurr="$lnext";
        lnext="$line";
        #declare -p lprev lcurr lnext;  # debug

        # Update hierarchy tracker states
        lprev_hier="$lcurr_hier";
        lcurr_hier="$lnext_hier";
        lnext_hier="$(echo "$lnext" | get_path_hierarchy_level)";

        # Skip first iteration
        if [[ "$n" -eq 0 ]]; then
            # yell "$n:DEBUG:Skipping first iteration.";  # debug
            ((n++));
            #printf -- "----\n" 1>&2;  # debug
            continue; fi;

        # Get path fork levels
        fork_level_next="$(get_path_fork_level "$lcurr" "$lnext")";
        fork_level_prev="$(get_path_fork_level "$lcurr" "$lprev")";

        # Count relative ups needed (`../`)
        relups_next="$((lcurr_hier - fork_level_next + 1))";
        relups_prev="$((lcurr_hier - fork_level_prev + 1))";

        # Initialize Next and Prev links with relative ups to fork.
        link_next="";
        for (( i=0; i<relups_next; i++ )); do link_next+="../"; done;
        if [[ "$relups_next" -eq 0 ]]; then link_next+="/"; fi; # handle new subpage path dive
        link_prev="";
        for (( i=0; i<relups_prev; i++ )); do link_prev+="../"; done;

        # Append branchs from fork to Next and Prev targets
        link_next+="$(prune_path_rootside "$lnext" "$fork_level_next")";
        link_prev+="$(prune_path_rootside "$lprev" "$fork_level_prev")";

        # Print navigation link wikicode
        if [[ -z "$lprev" ]]; then
            printf "[[%s|Next]], [[../|Up]]\n" "$link_next";
        elif [[ -n "$lprev" ]]; then
            printf "[[%s|Next]], [[%s|Previous]], [[../|Up]]\n" "$link_next" "$link_prev";
        elif [[ -z "$lnext" ]]; then
            printf "[[%s|Previous]], [[../|Up]]\n" "$link_prev";
        else
            echo "FATAL:Here be dragons." 1>&2;
        fi;

        #declare -p n lprev lcurr lnext lprev_hier lcurr_hier lnext_hier; # debug
        #declare -p fork_level_next fork_level_prev relups_next relups_prev; # debug
        #declare -p link_next link_prev; # debug
        ((n++));
    done < <(read_stdin); # read stdin plus one more blank line
}; # Generate wikicode from validated subpage lines
main() {
    read_input "$@" | validate_subpage_list | generate_wikicode;
}; # main program

main "$@";
