#!/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
#   -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:
#   -Mp on 1.2.3 -> 2.0.1     (== -pM)
#   -pp on 1.2.3 -> 1.2.5     (patch bumped twice)
# Each level zeroes the less-significant fields before they are bumped.
#
# Without -s any existing "sub" field is *dropped* (not zeroed).
# For a two-part tag (major.minor) the default becomes -m; -p appends ".1"
# and -s appends ".0.1".
#
# Non-option arguments enable calculator mode: each argument is treated as a
# version string, bumped per the flags, and the result printed (one per line,
# no "New tag " prefix, no tagging). No git repository is required in this
# mode.
#
# Leading zeros in each field are preserved on output, carrying naturally:
# 1.2.003 -p -> 1.2.004, 1.2.009 -p -> 1.2.010, 1.2.099 -p -> 1.2.100,
# 1.2.999 -p -> 1.2.1000. Fields that are zeroed by a higher-level bump keep
# their original width (1.02.009 -M -> 2.00.000); newly appended fields are
# unpadded.

usage() {
    cat >&2 <<EOF
Usage: $(basename "$0") [-Mmpsn] [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)
  -n, --no-tag  Compute and print the new tag but do not create it
  -h, --help    Show this help
Bump flags may be combined and repeated (applied major->minor->patch->sub),
e.g. -Mp on 1.2.3 -> 2.0.1, -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 'Mmpsnh' --long major,minor,patch,sub,no-tag,help \
        -n "$(basename "$0")" -- "$@")" || usage
eval set -- "$TEMP"
unset TEMP

OPS=""          # collected bump letters, e.g. "Mp" or "pps"
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 ;;
        -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
}

# 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"
}

# 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"
    if [ -z "$_OPS" ]; then
        if [ "$n" -eq 2 ]; then _OPS="m"; else _OPS="p"; fi
    fi

    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

    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

    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 "$PREFIX$NUM"
}

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"
