// ==========================================================================
//         ____            _                     _____           _
//        / ___| _   _ ___| |_ ___ _ __ ___     |_   _|__   ___ | |___
//        \___ \| | | / __| __/ _ \ '_ ` _ \ _____| |/ _ \ / _ \| / __|
//         ___) | |_| \__ \ ||  __/ | | | | |_____| | (_) | (_) | \__ \.
//        |____/ \__, |___/\__\___|_| |_| |_|     |_|\___/ \___/|_|___/
//               |___/
//                             --- System-Tools ---
//                  https://www.nntb.no/~dreibh/system-tools/
// ==========================================================================
//
// Text-Block
// Copyright (C) 2025-2026 by Thomas Dreibholz
//
// 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 <http://www.gnu.org/licenses/>.
//
// Contact: thomas.dreibholz@gmail.com

#define _GNU_SOURCE
#define __EXTENSIONS__
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef ENABLE_NLS
#include <libintl.h>
#else
#define bindtextdomain(domain, dirname) { }
#define textdomain(domain) { }
#define gettext(string) string
#define ngettext(singular, plural, n) ((n) == 1 ? (singular) : (plural))
#endif

#include "package-version.h"

#if defined(__STDC_VERSION__) && (__STDC_VERSION__ < 202311L)
#ifndef nullptr
#define nullptr ((void*)0)
#endif
#endif


// #define DEBUG_MODE


typedef enum textblockmode {
   Cat,
   Discard,
   Enumerate,
   Highlight,
   Extract,
   InsertFront,
   InsertBack,
   Replace,
   Remove
} textblockmode_t;


static textblockmode_t Mode                 = Cat;
static int             MinActions           = -1;
static int             MaxActions           = -1;
static const char*     BeginTag             = nullptr;
static size_t          BeginTagLength       = 0;
static const char*     EndTag               = nullptr;
static size_t          EndTagLength         = 0;
static long long       SelectBegin          = 0;
static long long       SelectEnd            = 0;
static long long       TotalInputLines      = -1;
static bool            IncludeTags          = false;
static bool            FullTagLines         = false;
static char            EnumerateFormat[128] = "%06llu";
static const char*     Enumerate1           = "\x1b[36m";
static const char*     Enumerate2           = "\x1b[0m ";
static const char*     HighlightBegin       = "⭐";
static const char*     HighlightEnd         = "🛑";
static const char*     HighlightUnmarked1   = "\x1b[34m";
static const char*     HighlightUnmarked2   = "\x1b[0m";
static const char*     HighlightMarked1     = "\x1b[31m";
static const char*     HighlightMarked2     = "\x1b[0m";
static const char*     InputFileName        = nullptr;
static char*           RealInputFileName    = nullptr;
static FILE*           InputFile            = nullptr;
static bool            OpenInputFile        = false;
static const char*     OutputFileName       = nullptr;
static char*           OutputTempFileName   = nullptr;
static FILE*           OutputFile           = nullptr;
static bool            OpenOutputFile       = false;
static bool            OpenOutputAppend     = false;
static bool            OutputDeleteOnError  = true;
static bool            InPlaceUpdate        = false;
static const char*     InsertFileName       = nullptr;
static FILE*           InsertFile           = nullptr;
static bool            OpenInsertFile       = false;
static bool            InMarkedBlock        = false;
static char*           Buffer               = nullptr;
static size_t          BufferSize           = 65536;

static int             Actions;
static long long       LineNo;
static const char*     EndOfLine;
static const char*     MarkerTag;
static size_t          MarkerTagLength;
static const char*     Pointer;


// ###### Clean up ##########################################################
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202000L)
[[ noreturn ]]
#endif
static void cleanUp(int exitCode)
{
   // ====== Close insert file ==============================================
   if(OpenInsertFile) {
      if(InsertFile) {
         fclose(InsertFile);
      }
      OpenInsertFile = false;
   }
   InsertFile = nullptr;

   // ====== Close output file ==============================================
   if(OpenOutputFile) {
      if(OutputFile) {
         if(fclose(OutputFile) != 0) {
            fprintf(stderr, gettext("ERROR: Unable to open output file %s: %s!"),
                  OutputFileName, strerror(errno));
            fputs("\n", stderr);
            exitCode = 1;
         }
      }
      if( (exitCode != 0) && (OutputDeleteOnError) ) {
         const char* brokenFileName = (OutputTempFileName != nullptr) ?
                                         OutputTempFileName : OutputFileName;
         if(brokenFileName) {
            if(unlink(brokenFileName) != 0) {
               fprintf(stderr, gettext("ERROR: Unable to delete broken output file %s: %s!"),
                     brokenFileName, strerror(errno));
               fputs("\n", stderr);
            }
         }
      }
      else if( (exitCode == 0) && (OutputTempFileName != nullptr) ) {
         // ------ Restore original file permissions and ownership ----------
         struct stat fileInfo;
         if(stat(InputFileName, &fileInfo) == 0) {
            // Try to change ownership:
            int result = chown(OutputTempFileName, fileInfo.st_uid, fileInfo.st_gid);
            (void)result;   // just ignore errors
            // Restore file permissions:
            if(chmod(OutputTempFileName, fileInfo.st_mode & 07777) != 0) {
               fprintf(stderr, gettext("WARNING: Unable to restore file permissions for %s: %s!"),
                       InputFileName, strerror(errno));
               fputs("\n", stderr);
            }
         }

         // ------ Overwrite original file ----------------------------------
         if(rename(OutputTempFileName, InputFileName) != 0) {
            fprintf(stderr, gettext("ERROR: Unable to change name of temporary output file from %s to %s: %s!"),
                    OutputTempFileName, InputFileName, strerror(errno));
            fputs("\n", stderr);
         }
      }
      OpenOutputFile = false;
   }
   OutputFile = nullptr;

   if(OutputTempFileName) {
      free(OutputTempFileName);
      OutputTempFileName = nullptr;
   }

   // ====== Close input file ===============================================
   if(OpenInputFile) {
      if(InputFile) {
         fclose(InputFile);
      }
      OpenInputFile = false;
   }
   InputFile = nullptr;
   if(RealInputFileName) {
      free(RealInputFileName);
      RealInputFileName = nullptr;
   }

   // ====== Free buffer ====================================================
   if(Buffer) {
      free(Buffer);
      Buffer = nullptr;
   }

   exit(exitCode);
}


// ###### Generate temporary output file name ###############################
static char* makeTempOutputFileName(const char* outputFileName)
{
   const size_t ouputFileNameLength = strlen(outputFileName);
   OutputTempFileName = malloc(ouputFileNameLength + 2);
   if(OutputTempFileName == nullptr) {
      fprintf(stderr, gettext("ERROR: malloc() failed: %s!"), strerror(errno));
      fputs("\n", stderr);
      cleanUp(1);
   }
   strncpy(OutputTempFileName, outputFileName, ouputFileNameLength);
   OutputTempFileName[ouputFileNameLength + 0] = '~';
   OutputTempFileName[ouputFileNameLength + 1] = 0x00;
   return OutputTempFileName;
}


// ###### Write data to output file #########################################
static void writeToOutputFile(const char* data, const size_t length)
{
   if(length > 0) {
      if(fwrite(data, 1, length, OutputFile) < length) {
         fprintf(stderr, gettext("ERROR: Unable to write to output file %s: %s!"),
                 OutputFileName, strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
   }
}


// ###### Count lines of input file #########################################
static long long countLines(FILE* inputFile)
{
   long long lines = 0;

   while( (getline((char**)&Buffer, &BufferSize, inputFile)) > 0 ) {
      // getline() may have reallocated the buffer, if it was necessary!
      lines++;
   }
   if(inputFile != stdin) {
      rewind(inputFile);
   }

   return lines;
}


// ###### Write contents of insert file #####################################
static void copyInsertFileIntoOutputFile(void)
{
   char   buffer[65536];
   size_t bytesRead;
   while( (bytesRead = fread((char*)&buffer, 1, sizeof(buffer), InsertFile)) > 0 ) {
      writeToOutputFile(buffer, bytesRead);
   }
   if(InsertFile != stdin) {
      rewind(InsertFile);
   }
}


// ###### Process unmarked text #############################################
static void processUnmarked(const char*   text,
                            const ssize_t textLength,
                            const bool    beginOfMarking)
{
   assert(InMarkedBlock == false);
   assert(textLength >= 0);

   const size_t outputLength = (size_t)textLength;
   switch(Mode) {
      case Cat:
      case Remove:
      case Replace:
         writeToOutputFile(text, outputLength);
       break;
      case InsertFront:
         writeToOutputFile(text, outputLength);
         if(beginOfMarking) {
            copyInsertFileIntoOutputFile();
         }
       break;
      case InsertBack:
         writeToOutputFile(text, outputLength);
       break;
      case Enumerate:
         fputs(Enumerate1, OutputFile);
         fprintf(OutputFile, EnumerateFormat, LineNo);
         fputs(Enumerate2, OutputFile);
         writeToOutputFile(text, outputLength);
       break;
      case Highlight:
         if(outputLength > 0) {
            fputs(HighlightUnmarked1, OutputFile);
            writeToOutputFile(text, outputLength);
            fputs(HighlightUnmarked2, OutputFile);
         }
         if(beginOfMarking) {
            fputs(HighlightBegin, OutputFile);
         }
       break;
      default:
       break;
   }

   if(beginOfMarking) {
      InMarkedBlock = true;
      Actions++;
   }
}


// ###### Process marked text ###############################################
static void processMarked(const char*   text,
                          const ssize_t textLength,
                          const bool    endOfMarking)
{
   assert(InMarkedBlock == true);
   assert(textLength >= 0);

   const size_t outputLength = (size_t)textLength;
   switch(Mode) {
      case Extract:
         writeToOutputFile(text, outputLength);
       break;
      case Replace:
         if(endOfMarking) {
            copyInsertFileIntoOutputFile();
         }
       break;
      case InsertFront:
         writeToOutputFile(text, outputLength);
       break;
      case InsertBack:
         writeToOutputFile(text, outputLength);
         if(endOfMarking) {
            copyInsertFileIntoOutputFile();
         }
       break;
      case Highlight:
         if(outputLength > 0) {
            fputs(HighlightMarked1, OutputFile);
            writeToOutputFile(text, outputLength);
            fputs(HighlightMarked2, OutputFile);
         }
         if(endOfMarking) {
            fputs(HighlightEnd, OutputFile);
         }
       break;
      default:
       break;
   }

   if(endOfMarking) {
      InMarkedBlock = false;
   }
}


// ###### Version ###########################################################
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202000L)
[[ noreturn ]]
#endif
static void version(void)
{
   printf("text-block %s\n", SYSTEMTOOLS_VERSION);
   exit(0);
}


// ###### Usage #############################################################
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202000L)
[[ noreturn ]]
#endif
static void usage(const char* program, const int exitCode)
{
   fprintf(stderr, "%s %s"
           " [-C|--cat]"
           " [-0|--discard]"
           " [-H|--highlight]"
           " [-E|--enumerate]"
           " [-X|--extract]"
           " [-D|--delete|--remove]"
           " [-F|--insert-front insert_file]"
           " [-B|--insert-back insert_file]"
           " [-R|--replace insert_file]"
           " [-i|--input input_file]"
           " [-o|--output output_file]"
           " [-a|--append]"
           " [-p|--in-place]"
           " [-k|--keep-broken]"
           " [--min-actions|-m actions]"
           " [--max-actions|-M actions]"
           " [-s|--select from_line to_line]"
           " [-b|--begin-tag begin_tag]"
           " [-e|--end-tag end_tag]"
           " [-t|--tag tag]"
           " [-y|--include-tags]"
           " [-x|--exclude-tags]"
           " [-f|--full-tag-lines]"
           " [-g|--tags-only]"
           " [--highlight-[begin|end|unmarked1|unmarked2|marked1|marked2] label]"
           " [--highlight-params begin_label end_label unmarked1_label unmarked2_label marked1_label marked2_label]"
           " [--enumerate-format format]"
           " [--enumerate-label[1|2] string]"
           " [-q|--suppress-warnings]"
           " [-h|--help]"
           " [-v|--version]\n",
           gettext("Usage:"), program);
   exit(exitCode);
}



// ###### Main program ######################################################
int main (int argc, char** argv)
{
   // ====== Initialise i18n support ========================================
   if(setlocale(LC_ALL, "") == nullptr) {
      setlocale(LC_ALL, "C.UTF-8");   // "C" should exist on all systems!
   }
   bindtextdomain("text-block", SYSTEMTOOLS_LOCALEDIR);
   textdomain("text-block");

   // ====== Handle arguments ===============================================
   bool showWarnings = true;
   static const struct option long_options[] = {
      { "cat",                 no_argument,       0, 'C' },
      { "discard",             no_argument,       0, '0' },
      { "highlight",           no_argument,       0, 'H' },
      { "enumerate",           no_argument,       0, 'E' },
      { "extract",             no_argument,       0, 'X' },
      { "delete",              no_argument,       0, 'D' },
      { "remove",              no_argument,       0, 'D' },   // Alias for "delete"
      { "insert-front",        required_argument, 0, 'F' },
      { "insert-back",         required_argument, 0, 'B' },
      { "replace",             required_argument, 0, 'R' },

      { "input",               required_argument, 0, 'i' },
      { "output",              required_argument, 0, 'o' },
      { "append",              no_argument,       0, 'a' },
      { "in-place",            no_argument,       0, 'p' },
      { "keep-broken",         no_argument,       0, 'k' },
      { "min-actions",         required_argument, 0, 'm' },
      { "max-actions",         required_argument, 0, 'M' },

      { "select",              required_argument, 0, 's' },
      { "begin-tag",           required_argument, 0, 'b' },
      { "end-tag",             required_argument, 0, 'e' },
      { "tag",                 required_argument, 0, 't' },
      { "exclude-tags",        no_argument,       0, 'x' },
      { "include-tags",        no_argument,       0, 'y' },
      { "full-tag-lines",      no_argument,       0, 'f' },
      { "tags-only",           no_argument,       0, 'g' },

      { "enumerate-format",    required_argument, 0, 0x1000 },
      { "enumerate-label1",    required_argument, 0, 0x1001 },
      { "enumerate-label2",    required_argument, 0, 0x1002 },
      { "highlight-begin",     required_argument, 0, 0x2000 },
      { "highlight-end",       required_argument, 0, 0x2001 },
      { "highlight-unmarked1", required_argument, 0, 0x2002 },
      { "highlight-unmarked2", required_argument, 0, 0x2003 },
      { "highlight-marked1",   required_argument, 0, 0x2004 },
      { "highlight-marked2",   required_argument, 0, 0x2005 },
      { "highlight-params",    required_argument, 0, 0x2006 },

      { "suppress-warnings",   no_argument,       0, 'q' },
      { "help",                no_argument,       0, 'h' },
      { "version",             no_argument,       0, 'v' },
      {  nullptr,              0,                 0, 0   }
   };

   int option;
   int longIndex;
   while( (option = getopt_long(argc, argv, "C0HEXDF:B:R:i:o:apkm:M:b:e:t:s:xyfgqhv", long_options, &longIndex)) != -1 ) {
      switch(option) {
         case 'C':
            Mode = Cat;
          break;
         case '0':
            Mode = Discard;
          break;
         case 'H':
            Mode = Highlight;
          break;
         case 'E':
            Mode = Enumerate;
          break;
         case 'X':
            Mode = Extract;
          break;
         case 'D':
            Mode = Remove;
          break;
         case 'F':
            Mode           = InsertFront;
            InsertFileName = optarg;
          break;
         case 'B':
            Mode           = InsertBack;
            InsertFileName = optarg;
          break;
         case 'R':
            Mode           = Replace;
            InsertFileName = optarg;
          break;
         case 'i':
            InputFileName  = optarg;
          break;
         case 'o':
            OutputFileName = optarg;
          break;
         case 'a':
            OpenOutputAppend = true;
          break;
         case 'p':
            InPlaceUpdate = true;
          break;
         case 'k':
            OutputDeleteOnError = false;
          break;
         case 'm':
            MinActions = atoi(optarg);
          break;
         case 'M':
            MaxActions = atoi(optarg);
          break;
         case 's':
            if(optind < argc) {
               SelectBegin = atoll(optarg);
               SelectEnd   = atoll(argv[optind]);
               optind++;
            }
            else {
               fputs(gettext("ERROR: Too few arguments for line selection (--select|-s)!"), stderr);
               fputs("\n", stderr);
               return 1;
            }
          break;
         case 'b':
            BeginTag = optarg;
          break;
         case 'e':
            EndTag = optarg;
          break;
         case 't':
            BeginTag = optarg;
            EndTag   = nullptr;
          break;
         case 'x':
            IncludeTags = false;
          break;
         case 'y':
            IncludeTags = true;
          break;
         case 'f':
            FullTagLines = true;
          break;
         case 'g':
            FullTagLines = false;
          break;
         case 'q':
            showWarnings = false;
          break;
         case 0x1000:
            for(unsigned int i = 0; i < strlen(optarg); i++) {
               if( (optarg[i] == '%') ||
                   ( !isalnum((int)optarg[i]) && (optarg[i] != '-') && (optarg[i] != '\'') ) ) {
                  fputs(gettext("ERROR: Invalid value for enumeration format (--enumerate-format)!"), stderr);
                  fputs("\n", stderr);
                  return 1;
               }
            }
            snprintf((char*)&EnumerateFormat, sizeof(EnumerateFormat), "%%%sllu", optarg);
          break;
         case 0x1001:
            Enumerate1 = optarg;
          break;
         case 0x1002:
            Enumerate2 = optarg;
          break;
         case 0x2000:
            HighlightBegin = optarg;
          break;
         case 0x2001:
            HighlightEnd = optarg;
          break;
         case 0x2002:
            HighlightUnmarked1 = optarg;
          break;
         case 0x2003:
            HighlightUnmarked2 = optarg;
          break;
         case 0x2004:
            HighlightMarked1 = optarg;
          break;
         case 0x2005:
            HighlightMarked2 = optarg;
          break;
         case 0x2006:
            // NOTE: optind already points to the next option here!
            //       For options with two or more parameters, this
            //       will be the *second* parameter!
            if(optind + 4 < argc) {
               // NOTE: optind points to the *second* parameter here!
               HighlightBegin     = optarg;
               HighlightEnd       = argv[optind];
               HighlightUnmarked1 = argv[optind + 1];
               HighlightUnmarked2 = argv[optind + 2];
               HighlightMarked1   = argv[optind + 3];
               HighlightMarked2   = argv[optind + 4];
               optind += 5;
            }
            else {
               fputs(gettext("ERROR: Too few arguments for highlight parameters (--highlight-params)!"), stderr);
               fputs("\n", stderr);
               return 1;
            }
          break;
         case 'v':
            version();
          break;
         case 'h':
         case '?':
            // Exit with 0 on h/help, exit with 1 on '?' (unknown option):
            usage(argv[0], (option == 'h') ? 0 : 1);
          break;
         case '-':
          break;
         default:
            // This should not happen: wrong getopt parameters, or missing case?
            fprintf(stderr, "INTERNAL ERROR: Unhandled option c=%c code=%x!\n",
                    (isprint(option) ? (char)option : ' '), option);
            return 1;
          break;
      }
   }
   if(optind < argc) {
      usage(argv[0], 1);
   }

   // ====== Check parameters ===============================================
   if( (MaxActions >= 0) && (MinActions > MaxActions) ) {
      fputs(gettext("ERROR: Invalid min/max settings (--min-actions/-m/--max-actions/-M)!"), stderr);
      fputs("\n", stderr);
      return 1;
   }
   if( (BeginTag != nullptr) && (BeginTag[0] == 0x00) ) {
      BeginTag = nullptr;
   }
   if( (EndTag   == nullptr) || (EndTag[0] == 0x00) ||
       (BeginTag == nullptr) || (strcmp(EndTag, BeginTag) == 0) ) {
      EndTag = BeginTag;
   }
   if( (BeginTag) && ((SelectBegin != 0) || (SelectEnd != 0)) ) {
      fputs(gettext("ERROR: Line selection (--select|-s) and tags (--begin-tag/-b/--end-tag/-e/--tag/-t) are mutually exclusive!"), stderr);
      fputs("\n", stderr);
      return 1;
   }
   BeginTagLength = (BeginTag != nullptr) ? strlen(BeginTag) : 0;
   EndTagLength   = (EndTag != nullptr)   ? strlen(EndTag)   : 0;

   switch(Mode) {
      case Cat:
      case Discard:
      case Enumerate:
         if( showWarnings && ( (BeginTag != nullptr) || (EndTag != nullptr) ) ) {
            fputs(gettext("WARNING: Begin/end tags (--begin-tag/-b/--end-tag/-e/--tag/-t) have no effect for Cat, Discard, or Enumerate!"), stderr);
            fputs("\n", stderr);
         }
         BeginTag = nullptr;
         EndTag   = nullptr;
       break;
      case Extract:
      case Remove:
      case Replace:
         if( showWarnings && (BeginTag != nullptr) && (BeginTag == EndTag) && (!IncludeTags) )  {
            fputs(gettext("WARNING: Identical begin/end tags (--tag/-t) with excluded tags (--exclude-tags/-x) are not useful for Extract, Remove, or Replace!"), stderr);
            fputs("\n", stderr);
         }
       break;
      default:
       break;
   }

   // ====== Allocate buffer ================================================
   Buffer = (char*)malloc(BufferSize);
   if(Buffer == nullptr) {
      fprintf(stderr, gettext("ERROR: malloc() failed: %s!"), strerror(errno));
      fputs("\n", stderr);
      cleanUp(1);
   }

   // ====== Open input file ================================================
   InputFile = stdin;
   if( (InputFileName != nullptr) && (strcmp(InputFileName, "-") == 0) ) {
      InputFileName = nullptr;   // Special case: - => stdin
   }
   if( (InputFileName != nullptr) && InPlaceUpdate ) {
      RealInputFileName = realpath(InputFileName, nullptr);
      if(RealInputFileName == nullptr) {
         fprintf(stderr, gettext("ERROR: Unable to resolve absolute path for input file %s: %s!"),
                 InputFileName, strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
      InputFileName = RealInputFileName;
   }
   if(InputFileName != nullptr) {
      InputFile = fopen(InputFileName, "r");
      if(InputFile == nullptr) {
         fprintf(stderr, gettext("ERROR: Unable to open input file %s: %s!"),
                 InputFileName, strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
      OpenInputFile = true;
#ifdef POSIX_FADV_SEQUENTIAL
      const int advice = ((SelectBegin >= 0) && (SelectEnd >= 0)) ?
         POSIX_FADV_SEQUENTIAL|POSIX_FADV_WILLNEED|POSIX_FADV_NOREUSE :
         POSIX_FADV_SEQUENTIAL|POSIX_FADV_WILLNEED;
      posix_fadvise(fileno(InputFile), 0, 0, advice);
#endif
      TotalInputLines = countLines(InputFile);
      if(SelectBegin < 0) {
         SelectBegin = TotalInputLines + SelectBegin + 1;
      }
      if(SelectEnd < 0) {
         SelectEnd = TotalInputLines + SelectEnd + 1;
      }
      if(showWarnings && ( (SelectEnd < SelectBegin) && (SelectEnd != 0)) ) {
         fprintf(stderr,
                 gettext("WARNING: Line selection range (%lld — %lld) is not useful. Input file too short?"),
                 SelectBegin, SelectEnd);
         fputs("\n", stderr);
      }
   }
   else {
      if((SelectBegin < 0) || (SelectEnd < 0)) {
         fputs(gettext("ERROR: Select from end of file (negative line number) is only possible with input file!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
      if(InPlaceUpdate) {
         fputs(gettext("ERROR: In-place update (--in-place|-p) requires an input file (--input/-i)!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
   }

   // ====== Open output file ===============================================
   OutputFile = stdout;
   if( (OutputFileName != nullptr) && (strcmp(OutputFileName, "-") == 0) ) {
      OutputFileName = nullptr;   // Special case: - => stdout
   }
   if(InPlaceUpdate) {
      if(OutputFileName != nullptr) {
         fputs(gettext("ERROR: In-place update (--in-place|-p) and output file (--output/-o) are mutually exclusive!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
      if(makeTempOutputFileName(InputFileName) == nullptr) {
         fputs(gettext("ERROR: Cannot generate temporary output file name!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
   }
   if( (OutputFileName != nullptr) || (OutputTempFileName != nullptr) ) {
      OutputFile = fopen( (OutputTempFileName != nullptr) ?
                             OutputTempFileName : OutputFileName,
                          (OpenOutputAppend == false) ? "w" : "a");
      if(OutputFile == nullptr) {
         fprintf(stderr, gettext("ERROR: Unable to create output file %s: %s!"),
                 OutputFileName, strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
      OpenOutputFile = true;
#ifdef POSIX_FADV_SEQUENTIAL
      posix_fadvise(fileno(OutputFile), 0, 0, POSIX_FADV_SEQUENTIAL|POSIX_FADV_NOREUSE);
#endif
   }

   // ====== Open insert file ===============================================
   if( (InsertFileName != nullptr) && (strcmp(InsertFileName, "-") == 0) ) {
      // ------ Insert file is stdin => copy to temporary file --------------
      InsertFileName = nullptr;   // Special case: - => stdin
      if(InputFile == stdin) {
         fputs(gettext("ERROR: Insert from standard input requires an input file!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
      InsertFile = tmpfile();
      if(InsertFile == nullptr) {
         fprintf(stderr, gettext("ERROR: Unable to create temporary file for insertion: %s!"), strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
      char   buffer[65536];
      size_t bytesRead;
      while( (bytesRead = fread(buffer, 1, sizeof(buffer), stdin)) > 0 ) {
         if(fwrite(buffer, 1, bytesRead, InsertFile) < bytesRead) {
            fprintf(stderr, gettext("ERROR: Failed to write to temporary file: %s!"), strerror(errno));
            fputs("\n", stderr);
            cleanUp(1);
         }
      }
      rewind(InsertFile);
      OpenInsertFile = true;
   }
   if(InsertFileName != nullptr) {
      // ------ Insert file is a file => just open it -----------------------
      InsertFile = fopen(InsertFileName, "r");
      if(InsertFile == nullptr) {
         fprintf(stderr, gettext("ERROR: Unable to open insert file %s: %s!"),
                 InsertFileName, strerror(errno));
         fputs("\n", stderr);
         cleanUp(1);
      }
      OpenInsertFile = true;
   }
   if(InsertFile != nullptr) {
#ifdef POSIX_FADV_SEQUENTIAL
      posix_fadvise(fileno(InsertFile), 0, 0, POSIX_FADV_SEQUENTIAL|POSIX_FADV_WILLNEED);
#endif
   }
   else {
      if( (Mode == InsertFront) || (Mode == InsertBack) || (Mode == Replace) ) {
         fputs(gettext("ERROR: No insert file provided!"), stderr);
         fputs("\n", stderr);
         cleanUp(1);
      }
   }

   // ====== Read lines from input file =====================================
#ifdef DEBUG_MODE
   printf("Select Begin=%lld\n", SelectBegin);
   printf("Select End=%lld\n", SelectEnd);
   printf("Begin Tag=%s\n",   BeginTag);
   printf("End Tag=%s\n",     EndTag);
   printf("IncludeTags=%u\n", IncludeTags);
   printf("FullTagLines=%u\n", FullTagLines);
#endif

   LineNo          = 0;
   Actions         = 0;
   MarkerTag       = BeginTag;
   MarkerTagLength = BeginTagLength;

   ssize_t lineLength;
   errno = 0;
   while( (lineLength = (getline((char**)&Buffer, &BufferSize, InputFile))) > 0 ) {
      // getline() may have reallocated the buffer, if it was necessary!

      // ====== Process line ================================================
      LineNo++;
      EndOfLine = Buffer + lineLength;
#ifdef DEBUG_MODE
      printf("%llu (l=%u m=%s):\t%s",
             (unsigned long long)LineNo, (unsigned int)lineLength, MarkerTag,
             Buffer);
#endif

      // ====== Tag handling ================================================
      if(MarkerTag != nullptr) {
         Pointer = Buffer;
         const char* next;
         bool        foundMarker = false;
         while( (next = (MarkerTag != nullptr) ? strstr(Pointer, MarkerTag) : nullptr) != nullptr ) {
            foundMarker = true;

            // ====== Begin marker found ====================================
            if(MarkerTag == BeginTag) {
               // ------ Begin of marking with full tag lines ---------------
               if(FullTagLines) {
                  next += MarkerTagLength;
                  if(IncludeTags) {
                     processUnmarked(Buffer, 0, true);
                     processMarked(Pointer, (ssize_t)(next - Pointer), false);
                  }
                  else {
                     processUnmarked(Pointer, (ssize_t)(next - Pointer), false);
                     // Postponed start of block until end of the line!
                  }
               }
               // ------ Begin of marking with tags only --------------------
               else {
                  if(!IncludeTags) {
                     next += MarkerTagLength;
                  }
                  processUnmarked(Pointer, (ssize_t)(next - Pointer), true);
                  // ------ Special case: BeginTag == EndTag ----------------
                  if(BeginTag == EndTag) {
                     if(IncludeTags) {
                        processMarked(next, (ssize_t)MarkerTagLength, true);
                        next += MarkerTagLength;
                     }
                     else {
                        processMarked(next, 0, true);
                     }
                  }
               }
               MarkerTag       = EndTag;
               MarkerTagLength = EndTagLength;
            }

            // ====== End marker found ======================================
            else /* if(MarkerTag == EndTag) */ {
               if(FullTagLines) {
                  next += MarkerTagLength;
                  if(IncludeTags) {
                     processMarked(Pointer, (ssize_t)(next - Pointer), false);
                     // Postponed end of block until end of the line!
                  }
                  else {
                     processMarked(Buffer, 0, true);
                     processUnmarked(Pointer, (ssize_t)(next - Pointer), false);
                  }
               }
               else {
                  if(IncludeTags) {
                     next += MarkerTagLength;
                  }
                  processMarked(Pointer, (ssize_t)(next - Pointer), true);
               }
               MarkerTag       = BeginTag;
               MarkerTagLength = BeginTagLength;
            }

            Pointer = next;

            // ====== Special case: FullTagLines with multiple markers ======
            if((FullTagLines) && (foundMarker)) {
               // FullTagLines with multiple markers in a line is not useful
               // => Interpret only the *first* marker here!
               break;
            }
         }

         // ====== No (further) marker found ================================
         // Still need to handle the rest of the line ...
         if( (FullTagLines) && (foundMarker) ) {
            if(IncludeTags) {
               processMarked(Pointer, (ssize_t)(EndOfLine - Pointer), (MarkerTag == BeginTag));
            }
            else {
               if(BeginTag != EndTag) {
                  processUnmarked(Pointer, (ssize_t)(EndOfLine - Pointer), (MarkerTag == EndTag));
               }
               // ------ Special case: BeginTag == EndTag -------------------
               else {
                  processUnmarked(Pointer, (ssize_t)(EndOfLine - Pointer), true);
                  processMarked(EndOfLine, 0, true);
               }
            }
         }
         else {
            if(MarkerTag == BeginTag) {
               processUnmarked(Pointer, (ssize_t)(EndOfLine - Pointer), false);
            }
            else {
               processMarked(Pointer, (ssize_t)(EndOfLine - Pointer), false);
            }
         }
      }

      // ====== Select handling =============================================
      else if( (SelectBegin > 0) && (LineNo >= SelectBegin) &&
               ( (SelectEnd == 0) || (LineNo <= SelectEnd) ) ) {
         if(LineNo == SelectBegin) {
            processUnmarked(Buffer, 0, true);
         }
         processMarked(Buffer, (ssize_t)(EndOfLine - Buffer), false);
         if(LineNo == SelectEnd) {
            processMarked(EndOfLine, 0, true);
         }
      }

      // ====== Just process a regular, unmarked line =======================
      else {
         processUnmarked(Buffer, (ssize_t)(EndOfLine - Buffer), false);
      }


      // ====== Prepare next iteration ======================================
      errno = 0;
   }
   if( (lineLength < 0) && (errno != 0) ) {
      fprintf(stderr, gettext("ERROR: Read error: %s!"), strerror(errno));
      fputs("\n", stderr);
      cleanUp(1);
   }

   // ====== Clean up =======================================================
   if(InMarkedBlock) {
      // Finish the marked block, if it is still active:
      processMarked(Buffer, 0, true);
   }
   const bool success =
      ( (MinActions < 0) || (Actions >= MinActions) ) &&
      ( (MaxActions < 0) || (Actions <= MaxActions) );
   if(!success) {
      fputs(gettext("ERROR: Number of actions outside of set limits (--min-actions/-m/--max-actions/-M)!"), stderr);
      fputs("\n", stderr);
   }
   cleanUp( (success == true) ? 0 : 1 );
}
