/*=-=-=-=-=-=-=-=-=-=-  RCCL/RCI Notice of Copyright  -=-=-=-=-=-=-=-=-=-=-=*
 *									    *
 *	Copyright (C) 1987 by Mike Parker. Permission to use, distribute    *
 *	and copy is granted for non-commercial and research purposes only,  *
 *	providing the authors are acknowledged and this copyright notice is *
 *	displayed. The authors provide this software in hope that it will   *
 *	useful, but do not assume responsibility for its use.		    *
 *									    *
 *	This software was developed using the facilities of the McGill 	    *
 *	University Research Center for Intelligent Machines, Montreal,	    *
 *	Canada, for a project undertaken and supported by the RCA Advanced  *
 *	Technology Laboratories, Moorestown, New Jersey.		    *
 *									    *
 *=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-*/

/*
 * Qsubst -- designed for renaming routines existing in a whole bunch of
 *  files.  Must be compiled with -ltermcap.
 *
 * Usage:
 *
 * qsubst str1 str2 [ options ]
 *
 * Qsubst reads its options (see below) to get a list of files.  For each
 *  file on this list, it then replaces str1 with str2 wherever possible in
 *  that file, depending on user input (see below).  The result is written
 *  back onto the original file.
 *
 * For each possible substitution, the user is prompted with a few lines
 *  before and after the line containing the string to be substituted.  The
 *  string itself is underlined, if the terminal supports this.  Then one
 *  character is read from the terminal.  This is then interpreted as
 *  follows (this is designed to be like Emacs' query-replace-string):
 *
 *	space	replace this occurrence and go on to the next one
 *	.	replace this occurrence and don't change any more in this
 *		file (ie, go on to the next file).
 *	,	tentatively replace this occurrence.  The lines as they would
 *		look if the substitution were made are printed out.  Then
 *		another character is read and it is used to decide the
 *		result (possibly undoing the tentative replacement).
 *	n	don't change this one, but go on to the next one
 *	^G	don't change this one or any others in this file, but instead
 *		go on to the next file.
 *	!	change the rest in this file without asking, then go on to
 *		the next file (at which point qsubst will start asking
 *		again).
 *	?	print out the current filename and ask again.
 *
 * The first two arguments to qsubst are always the string to replace and the
 *  string to replace it with.  The options are as follows:
 *
 *	-w	The search string is considered as a C symbol; it must
 *		be bounded by non-symbol characters.  This option
 *		toggles.  (`w' for `word'.)
 *	-!	Enter ! mode automatically at the beginning of each file
 *	-go	Same as -!
 *	-noask	Same as -!
 *	-nogo	Negate -go
 *	-ask	Negate -noask (same as -nogo)
 *	-cN	(N is a number) Give N lines of context above and below the
 *		line with the match when prompting the user.
 *	-CAN	(N is a number) Give N lines of context above the line with
 *		the match when prompting the user.
 *	-CBN	(N is a number) Give N lines of context below the line with
 *		the match when prompting the user.
 *	-f filename
 *		The filename following the -f argument is one of the files
 *		qsubst should perform substitutions in.
 *	-F filename
 *		Qsubst should read the named file to get the names of files
 *		to perform substitutions in.  The names should appear one to
 *		a line.
 *
 * The default amount of context is -c2, that is, two lines above and two
 *  lines below the line with the match.
 *
 * Arguments not beginning with a - sign in the options field are implicitly
 *  preceded by -f.  Thus, -f is really needed only when the file name begins
 *  with a - sign.
 *
 * Qsubst reads its options in order and processes files as it gets them.
 *  This means, for example, that a -go will affect only files from -f or -F
 *  options appearing after the -go option.
 *
 * The most context you can get is ten lines each, above and below
 *  (corresponding to -c10).
 *
 * Str1 is limited to 512 characters; there is no limit on the size of str2.
 *  Neither one may contain a null.
 *
 * Nulls in the file may cause qsubst to make various mistakes.
 *
 * If any other program modifies the file while qsubst is running, all bets
 *  are off.
 */

#include <stdio.h>
#if defined(sgi) || defined(linux)
#include <termios.h>
#else
#include <sgtty.h>
#endif
#include <ctype.h>
#include <signal.h>
#include <sys/file.h>

#ifndef sigmask
#define sigmask(sig) (1<<((sig)-1))
#endif

#define MAX_C_A 10
#define MAX_C_B 10
#define BUF_SIZ 1024

char **argvec;

char *getenv();
char *tgetstr();
char *malloc();
char *sindex();
#ifdef NO_PROTOTYPES
char *mktemp();
#endif

int debugging;
FILE *tempf;
long int tbeg;
FILE *workf;
char *str1;
char *str2;
int s1l;
int s2l;
long int nls[MAX_C_A+1];
char buf[(BUF_SIZ*2)+2];
char *bufp;
char *bufp0;
char *bufpmax;
int rahead;
int cabove;
int cbelow;
int wordmode;
int flying;
int flystate;
int allfly;
char *nullstr = "";
int ul_;
char *current_file;
char *beginul;
char *endul;
char tcp_buf[1024];
char cap_buf[1024];
#if defined(sgi) || defined(linux)
struct termios orig_sg;
#else
struct sgttyb orig_sg;
#endif

tstp_self()
{
#if defined(vax) || defined(sgi) || defined(_AIX)
 int (*old_tstp)();
#else
 void (*old_tstp)();
#endif
 int mask;

 mask = sigblock(0);
 old_tstp = signal(SIGTSTP,SIG_DFL);
 sigsetmask(mask&~sigmask(SIGTSTP));
 kill(getpid(),SIGTSTP);
 signal(SIGTSTP,old_tstp);
}
#if defined(linux)
void
#endif
sigtstp()
{
#if defined(sgi)
 struct termios sg;

 if (ioctl(fileno(stdin),TCGETA,&sg) < 0)
  { tstp_self();
    return;
  }
 ioctl(fileno(stdin),TCSETA,&orig_sg);
 tstp_self();
 ioctl(fileno(stdin),TCSETA,&sg);
#elif defined(linux)
 struct termios sg;

 if (tcgetattr(fileno(stdin),&sg) < 0)
  { tstp_self();
    return;
  }
 tcsetattr(fileno(stdin),TCSANOW,&orig_sg);
 tstp_self();
 tcsetattr(fileno(stdin),TCSANOW,&sg);

#else
 struct sgttyb sg;

 if (ioctl(fileno(stdin),TIOCGETP,&sg) < 0)
  { tstp_self();
    return;
  }
 ioctl(fileno(stdin),TIOCSETN,&orig_sg);
 tstp_self();
 ioctl(fileno(stdin),TIOCSETN,&sg);
#endif
}

main(ac,av)
int ac;
char **av;
{
 int skip;
 char *cp;

 argvec = av;
 debugging = !!getenv("QSUBST_DEBUG");
 if (ac < 3)
  { fprintf(stderr,"Usage: %s str1 str2 [ -w -! -noask -go -f file -F file ]\n",
				argvec[0]);
    exit(1);
  }
 cp = getenv("TERM");
 if (cp == 0)
  { beginul = nullstr;
    endul = nullstr;
  }
 else
  { if (tgetent(tcp_buf,cp) != 1)
     { beginul = nullstr;
       endul = nullstr;
     }
    else
     { cp = cap_buf;
       if (tgetflag("os") || tgetflag("ul"))
	{ ul_ = 1;
	}
       else
	{ ul_ = 0;
	  beginul = tgetstr("us",&cp);
	  if (beginul == 0)
	   { beginul = tgetstr("so",&cp);
	     if (beginul == 0)
	      { beginul = nullstr;
		endul = nullstr;
	      }
	     else
	      { endul = tgetstr("se",&cp);
	      }
	   }
	  else
	   { endul = tgetstr("ue",&cp);
	   }
	}
     }
  }
#if defined(linux)
 cp = (char*)mkstemp("/tmp/qsubst.XXXXXX");
#else
 cp = (char*)mktemp("/tmp/qsubst.XXXXXX");
#endif
 tempf = fopen(cp,"w+");
 if (tempf == NULL)
  { fprintf(stderr,"%s: cannot create temp file %s\n",argvec[0],cp);
    exit(1);
  }
 unlink(cp);
 if ( (access(av[1],R_OK|W_OK) == 0) &&
      (access(av[ac-1],R_OK|W_OK) < 0) &&
      (access(av[ac-2],R_OK|W_OK) < 0) )
  { fprintf(stderr,"\
%s: argument order has changed, it's now: str1 str2 files...\n",argvec[0]);
  }
 str1 = av[1];
 str2 = av[2];
 av += 2;
 ac -= 2;
 s1l = strlen(str1);
 s2l = strlen(str2);
 if (s1l > BUF_SIZ)
  { fprintf(stderr,"%s: search string too long (max %d chars)\n",
						argvec[0],BUF_SIZ);
    exit(1);
  }
#if defined(sgi)
 ioctl(fileno(stdin),TCGETA,&orig_sg);
#elif defined(linux)
 tcgetattr(fileno(stdin),&orig_sg);
#else
 ioctl(fileno(stdin),TIOCGETP,&orig_sg);
#endif
 signal(SIGTSTP,sigtstp);
 allfly = 0;
 cabove = 2;
 cbelow = 2;
 skip = 0;
 for (ac--,av++;ac;ac--,av++)
  { if (skip > 0)
     { skip --;
       continue;
     }
    if (**av == '-')
     { ++*av;
       if (!strcmp(*av,"w"))
	{ wordmode = ! wordmode;
	}
       else if ( (strcmp(*av,"!") == 0) ||
		 (strcmp(*av,"go") == 0) ||
		 (strcmp(*av,"noask") == 0) )
	{ allfly = 1;
	}
       else if ( (strcmp(*av,"nogo") == 0) ||
		 (strcmp(*av,"ask") == 0) )
	{ allfly = 0;
	}
       else if (**av == 'c')
	{ cabove = atoi(++*av);
	  cbelow = cabove;
	  limit_above_below();
	}
       else if (**av == 'C')
	{ ++*av;
	  if (**av == 'A')
	   { cabove = atoi(++*av);
	     limit_above_below();
	   }
	  else if (**av == 'B')
	   { cbelow = atoi(++*av);
	     limit_above_below();
	   }
	  else
	   { fprintf(stderr,"%s: -C must be -CA or -CB\n",argvec[0]);
	   }
	}
       else if ( (strcmp(*av,"f") == 0) ||
		 (strcmp(*av,"F") == 0) )
	{ if (++skip >= ac)
	   { fprintf(stderr,"%s: -%s what?\n",argvec[0],*av);
	   }
	  else
	   { if (**av == 'f')
	      { process_file(av[skip]);
	      }
	     else
	      { process_indir_file(av[skip]);
	      }
	   }
	}
     }
    else
     { process_file(*av);
     }
  }
 exit(0);
}

limit_above_below()
{
 if (cabove > MAX_C_A)
  { cabove = MAX_C_A;
  }
 if (cbelow > MAX_C_B)
  { cbelow = MAX_C_B;
  }
}

char getc_cbreak()
{
#if defined(sgi) || defined(linux)
 struct termios sg;
 struct termios osg;
 char c;
#if defined(linux)
 if (tcgetattr(fileno(stdin),&sg) < 0)
  { return(getchar());
  }
#else
 if (ioctl(fileno(stdin),TCGETA,&sg) < 0)
  { return(getchar());
  }
#endif
 osg = sg;
 sg.c_lflag &= ~(ICANON|ECHO|ECHOE|ECHOK|ECHONL);
 sg.c_cc[VMIN] = 1;
 sg.c_cc[VTIME] = 0;

#if defined(linux)
 tcsetattr(fileno(stdin),TCSANOW,&sg);
 c = getchar();
 tcsetattr(fileno(stdin),TCSANOW,&osg);

#else
 ioctl(fileno(stdin),TCSETA,&sg);
 c = getchar();
 ioctl(fileno(stdin),TCSETA,&osg);
#endif

#else
 struct sgttyb sg;
 struct sgttyb osg;
 char c;

 if (ioctl(fileno(stdin),TIOCGETP,&sg) < 0)
  { return(getchar());
  }
 osg = sg;
 sg.sg_flags |= CBREAK;
 sg.sg_flags &= ~ECHO;
 ioctl(fileno(stdin),TIOCSETN,&sg);
 c = getchar();
 ioctl(fileno(stdin),TIOCSETN,&osg);
#endif
 return(c);
}

int issymchar(c)
char c;
{
 return( isascii(c) &&
	 ( isalnum(c) ||
	   (c == '_') ||
	   (c == '$') ) );
}

int foundit()
{
 if (wordmode)
  { return( !issymchar(bufp[-1]) &&
	    !issymchar(bufp[-2-s1l]) &&
	    !bcmp(bufp-1-s1l,str1,s1l) );
  }
 else
  { return(!bcmp(bufp-s1l,str1,s1l));
  }
}

int doit()
{
 long int save;
 int i;
 char c;
 int lastnl;
 int use_replacement;

 if (flying)
  { return(flystate);
  }
 use_replacement = 0;
 save = ftell(workf);
 do
  { for (i=MAX_C_A-cabove;nls[i]<0;i++) ;
    fseek(workf,nls[i],0);
    for (i=save-nls[i]-rahead;i;i--)
     { putchar(getc(workf));
     }
    put_ul(use_replacement?str2:str1);
    fseek(workf,save+s1l-rahead,0);
    lastnl = 0;
    i = cbelow + 1;
    while (i > 0)
     { int c;
       c = getc(workf);
       if (c == EOF)
	{ clearerr(workf);
	  break;
	}
       putchar(c);
       lastnl = 0;
       if (c == '\n')
	{ i --;
	  lastnl = 1;
	}
     }
    if (! lastnl) printf("\n[no final newline] ");
    fseek(workf,save,0);
    i = -1;
    while (i == -1)
     { switch (getc_cbreak())
	{ case ' ':
	     i = 1;
	     break;
	  case '.':
	     i = 1;
	     flying = 1;
	     flystate = 0;
	     break;
	  case 'n':
	     i = 0;
	     break;
	  case '\7':
	     i = 0;
	     flying = 1;
	     flystate = 0;
	     break;
	  case '!':
	     i = 1;
	     flying = 1;
	     flystate = 1;
	     break;
	  case ',':
	     use_replacement = ! use_replacement;
	     i = -2;
	     printf("(using %s string gives)\n",use_replacement?"new":"old");
	     break;
	  case '?':
	     printf("File is `%s'\n",current_file);
	     break;
	  default:
	     putchar('\7');
	     break;
	}
     }
  } while (i < 0);
 if (i)
  { printf("(replacing");
  }
 else
  { printf("(leaving");
  }
 if (flying)
  { if (flystate == i)
     { printf(" this and all the rest");
     }
    else if (flystate)
     { printf(" this, replacing all the rest");
     }
    else
     { printf(" this, leaving all the rest");
     }
  }
 printf(")\n");
 return(i);
}

process_indir_file(fn)
char *fn;
{
 char newfn[1024];
 FILE *f;

 f = fopen(fn,"r");
 if (f == NULL)
  { fprintf(stderr,"%s: cannot read %s\n",argvec[0],fn);
    return;
  }
 while (fgets(newfn,sizeof(newfn),f) == newfn)
  { newfn[strlen(newfn)-1] = '\0';
    process_file(newfn);
  }
 fclose(f);
}

process_file(fn)
char *fn;
{
 int i;
 long int n;
 char *cp;
 int c;

 workf = fopen(fn,"r+");
 if (workf == NULL)
  { fprintf(stderr,"%s: cannot read %s\n",argvec[0],fn);
    return;
  }
 printf("(file: %s)\n",fn);
 current_file = fn;
 for (i=0;i<=MAX_C_A;i++)
  { nls[i] = -1;
  }
 nls[MAX_C_A] = 0;
 tbeg = -1;
 if (wordmode)
  { bufp0 = &buf[1];
    rahead = s1l + 1;
    buf[0] = '\0';
  }
 else
  { bufp0 = &buf[0];
    rahead = s1l;
  }
 if (debugging)
  { printf("[rahead = %d, bufp0-buf = %d]\n",rahead,bufp0-&buf[0]);
  }
 n = 0;
 bufp = bufp0;
 bufpmax = &buf[sizeof(buf)-s1l-2];
 flying = allfly;
 flystate = 1;
 while (1)
  { c = getc(workf);
    if (c == EOF)
     { if (tbeg >= 0)
	{ if (bufp > bufp0) fwrite(bufp0,1,bufp-bufp0,tempf);
	  fseek(workf,tbeg,0);
	  n = ftell(tempf);
	  fseek(tempf,0L,0);
	  for (;n;n--)
	   { putc(getc(tempf),workf);
	   }
	  fflush(workf);
	  ftruncate(fileno(workf),ftell(workf));
	}
       fclose(workf);
       return;
     }
    *bufp++ = c;
    n ++;
    if (debugging)
     { printf("[got %c, n now %d, bufp-buf %d]\n",c,n,bufp-bufp0);
     }
    if ((n >= rahead) && foundit() && doit())
     { int wbehind;
       if (debugging)
	{ printf("[doing change]\n");
	}
       wbehind = 1;
       if (tbeg < 0)
	{ tbeg = ftell(workf) - rahead;
	  fseek(tempf,0L,0);
	  if (debugging)
	   { printf("[tbeg set to %d]\n",(int)tbeg);
	   }
	  wbehind = 0;
	}
       if (bufp[-1] == '\n') add_shift(nls,ftell(workf),MAX_C_A+1);
       if ((n > rahead) && wbehind)
	{ fwrite(bufp0,1,n-rahead,tempf);
	  if (debugging)
	   { printf("[writing %d from bufp0]\n",n-rahead);
	   }
	}
       fwrite(str2,1,s2l,tempf);
       n = rahead - s1l;
       if (debugging)
	{ printf("[n now %d]\n",n);
	}
       if (n > 0)
	{ bcopy(bufp-n,bufp0,n);
	  if (debugging)
	   { printf("[copying %d back]\n",n);
	   }
	}
       bufp = bufp0 + n;
     }
    else
     { if (bufp[-1] == '\n') add_shift(nls,ftell(workf),MAX_C_A+1);
       if (bufp >= bufpmax)
	{ if (tbeg >= 0)
	   { fwrite(bufp0,1,n-rahead,tempf);
	     if (debugging)
	      { printf("[flushing %d]\n",n-rahead);
	      }
	   }
	  n = rahead;
	  bcopy(bufp-n,bufp0,n);
	  if (debugging)
	   { printf("[n now %d]\n[copying %d back]\n",n,n);
	   }
	  bufp = bufp0 + n;
	}
     }
  }
}

add_shift(a,e,n)
register long int *a;
register long int e;
register int n;
{
 register int i;

 n --;
 for (i=0;i<n;i++)
  { a[i] = a[i+1];
  }
 a[n] = e;
}

putcharf(c)
char c;
{
 putchar(c);
}

put_ul(s)
char *s;
{
 if (ul_)
  { for (;*s;s++)
     { printf("_\b%c",*s);
     }
  }
 else
  { tputs(beginul,1,putcharf);
    fputs(s,stdout);
    tputs(endul,1,putcharf);
  }
}
