#!/usr/bin/env bash
#
# rename_crossplatform_safe.sh
#
# Recursively (or not) rename files so that each name:
#   • Contains only basic ASCII (either transliterated or stripped)
#   • Is safe for Windows (no <>:"/\\|?* , no control chars, no reserved device names)
#   • Is safe for Linux ext4 (no '/' or NUL, =255 bytes, optional leading-dot removal)
#
# Options:
#   --remove          Strip any non-ASCII characters instead of transliterating.
#   --keep-dot        Preserve a leading '.' (otherwise it is removed).
#   --dry-run         Show what would be done without touching the filesystem.
#   -r, --recursive   Walk sub-directories and rename files there as well.
#
# Example:
#   ./rename-crossplatform-safe.sh -r --dry-run
#   ./rename-crossplatform-safe.sh --remove --keep-dot
#
# Make it executable:
# chmod +x rename-crossplatform-safe.sh
#
# What the script does internally:
# ASCII conversion: iconv -f UTF-8 -t ASCII//TRANSLIT for transliteration; tr -cd '\0-\177' for pure stripping.
# Windows sanitisation: sed removes `< > : " / \
# Linux ext4 sanitisation: Deletes / and NUL, optionally strips a leading dot, enforces the 255-byte limit with a
# byte-count loop that keeps UTF-8 integrity.
# Recursive walk: find . -type f -print0 (null-separated for safety).
# Dry-run: Prints actions prefixed with [DRY-RUN] instead of invoking mv .
# Conflict handling: If the target name already exists, a warning is emitted and the file is left untouched.
#
# The script mirrors the behaviour of the Python version while staying entirely in Bash, making it easy to drop
# into any POSIX-compatible environment. 
#
# -------------------------------------------------------------------------

set -euo pipefail

# ---------- Helper functions -------------------------------------------------

# Print usage information
usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Options:
  --remove          Strip any non-ASCII characters instead of transliterating.
  --keep-dot        Preserve a leading '.' (otherwise it is removed).
  --dry-run         Show what would be done without touching the filesystem.
  -r, --recursive   Walk sub-directories and rename files there as well.
  -h, --help        Show this help message and exit.
EOF
    exit 1
}

# Transliterate UTF-8 ? ASCII using iconv (//TRANSLIT)
transliterate() {
    local txt=$1
    # iconv returns an empty string on failure, so we fallback to the original
    printf '%s' "$txt" | iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null || printf '%s' "$txt"
}

# Remove every non-ASCII byte (keep only 0-127)
strip_non_ascii() {
    local txt=$1
    printf '%s' "$txt" | LC_ALL=C tr -cd '\0-\177'
}

# Windows-specific sanitisation
sanitize_windows() {
    local name=$1

    # 1?? Remove illegal characters <>:"/\\|?* and control chars (0-31)
    #    We use sed to delete them.
    name=$(printf '%s' "$name" |
        sed -e 's/[<>:"\/\\|?*]//g' \
            -e 's/[\x00-\x1F]//g')

    # 2?? Trim trailing spaces or periods (Windows ignores them)
    name=${name%"${name##*[! .] }"}   # remove trailing spaces
    name=${name%"${name##*[!.] }"}    # remove trailing periods

    # 3?? Guard against reserved device names (case-insensitive)
    #    List taken from the Python version.
    local upper=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]')
    case "$upper" in
        CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])
            name="${name}_"
            ;;
    esac

    # If the name became empty, give it a placeholder.
    [[ -z "$name" ]] && name="file"
    printf '%s' "$name"
}

# Linux ext4 sanitisation
sanitize_ext4() {
    local name=$1
    local keep_dot=$2   # "yes" or "no"

    # 1?? Remove '/' and NUL
    name=${name//\//}               # slash
    name=${name//$'\0'/}            # NUL

    # 2?? Optionally strip a leading dot (hidden file)
    if [[ "$keep_dot" != "yes" && $name == .* ]]; then
        name=${name#.}
    fi

    # 3?? Truncate to 255 bytes (UTF-8 safe)
    #    We count bytes, not characters.
    local max=255
    while (( ${#name} > max )); do
        # Cut off the last byte and re-encode to UTF-8 safely.
        name=$(printf '%s' "$name" | head -c "$max")
        # If we cut in the middle of a multibyte char, drop the incomplete tail.
        # The following trick forces a round-trip through UTF-8 decoding.
        name=$(printf '%s' "$name" | iconv -f UTF-8 -t UTF-8 -c 2>/dev/null || printf '%s' "$name")
    done

    [[ -z "$name" ]] && name="file"
    printf '%s' "$name"
}

# Perform (or simulate) the rename
do_rename() {
    local src=$1
    local dst=$2
    local dry=$3

    if [[ "$src" == "$dst" ]]; then
        return
    fi

    if [[ -e "$dst" ]]; then
        printf '[??] Conflict: %s already exists – leaving %s unchanged.\n' "$(basename "$dst")" "$(basename "$src)" >&2
        return
    fi

    if [[ "$dry" == "yes" ]]; then
        printf '[DRY-RUN] Would rename: %s ? %s\n' "$(basename "$src")" "$(basename "$dst")"
    else
        mv -- "$src" "$dst"
        printf 'Renamed: %s ? %s\n' "$(basename "$src")" "$(basename "$dst")"
    fi
}

# ---------- Argument parsing -------------------------------------------------

REMOVE=no
KEEP_DOT=no
DRY_RUN=no
RECURSE=no

while (( $# )); do
    case "$1" in
        --remove)      REMOVE=yes ;;
        --keep-dot)    KEEP_DOT=yes ;;
        --dry-run)     DRY_RUN=yes ;;
        -r|--recursive) RECURSE=yes ;;
        -h|--help)     usage ;;
        *) printf 'Unknown option: %s\n' "$1" >&2; usage ;;
    esac
    shift
done

# ---------- Main processing --------------------------------------------------

# Function that processes a single file path
process_path() {
    local filepath=$1

    # Split into directory, basename, stem, and extension
    local dir=$(dirname "$filepath")
    local base=$(basename "$filepath")
    local stem=${base%.*}
    local ext=${base##"$stem"}
    # $ext includes the dot if there is one, otherwise empty

    # 1?? ASCII conversion
    if [[ "$REMOVE" == "yes" ]]; then
        stem=$(strip_non_ascii "$stem")
    else
        stem=$(transliterate "$stem")
    fi

    # 2?? Windows sanitisation
    stem=$(sanitize_windows "$stem")

    # 3?? Linux ext4 sanitisation
    if [[ "$KEEP_DOT" == "yes" ]]; then
        stem=$(sanitize_ext4 "$stem" yes)
    else
        stem=$(sanitize_ext4 "$stem" no)
    fi

    local newname="${stem}${ext}"
    local newpath="${dir}/${newname}"

    do_rename "$filepath" "$newpath" "$DRY_RUN"
}

if [[ "$RECURSE" == "yes" ]]; then
    # Use find to locate regular files (not directories, not symlinks)
    while IFS= read -r -d '' file; do
        process_path "$file"
    done < <(find . -type f -print0)
else
    # Non-recursive: only files directly under the current directory
    for file in ./*; do
        [[ -f "$file" ]] && process_path "$file"
    done
fi

if [[ "$DRY_RUN" == "yes" ]]; then
    printf '\n? Dry-run completed – no files were modified.\n'
else
    printf '\n? Done.\n'
fi
