/****************************************************************************
*
*   This program is free software: you can redistribute it and/or modify it
*   under the terms of the GNU General Public License as published by the Free
*   Software Foundation, either version 3 of the License, or (at your option)
*   any later version.
*
*   This program is distributed in the hope that it will be useful, but WITHOUT
*   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
*   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
*   more details.
*
*   You should have received a copy of the GNU General Public License along
*   with this program.  If not, see <https://www.gnu.org/licenses/>.
*
*****************************************************************************/

char /*-------------------------------------------------------------------+
*                                                                        */
*    Program =   "frenum",
*    Purpose =   "mass renumbering of files",
*    Version =   "1.0.8-1-12",
*    Date =      "12 Jan 2008",
*    Author =    "Renuk de Silva (renuk007@users.sourceforge.net)",
*    Copyright = "Copyright (C) 2008 Renuk de Silva, Colombo, Sri Lanka.\n"
                 "Modifications Copyright (C) 2025-2026 Brian Lindholm.",
*    License =   "License GPLv3: GNU General Public License version 3\n"
                 "This is free software; you are free to change and"
                 " redistribute it.\nThere is NO WARRANTY, to the extent"
                 " permitted by law.";                                   /*
*
+------------------------------------------------------------------------*/

#include <config.h>

#include <ctype.h>
#ifdef HAVE_LIBINTL_H
# include <libintl.h>
#endif
#ifdef HAVE_STDIO_H
# include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

#define NO 0
#define YES (!NO)
#define when1(x) case x:
#define when(x) break; case x:
#define or :case
#define otherwise break; default:

#define MaxWidthLow -20
#define MaxWidthHigh 10

int increment = 1;  /* when 0, turn off renumbering */
int next_number = 1, field_width = 0;
int strict_char_set = NO, no_numerics = NO;
int trunc_name = 0, chop_off = 0;
int dirs_also = NO, list_only = NO, ignore_file_errors = NO;
int verbose = NO, quiet = YES;
char *change_case = NULL, *prefix = NULL, *suffix = NULL;
int extension = NO;
char *translate_from = NULL, *translate_to = NULL;
enum { Std_No = 0, Std_Yes, Std_Required };
int read_std_in = Std_Yes;
typedef struct { int max; char *txt; } StrBuf;
StrBuf new_file_name = { 0, NULL };

void Shit(char *what)  /* don't bother translating at this level of disaster */
{
   fprintf(stderr, "\n\r*** System failure -- %s ***\n\r", what);
   exit(2);
}

void Expand(StrBuf *d)
{
   int n;
   char *p;

   if (d->max < 1) {
      n = 1000;
      p = malloc(n);
   } else {
      n = d->max + 1000;
      p = realloc(d->txt, n);
   }
   if (p == NULL) Shit("out of memory");
   d->max = n;
   d->txt = p;
}

enum { H_duh, H_elp, H_elp2, H_ver };

void Help_1()
{
   printf(gettext(
      "\rUsage: %s { -options | files }...\n"
      "  -n, --Num[ber]  nn   # increment is nn, discard original name (default 1)\n"
      "  -b, --Begin     nn   # start with nn                          (default 1)\n"
      "  -w, --Width     nn   # zero-filled field width          (range %d to %d)\n"),
      Program, MaxWidthLow, MaxWidthHigh);
   printf(gettext(
      "  -t, --Trunc[ate] nn  # delete from nn or first non-alpha\n"
      "  -x, --Skip      nn   # delete the first nn chars\n"
      "  -p, --Prefix    aaa\n"
      "  -s, --Suffix    aaa\n"
      "  -., --Extension aaa  # Suffix starting with dot, also deletes any existing\n"
      "  -/, --Translate aaa/bbb # character replacement\n"
      "  -c, --Case { lower | Upper | initial-caps }\n"
      "  -o, --Opt[imal]      # permit only alphanumerics, -,+,.,_\n"
      "  -z, --Zap[-Numerics] # delete all non-alphas\n"
      "  -d, --Dirs[-Also]    # don't ignore directory names\n"
      "  -l, --List[-Only]    # don't do it, just report what would happen\n"
      "  -k, --Keep-Going     # don't stop on file errors, go to next\n"
      "  -v, --Ver[sion]\n"
      "  -h, --Help\n"
      "  -h2, --Help 2        # more help\n"
      "  -q, --Quiet, --Silent # default if output to pipe\n"
      "  -u, --Verbose        # require more explanation\n"
      "  -, --Standard-input  # read stdin for more file names\n"));
   printf(gettext(
      "\nReport bugs to: %s.\n"), gettext(Author));
}

void Help_2()
{
   printf(
      "No files will be overwritten; if there is a name clash, frenum will\n"
      "re-compute the next file name in the series.  If no files are found\n"
      "on the command line,  the standard input is read.  All  non-regular\n"
      "files are ignored without error.  The pathname up to the last slash\n"
      "is also left as-is;  the transformation rules are applied  only  to\n"
      "the file name.\n"
      "\n"
      "Option --Number 0 turns off numbering; name clashes may result.\n"
      "\n"
      "If a negative width is specified, it is taken as the fixed width of\n"
      "the entire file name (including all prefixes, but not including any\n"
      "specified extension or suffix).\n"
      "\n"
      "The --Skip and --Trunc options together define a segment (substring)\n"
      "of the original file name.  If the file name already has  an  exten-\n"
      "sion, \"-.aaa\" will replace it. If there are multiple extensions, the\n"
      "rearmost one will be replaced.\n"
      "\n"
      "--Quiet only turns off extra status messages,  but  renumbered  file\n"
      "names are still echoed to the standard output.  It is ignored if you\n"
      "specify the --List option.\n"
      "\n");
}

void Usage(int level)
{
   switch (level) {
      when1 (H_elp) Help_1();
      when (H_elp2) Help_2();
      when (H_ver)
         printf("%s %s\n", Program, Version);
         printf("%s\n", gettext(Copyright));
         printf("%s\n", gettext(License));
         printf(gettext("Purpose: %s\n"), gettext(Purpose));
         printf(gettext("Created %s by %s\n"), gettext(Date), gettext(Author));
      otherwise
         fprintf(stderr, gettext("\rUsage: %s { -options | files }...\n"
            "Try \"%s --Help\"\n"), Program, Program);
   }
   exit(0);
}

void Complain(const char *Mes, const char *p1, const char *p2)
{
   fprintf(stderr, gettext(">> Error: "));
   fprintf(stderr, gettext(Mes), p1, p2);
   fprintf(stderr, gettext(" <<\n"));
   Usage(H_duh);
}

void Abort(const char *Problem, const char *Culprit)
{
   fprintf(stderr, gettext("ERROR: "));
   fprintf(stderr, gettext(Problem), Culprit);
   if (ignore_file_errors) return;
   exit(0);
}

void Init()
{
   verbose = isatty(1);
   Expand(&new_file_name);
}

int Len(char *s)
{
   if (s == NULL) return(0);
   return(strlen(s));
}

enum { Arg_End = 0, Arg_Int, Arg_IntW, Arg_Str, Arg_Flag, Arg_Ext, Arg_Stdin, Arg_Help, Arg_Ver };

struct {
   char flag; /* short option flag */
   int type; /* classification */
   int *val_int; /* integer variable affected */
   char **val_str; /* string variable affected */
   char *long_option; /* long option name */
   int min_chars; /* minimum allowed matching characters for long_option */
} Arg[] = {
/*   Flag Type       Integer Value        String Value     Long Option Name  Minimum chars */
   {  0 , Arg_Stdin, NULL,                NULL,            "Standard-input", 0},
   { '.', Arg_Ext,   NULL,                NULL,            "Extension",      0},
   { '/', Arg_Str,   NULL,                &translate_from, "Translate",      0},
   { 'b', Arg_Int,   &next_number,        NULL,            "Begin",          0},
   { 'c', Arg_Str,   NULL,                &change_case,    "Case",           0},
   { 'd', Arg_Flag,  &dirs_also,          NULL,            "Dirs-Also",      4},
   { 'h', Arg_Help,  NULL,                NULL,            "Help",           0},
   { 'k', Arg_Flag,  &ignore_file_errors, NULL,            "Keep-Going",     0},
   { 'l', Arg_Flag,  &list_only,          NULL,            "List-Only",      4},
   { 'n', Arg_Int,   &increment,          NULL,            "Number",         3},
   { 'o', Arg_Flag,  &strict_char_set,    NULL,            "Optimal",        3},
   { 'p', Arg_Str,   NULL,                &prefix,         "Prefix",         0},
   { 'q', Arg_Flag,  &quiet,              NULL,            "Quiet",          0},
   { 'q', Arg_Flag,  &quiet,              NULL,            "Silent",         0},
   { 's', Arg_Str,   NULL,                &suffix,         "Suffix",         0},
   { 't', Arg_Int,   &trunc_name,         NULL,            "Truncate",       5},
   { 'u', Arg_Int,   &verbose,            NULL,            "Verbose",        0},
   { 'v', Arg_Ver,   NULL,                NULL,            "Version",        3},
   { 'w', Arg_IntW,  &field_width,        NULL,            "Width",          0},
   { 'x', Arg_Int,   &chop_off,           NULL,            "Skip",           0},
   { 'z', Arg_Flag,  &no_numerics,        NULL,            "Zap-Numerics",   3},
   {  0,  Arg_End,   NULL,                NULL,            "",               0}
};

void Bad_Option(int long_option, char *arg1)
{
   int i;

   if (long_option) for (i = 0;; i++) {
       if (Arg[i].type == Arg_End) break;
       if (strncasecmp(Arg[i].long_option, arg1, 2) == 0)
          Complain("Unrecognized option \"%s\", possibly meant to be \"%s\"", arg1, Arg[i].long_option);
    }
   Usage(H_duh);
}

void Check_Case_Option(char *data)
{
   static const char *opt[] = { "Upper", "Lower", "Initial-Caps" };
   int i, l;

   l = strlen(data); if (l < 1) l = 1;
   for (i = 0; i < 3; i++) if (strncasecmp(opt[i], data, l) == 0) return;
   Complain("\"Case\" option argument is invalid (%s)", data, "");
}

void Check_Translation()
{
   if ((translate_to = strchr(translate_from, '/')) == NULL)
      Complain("\"Translate\" option argument has no replacement section", "", "");
   *translate_to++ = '\0';
   if (strlen(translate_from) != strlen(translate_to))
      Complain("\"Translate\" option's find and replace parts of different size"
         " (\"%s\" vs \"%s\")", translate_from, translate_to);
}

void Check_Width_Option(char *data, int w)
{
   if (w < MaxWidthLow || w > MaxWidthHigh) Complain("Width value is not in the allowed range (%s)", data, "");
}

void Check_Extension_Option(char *data)
{
   static StrBuf ext_buf = { 0, NULL };

   extension = YES;
   if (*data == '.')
      suffix = data;
   else {
      while (ext_buf.max < Len(data)+2) Expand(&ext_buf);
      sprintf(ext_buf.txt, ".%s", data);
      suffix = ext_buf.txt;
   }

}

#define ret_n *arg_no += count; return

void Set_Options(char *arg[], int *arg_no, int maxargs)
{
   int i, count, l, lmax, t, v;
   char flag, *arg1, *arg2, *data;
   int long_option = NO;

   arg1 = arg[ *arg_no] + 1;
   arg2 = (*arg_no < maxargs-1 ? arg[ *arg_no + 1] : "");
   count = 0;
   data = arg1 + 1;
   if (*data == '\0') { count = 1; data = arg2; }
   while (*arg1 == '-') { arg1++; count = 1; data = arg2; long_option = YES; }
   flag = tolower(*arg1);
   if (flag == 0) { read_std_in = Std_Required; return; }
   lmax = Len(arg1);
   for (i = 0;; i++) {
      if (Arg[i].type == Arg_End) Bad_Option(long_option, arg1);
      if (long_option) {
         l = Arg[i].min_chars;
         if (l == 0) l = Len(Arg[i].long_option);
         if (lmax > l) l = lmax;
      } else
         l = 1;
      if ((l == 1 && Arg[i].flag == flag) || (l > 1 && strncasecmp(Arg[i].long_option, arg1, l) == 0)) {
        flag = Arg[i].flag;
        switch (t = Arg[i].type) {
           when1 (Arg_Stdin) read_std_in = Std_Required; return;
           when (Arg_Ext) Check_Extension_Option(data); ret_n;
           when (Arg_Int or Arg_IntW)
              if (!isdigit(*data) && *data != '-')
                 Complain("Numeric option (%s) has improper value (%s)", Arg[i].long_option, data);
              v = *Arg[i].val_int = atoi(data);
              if (t==Arg_IntW)
                 Check_Width_Option(data, v);
              else if (v < 0)
                 Usage(H_elp);
              ret_n;
           when (Arg_Str)
              *Arg[i].val_str = data;
              if (Arg[i].flag == 'c') Check_Case_Option(data);
              ret_n;
           when (Arg_Flag)
              *Arg[i].val_int = YES;
              if (quiet) verbose = NO;
              return;  /* didn't use additional arg */
           when (Arg_Help)
              if (*data == '2') Usage(H_elp2);
              Usage(H_elp);
           when (Arg_Ver)
              Usage(H_ver);
         }
      }
   }
}

enum { NoSuchFile = 1, NormalFile, Directory, SpecialFile };

int File_Status(char *file)
{
   struct stat statbuf;

   if (stat(file, &statbuf) == 0) {
      if ((statbuf.st_mode&S_IFMT) == S_IFREG) return(NormalFile);
      if ((statbuf.st_mode&S_IFMT) == S_IFDIR) return(Directory);
      return(SpecialFile);
   }
   return(NoSuchFile);
}

void Pretend_Rename(char *old_name, int file_type)
{
   switch (file_type) {
      when1 (NoSuchFile)
         if (verbose) printf(gettext("(File \"%s\" doesn't exist, but if it did ...)\n"), old_name);
      when (NormalFile)  /* do nothing */
      when (Directory)
         if (!dirs_also) {
            printf(gettext("-- \"%s\" is a directory.\n"), old_name);
            return;
         }
      otherwise
         printf(gettext("-- \"%s\" is not a file.\n"), old_name);
         return;
   }
   if (strcmp(old_name, new_file_name.txt) != 0 ) {
      if (verbose) {
         if (file_type == Directory)
            printf(gettext("Directory "));
         else
            printf(gettext("File "));
      }
      printf(gettext("\"%s\" --> \"%s\"\n"), old_name, new_file_name.txt);
      switch (File_Status(new_file_name.txt)) {
         when1 (NoSuchFile)  /* do nothing */
         when (NormalFile)
            printf(gettext("Warning: there is already a file with this name.\n"));
         when (Directory)
            printf(gettext("Warning: there is already a directory with this name.\n"));
         otherwise
            printf(gettext("Warning: this name is already in use.\n"));
      }
   } else
      printf(gettext("File \"%s\" is unchanged.\n"), old_name);
}

void Really_Rename(char *old_name, int file_type)
{
   StrBuf command_line = { 0, NULL };

   if (strcmp(old_name, new_file_name.txt) != 0) {
      if (File_Status(new_file_name.txt) != NoSuchFile) {
         Abort("file %s exists, can't overwrite.\n", new_file_name.txt);
         return;
      }
      if (file_type == Directory) {
         while (Len(old_name)+Len(new_file_name.txt) > command_line.max-10) Expand(&command_line);
         sprintf(command_line.txt, "mv '%s' '%s'", old_name, new_file_name.txt);
         if (system(command_line.txt) != 0) {
            Abort("dir '%s' can't be renamed.\n", new_file_name.txt);
            return;
         }
      } else {
         if (link(old_name, new_file_name.txt) != 0) {
            Abort("can't create %s\n", new_file_name.txt);
            return;
         }
         if (unlink(old_name) != 0) {
            Abort("can't delete %s\n", old_name);
            return;
         }
      }
      if (verbose) {
         if (file_type == Directory)
            fprintf(stderr, gettext("Dir %s --> %s\n"), old_name, new_file_name.txt);
         else
            fprintf(stderr, gettext("File %s --> %s\n"), old_name, new_file_name.txt);
      } else
         printf("%s\n", new_file_name.txt);
   } else
      if (verbose) fprintf(stderr, gettext("File %s is unchanged.\n"), old_name);
}

#define ToEnd(x) (x+strlen(x))

char * Cleanup(char *src, StrBuf *workbuf)
/* This routine handles prefix, trunc_name, chop_off, change_case, no_numerics and strict_char_set */
{
   char *dst, ch, *p, *basename;
   int i, first, offset;

   while (Len(src) + Len(prefix) > workbuf->max - 500) Expand(workbuf);
   if ((p = strrchr(src, '/')) != NULL) {
      /* If there is a pathname, just copy everything up to the last slash without attempting to modify it */
      *p = '\0';  /* truncate at the slash */
      offset = Len(src);
      strcpy(workbuf->txt, src);
      dst =  workbuf->txt + offset;
      *dst++ = *p++= '/';  /* replace the removed slash */
      src = p;  /* reset the source also */
   } else
      dst = workbuf->txt;
   basename = dst;
   if (prefix) { strcpy(dst, prefix); dst = ToEnd(dst); }
   first = YES;
   if (chop_off && (int) strlen(src) > chop_off) src += chop_off;
   if (chop_off || trunc_name || no_numerics || change_case || strict_char_set)
      for (i = 0; *src; src++) {
         ch = *src;
         if (trunc_name && !isalpha(ch)) break;
         if (trunc_name && (trunc_name <= i)) break;
         if (no_numerics && isdigit(ch)) ch = 0;
         if ((strict_char_set || no_numerics) && (!isalnum(ch) && (strchr("+-._/", ch) == NULL))) ch = 0;
         if (change_case) {
            if (isalpha(ch)) {
               switch (*change_case) {
                  when1 ('l' or 'L') ch = tolower(ch);
                  when ('u' or 'U') ch = toupper(ch);
                  otherwise
                     if (first) {
                        ch = toupper(ch);
                        first = NO;
                     }
                     else
                        ch = tolower(ch);
               }
            } else
               first = YES;
         }
         if (ch) {
            *dst++ = ch;
            i++;
         }
      }
   *dst = '\0';
   return(basename);
}

void Chop_Extension(StrBuf *s)
{
   char *p;

   if ((extension) && ((p = strrchr(s->txt, '.')) != NULL)) *p= '\0';
}

void Reformat(char *name, int w, int num)
{
   /* Force the name plus the number to fit into "w" characters. Pad with zeroes if too short, or truncate the alphas
    * if too long */
   char numbuf[ 30];
   int len_name, len_num;

   len_name = strlen(name);
   sprintf(numbuf, "%d", num);
   len_num = strlen(numbuf);
   if (len_num > w) Complain("Number too large for field", "", "");
   if (len_name + len_num <= w)
      sprintf(name + len_name, "%0*d", w - len_name, num);
   else
      strcpy(name + w - len_num, numbuf);  /* truncation is necessary */
}

void Compute_New_Name(char *file)
{
   char *p, *basename;
   int file_ok = NO, w;
   static StrBuf workfile = { 0, NULL };

   new_file_name.txt[0] = '\0';
   basename = Cleanup(file, &workfile);
   w = field_width; if (w < 0) w = -w;
   while(Len(workfile.txt)+Len(suffix)+w > new_file_name.max - 100) Expand(&new_file_name);
   if (increment) {
      while (! file_ok) {
         if (trunc_name || chop_off || no_numerics || change_case || strict_char_set)
            Chop_Extension(&workfile);
         strcpy(new_file_name.txt, workfile.txt);
         p = ToEnd(new_file_name.txt);
         if (w) {
            if (field_width < 0) {
               Reformat(basename, w, next_number);
               strcpy(new_file_name.txt, workfile.txt);
            }
            else
              sprintf(p, "%0*d", w, next_number);
         } else
            sprintf(p, "%d", next_number);
         p = ToEnd(p);
         next_number += increment;
         if (suffix) sprintf(p, "%s", suffix);
         file_ok = (File_Status(new_file_name.txt) == NoSuchFile);
      }
   } else if (prefix && suffix) {
      Chop_Extension(&workfile);
      sprintf(new_file_name.txt, "%s%s%s", prefix, workfile.txt, suffix);
   } else if (prefix) {
      sprintf(new_file_name.txt, "%s%s", prefix, workfile.txt);
   } else if (suffix) {
      Chop_Extension(&workfile);
      sprintf(new_file_name.txt, "%s%s", workfile.txt, suffix);
   } else
      strcpy(new_file_name.txt, workfile.txt);
}

void Rename_File(char *file)
{
   int typ;

   typ = File_Status(file);
   if (list_only || typ == NormalFile || (typ == Directory && dirs_also)) Compute_New_Name(file);
   if (list_only)
      Pretend_Rename(file, typ);
   else
      if (typ == NormalFile || (typ == Directory && dirs_also )) Really_Rename(file, typ);
}

int main(int param_count, char *params[])
{
   int i;
   char read_buf[4096], *p;

   Init();
   for (i = 1; i < param_count; i++) {
      if (params[i][0] == '-')
         Set_Options(params, &i, param_count);
      else {
         Rename_File(params[i]);
         if (read_std_in == Std_Yes) read_std_in = Std_No;
      }
   }
   if (read_std_in)
      while (fgets(read_buf, sizeof(read_buf)-1, stdin) != NULL) {
         for (p = read_buf; *p && *p >= ' '; p++);
         *p = '\0';
         Rename_File(read_buf);
      }
   exit(0);
}
