#!/bin/bash

function usage(){
 echo "You must specify at least one filespec.";
 echo "";
 echo "Usage: filecase [-s] [-h] [-p] [-q] [-d] [-l | -u] filespec ...";
 echo "";
 echo "Run \"filecase --help\" for full help.";
 echo "";
 exit 0;
};

function help(){
 echo "filecase for unix version 1.0 built 20231228";
 echo "Copyright (c) 2023 Alexey Kuryakin daqgroup@mail.ru under MIT license.";
 echo "Utility to rename specified files/directories to upper/lower case.";
 echo "";
 echo "Usage: filecase [-s] [-h] [-p] [-q] [-d] [-l | -u] filespec ...";
 echo "";
 echo "  -s Process subdirectories.";
 echo "  -h Process hidden/system files/directories (in Unix - dot-files).";
 echo "  -p Prompt for each file/directory to be renamed (Yes|No|All|Quit).";
 echo "  -q Quiet mode; Only display errors.";
 echo "  -d Rename directory names as well as file names.";
 echo "  -l Convert names to lowercase (default).";
 echo "  -u Convert names to uppercase.";
 exit 0;
};

declare files="";   # list of files to process
declare -i opt_s=0; # option subdir
declare -i opt_h=0; # option hidden
declare -i opt_p=0; # option prompt
declare -i opt_q=0; # option quiet
declare -i opt_d=0; # option directories
declare -i opt_l=1; # option lowercase
declare -i con_a=0; # confirm all
declare -i con_q=0; # confirm quit
declare -i tout=60; # prompt timeout, sec
declare -i recursion=0;    # recursion level
readonly MAX_RECURSION=99; # recursion limit

# test functions
function isempty() { test -z "$1";     }; # $1 is empty?
function noempty() { test -n "$1";     }; # $1 is not empty?
function isreg()   { test -f "$1";     }; # $1 is regular file?
function isdir()   { test -d "$1";     }; # $1 is directory?
function istty()   { test -t "$1";     }; # $1 is terminal (0,1,2=stdin,stdout,stderr)
function issym()   { test -L "$1";     }; # $1 is symbolic link?
function issame()  { test "$1" = "$2"; }; # $1 is same as $2?

# stream functions
function fault() { 1>&2 echo "$1"; };
function abort() { 1>&2 echo "$2"; exit $1; };
function debug() { if issame $opt_q 0; then echo "$1"; fi; };

# case conversion functions
function uppercase() { echo "$1" | tr '[:lower:]' '[:upper:]'; };
function lowercase() { echo "$1" | tr '[:upper:]' '[:lower:]'; };

# is $1 hidden file?
function isdotfile(){
 local fn="$(basename "$1")";
 local c1="$(cut -c 1 <<< $fn)";
 test "$c1" = ".";
};

function confirm(){
 if issame $opt_p 0; then return 0; fi; # confirm if no prompt
 if issame $con_a 1; then return 0; fi; # confirm all
 if issame $con_q 1; then exit 0;   fi; # quit
 local code=0;
 while true; do
  if read -i Q -p "$1" -n 1 -t $tout ans; then
   echo "";
   case $ans in
    y|Y) code=0; ;;
    n|N) code=1; ;;
    a|A) code=0; con_a=1; ;;
    q|Q) code=1; con_q=1; ;;
    *)   continue; ;;
   esac;
   break;
  else
   con_q=1;
   code=1;
   break;
  fi;
 done;
 if issame $con_q 1; then
  exit 0; # Quit pressed
 fi;
 return $code;
};

function handlepar(){
 files="$files $1";
};

function checktty(){
 if istty 0 && istty 1 ; then return 0; fi;
 opt_p=0; # when stdin/stdout is not tty
};

function handleargs(){
 while noempty "$1"; do
  case $1 in
   -s) opt_s=1; ;;
   -h) opt_h=1; ;;
   -p) opt_p=1; ;;
   -q) opt_q=1; ;;
   -d) opt_d=1; ;;
   -l) opt_l=1; ;;
   -u) opt_l=0; ;;
   -t) shift; tout="$1"; ;;
   -help|--help) help; ;;
   -*) abort 1 "Error: unknown option $1"; ;;
   *)  handlepar "$1"; ;;
  esac;
  shift;
 done;
 checktty;
};

function confirmrename(){
 if noempty "$1" && noempty "$2"; then
  if issame "$1" "$2"; then
   debug "filecase: passed $1";
   return 0;
  fi;
  if confirm "Rename $1? (Yes|No|All|Quit):"; then
   if mv "$1" "$2"; then
    debug "filecase: rename $1 to $2";
   else
    fault "filecase: failed $1";
   fi;
  fi;
 fi;
};

function handlefile(){
 # skip hidden dot files
 if issame $opt_h 0 && isdotfile "$1"; then
  return 0; # skip hidden if not [-h] option
 fi;
 # regular files
 if isreg "$1"; then
  local fp="$(realpath "$1")"; # full file path
  local fd="$(dirname "$fp")"; # file directory
  local fb="$(basename "$1")"; # file base name
  if issame $opt_l 0; then fb="$(uppercase $fb)"; fi;
  if issame $opt_l 1; then fb="$(lowercase $fb)"; fi;
  local fn="$(realpath "$fd/$fb")"; # new file name
  confirmrename "$fp" "$fn";
 fi;
 # directories
 if isdir "$1"; then
  local fp="$(realpath "$1")"; # full file path
  local fd="$(dirname "$fp")"; # file directory
  local fb="$(basename "$1")"; # file base name
  if issame $opt_l 0; then fb="$(uppercase $fb)"; fi;
  if issame $opt_l 1; then fb="$(lowercase $fb)"; fi;
  local fn="$(realpath "$fd/$fb")"; # new file name
  if issame $opt_d 0; then fn="$fp"; fi;
  confirmrename "$fp" "$fn";
  if issame $opt_s 1 && isdir "$fn" && [ $recursion -lt $MAX_RECURSION ]; then
   for fx in $(ls -1 -A "$fn"); do
    let recursion++;
    handlefile "$fn/$fx";
    let recursion--;
   done;
  fi;
 fi;
};

function handlefiles(){
 while noempty "$1"; do
  handlefile "$1";
  shift;
 done;
};

function filecase(){
 if issame $# 0; then
  usage;
 fi;
 handleargs "$@";
 if isempty "$files"; then
  abort 1 "Error: no files specified";
 fi;
 handlefiles $files;
};

filecase "$@";

##############
## END OF FILE
##############
