static char *rcs_version = "i77.c V$Revision: 1.6 $";
/*
 * imp77_args.c - Command-line argument parser for the imp77 compiler/translator.
 *
 * Supports:
 *   Source files: *.imp, *.c, *.o
 *   Output:       -o <file>
 *   Compile-only: -c
 *   Include dirs: -I<dir> or -I <dir>  (i.e. one argv parameter or two)
 *   Libraries:    -l<lib> or -l <lib>
 *   Library dirs: -L<dir> or -L <dir>
 *   Defines:      -D<name>[=value] or -D <name>[=value]
 *   Boolean flags (--flag / --no-flag):
 *                 --icode, --check, --array, --unass
 *   Info options: -h / --help, -d / --decode, -v / --verbose
 */

// To do: add perms.c or perms.o or -Li2c  (half done)
//        (build library)
//        Check that gtcpp is available and give more helpful message if not
//        Fix gtcpp to understand imp comments - it fails if there are unbalanced quotes within an imp comment :-(
//        (note that gtcpp is not used if no *.i77 files are given.)
//        add -x option to 'compile and go' (eXecute)
//        gdb and valgrind can be added by i2c but not yet passing
//         options through to make that happen.
//        Added support for gtcpp to allow pre-processing of imp77 sources
//         - had to add uncomment-imp to fix problems with C-style pre-processor
//         but really gtcpp needs to be made imp-aware


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>

#define append(p, format...) p += sprintf(p, format)

char *Progname;

#define MAX_FILES 256
#define MAX_INCDIRS 64
#define MAX_LIBS 64
#define MAX_LIBDIRS 64
#define MAX_DEFINES 64

typedef struct {
  const char *i77_files[MAX_FILES];
  int i77_count;

  const char *imp_files[MAX_FILES];
  int imp_count;

  const char *c_files[MAX_FILES];
  int c_count;

  const char *obj_files[MAX_FILES];
  int obj_count;

  const char *output;

  const char *inc_dirs[MAX_INCDIRS];
  int inc_count;

  const char *libs[MAX_LIBS];
  int lib_count;

  const char *lib_dirs[MAX_LIBDIRS];
  int lib_dir_count;

  const char *defines[MAX_DEFINES];
  int def_count;

  bool flag_icode;
  bool flag_check;
  bool flag_array;
  bool flag_unass;
//bool flag_bounds;

  bool flag_help;
  bool flag_quiet;
  bool flag_verbose;
  bool flag_gcc_debug;
  bool flag_developer_debug;
  bool flag_compile_only;
  bool flag_dry_run;
} CompilerOptions;

static CompilerOptions opts = { 0 };

void call(char *command) {
  int rc;

  if (!opts.flag_quiet) fprintf(stderr, "%s\n", command);
  if (opts.flag_dry_run) return;

#ifndef DO_NOT_CALL
  rc = system(command);
#else
  //fprintf(stderr, "Calling: %s\n", command);
  rc  = 0;
#endif
  if (rc == -1) {
    fprintf(stderr, "%s: error invoking '%s' - %s\n", Progname, command, strerror(errno));
  } else {
    //fprintf(stderr, "%s: system returns %d\n", Progname, rc);
    // Check if the command terminated normally
    if (WIFEXITED(rc)) {
      int exit_code = WEXITSTATUS(rc);
      if (exit_code != 0) exit(exit_code);
    } else if (WIFSIGNALED(rc)) {
      // Check if it was killed by a signal instead
      fprintf(stderr, "%s: '%s' killed by signal - %s (%d)\n", Progname, command, strerror(errno), WTERMSIG(rc));
    }
  }
}

static const char *file_ext(const char *filename) {
  const char *base = strrchr(filename, '/');
  const char *dot;

  base = base ? base + 1 : filename;
  dot = strrchr(base, '.');

  return strdup((dot == NULL || dot == base) ? "" : dot);
}

static char *basename(const char *s) {
  char *fname = strdup(s);
  char *start = strrchr(fname, '/');
  char *sep;
  if (start) start += 1; else start = fname;
  sep = strrchr(start, '.');
  if (sep) *sep = '\0';
  return fname;
}

static bool list_add(const char **list, int *count, int max, const char *val) {
  if (*count >= max) return false;
  list[(*count)++] = val;
  return true;
}

int parse_args(int argc, char *argv[], CompilerOptions *opts) {
  char *first_file = NULL;
  memset(opts, 0, sizeof(*opts));

  for (int argindex = 1; argindex < argc; argindex++) {
    const char *arg = argv[argindex];

    if (strncmp(arg, "--", 2) == 0) {
      const char *name = arg + 2;

      /*
  bool flag_icode;
  bool flag_check;
  bool flag_array;
  bool flag_unass;
//bool flag_bounds;
  bool flag_help;
  bool flag_quiet;
  bool flag_verbose;
  bool flag_gcc_debug;
  bool flag_developer_debug;
  bool flag_compile_only;
  bool flag_dry_run;
       */
      struct {
        const char *name;
        bool *flag;
      } bool_flags[] = {{"icode", &opts->flag_icode},
                        {"check", &opts->flag_check},
                        {"array", &opts->flag_array},
                        {"unass", &opts->flag_unass},
                        {"dry-run", &opts->flag_dry_run},
                        /*{"bounds", &opts->flag_bounds},*/
                        {NULL, NULL}};

      bool matched = false;
      for (int f = 0; bool_flags[f].name; f++) {
        if (strcmp(name, bool_flags[f].name) == 0) {
          *bool_flags[f].flag = true;
          matched = true;
          break;
        }

        char negated[64];
        snprintf(negated, sizeof(negated), "no-%s", bool_flags[f].name);
        if (strcmp(name, negated) == 0) {
          *bool_flags[f].flag = false;
          matched = true;
          break;
        }
      }
      if (matched) continue;

      if (strcmp(name, "help") == 0) {
        opts->flag_help = true;
        continue;
      }
      if (strcmp(name, "verbose") == 0) {
        opts->flag_verbose = true;
        continue;
      }
      if (strcmp(name, "quiet") == 0) {
        opts->flag_quiet = true;
        continue;
      }
      if (strcmp(name, "debug") == 0) {
        opts->flag_gcc_debug = true;
        continue;
      }
      if (strcmp(name, "ddebug") == 0) {
        opts->flag_developer_debug = true;
        continue;
      }

      fprintf(stderr, "%s: unknown option '%s'\n", Progname, arg);
      return 1;
    }

    if (arg[0] == '-' && arg[1] != '\0') {
      char sw = arg[1];

      if (sw == 'h' && arg[2] == '\0') {
        opts->flag_help = true;
        continue;
      }
      if (sw == 'v' && arg[2] == '\0') {
        opts->flag_verbose = true;
        continue;
      }
      if (sw == 'q' && arg[2] == '\0') {
        opts->flag_quiet = true;
        continue;
      }
      if (sw == 'g' && arg[2] == '\0') {
        opts->flag_gcc_debug = true;
        continue;
      }
      if (sw == 'd' && arg[2] == '\0') {
        opts->flag_developer_debug = true;
        continue;
      }
      if (sw == 'c' && arg[2] == '\0') {
        opts->flag_compile_only = true;
        continue;
      }

      if (sw == 'o') {
        const char *val = (arg[2] != '\0') ? arg + 2 : (++argindex < argc ? argv[argindex] : NULL);
        if (!val) {
          fprintf(stderr, "%s: -o requires an argument\n", Progname);
          return 1;
        }
        opts->output = val;
        continue;
      }

      if (sw == 'I') {
        const char *val = (arg[2] != '\0') ? arg + 2 : (++argindex < argc ? argv[argindex] : NULL);
        if (!val) {
          fprintf(stderr, "%s: -I requires an argument\n", Progname);
          return 1;
        }
        if (!list_add(opts->inc_dirs, &opts->inc_count, MAX_INCDIRS, val)) {
          fprintf(stderr, "%s: too many -I directories\n", Progname);
          return 1;
        }
        continue;
      }

      if (sw == 'l') {
        const char *val = (arg[2] != '\0') ? arg + 2 : (++argindex < argc ? argv[argindex] : NULL);
        if (!val) {
          fprintf(stderr, "%s: -l requires an argument\n", Progname);
          return 1;
        }
        if (!list_add(opts->libs, &opts->lib_count, MAX_LIBS, val)) {
          fprintf(stderr, "%s: too many -l libraries\n", Progname);
          return 1;
        }
        continue;
      }

      if (sw == 'L') {
        const char *val = (arg[2] != '\0') ? arg + 2 : (++argindex < argc ? argv[argindex] : NULL);
        if (!val) {
          fprintf(stderr, "%s: -L requires an argument\n", Progname);
          return 1;
        }
        if (!list_add(opts->lib_dirs, &opts->lib_dir_count, MAX_LIBDIRS, val)) {
          fprintf(stderr, "%s: too many -L directories\n", Progname);
          return 1;
        }
        continue;
      }

      if (sw == 'D') {
        const char *val = (arg[2] != '\0') ? arg + 2 : (++argindex < argc ? argv[argindex] : NULL);
        if (!val) {
          fprintf(stderr, "%s: -D requires an argument\n", Progname);
          return 1;
        }
        if (!list_add(opts->defines, &opts->def_count, MAX_DEFINES, val)) {
          fprintf(stderr, "%s: too many -D defines\n", Progname);
          return 1;
        }
        continue;
      }

      fprintf(stderr, "%s: unknown option '%s'\n", Progname, arg);
      return 1;
    }

    {
      const char *ext = file_ext(arg);

      if (first_file == NULL) first_file = strdup(arg); // to be used for default executable

      if (*ext == '\0') {
        // (-q will only work if it comes before this filename.)
        char tmp[1024];
        FILE *check;
        sprintf(tmp, "%s.i77", arg);
        check = fopen(tmp, "r");
        if (check != NULL) {
          fclose(check);
          if (!opts->flag_quiet) fprintf(stderr, "%s: Assuming %s.i77\n", Progname, arg);
        } else {
          sprintf(tmp, "%s.imp", arg);
          check = fopen(tmp, "r");
          if (check != NULL) {
            fclose(check);
            if (!opts->flag_quiet) fprintf(stderr, "%s: Assuming %s.imp\n", Progname, arg);
          } else {
            fprintf(stderr, "%s: Cannot open %s.i77 or %s.imp - %s\n", Progname, arg, arg, strerror(errno));
            return 1;
          }
        }
        arg = strdup(tmp); ext = file_ext(arg);
      }
      
      if (strcmp(ext, ".i77") == 0) {
        if (!list_add(opts->i77_files, &opts->i77_count, MAX_FILES, arg)) {
          fprintf(stderr, "%s: too many input files\n", Progname);
          return 1;
        }
      } else if (strcmp(ext, ".imp") == 0) {
        if (!list_add(opts->imp_files, &opts->imp_count, MAX_FILES, arg)) {
          fprintf(stderr, "%s: too many input files\n", Progname);
          return 1;
        }
      } else if (strcmp(ext, ".c") == 0) {
        if (!list_add(opts->c_files, &opts->c_count, MAX_FILES, arg)) {
          fprintf(stderr, "%s: too many input files\n", Progname);
          return 1;
        }
      } else if (strcmp(ext, ".o") == 0) {
        if (!list_add(opts->obj_files, &opts->obj_count, MAX_FILES, arg)) {
          fprintf(stderr, "%s: too many input files\n", Progname);
          return 1;
        }
      } else {
        if (*ext == '\0') {
          fprintf(stderr, "%s: %s - no extension given?\n", Progname, arg);
        } else {
          fprintf(stderr, "%s: %s - no compile rule for *%s files\n", Progname, arg, ext);
        }
        return 1;
      }
    }
  }

  int input_count = opts->i77_count + opts->imp_count + opts->c_count + opts->obj_count;
  if (opts->output == NULL) {
    if (opts->flag_compile_only) {
      //
    } else {
      if (input_count == 1) {
        if (opts->i77_count == 1) {
          opts->output = basename(opts->i77_files[0]);
        } else if (opts->imp_count == 1) {
          opts->output = basename(opts->imp_files[0]);
        } else if (opts->c_count == 1) {
          opts->output = basename(opts->c_files[0]);
        } else if (opts->obj_count == 1) {
          opts->output = basename(opts->obj_files[0]);
        } else {
          // internal error
        }
      } else {
        // This is compatible with how cc command-line works:
        //opts->output = "a.out";
        // We *could* if we preferred (for Imp) generate an executable named after the first file parameter:
        if (first_file) opts->output = basename(first_file);
      }
    }
  } else {
    if (opts->flag_compile_only) {
      if (input_count > 1) {
        fprintf(stderr, "%s: with -c and -o, more than one input file was supplied\n", Progname);
        return 1;
      }
      if (strcmp(file_ext(opts->output), ".o") != 0) {
        fprintf(stderr, "%s: with -c, output file '%s' must end in .o\n", Progname, opts->output);
        return 1;
      }
    } else {
      //
    }
  }

  if (opts->flag_compile_only && opts->obj_count != 0) {
    fprintf(stderr, "%s: using -c with *.o files does nothing.\n", Progname);
    return 1;
  }

  if (opts->output != NULL) {
    const char *extension = file_ext(opts->output);
    if (strcmp(extension, ".imp") == 0 || strcmp(extension, ".c") == 0) {
      fprintf(stderr, "%s: -o %s - this is unlikely to be the name of an executable!\n", Progname, opts->output);
      return 1;
    }
    if (*extension != '\0') {
      if (opts->flag_compile_only && strcmp(extension, ".o") == 0) {
      } else {
        fprintf(stderr, "%s: warning: unexpected %s extension on output file %s\n", Progname, extension, opts->output);
      }
    }
  }
  
  return 0;
}

static void print_usage(const char *prog) {
  fprintf(stderr, 
      "Usage: %s [options] file...\n"
      "\n"
      "Files:\n"
      "  *.imp          IMP-77 source files (translated to C)\n"
      "  *.c            C source files (passed to the C compiler)\n"
      "  *.o            Object files (passed to the linker)\n"
      "\n"
      "Options:\n"
      "  -c             Compile only; do not link\n"
      "  -g             Enable gdb support\n"
      "  -o <file>      Place output into <file>\n"
      "  -I<dir>        Add <dir> to include search path\n"
      "  -L<dir>        Add <dir> to library search path\n"
      "  -l<lib>        Link with library <lib>\n"
      "  -D<name>[=val] Define preprocessor macro\n"
      "  --(no-)icode   Keep intermediate-code output\n"
      "  --(no-)check   Enable checks with runtime overhead\n"
      "                 (includes --array, --unass)\n"
      "  --(no-)array   Array bounds checks\n"
      "  --(no-)unass   Uninitialised variable checks\n"
//    "  --bounds       Enable runtime bounds checking\n"
//    "  --no-bounds    Disable runtime bounds checking\n"
//    "  -h, --help     Show this help message\n"
      "  --dry-run      Don't run subshells\n"
      "  -q, --quiet    No output except from subshells\n"
      "  -v, --verbose  Enable verbose output\n\n",
//    "  -d, --debug    Enable developer debugging\n\n",
      prog);
}

static void print_options(const CompilerOptions *opts) {

  fprintf(stderr, "Flags:\n");
  fprintf(stderr, "  --icode   : %s\n", opts->flag_icode ? "on" : "off");
  fprintf(stderr, "  --check   : %s\n", opts->flag_check ? "on" : "off");
  fprintf(stderr, "  --array   : %s\n", opts->flag_array ? "on" : "off");
  fprintf(stderr, "  --unass   : %s\n", opts->flag_unass ? "on" : "off");
//fprintf(stderr, "  --bounds  : %s\n", opts->flag_bounds ? "on" : "off");
  fprintf(stderr, "  --verbose : %s\n", opts->flag_verbose ? "on" : "off");
  fprintf(stderr, "  --dry-run : %s\n", opts->flag_dry_run ? "on" : "off");
  fprintf(stderr, "  --quiet   : %s\n", opts->flag_quiet ? "on" : "off");
  fprintf(stderr, "  -c        : %s\n", opts->flag_compile_only ? "on" : "off");
  fprintf(stderr, "  -g        : %s\n", opts->flag_gcc_debug ? "on" : "off");
  fprintf(stderr, "  -d        : %s\n", opts->flag_developer_debug ? "on" : "off");
  fprintf(stderr, "\n");

  fprintf(stderr, "Output file       : %s\n", opts->output ? opts->output : "(none)");
  fprintf(stderr, "\n");

  fprintf(stderr, "Include directories (%d):\n", opts->inc_count);
  for (int i = 0; i < opts->inc_count; i++) fprintf(stderr, "  -I %s\n", opts->inc_dirs[i]);

  fprintf(stderr, "Library directories (%d):\n", opts->lib_dir_count);
  for (int i = 0; i < opts->lib_dir_count; i++) fprintf(stderr, "  -L %s\n", opts->lib_dirs[i]);

  fprintf(stderr, "Libraries (%d):\n", opts->lib_count);
  for (int i = 0; i < opts->lib_count; i++) fprintf(stderr, "  -l %s\n", opts->libs[i]);

  fprintf(stderr, "Defines (%d):\n", opts->def_count);
  for (int i = 0; i < opts->def_count; i++) fprintf(stderr, "  -D \"%s\"\n", opts->defines[i]);

  fprintf(stderr, "\nIMP-77 source files with pre-processor directives (%d):\n", opts->i77_count);
  for (int i = 0; i < opts->i77_count; i++) fprintf(stderr, "  %s\n", opts->i77_files[i]);

  fprintf(stderr, "\nOriginal IMP-77 source files (%d):\n", opts->imp_count);
  for (int i = 0; i < opts->imp_count; i++) fprintf(stderr, "  %s\n", opts->imp_files[i]);

  fprintf(stderr, "C source files (%d):\n", opts->c_count);
  for (int i = 0; i < opts->c_count; i++) fprintf(stderr, "  %s\n", opts->c_files[i]);

  fprintf(stderr, "Object files (%d):\n", opts->obj_count);
  for (int i = 0; i < opts->obj_count; i++) fprintf(stderr, "  %s\n", opts->obj_files[i]);
}

int main(int argc, char *argv[]) {
  char *p;
  
  Progname = strdup(argv[0]);
  p = strrchr(Progname, '/');
  if (p) Progname = p+1;
  
  if (argc == 1) {
    fprintf(stderr, "Try %s -h for help with parameters.\n\n", Progname);
    exit(EXIT_FAILURE);
  }
  if (parse_args(argc, argv, &opts) != 0) exit(EXIT_FAILURE);

  if (opts.flag_help) {
    print_usage(Progname);
    exit(EXIT_SUCCESS);
  }

  // Still to add perms.c or perms.o to the compilation, and set up
  // a default path for Imp77 system include files (esp. perms.inc)
  
  if (opts.flag_developer_debug)  print_options(&opts);
  if (!opts.flag_quiet) fprintf(stderr, "\n");

  char *cp, command[1024];
  char obj[128];
  
  while (opts.i77_count-- > 0) {
    FILE *check;
    check = fopen(opts.i77_files[opts.i77_count], "r");
    if (check == NULL) {
      fprintf(stderr, "%s: Cannot open %s - %s\n", Progname, opts.i77_files[opts.i77_count], strerror(errno));
      exit(1);
    } else {
      fclose(check);
    }
    cp = command;
    append(cp, "uncomment-imp");
    append(cp, " < %s.i77", basename(opts.i77_files[opts.i77_count]));
    append(cp, " | ");
    append(cp, "gtcpp -P");
    for (int i = 0; i < opts.def_count; i++) append(cp, " -D \"%s\"", opts.defines[i]);
    append(cp, " > %s.imp", basename(opts.i77_files[opts.i77_count]));
    call(command);
    cp = command;
    char tmp[1024];
    sprintf(tmp, "%s.imp", basename(opts.i77_files[opts.i77_count]));
    (void)list_add(opts.imp_files, &opts.imp_count, MAX_FILES, tmp);
  }
  
  while (opts.imp_count-- > 0) {
    cp = command;
    append(cp, "i2c -c --perms /usr/local/include/i2c/perms.inc");
    if (opts.flag_verbose) append(cp, " -v");
    if (opts.flag_developer_debug) append(cp, " -d");
    if (opts.flag_icode) append(cp, " --icode");
    if (opts.flag_check) append(cp, " --check");
    if (opts.flag_array) append(cp, " --array");
    if (opts.flag_unass) append(cp, " --unass");
//  if (opts.flag_bounds) fprintf(stderr, " --bounds");
    // -I for Imp files is used for *.inc files
    for (int i = 0; i < opts.inc_count; i++) append(cp, " -I %s", opts.inc_dirs[i]);
    append(cp, " %s", opts.imp_files[opts.imp_count]);
    call(command);
    cp = command;

    // *Or* Could add the generated .c file to the list of C files.
    append(cp, "gcc");
    if (opts.flag_gcc_debug) append(cp, " -g");
    for (int i = 0; i < opts.def_count; i++) append(cp, " -D \"%s\"", opts.defines[i]);
    if (opts.flag_compile_only) {
      if (opts.output) append(cp, " -o %s", opts.output);
      for (int i = 0; i < opts.inc_count; i++) append(cp, " -I %s", opts.inc_dirs[i]);
      append(cp, " -I /usr/local/include/i2c");
      append(cp, " -c %s.c", basename(opts.imp_files[opts.imp_count]));
      sprintf(obj, "%s.o", opts.output);
    } else {
      for (int i = 0; i < opts.inc_count; i++) append(cp, " -I %s", opts.inc_dirs[i]);
      append(cp, " -I /usr/local/include/i2c");
      append(cp, " -c %s.c", basename(opts.imp_files[opts.imp_count]));
      sprintf(obj, "%s.o", basename(opts.imp_files[opts.imp_count]));
    }
    call(command);
    
    opts.obj_files[opts.obj_count++] = strdup(obj);
  }

  while (opts.c_count-- > 0) {
    cp = command;
    append(cp, "gcc");
    if (opts.flag_gcc_debug) append(cp, " -g");
    for (int i = 0; i < opts.def_count; i++) append(cp, " -D \"%s\"", opts.defines[i]);
    if (opts.flag_compile_only) {
      if (opts.output) append(cp, " -o %s", opts.output);
      sprintf(obj, "%s.o", opts.output);
    } else {
      sprintf(obj, "%s.o", basename(opts.c_files[opts.c_count]));
    }
    // -I for Imp files is used for *.c files
    for (int i = 0; i < opts.inc_count; i++) append(cp, " -I %s", opts.inc_dirs[i]);
    append(cp, " -I /usr/local/include/i2c");
    append(cp, " -c %s.c", basename(opts.c_files[opts.c_count]));
    call(command);
    
    opts.obj_files[opts.obj_count++] = strdup(obj);
  }

  if (!opts.flag_compile_only) {
    cp = command;
    append(cp, "gcc");
    if (opts.flag_gcc_debug) append(cp, " -g");
    if (opts.output != NULL) append(cp, " -o %s", opts.output);
    while (opts.obj_count-- > 0) {
      append(cp, " %s", opts.obj_files[opts.obj_count]);
    }
    for (int i = 0; i < opts.lib_dir_count; i++) append(cp, " -L%s", opts.lib_dirs[i]);
    append(cp, " -L /usr/local/lib/i2c");
    append(cp, " /usr/local/lib/i2c/perms.o");
    char *libm = "";
    for (int i = 0; i < opts.lib_count; i++) {
      if (strcmp(opts.libs[i], "m") == 0) {     // For obscure reasons, -lm must be moved to the end...
        libm = " -lm";
      } else {
        append(cp, " -l%s", opts.libs[i]);
      }
    }
    //append(cp, "%s", libm);
    append(cp, " -lm"); // actually we always want -lm, not just when the user asks for it.
    call(command);
  }
  
  exit(EXIT_SUCCESS);
  return EXIT_FAILURE;
}
