#!/bin/bash

set -eu

# git-nt -- tag the current repository with the next-higher version number,
# or (when given non-option arguments) bump each supplied version string.
#
# Tag/version format: major.minor.patch[.sub], optionally with a leading
# prefix (e.g. "v1.2.3").
#
#   -M, --major   increment major, zero the following fields
#   -m, --minor   increment minor, zero the following fields
#   -p, --patch   increment patch (default, except for two-part tags)
#   -s, --sub     add/increment a fourth "sub" field -> major.minor.patch.sub
#   -k, --keep-length  keep the length of the version number's parts: every
#                 output part occupies exactly the room its input counterpart
#                 had. Zeroed fields are drawn zero-filled at that width
#                 (1.2.09 -mk -> 1.3.00); a field that would NEED MORE DIGITS
#                 than it started with is refused instead (exit 2):
#                   1.2.9 -k   fails  (patch would reach two digits: ..10)
#                   1.2.8 -k   works  (1.2.9, nobody gained space)
#                   3.99.1 -mk fails  (minor would climb from 99 to 100)
#                 Without -k, spillage simply widens the field (1.2.9 ->
#                 1.2.10) or drops unjustified padding from zeroed fields
#                 (1.2.11 -m -> 1.3.0).
#   -n, --no-tag  compute and print the new tag but do not create it
#
# Bump flags combine freely and may repeat; they are applied in significance
# order (major, minor, patch, sub), so command-line order does not matter.
# Combining different levels (-Mp, -km aside) retains the fully historical
# formatting, described further down.
#
# Calculator mode: non-option arguments are each treated as a version
# string, bumped per the flags, and the result is printed (one per line,
# no "New tag " prefix, no tagging). No git repository is required there.
#
# Rendering of the individual parts:
#
# Surviving parts are printed with the width they already had, growing
# past it when they overflow (so "9" may become "10"):
#   1.2.9      -> 1.2.10        (default patch bump)
#   1.2.08     -> 1.2.09        (two-digit patch stays two-digit)
#   1.2.009 -p -> 1.2.010
#   1.02.7     -> 1.02.8        (major keeps its private padding habit)
#
# Parts annulled by a stronger bump lose their job and, with it, any
# decorative zero padding that was only ever justified by the number that
# stood there. They show up as a bare "0" -- unless their original spelling
# itself advertises deliberate zero fill ("09", "007"), whose width is then
# honoured, or unless -k pins the width outright:
#   1.2.11 -m  -> 1.3.0        (bare terminus: no padding heritage)
#   1.2.09 -m  -> 1.3.00       (the "09" family keeps its two digits)
#   1.2.11 -mk -> 1.3.00       (-k preserves every parted length)
#   1.02.009 -Mk -> 2.00.000   (ditto, majors and minors alike)

usage() {
    cat >&2 <<EOF
Usage: $(basename "$0") [-Mmpskneh] [version ...]
  -M, --major        Increment major version (zero following fields)
  -m, --minor        Increment minor version (zero following fields)
  -p, --patch        Increment patch version (default, unless two-part tag)
  -s, --sub          Add/increment fourth "sub" field (major.minor.patch.sub)
  -k, --keep-length  Same-width parts mandatory: refuse on digit-count gain
  -n, --no-tag       Compute and print the new tag but do not create it
  -h, --help         Show this help
Different bump levels may be combined (-Mp on 1.2.3 -> 2.0.1); repeating a
letter repeats its bump (-pp -> +2 patch).
With non-option arguments, bump each version string and print the result
(no tagging, no "New tag " prefix).
EOF
    exit 2
}

TEMP="$(getopt -o 'Mmpsknh' --long major,minor,patch,sub,keep-length,no-tag,help \
        -n "$(basename "$0")" -- "$@")" || usage
eval set -- "$TEMP"
unset TEMP

OPS=""          # collected bump letters, e.g. "Mp" or "pps"
KEEP=""         # non-empty: wiped fields keep their original width (-k)
NOTAG=""
ARGS=()
while true; do
    case "$1" in
        -M|--major)       OPS="${OPS}M"; shift ;;
        -m|--minor)       OPS="${OPS}m"; shift ;;
        -p|--patch)       OPS="${OPS}p"; shift ;;
        -s|--sub)         OPS="${OPS}s"; shift ;;
        -k|--keep-length) KEEP=1;        shift ;;
        -n|--no-tag)      NOTAG=1;      shift ;;
        -h|--help)        usage ;;
        --)
            shift
            ARGS=("$@")
            break ;;
        *) echo "$(basename "$0"): internal error: $1" >&2; exit 2 ;;
    esac
done

# Decimal integer value of one dotted field: leading zeros stripped, empty or
# non-numeric -> 0. Base-10 avoids bash treating "08"/"09" as invalid octal.
to_int() {
    local v="${1:-}"
    [[ "$v" =~ ^[0-9]+$ ]] || v=0
    echo "$((10#$v))"
}

# Display width (min 1) of a field, taken from its original spelling so leading
# zeros are preserved on output: "003" -> 3, "9" -> 1, "" -> 1.
field_width() {
    local v="${1:-}"
    if [[ "$v" =~ ^[0-9]+$ ]]; then
        echo "${#v}"
    else
        echo 1
    fi
}

# Width an annulled field deserves by its own evidence: a multi-character
# spelling containing a zero donates that length ("09" -> 2, "007" -> 3),
# anything else settles for the plain one-char "0".
evidence_width() {
    local v="${1:-}"
    if [ "${#v}" -gt 1 ] && [[ "$v" == *0* ]]; then
        echo "${#v}"
    else
        echo 1
    fi
}

# Format integer $1 zero-padded to min width $2; grows naturally if wider
# (e.g. w=3: 4 -> "004", 10 -> "010", 1234 -> "1234").
fmt_field() {
    local v="$1" w="${2:-1}"
    [ "$w" -ge 1 ] 2>/dev/null || w=1
    printf "%0${w}d" "$v"
}

# Fully historical formatting, consulted whenever today's simplified
# rendering does not apply: differing bump levels at once, -s involvement,
# or versions with four or more parts. Inputs are the post-arithmetic
# integers a..d, the original widths w0..w3, the field count n and the
# occurrence counts cs/cp.
format_classic() {
    local a="$1" b="$2" c="$3" d="$4" n="$5" cs="$6" cp="$7"
    local w0="$8" w1="$9" w2="${10}" w3="${11}"

    local width
    if [ "$cs" -gt 0 ]; then
        width=4
    else
        width=$n
        [ "$cp" -gt 0 ] && [ "$width" -lt 3 ] && width=3
        [ "$width" -gt 3 ] && width=3
        [ "$width" -lt 2 ] && width=2
    fi

    local A B C D
    A=$(fmt_field "$a" "$w0")
    B=$(fmt_field "$b" "$w1")
    C=$(fmt_field "$c" "$w2")
    D=$(fmt_field "$d" "$w3")
    local NUM
    case "$width" in
        1) NUM="$A" ;;
        2) NUM="$A.$B" ;;
        3) NUM="$A.$B.$C" ;;
        4) NUM="$A.$B.$C.$D" ;;
        *) NUM="$A.$B.$C" ;;
    esac
    echo "$NUM"
}

# Reject any field destined for the output whose post-bump value would no
# longer fit the width its input spelling occupied (used by -k). Args are
# pairwise value,width for the four slots followed by the live-slot count.
audit_keep_length() {
    local -a VS=("$1" "$3" "$5" "$7")
    local -a WS=("$2" "$4" "$6" "$8")
    local -a NM=(major minor patch sub)
    local q limit="${9:-0}"
    for ((q = 0; q < limit; q++)); do
        local v="${VS[$q]}" wn="${WS[$q]}"
        [ "${#v}" -le "$wn" ] && continue
        echo "$(basename "$0"): -k/--keep-length: ${NM[$q]} would widen to '${v}' (${#v} digits > ${wn}); rerun without -k" >&2
        return 1
    done
    return 0
}

# Echo the bumped form of the version string given as $1.
bump_version() {
    local TR="$1"
    local PREFIX="" REST="$TR"
    if [[ "$TR" =~ ^([^0-9]*)([0-9].*)$ ]]; then
        PREFIX="${BASH_REMATCH[1]}"
        REST="${BASH_REMATCH[2]}"
    fi

    local IFS=.
    local -a P
    read -ra P <<< "$REST"
    local n=${#P[@]}

    local r0="${P[0]:-}" r1="${P[1]:-}" r2="${P[2]:-}" r3="${P[3]:-}"
    local w0 w1 w2 w3
    w0=$(field_width "$r0") w1=$(field_width "$r1") w2=$(field_width "$r2") w3=$(field_width "$r3")
    local a b c d
    a=$(to_int "$r0") b=$(to_int "$r1") c=$(to_int "$r2") d=$(to_int "$r3")

    local _OPS="$OPS"
    local cM=0 cm=0 cp=0 cs=0 i=0
    while [ "$i" -lt "${#_OPS}" ]; do
        case "${_OPS:$i:1}" in
            M) cM=$((cM + 1)) ;;
            m) cm=$((cm + 1)) ;;
            p) cp=$((cp + 1)) ;;
            s) cs=$((cs + 1)) ;;
        esac
        i=$((i + 1))
    done

    # Impose the standing default for untouched runs: minor on two-part
    # tags, patch elsewhere. Recorded as a synthetic single-letter op so
    # the single/different-level distinction below sees reality.
    if [ "$cM" -eq 0 ] && [ "$cm" -eq 0 ] && [ "$cp" -eq 0 ] && [ "$cs" -eq 0 ]; then
        if [ "$n" -eq 2 ]; then
            _OPS="m"; cm=1
        else
            _OPS="p"; cp=1
        fi
    fi

    # How many DIFFERENT bump levels participate? More than one means
    # strongly coupled bookkeeping best answered by tradition.
    local kinds="${_OPS//[^Mmps]/}"
    local uniq
    uniq=$(printf %s "$kinds" | fold -w1 | sort -u | tr -d '\n')

    # Arithmetic first: dominators flatten everybody junior.
    if [ "$cM" -gt 0 ]; then a=$((a + cM)); b=0; c=0; d=0; fi
    if [ "$cm" -gt 0 ]; then b=$((b + cm)); c=0; d=0; fi
    if [ "$cp" -gt 0 ]; then c=$((c + cp)); d=0; fi
    if [ "$cs" -gt 0 ]; then d=$((d + cs)); fi

    # -k enforces constant widths over every slot the ensuing render will
    # address; the very figure doubling as that render's field tally turns
    # out to enumerate them, courtesy of the shared sizing heuristics.
    if [ -n "$KEEP" ]; then
        local lw
        lw=$n
        [ "$cs" -gt 0 ] && lw=4
        [ "$cp" -gt 0 ] && [ "$lw" -lt 3 ] && lw=3
        [ "$lw" -gt 3 ] && lw=3
        [ "$lw" -lt 2 ] && lw=2
        audit_keep_length "$a" "$w0" "$b" "$w1" "$c" "$w2" "$d" "$w3" "$lw" || exit 2
    fi

    if [ "${#uniq}" -ne 1 ] || [ "$cs" -gt 0 ] || [ "$n" -ge 4 ]; then
        echo "$PREFIX$(format_classic "$a" "$b" "$c" "$d" "$n" "$cs" "$cp" "$w0" "$w1" "$w2" "$w3")"
        return
    fi

    # ---- contemporary condensed rendering -------------------------------
    # Exactly one bump level, at most three fields. Alive slots reprint
    # with their trained width; slots this run liquidated wear EITHER the
    # width their dying spelling justified (contain a zero: "09" -> "00")
    # OR, under -k, precisely the width they walked in with.

    local BW="$w1" CW="$w2" DW="$w3"
    if [ -z "$KEEP" ]; then
        [ "$cM" -gt 0 ] && BW=$(evidence_width "$r1")
        { [ "$cM" -gt 0 ] || [ "$cm" -gt 0 ]; } && CW=$(evidence_width "$r2")
        { [ "$cM" -gt 0 ] || [ "$cm" -gt 0 ] || [ "$cp" -gt 0 ]; } && DW=$(evidence_width "$r3")
    fi

    # Printed depth mimics the classic sizing instinct: three fields is
    # home ground, patch pushes two-part tags up to it, singletons rise to
    # a pair; only -s dreams of four, and that detour lives upstairs.
    local depth="$n"
    [ "$cp" -gt 0 ] && [ "$depth" -lt 3 ] && depth=3
    [ "$depth" -gt 3 ] && depth=3
    [ "$depth" -lt 2 ] && depth=2

    local A B C OUT
    A=$(fmt_field "$a" "$w0")
    B=$(fmt_field "$b" "$BW")
    C=$(fmt_field "$c" "$CW")
    case "$depth" in
        1) OUT="$A" ;;
        2) OUT="$A.$B" ;;
        *) OUT="$A.$B.$C" ;;
    esac
    echo "$PREFIX$OUT"
}

if [ ${#ARGS[@]} -gt 0 ]; then
    # Calculator mode: bump each supplied version, print result, no tagging.
    for v in "${ARGS[@]}"; do
        bump_version "$v"
    done
    exit 0
fi

# Default mode: derive the next tag from the current repository.
T=$(git describe --tags)
TR=${T%-*-g*}
if [ "$T" = "$TR" ] ; then
    echo "Already tagged $T" >&2
    exit 1
fi
NEW=$(bump_version "$TR")
D=$(date +'%Y-%m-%d %H:%M:%S')
echo "New tag $NEW" >&2
[ -n "$NOTAG" ] || git tag -m "Tagged $NEW on $D" "$NEW"
