// Text ersetzen, mit strrepl-Funktion auf Basis von strcat
// 
// Aufruf: subst suchwort ersatzwort [infile [outfile]]
// Ist kein Filename angegeben, wird stdin bzw. stdout verwendet
// 
// Klaus Kusche, 2020

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// Max. Zeilenlänge der Eingabe
#define LINELEN 4096

const char *progName;  // Der Programmname (argv[0]), für Fehlermeldungen

void errMsg(const char *operation, const char *fileName);
char *strrepl(char dest[], const char src[], int destLen,
              const char oldStr[], const char newStr[]);

// Gib eine schöne Fehlermeldung aus und beende das Programm
// operation ... wobei ist der Fehler passiert?
// fileName  ... welcher File war betroffen?
void errMsg(const char *operation, const char *fileName)
{
  fprintf(stderr, "%s: error %s %s: %s\n",
          progName, operation, fileName, strerror(errno));
  exit(EXIT_FAILURE);
}

// Die String-Ersetz-Funktion:
// Kopiere src nach dest
// und ersetze dabei alle Vorkommen von oldStr durch newStr
// destLen ist die Größe von dest (zur Längenprüfung)
char *strrepl(char dest[], const char src[], int destLen,
              const char oldStr[], const char newStr[])
{
  // wie viel Platz ist noch in dest?
  int destFree = destLen - 1;   // -1 wegen Platz für die Ende-Markierung

  int oldStrLen = strlen(oldStr);
  if (oldStrLen == 0) {
    // Suchstring ist leer ==> Ergebnis ist unverändert kopierte Eingabe src
    if (strlen(src) > destFree) {
      dest[0] = '\0';  // dest soll bei Fehler auf leer gesetzt werden
      return NULL;
    }
    strcpy(dest, src);
  } else {
    int newStrLen = strlen(newStr);
    // damit strcat / strncat anhängen kann,
    // muss der Zielstring eine gültige Ende-Markierung haben
    dest[0] = '\0';
    
    // aktuelle Position in src, ab der oldStr gesucht wird
    // (alles davor ist schon verarbeitet)
    const char *curPos = src;  // beginne ganz vorne ...
    for (;;) {
      // ... suche das nächste Vorkommen von oldStr in src ab Position curPos 
      const char *wordPos = strstr(curPos, oldStr);
      // ... wenn nicht gefunden: Schleife fertig!
      if (wordPos == NULL) break;
      // Hänge zuerst alles zwischen alter und neuer Fundstelle an dest an ...
      // (Differenz der Pointer = Anzahl der Zeichen zwischen den Pointern)
      int len = wordPos - curPos;
      destFree -= len;
      if (destFree < 0) {
        dest[0] = '\0';  // dest soll bei Fehler auf leer gesetzt werden
        return NULL;
      } 
      strncat(dest, curPos, len);
      // ... und dann das neue Wort 
      destFree -= newStrLen;
      if (destFree < 0) {
        dest[0] = '\0';  // dest soll bei Fehler auf leer gesetzt werden
        return NULL;
      } 
      strcat(dest, newStr);
      // suche in src unmittelbar nach dem gefundenen oldStr weiter
      curPos = wordPos + oldStrLen;
    }
    // hänge alles hinter der letzten Fundstelle in src an dest an
    // (wenn oldStr nie gefunden wurde: alles ab Beginn von src)
    if (strlen(curPos) > destFree) {  // es hat keinen Platz mehr!
      dest[0] = '\0';  // dest soll bei Fehler auf leer gesetzt werden
      return NULL;
    } 
    strcat(dest, curPos);
  }

  return dest;
}

int main(int argc, const char *argv[])
{
  FILE *inFile, *outFile;               // Eingabe-File und Ausgabe-File
  const char *inFileName, *outFileName; // Filenamen der beiden Files
  char input[LINELEN + 2];              // String zum Einlesen einer Zeile
                                        // +1 für \n, +1 für \0
  char output[LINELEN + 2];             // String für Ergebnis von strrepl

  progName = argv[0];
  if ((argc < 3) || (argc > 5)) {
    fprintf(stderr, "Aufruf: %s suchwort ersatzwort [infile [outfile]]\n",
            progName);
    exit(EXIT_FAILURE);
  }
  
  // mach den File auf, wenn auf der Befehlszeile ein Filenamen angegeben wurde
  // verwende stdin bzw. stdout, wenn kein Filenamen angegeben wurde
  if (argc >= 4) {
    inFileName = argv[3];
    if ((inFile = fopen(inFileName, "r")) == NULL) {
      errMsg("opening (for reading)", inFileName);
    }
  } else {
    inFileName = "stdin";
    inFile = stdin;
  }

  if (argc >= 5) {
    outFileName = argv[4];
    if ((outFile = fopen(outFileName, "w")) == NULL) {
      errMsg("opening (for writing)", outFileName);
    }
  } else {
    outFileName = "stdout";
    outFile = stdout;
  }

  // Input zeilenweise verarbeiten bis der File zu Ende ist
  while (fgets(input, sizeof(input), inFile)) {
    if (strchr(input, '\n') == NULL) {
      // Eingelesene Zeile enthält kein \n
      // ==> passiert nur, wenn kein Platz mehr für das \n war
      // ==> Zeile ist länger als LINELEN Zeichen
      // Problem unter Windows:
      // Letzte Zeile von Textfiles hat oft kein \n
      // ==> falsche Fehlermeldung!
      // (in Linux hat auch die letzte Zeile *immer* ein \n)
      fprintf(stderr, "%s: error reading %s: Incomplete line\n",
              progName, inFileName);
      exit(EXIT_FAILURE);
    }
    if (strrepl(output, input, sizeof(output), argv[1], argv[2])) {
      // strrepl hat funktioniert
      fputs(output, outFile);
    } else {
      fprintf(stderr, "%s: error replacing in %s: result too long\n",
              progName, input);
      exit(EXIT_FAILURE);      
    }
  }

  // Hat das fgets NULL geliefert,
  // weil das File-Ende erreicht wurde,
  // oder weil ein Fehler aufgetreten ist?
  if (ferror(inFile)) {
    errMsg("reading", inFileName);
  }

  // Files schließen
  // es schadet auch nichts, die std-Files zu schließen
  if (fclose(inFile) == EOF) {
    errMsg("closing", inFileName);
  }
  if (fclose(outFile) == EOF) {
    errMsg("closing", outFileName);
  }
  
  exit(EXIT_SUCCESS);
}
