#include "slimcc.h"

typedef enum {
  FILE_NONE = 0,
  FILE_C,
  FILE_ASM,
  FILE_PP_ASM,
  FILE_LDARG,
} FileType;

typedef enum {
  PR_NORMAL = 0,
  PR_NOFORK,
  PR_PIPE,
  PR_HHH,
} ProcMode;

typedef struct {
  const char *arg;
  bool is_def;
} MacroChange;

typedef struct {
  MacroChange *data;
  int capacity;
  int len;
} MacroChangeArr;

StringArray include_paths;
StringArray iquote_paths;
StringArray display_files;
bool opt_fcommon;
int opt_fpic;
int opt_fpie;
bool opt_femulated_tls;
int opt_fn_align = 1;
bool opt_use_plt = true;
bool opt_optimize = true;
bool opt_reuse_stack = true;
bool opt_g;
bool opt_func_sections;
bool opt_data_sections;
bool opt_werror;
bool opt_cc1_asm_pp;
const char *opt_visibility;
StdVer opt_std = STD_C17;
bool is_iso_std;
bool opt_fdefer_ts;
bool opt_short_enums;
bool opt_gnu_keywords;
bool opt_gnu89_inline;
bool opt_ms_anon_struct;
bool opt_disable_visibility;
bool opt_fake_always_inline;

static StringArray opt_imacros;
static StringArray opt_include;
bool opt_E;
bool opt_dM;
static bool opt_P;
static bool opt_M;
static bool opt_MM;
static bool opt_MD;
static bool opt_MMD;
static bool opt_MG;
static bool opt_MP;
static bool opt_S;
static bool opt_c;
static bool opt_verbose;
bool opt_pie;
bool opt_nopie;
bool opt_pthread;
bool opt_r;
bool opt_rdynamic;
bool opt_static;
bool opt_static_pie;
bool opt_static_libgcc;
bool opt_shared;
bool opt_s;
bool opt_nostartfiles;
bool opt_nodefaultlibs;
bool opt_nolibc;
const char *default_ld = "ld";
const char *default_as = "as";
const char *dumpmachine_str;
static const char *opt_use_ld;
static const char *opt_use_as;
static const char *opt_MF;
static const char *opt_MT;
static const char *opt_o;

static StringArray ld_paths;
static StringArray input_args;
static StringArray sysincl_paths;
static StringArray dep_files;
static StringArray tmpfiles;
static const char *tmp_folder;
static StringArray as_args;
static MacroChangeArr macrodefs;
static int incl_cnt;
static ProcMode proc_mode;

static bool is_fork_child;
char *argv0;

static void cc1(const char *input_file, const char *output, bool is_asm_pp);

#if defined(USE_ASAN)
__attribute__((visibility("default"))) const char *__asan_default_options(void) {
  return "detect_leaks=0";
}
#endif

#if defined(USE_UBSAN)
__attribute__((visibility("default"))) const char *__ubsan_default_options(void) {
  return "print_stacktrace=1";
}
#endif

static void version(void) {
  puts("slimcc version 0.0");
}

static bool startswith(const char *arg, const char **p, const char *str) {
  size_t len = strlen(str);
  if (!strncmp(arg, str, len)) {
    *p = arg + len;
    return true;
  }
  return false;
}

static bool take_arg(char **argv, int *i, const char **arg, const char *opt) {
  if (strcmp(argv[*i], opt))
    return false;
  *i += 1;
  if (argv[*i]) {
    *arg = argv[*i];
    return true;
  }
  fprintf(stderr, "missing argument to %s\n", opt);
  exit(1);
}

static bool take_arg_s(char **argv, int *i, const char **p, const char *str) {
  return take_arg(argv, i, p, str) || startswith(argv[*i], p, str);
}

static bool comma_arg(const char *arg, StringArray *arr, const char *str) {
  if (startswith(arg, &arg, str)) {
    arg = strtok(strdup(arg), ",");
    while (arg) {
      strarray_push(arr, arg);
      arg = strtok(NULL, ",");
    }
    return true;
  }
  return false;
}

void add_include_path(StringArray *arr, const char *path) {
  size_t orig_len = strlen(path);
  size_t len = orig_len;

  while (len > 1 && path[len - 1] == '/')
    len--;

  for (int i = 0; i < arr->len; i++) {
    const char *s2 = arr->data[i];
    if (!strncmp(s2, path, len) && s2[len] == '\0')
      return;
  }

  if (len != orig_len)
    path = strndup(path, len);

  strarray_push(arr, path);
}

static FileType parse_opt_x(const char *s) {
  if (!strcmp(s, "c"))
    return FILE_C;
  if (!strcmp(s, "assembler"))
    return FILE_ASM;
  if (!strcmp(s, "assembler-with-cpp"))
    return FILE_PP_ASM;
  if (!strcmp(s, "none"))
    return FILE_NONE;
  error("<command line>: unknown argument for -x: %s", s);
}

static bool set_bool(const char *p, bool val, const char *str, bool *opt) {
  if (!strcmp(p, str)) {
    *opt = val;
    return true;
  }
  return false;
}

static bool set_true(const char *p, const char *str, bool *opt) {
  return set_bool(p, true, str, opt);
}

static void set_std(bool is_iso, const char *arg) {
  char *end;
  int val = strtoul(arg, &end, 10);

  if (end - arg == 2) {
    is_iso_std = is_iso;

    switch (val) {
    case 89:
    case 90: opt_std = STD_C89; return;
    case 99: opt_std = STD_C99; return;
    case 11: opt_std = STD_C11; return;
    case 17:
    case 18: opt_std = STD_C17; return;
    case 23: opt_std = STD_C23; return;
    }
  }
  error("unknown c standard");
}

static void set_std_iso(const char *arg) {
  char *end;
  int val = strtoul(arg, &end, 10);

  if (end != arg) {
    is_iso_std = true;

    switch (val) {
    case 1990:   opt_std = STD_C89; return;
    case 199409: opt_std = STD_C94; return;
    case 1999:   opt_std = STD_C99; return;
    case 2011:   opt_std = STD_C11; return;
    case 2017:
    case 2018:   opt_std = STD_C17; return;
    case 2024:   opt_std = STD_C23; return;
    }
  }
  error("unknown c standard");
}

static void macrochange_push(MacroChangeArr *arr, const char *arg, bool is_def) {
  if (arr->len == arr->capacity) {
    arr->capacity += 8;
    arr->data = realloc(arr->data, sizeof(MacroChange) * arr->capacity);
  }
  MacroChange *m = &arr->data[arr->len++];
  m->arg = arg;
  m->is_def = is_def;
}

static void cli_macros(bool is_asm_pp) {
  if (is_asm_pp) {
    define_macro("__ASSEMBLER__", "1");
  } else {
    if (is_iso_std)
      define_macro("__STRICT_ANSI__", "1");

    switch (opt_std) {
    case STD_C94: define_macro("__STDC_VERSION__", "199409L"); break;
    case STD_C99: define_macro("__STDC_VERSION__", "199901L"); break;
    case STD_C11: define_macro("__STDC_VERSION__", "201112L"); break;
    case STD_C17: define_macro("__STDC_VERSION__", "201710L"); break;
    case STD_C23: define_macro("__STDC_VERSION__", "202311L"); break;
    }

    if (opt_std >= STD_C99)
      define_macro("__GNUC_STDC_INLINE__", "1");

    define_macro("__STDC_UTF_16__", "1");
    define_macro("__STDC_UTF_32__", "1");

    define_macro("__STDC_DEFER_TS25755__", opt_fdefer_ts ? "2" : "1");
  }

  if (opt_pthread)
    define_macro("_REENTRANT", "1");

  if (opt_fpic == 1) {
    define_macro("__pic__", "1");
    define_macro("__PIC__", "1");
  } else if (opt_fpic == 2) {
    define_macro("__pic__", "2");
    define_macro("__PIC__", "2");
  } else if (opt_fpie == 1) {
    define_macro("__pic__", "1");
    define_macro("__PIC__", "1");
    define_macro("__pie__", "1");
    define_macro("__PIE__", "1");
  } else if (opt_fpie == 2) {
    define_macro("__pic__", "2");
    define_macro("__PIC__", "2");
    define_macro("__pie__", "2");
    define_macro("__PIE__", "2");
  }

  for (int i = 0; i < macrodefs.len; i++) {
    MacroChange *m = &macrodefs.data[i];
    if (m->is_def)
      define_macro_cli(m->arg);
    else
      undef_macro(m->arg);
  }
}

static char *quote_makefile(const char *s) {
  char *buf = calloc(1, strlen(s) * 2 + 1);

  for (int i = 0, j = 0; s[i]; i++) {
    switch (s[i]) {
    case '$':
      buf[j++] = '$';
      buf[j++] = '$';
      break;
    case '#':
      buf[j++] = '\\';
      buf[j++] = '#';
      break;
    case ' ':
    case '\t':
      for (int k = i - 1; k >= 0 && s[k] == '\\'; k--)
        buf[j++] = '\\';
      buf[j++] = '\\';
      buf[j++] = s[i];
      break;
    default: {
      buf[j++] = s[i];
      break;
    }
    }
  }
  return buf;
}

static void build_incl_paths(const char *opt_B, bool opt_nostdinc, StringArray *isystem,
                             StringArray *idirafter) {
  if (opt_B)
    add_include_path(&sysincl_paths, opt_B);

  for (int i = 0; i < isystem->len; i++)
    add_include_path(&sysincl_paths, isystem->data[i]);

  if (!opt_nostdinc)
    platform_stdinc_paths(&sysincl_paths);

  for (int i = 0; i < idirafter->len; i++)
    add_include_path(&sysincl_paths, idirafter->data[i]);

  // Filter system directories passed as -I
  for (int i = 0; i < include_paths.len; i++) {
    bool match = false;
    for (int j = 0; j < sysincl_paths.len; j++)
      if ((match = !strcmp(sysincl_paths.data[j], include_paths.data[i])))
        break;
    if (!match)
      include_paths.data[incl_cnt++] = include_paths.data[i];
  }
  include_paths.len = incl_cnt;

  for (int i = 0; i < sysincl_paths.len; i++)
    strarray_push(&include_paths, sysincl_paths.data[i]);
}

static void build_ld_paths(const char *opt_B, StringArray *paths) {
  if (opt_B)
    strarray_push(&ld_paths, opt_B);

  for (int i = 0; i < paths->len; i++)
    strarray_push(&ld_paths, paths->data[i]);

  platform_search_dirs(&ld_paths);
}

static void parse_args(int argc, char **argv, bool *run_ld) {
  const char *arg;
  int input_cnt = 0;
  const char *opt_B = NULL;
  bool opt_pipe = false;
  bool opt_hash_hash_hash = false;
  bool has_wl = false;
  bool has_gnu_keywords_option = false;
  bool opt_nostdinc = false;
  StringArray libpaths = {0};
  StringArray isystem = {0};
  StringArray idirafter = {0};

  for (int i = 1; i < argc; i++) {
    if (*argv[i] == '\0')
      continue;

    if (*argv[i] != '-' || argv[i][1] == '\0') {
      strarray_push(&input_args, argv[i]);
      input_cnt++;
      continue;
    }

    if (!strcmp(argv[i], "-###")) {
      opt_hash_hash_hash = true;
      continue;
    }

    if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) {
      opt_verbose = true;
      continue;
    }

    if (!strcmp(argv[i], "--help")) {
      puts("slimcc [ -o <path> ] <file>");
      exit(0);
    }

    if (!strcmp(argv[i], "--version")) {
      version();
      exit(0);
    }

    if (!strcmp(argv[i], "-dumpmachine")) {
      if (!dumpmachine_str)
        error("'-dumpmachine' not configured");
      puts(dumpmachine_str);
      exit(0);
    }

    if (!strcmp(argv[i], "-print-search-dirs") ||
        !strcmp(argv[i], "--print-search-dirs")) {
      StringArray dirs = {0};
      platform_search_dirs(&dirs);
      printf("libraries: =");
      for (int i = 0; i < dirs.len; i++)
        printf("%s%s", dirs.data[i], (i + 1 != dirs.len) ? ":" : "\n");
      exit(0);
    }

    if (!strcmp(argv[i], "-hashmap-test")) {
      hashmap_test();
      exit(0);
    }

    if (take_arg_s(argv, &i, &arg, "-o")) {
      opt_o = arg;
      continue;
    }

    if (!strcmp(argv[i], "-S")) {
      opt_S = true;
      continue;
    }

    if (!strcmp(argv[i], "-c")) {
      opt_c = true;
      continue;
    }

    if (!strcmp(argv[i], "-E")) {
      opt_E = true;
      continue;
    }

    if (!strcmp(argv[i], "-P")) {
      opt_P = true;
      continue;
    }

    if (!strcmp(argv[i], "-dM")) {
      opt_dM = true;
      continue;
    }

    if (take_arg_s(argv, &i, &opt_B, "-B"))
      continue;

    if (take_arg_s(argv, &i, &arg, "-I")) {
      add_include_path(&include_paths, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-isystem")) {
      strarray_push(&isystem, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-idirafter")) {
      strarray_push(&idirafter, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-iquote")) {
      add_include_path(&iquote_paths, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-D")) {
      macrochange_push(&macrodefs, arg, true);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-U")) {
      macrochange_push(&macrodefs, arg, false);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-imacros")) {
      strarray_push(&opt_imacros, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-include")) {
      strarray_push(&opt_include, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-x")) {
      strarray_push(&input_args, "-x");
      strarray_push(&input_args, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-L")) {
      strarray_push(&libpaths, arg);
      continue;
    }

    if (comma_arg(argv[i], &as_args, "-Wa,"))
      continue;

    if (startswith(argv[i], &arg, "-Wl,")) {
      strarray_push(&input_args, argv[i]);
      has_wl = true;
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-l")) {
      strarray_push(&input_args, format("-Wl,-l%s", arg));
      has_wl = true;
      continue;
    }

    if (take_arg(argv, &i, &arg, "-Xlinker")) {
      strarray_push(&input_args, format("-Wl,%s", arg));
      has_wl = true;
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-z")) {
      strarray_push(&input_args, format("-Wl,-z,%s", arg));
      has_wl = true;
      continue;
    }

    if (!strcmp(argv[i], "-s")) {
      opt_s = true;
      continue;
    }

    if (!strcmp(argv[i], "-M")) {
      opt_M = true;
      continue;
    }

    if (!strcmp(argv[i], "-MM")) {
      opt_M = opt_MM = true;
      continue;
    }

    if (!strcmp(argv[i], "-MD")) {
      opt_MD = true;
      continue;
    }

    if (!strcmp(argv[i], "-MMD")) {
      opt_MD = opt_MMD = true;
      continue;
    }

    if (startswith(argv[i], &arg, "-Wp,-MD,")) {
      opt_MD = true;
      opt_MF = arg;
      continue;
    }

    if (startswith(argv[i], &arg, "-Wp,-MMD,")) {
      opt_MD = opt_MMD = true;
      opt_MF = arg;
      continue;
    }

    if (!strcmp(argv[i], "-MG")) {
      opt_MG = true;
      continue;
    }

    if (!strcmp(argv[i], "-MP")) {
      opt_MP = true;
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-MF")) {
      opt_MF = arg;
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-MT") || startswith(argv[i], &arg, "-Wp,-MT,")) {
      if (opt_MT == NULL)
        opt_MT = arg;
      else
        opt_MT = format("%s %s", opt_MT, arg);
      continue;
    }

    if (take_arg_s(argv, &i, &arg, "-MQ") || startswith(argv[i], &arg, "-Wp,-MQ,")) {
      if (opt_MT == NULL)
        opt_MT = quote_makefile(arg);
      else
        opt_MT = format("%s %s", opt_MT, quote_makefile(arg));
      continue;
    }

    if (startswith(argv[i], &arg, "-g")) {
      opt_g = strcmp(arg, "0");
      continue;
    }

    if (startswith(argv[i], &arg, "-O")) {
      switch (*arg) {
      case 's':
      case 'z': {
        opt_fn_align = 1;
        break;
      }
      case '2':
      case '3':
        if (opt_fn_align == 1)
          opt_fn_align = 16;
        break;
      }
      continue;
    }

    if (!strcmp(argv[i], "-ansi")) {
      set_std(true, "89");
      continue;
    }

    if (startswith(argv[i], &arg, "-std=") ||
        startswith(argv[i], &arg, "--std=") ||
        take_arg(argv, &i, &arg, "--std")) {
      if (startswith(arg, &arg, "c"))
        set_std(true, arg);
      else if (startswith(arg, &arg, "gnu"))
        set_std(false, arg);
      else if (startswith(arg, &arg, "iso9899:"))
        set_std_iso(arg);
      else
        error("unknown c standard");
      continue;
    }

    if (startswith(argv[i], &arg, "-f")) {
      bool b = !startswith(arg, &arg, "no-");

      if (set_bool(arg, b, "common", &opt_fcommon) ||
          set_bool(arg, b, "plt", &opt_use_plt) ||
          set_bool(arg, b, "function-sections", &opt_func_sections) ||
          set_bool(arg, b, "data-sections", &opt_data_sections) ||
          set_bool(arg, b, "emulated-tls", &opt_femulated_tls) ||
          set_bool(arg, b, "short-enums", &opt_short_enums) ||
          set_bool(arg, b, "gnu89-inline", &opt_gnu89_inline))
        continue;

      if (set_bool(arg, b, "ms-anon-struct", &opt_ms_anon_struct))
        continue;

      if (!strcmp(arg, "asm") || !strcmp(arg, "gnu-keywords")) {
        opt_gnu_keywords = b;
        has_gnu_keywords_option = true;
        continue;
      }

      if (!strncmp(arg, "align-functions", 15)) {
        arg += 15;
        if (*arg == '\0') {
          opt_fn_align = b ? 16 : 1;
          continue;
        }
        if (b && *arg == '=') {
          char *end;
          if (!(opt_fn_align = strtoul(arg + 1, &end, 10)))
            opt_fn_align = 16;
          if (*end == '\0' && is_pow_of_two(opt_fn_align))
            continue;
        }
        error("unsupported form of '-falign-functions'");
      }

      if (!strcmp(arg, "pic")) {
        opt_fpic = 1 * b, opt_fpie = 0;
        continue;
      }
      if (!strcmp(arg, "PIC")) {
        opt_fpic = 2 * b, opt_fpie = 0;
        continue;
      }
      if (!strcmp(arg, "pie")) {
        opt_fpic = 0, opt_fpie = 1 * b;
        continue;
      }
      if (!strcmp(arg, "PIE")) {
        opt_fpic = 0, opt_fpie = 2 * b;
        continue;
      }

      // -f only options
      if (b) {
        if (set_true(arg, "defer-ts", &opt_fdefer_ts))
          continue;

        if (set_bool(arg, false, "signed-char", &ty_pchar->is_unsigned) ||
            set_bool(arg, true, "unsigned-char", &ty_pchar->is_unsigned))
          continue;

        if (startswith(arg, &arg, "stack-reuse=")) {
          opt_reuse_stack = !strcmp(arg, "all");
          continue;
        }

        if (set_true(arg, "disable-visibility", &opt_disable_visibility))
          continue;
        if (set_true(arg, "fake-always-inline", &opt_fake_always_inline))
          continue;

        if (startswith(arg, &opt_visibility, "visibility=") ||
            startswith(arg, &opt_use_as, "use-as="))
          continue;

        if (startswith(arg, &arg, "use-ld=")) {
          if (!strcmp(arg, "lld")) {
            opt_use_ld = "ld.lld";
            continue;
          }
          opt_use_ld = arg;
          continue;
        }
      }
    }

    if (argv[i][0] == '-') {
      arg = (argv[i][1] == '-') ? &argv[i][2] : &argv[i][1];

      if (set_true(arg, "r", &opt_r) ||
          set_true(arg, "rdynamic", &opt_rdynamic) ||
          set_true(arg, "static", &opt_static) ||
          set_true(arg, "static-pie", &opt_static_pie) ||
          set_true(arg, "static-libgcc", &opt_static_libgcc) ||
          set_true(arg, "shared", &opt_shared) ||
          set_true(arg, "pie", &opt_pie) ||
          set_true(arg, "nopie", &opt_nopie) ||
          set_true(arg, "pthread", &opt_pthread) ||
          set_true(arg, "pipe", &opt_pipe))
        continue;

      if (!strcmp(arg, "no-pie")) {
        opt_pie = false;
        opt_nopie = true;
        continue;
      }
    }

    if (!strcmp(argv[i], "-nostdinc")) {
      opt_nostdinc = true;
      continue;
    }

    if (set_true(argv[i], "-nostartfiles", &opt_nostartfiles) ||
        set_true(argv[i], "-nodefaultlibs", &opt_nodefaultlibs) ||
        set_true(argv[i], "-nolibc", &opt_nolibc))
      continue;

    if (!strcmp(argv[i], "-nostdlib")) {
      opt_nostartfiles = opt_nodefaultlibs = true;
      continue;
    }

    if (set_bool(argv[i], true, "-Werror", &opt_werror) ||
        set_bool(argv[i], false, "-Wno-error", &opt_werror))
      continue;

    if (startswith(argv[i], &arg, "-W")) {
      if (strchr(arg, ','))
        error("unknown argument: %s", argv[i]);
      continue;
    }

    // These options are ignored for now.
    if (startswith(argv[i], &arg, "-march=") ||
        !strcmp(argv[i], "-fdollars-in-identifiers") ||
        !strcmp(argv[i], "-ffreestanding") ||
        !strcmp(argv[i], "-ffp-contract=off") ||
        !strcmp(argv[i], "-fno-builtin") ||
        !strcmp(argv[i], "-fno-fast-math") ||
        !strcmp(argv[i], "-fno-lto") ||
        !strcmp(argv[i], "-fno-asynchronous-unwind-tables") ||
        !strcmp(argv[i], "-fno-delete-null-pointer-checks") ||
        !strcmp(argv[i], "-fno-exceptions") ||
        !strcmp(argv[i], "-fno-omit-frame-pointer") ||
        !strcmp(argv[i], "-fno-stack-protector") ||
        !strcmp(argv[i], "-fno-strict-aliasing") ||
        !strcmp(argv[i], "-fno-strict-overflow") ||
        !strcmp(argv[i], "-fwrapv") ||
        !strcmp(argv[i], "-m64") ||
        !strcmp(argv[i], "-malign-double") ||
        !strcmp(argv[i], "-mfpmath=sse") ||
        !strcmp(argv[i], "-mno-red-zone") ||
        !strcmp(argv[i], "-pedantic") ||
        !strcmp(argv[i], "--pedantic") ||
        !strcmp(argv[i], "-pedantic-errors") ||
        !strcmp(argv[i], "--pedantic-errors") ||
        !strcmp(argv[i], "-w"))
      continue;

    error("unknown argument: %s", argv[i]);
  }

  if (!opt_E && opt_dM)
    error("option -dM without -E not supported");

  if (opt_MG && !opt_M)
    error("option -MG must be used with -M or -MM");

  if (opt_disable_visibility && opt_visibility)
    error("-fvisibility disabled with -fdisable-visibility");

  if (!has_gnu_keywords_option)
    opt_gnu_keywords = !is_iso_std;

  if (opt_B) {
    char *as_b = format("%s/%s", opt_B, default_as);
    char *ld_b = format("%s/%s", opt_B, default_ld);
    if (file_exists(as_b))
      default_as = as_b;
    if (file_exists(ld_b))
      default_ld = ld_b;
  }

  build_incl_paths(opt_B, opt_nostdinc, &isystem, &idirafter);
  build_ld_paths(opt_B, &libpaths);

  bool no_input = !input_cnt && !has_wl;
  if (opt_hash_hash_hash || opt_verbose) {
    version();
    if (no_input)
      exit(0);
  }
  if (no_input)
    error("no input files");

  if (opt_hash_hash_hash)
    proc_mode = PR_HHH;
  else if (opt_pipe)
    proc_mode = PR_PIPE;
  else if (input_cnt == 1)
    proc_mode = PR_NOFORK;

  *run_ld = has_wl && !(opt_c || opt_S || opt_E);
}

static FILE *open_file(const char *path) {
  if (!path || strcmp(path, "-") == 0)
    return stdout;

  FILE *out = fopen(path, "w");
  if (!out)
    error("cannot open output file: %s: %s", path, strerror(errno));
  return out;
}

static void close_file(FILE *file) {
  if (file == stdout)
    fflush(file);
  else
    fclose(file);
}

static bool endswith(const char *p, const char *q) {
  int len1 = strlen(p);
  int len2 = strlen(q);
  return (len1 >= len2) && !strcmp(p + len1 - len2, q);
}

// Replace file extension
static const char *replace_extn(const char *tmpl, const char *extn) {
  char *filename = basename(strdup(tmpl));
  char *dot = strrchr(filename, '.');
  if (dot)
    *dot = '\0';
  return format("%s%s", filename, extn);
}

static void cleanup(void) {
  for (int i = 0; i < tmpfiles.len; i++)
    unlink(tmpfiles.data[i]);
  if (tmp_folder)
    rmdir(tmp_folder);
}

void cleanup_exit(int status) {
  if (is_fork_child)
    _exit(status);

  cleanup();

  exit(status);
}

static char *create_tmpfile(void) {
  if (!tmp_folder) {
    tmp_folder = mkdtemp(strdup("/tmp/slimcc-XXXXXX"));
    if (!tmp_folder)
      error("mkdtemp failed: %s", strerror(errno));
  }
  static int64_t i;
  char *path = format("%s/%" PRIi64, tmp_folder, i++);

  strarray_push(&tmpfiles, path);
  return path;
}

static char *create_pipefile(void) {
  char *path = create_tmpfile();
  if (mkfifo(path, (S_IRUSR | S_IWUSR)) == -1)
    error("mkfifo failed: %s", strerror(errno));
  return path;
}

void run_subprocess(const char **argv) {
  if (opt_verbose || proc_mode == PR_HHH) {
    fprintf(stderr, "\"%s\"", argv[0]);
    for (int i = 1; argv[i]; i++)
      fprintf(stderr, " \"%s\"", argv[i]);
    fprintf(stderr, "\n");
    if (proc_mode == PR_HHH)
      return;
  }

  extern char **environ;
  pid_t id;
  int status;
  if (posix_spawnp(&id, argv[0], NULL, NULL, (char *const *)argv, environ) ||
      waitpid(id, &status, 0) <= 0 ||
      status != 0) {
    fprintf(stderr, "exec failed: %s\n", argv[0]);
    cleanup_exit(1);
  }
}

static int fork_cc1(const char *input, const char *output, bool is_asm_pp) {
  pid_t id = fork();
  if (id == 0) {
    is_fork_child = true;
    cc1(input, output, is_asm_pp);
    _exit(0);
  }
  return id;
}

static void run_cc1(const char *input, const char *output, bool is_asm_pp) {
  if (proc_mode == PR_HHH)
    return;

  if (proc_mode == PR_NOFORK) {
    cc1(input, output, is_asm_pp);
    return;
  }

  int id = fork_cc1(input, output, is_asm_pp);

  int status;
  if (waitpid(id, &status, 0) <= 0 || status != 0)
    cleanup_exit(1);
}

static void run_cc1_as(const char *input, const char *output, bool is_asm_pp) {
  if (proc_mode == PR_HHH) {
    char *tmp = create_tmpfile();
    run_assembler(&as_args, tmp, output);
    return;
  }

  if (proc_mode == PR_PIPE) {
    char *tmp = create_pipefile();
    int id = fork_cc1(input, tmp, is_asm_pp);
    run_assembler(&as_args, tmp, output);

    int status;
    if (waitpid(id, &status, 0) <= 0 || status != 0)
      cleanup_exit(1);
    return;
  }

  char *tmp = create_tmpfile();
  run_cc1(input, tmp, is_asm_pp);
  run_assembler(&as_args, tmp, output);
}

static void print_linemarker(FILE *out, Token *tok) {
  const char *name = display_files.data[tok->display_file_no];
  if (!strcmp(name, "-"))
    name = "<stdin>";
  if (!tok->at_bol)
    fprintf(out, "\n");
  fprintf(out, "# %d \"%s\"\n", tok->display_line_no, name);
}

static void print_tokens(Token *tok, FILE *out) {
  int line = 0;
  int file_no = -1;
  tok->at_bol = false;

  for (; tok->kind != TK_EOF; tok = tok->next) {
    if (tok->kind == TK_FMARK)
      if (tok->display_file_no == tok->next->display_file_no)
        continue;

    if (tok->at_bol) {
      fprintf(out, "\n");
      line++;
    }
    if (!opt_P) {
      if (file_no != tok->display_file_no) {
        file_no = tok->display_file_no;
        print_linemarker(out, tok);
      } else {
        int diff = tok->display_line_no - line;
        if (diff > 0 && diff <= 8) {
          while (line++ < tok->display_line_no)
            fprintf(out, "\n");
        } else if (diff) {
          print_linemarker(out, tok);
        }
      }
      line = tok->display_line_no;
    }
    if (tok->has_space)
      fprintf(out, " ");

    fprintf(out, "%.*s", tok->len, tok->loc);
  }
  fprintf(out, "\n");
}

bool in_sysincl_path(int idx) {
  return idx >= incl_cnt;
}

bool ignore_missing_dep(const char *path, const char *filename, Token *tok) {
  if (!path) {
    if (opt_MG) {
      add_dep_file(filename, false);
      return true;
    }
    if (tok)
      error_tok(tok, "file not found");
    error("`%s` file not found", filename);
  }
  return false;
}

void add_dep_file(const char *path, bool is_sys) {
  if (opt_M || opt_MD) {
    if (is_sys && (opt_MM || opt_MMD))
      return;

    static HashMap map;
    HashEntry *ent = hashmap_get_or_insert(&map, path, strlen(path));
    if (ent->val)
      return;
    ent->val = (void *)1;
    strarray_push(&dep_files, path);
  }
}

static const char *skip_dot_slash(const char *p) {
  if (*p == '.' && p[1] == '/') {
    for (p += 2; *p == '/';)
      p++;
  }
  return p;
}

static void print_dependencies(const char *input) {
  const char *path;
  if (opt_MF)
    path = opt_MF;
  else if (opt_MD)
    path = replace_extn(opt_o ? opt_o : input, ".d");
  else if (opt_o)
    path = opt_o;
  else
    path = "-";

  FILE *out = open_file(path);
  if (opt_MT)
    fprintf(out, "%s:", opt_MT);
  else
    fprintf(out, "%s:", quote_makefile(replace_extn(input, ".o")));

  for (int i = 0; i < dep_files.len; i++)
    fprintf(out, " \\\n  %s", skip_dot_slash(dep_files.data[i]));

  fputc('\n', out);

  if (opt_MP)
    for (int i = 1; i < dep_files.len; i++)
      fprintf(out, "%s:\n", quote_makefile(skip_dot_slash(dep_files.data[i])));

  close_file(out);
}

static void cc1(const char *input_file, const char *output_file, bool is_asm_pp) {
  if (is_asm_pp)
    opt_E = opt_cc1_asm_pp = true;

  arena_on(&cc1_arena);
  arena_on(&pp_arena);

  init_macros();
  platform_init_cc1();
  cli_macros(is_asm_pp);

  FILE *out = open_file(output_file);

  Token *tok = preprocess(input_file, &opt_include, &opt_imacros);

  if (opt_M || opt_MD) {
    print_dependencies(input_file);
    if (opt_M) {
      close_file(out);
      arena_off(&pp_arena);
      arena_off(&cc1_arena);
      return;
    }
  }

  if (opt_E) {
    if (opt_dM)
      dump_defines(out);
    else
      print_tokens(tok, out);

    close_file(out);
    arena_off(&pp_arena);
    arena_off(&cc1_arena);
    return;
  }

  tok = prepare_parse(tok);

  arena_off(&pp_arena);

  Obj *prog = parse(tok);

  codegen(prog, out);

  close_file(out);
  arena_off(&cc1_arena);
}

void run_assembler_gnustyle(StringArray *args, const char *input, const char *output) {
  StringArray arr = {0};

  strarray_push(&arr, opt_use_as ? opt_use_as : default_as);
  strarray_push(&arr, input);
  strarray_push(&arr, "-o");
  strarray_push(&arr, output);
  strarray_push(&arr, "--fatal-warnings");

  for (int i = 0; i < args->len; i++)
    strarray_push(&arr, args->data[i]);

  strarray_push(&arr, NULL);

  run_subprocess(arr.data);
}

static char *find_file(const char *pattern) {
  char *path = NULL;
  glob_t buf = {0};
  glob(pattern, 0, NULL, &buf);
  if (buf.gl_pathc > 0)
    path = strdup(buf.gl_pathv[buf.gl_pathc - 1]);
  globfree(&buf);
  return path;
}

char *find_dir_w_file(const char *pattern) {
  static char *path;
  if (!path) {
    path = find_file(pattern);
    if (!path)
      return NULL;
    path = dirname(path);
  }
  return path;
}

bool file_exists(const char *path) {
  struct stat st;
  return !stat(path, &st);
}

LinkType link_type_gnustyle(void) {
  if (opt_r)
    return LT_RELO;
  if (opt_shared)
    return LT_SHARED;
  if (opt_static_pie)
    return LT_STATIC_PIE;
  if (opt_static)
    return LT_STATIC;
  if (opt_pie)
    return LT_PIE;
  return LT_DYNAMIC;
}

void ldarg_gnu_base(StringArray *arr, const char *output) {
  strarray_push(arr, opt_use_ld ? opt_use_ld : default_ld);
  strarray_push(arr, "-o");
  strarray_push(arr, output);
  strarray_push(arr, "-m");
  strarray_push(arr, "elf_x86_64");
  strarray_push(arr, "--eh-frame-hdr");

  if (opt_s)
    strarray_push(arr, "-s");
}

void ldarg_gnu_linktype(StringArray *arr, LinkType lt, const char *ldso_path) {
  switch (lt) {
  case LT_RELO:       strarray_push(arr, "-r"); break;
  case LT_SHARED:     strarray_push(arr, "-shared"); break;
  case LT_STATIC:     strarray_push(arr, "-static"); break;
  case LT_STATIC_PIE: strarray_push(arr, "-static");
  case LT_PIE:        strarray_push(arr, "-pie"); break;
  }

  switch (lt) {
  case LT_STATIC_PIE: {
    strarray_push(arr, "-no-dynamic-linker");
    break;
  }
  case LT_DYNAMIC:
  case LT_SHARED:
  case LT_PIE:
    if (ldso_path) {
      if (opt_rdynamic)
        strarray_push(arr, "--export-dynamic");

      strarray_push(arr, "-dynamic-linker");
      strarray_push(arr, ldso_path);
    }
  }
}

void ldarg_gnu_crtbegin(StringArray *arr, LinkType lt, const char *gcc_libpath) {
  if (gcc_libpath) {
    switch (lt) {
    case LT_STATIC_PIE:
    case LT_SHARED:
    case LT_PIE:        strarray_push(arr, format("%s/crtbeginS.o", gcc_libpath)); break;
    case LT_STATIC:     strarray_push(arr, format("%s/crtbeginT.o", gcc_libpath)); break;
    case LT_DYNAMIC:    strarray_push(arr, format("%s/crtbegin.o", gcc_libpath)); break;
    }
  }
}

void ldarg_gnu_inputs(StringArray *arr, StringArray *paths, StringArray *args) {
  for (int i = 0; i < paths->len; i++) {
    strarray_push(arr, "-L");
    strarray_push(arr, paths->data[i]);
  }

  for (int i = 0; i < args->len; i++)
    strarray_push(arr, args->data[i]);
}

static void link_libgcc(StringArray *arr, bool has_libgcc, bool is_static) {
  if (!has_libgcc)
    return;
  strarray_push(arr, "-lgcc");

  if (is_static) {
    strarray_push(arr, "-lgcc_eh");
  } else {
    strarray_push(arr, "--push-state");
    strarray_push(arr, "--as-needed");
    strarray_push(arr, "-lgcc_s");
    strarray_push(arr, "--pop-state");
  }
}

static void link_libc(StringArray *arr) {
  if (opt_pthread)
    strarray_push(arr, "-lpthread");
  if (!opt_nolibc) {
    strarray_push(arr, "-lc");
    strarray_push(arr, "--push-state");
    strarray_push(arr, "--as-needed");
    strarray_push(arr, "-lm");
    strarray_push(arr, "--pop-state");
  }
}

void ldarg_gnu_lc_lgcc(StringArray *arr, LinkType lt, bool has_libgcc) {
  if (lt == LT_STATIC_PIE || lt == LT_STATIC) {
    strarray_push(arr, "--start-group");
    link_libgcc(arr, has_libgcc, true);
    link_libc(arr);
    strarray_push(arr, "--end-group");
  } else {
    link_libgcc(arr, has_libgcc, opt_static_libgcc);
    link_libc(arr);
    link_libgcc(arr, has_libgcc, opt_static_libgcc);
  }
}

void ldarg_gnu_crtend(StringArray *arr, LinkType lt, const char *gcc_libpath) {
  if (gcc_libpath) {
    switch (lt) {
    case LT_STATIC_PIE:
    case LT_SHARED:
    case LT_PIE:        strarray_push(arr, format("%s/crtendS.o", gcc_libpath)); break;
    case LT_STATIC:
    case LT_DYNAMIC:    strarray_push(arr, format("%s/crtend.o", gcc_libpath)); break;
    }
  }
}

void run_linker_gnustyle(StringArray *paths, StringArray *args, const char *output,
                         const char *ldso_path, const char *libpath,
                         const char *gcc_libpath) {
  StringArray arr = {0};

  ldarg_gnu_base(&arr, output);

  LinkType lt = link_type_gnustyle();

  ldarg_gnu_linktype(&arr, lt, ldso_path);

  if (!opt_nostartfiles && lt != LT_RELO) {
    switch (lt) {
    case LT_STATIC_PIE: strarray_push(&arr, format("%s/rcrt1.o", libpath)); break;
    case LT_PIE:        strarray_push(&arr, format("%s/Scrt1.o", libpath)); break;
    case LT_STATIC:
    case LT_DYNAMIC:    strarray_push(&arr, format("%s/crt1.o", libpath)); break;
    }
    strarray_push(&arr, format("%s/crti.o", libpath));

    ldarg_gnu_crtbegin(&arr, lt, gcc_libpath);
  }

  ldarg_gnu_inputs(&arr, paths, args);

  if (!opt_nostartfiles && lt != LT_RELO) {
    ldarg_gnu_lc_lgcc(&arr, lt, gcc_libpath);

    ldarg_gnu_crtend(&arr, lt, gcc_libpath);

    strarray_push(&arr, format("%s/crtn.o", libpath));
  }

  strarray_push(&arr, NULL);

  run_subprocess(arr.data);
}

static FileType get_file_type(const char *filename) {
  if (endswith(filename, ".c") || endswith(filename, ".i"))
    return FILE_C;
  if (endswith(filename, ".s"))
    return FILE_ASM;
  if (endswith(filename, ".S"))
    return FILE_PP_ASM;
  if (!strcmp(filename, "-")) {
    if (!opt_E)
      error("-E or -x required when input is from standard input");
    return FILE_C;
  }
  if (endswith(filename, ".h")) {
    if (!opt_E)
      error("pch not supported");
    return FILE_C;
  }
  return FILE_LDARG;
}

int main(int argc, char **argv) {
  argv0 = argv[0];
  platform_init_driver();

  bool run_ld;
  parse_args(argc, argv, &run_ld);

  StringArray ld_args = {0};
  FileType opt_x = FILE_NONE;
  int src_cnt = 0;

  for (int i = 0; i < input_args.len; i++) {
    if (!strcmp(input_args.data[i], "-x")) {
      opt_x = parse_opt_x(input_args.data[++i]);
      continue;
    }
    if (comma_arg(input_args.data[i], &ld_args, "-Wl,"))
      continue;

    const char *input = input_args.data[i];

    FileType type = opt_x ? opt_x : get_file_type(input);

    if (type == FILE_LDARG) {
      strarray_push(&ld_args, input);
      run_ld = true;
      continue;
    }

    const char *output;
    if (opt_o) {
      if (opt_c || opt_S || opt_E)
        if (src_cnt++)
          error("cannot specify '-o' with '-c,' '-S' or '-E' with multiple files");
      output = opt_o;
    } else if (opt_S) {
      output = replace_extn(input, ".s");
    } else {
      output = replace_extn(input, ".o");
    }

    // Handle .s
    if (type == FILE_ASM) {
      if (opt_S || opt_E || opt_M)
        continue;

      if (opt_c) {
        run_assembler(&as_args, input, output);
        continue;
      }

      char *tmp = create_tmpfile();
      run_assembler(&as_args, input, tmp);
      strarray_push(&ld_args, tmp);
      run_ld = true;
      continue;
    }

    // Handle .S
    if (type == FILE_PP_ASM) {
      if (opt_S || opt_E || opt_M) {
        run_cc1(input, (opt_o ? opt_o : "-"), true);
        continue;
      }
      if (opt_c) {
        run_cc1_as(input, output, true);
        continue;
      }
      char *tmp = create_tmpfile();
      run_cc1_as(input, tmp, true);
      strarray_push(&ld_args, tmp);
      run_ld = true;
      continue;
    }

    if (opt_E || opt_M) {
      run_cc1(input, (opt_o ? opt_o : "-"), false);
      continue;
    }

    if (opt_S) {
      run_cc1(input, output, false);
      continue;
    }

    if (opt_c) {
      run_cc1_as(input, output, false);
      continue;
    }

    char *tmp = create_tmpfile();
    run_cc1_as(input, tmp, false);
    strarray_push(&ld_args, tmp);
    run_ld = true;
    continue;
  }

  if (run_ld) {
    if (opt_c || opt_S || opt_E || opt_M)
      fprintf(stderr, "linker input unused\n");
    else
      run_linker(&ld_paths, &ld_args, opt_o ? opt_o : "a.out");
  }

  cleanup();
}
