// Verkettete Liste:
// Files seitenweise in umgekehrter Seiten-Reihenfolge ausgeben
//
// Aufruf: pagerev page_length file1 file2 ...
//
// Klaus Kusche, 2020

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

// Max. Zeilen pro Seite
#define MAX_LINES 128
// Max. Zeilenlänge
#define MAX_LINELEN 256

// Typ für 1 Element der verketteten Liste = 1 Seite
typedef struct page {
  const char *file_name;         // Name des Files (für Seiten-Überschriften)
  int page_num;                  // Seitennummer (für Seiten-Überschriften)
  const char *text[MAX_LINES];   // Texte der Zeilen auf der Seite
  struct page *next;             // Listenverkettung zur nächsten Seite
} page_t;

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

// Anzahl der Zeilen pro Seite (argv[1] als int) (ohne Überschrift)
int page_len;

page_t *new_page(page_t *head, const char *file_name, int page_num);
char *checked_strdup(const char *str);
page_t *read_file(page_t *head, const char *file_name);
void write_pages(page_t *head);

// hänge eine neue Seite (für Seite page_num von File file_name)
// vorne an die Liste head
// Returnwert: Pointer auf die neue Seite = neuer Head der Liste
page_t *new_page(page_t *head, const char *file_name, int page_num)
{
  page_t *p = (page_t *) (malloc(sizeof (page_t)));
  if (p == NULL) {
    fprintf(stderr, "%s: Out of memory: %s\n", prog_name, strerror(errno));
    exit(EXIT_FAILURE);
  }
  p->file_name = file_name;
  p->page_num = page_num;
  // Nachfolger des neuen Elementes ist das bisherige erste Element
  p->next = head;
  return p;
}

// strdup mit Fehlerprüfung
char *checked_strdup(const char *str)
{
  char *p = strdup(str);   // dynamisch angelegte Kopie der Zeile
  if (p == NULL) {
    fprintf(stderr, "%s: Out of memory: %s\n", prog_name, strerror(errno));
    exit(EXIT_FAILURE);
  }
  return p;
}

// Lies den File file_name und hänge seine Seiten vorne an die Liste head an
// Returnwert: Neuer head der Liste
page_t *read_file(page_t *head, const char *file_name)
{
  FILE *f = fopen(file_name, "r");
  if (f == NULL) {
    fprintf(stderr, "%s: Can't open %s: %s\n", prog_name, file_name,
            strerror(errno));
    exit(EXIT_FAILURE);
  }

  char line[MAX_LINELEN];
  int page_num = 0;  // Seiten-Nummer, beginnt bei jedem File wieder von vorne
  int line_in_page;  // Nummer der aktuellen Zeile innerhalb der Seite
                     // Beginnt bei jeder neuen Seite wieder bei 0!

  // Schleife pro Zeile (fgets liefert am Fileende NULL = false)
  // Schmutztrick: Der Anfangswert page_len für line_in_page bewirkt,
  //               dass gleich für die erste Zeile eine neue Seite angelegt wird
  for (line_in_page = page_len; fgets(line, sizeof (line), f); ++line_in_page) {

    // Beginnt eine neue Seite, d.h. ist die alte Seite voll?
    if (line_in_page == page_len) {
      // hänge eine neue Seite vorne an die Liste an
      head = new_page(head, file_name, ++page_num);
      // Zeilennummern auf der neuen Seite beginnen wieder bei 0
      line_in_page = 0;
    }

    // Wurde die Zeile komplett incl. \n gelesen, oder fehlt das hinteren Ende?
    if (strchr(line, '\n') == NULL) {
      fprintf(stderr, "%s: Incomplete line (File %s, Page %d, Line %d)\n",
              prog_name, file_name, page_num, line_in_page + 1);
      exit(EXIT_FAILURE);
    }

    // Speichere eine Kopie der Zeile
    // als nächste Zeile in der aktuellen Seite (= erste Seite in der Liste)
    head->text[line_in_page] = checked_strdup(line);
  }

  // Endete die Lese-Schleife wegen Dateiende oder wegen Lesefehler?
  if (ferror(f)) {
    fprintf(stderr, "%s: Can't read %s: %s\n", prog_name, file_name,
            strerror(errno));
    exit(EXIT_FAILURE);
  }

  // Bei nur-Lese-Files gibt es bei close normalerweise keine Fehler...
  fclose(f);

  // Letzte Seite des soeben beendeten Files mit Leerzeilen auffüllen
  // (außer der File ist sich genau auf eine volle Seite ausgegangen)
  for ( ; line_in_page < page_len; ++line_in_page) {
    head->text[line_in_page] = "\n";
  }

  return head;  // neuen Listenanfang zurückgeben
}

// Gib alle Seiten der Liste head aus
void write_pages(page_t *head)
{
  // Liste der Seiten der Reihe nach durchlaufen...
  for (page_t *p = head; p != NULL; p = p->next) {
    if (printf("%-69s Page %05d\n\n", p->file_name, p->page_num) < 0) {
      fprintf(stderr, "%s: Can't write stdout: %s\n",
              prog_name, strerror(errno));
      exit(EXIT_FAILURE);
    }
    // ... und für jede Seite alle ihre Zeilen ausgeben
    for (int i = 0; i < page_len; ++i) {
      if (fputs(p->text[i], stdout) == EOF) {  
        fprintf(stderr, "%s: Can't write stdout: %s\n",
                prog_name, strerror(errno));
        exit(EXIT_FAILURE);
      }
    }
  }
}

int main(int argc, const char *argv[])
{
  // Die Liste der Seiten ist ein Stack (letzte Seite wird zuerst ausgegeben)
  // ==> Neue Elemente werden vorne angehängt
  // ==> Kein Tail-Pointer notwendig!
  page_t *head = NULL;

  prog_name = argv[0];

  if (argc < 3) {
    fprintf(stderr, "Usage: %s page_length file ...\n", prog_name);
    exit(EXIT_FAILURE);
  }
  page_len = atoi(argv[1]);
  if ((page_len < 1) || (page_len > MAX_LINES)) {
    fprintf(stderr, "%s: Page length must be 1...%d, is %d\n",
            prog_name, MAX_LINES, page_len);
    exit(EXIT_FAILURE);
  }

  // argv[1] ist Seitenlänge ==> Erster Filename ist argv[2]
  for (int i = 2; i < argc; ++i) {  
    head = read_file(head, argv[i]);
  }

  write_pages(head);

  exit(EXIT_SUCCESS);
}
