#ifndef __MAIN_C__
#define __MAIN_C__ 1

#include "icode2ast.h"
#include "filter.h"
#include "stack.h"
#include "errorf.h"
#include "genimp.h"
#include "genc.h"
#include "icode2ast_data.h"
#include "showicode.h"

#include <errno.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <signal.h>

static char *rcs_version = "main.c V$Revision: 1.27 $";

extern int pass1(const char *source, const char *perm, const char *icode, const char *list);
extern int systemf(char *s, ...);

int setup_output_filter(const char *filter_command);

char *progname = "icode2ast";

int opt_perms = 0;
int opt_verbose = 0;
int opt_trace = 0;
int opt_filter = 0;
int opt_target_imp = 0;

//#define FILTER_COMMAND "/bin/cat"
#define FILTER_COMMAND "/usr/bin/clang-format | /home/gtoal/src/i2c/icode2ast/hashline - | ecce - - -command \"(v.#line.m(v. .r)0 v.//.m-k, m)0; %c\" 2> /dev/null"

int gdb_trace_active = 0;
static pid_t filter_pid = -1;

extern int stackdebug;

typedef enum {
  PASS_LOAD = 0,
  PASS_ALIASES,
  PASS_LINENOS,
  PASS_FORMAL_PARAMS,
  PASS_RECORDFORMAT,
  PASS_PROC_PARAMS,
  PASS_STACK,
  PASS_REVERSE_ARRAY_INDICES,
  PASS_FOLD_BLOCKS,
  PASS_MISSING_RECORDFORMAT_SPECS,
  PASS_PROCPARAM_SPECS,
  PASS_PROPAGATE_TYPES,
  PASS_SEPARATE_PERMS,
  PASS_COUNT
} FilterPass;

static const char *pass_names[PASS_COUNT] = {
  [PASS_LOAD]                        = "load",
  [PASS_ALIASES]                     = "aliases",
  [PASS_LINENOS]                     = "linenos",
  [PASS_FORMAL_PARAMS]               = "params",
  [PASS_RECORDFORMAT]                = "recfms",
  [PASS_PROC_PARAMS]                 = "procparams",
  [PASS_STACK]                       = "stack",
  [PASS_REVERSE_ARRAY_INDICES]       = "revarray",
  [PASS_FOLD_BLOCKS]                 = "blocks",
  [PASS_MISSING_RECORDFORMAT_SPECS]  = "recspecs",
  [PASS_PROCPARAM_SPECS]             = "procspecs",
  [PASS_PROPAGATE_TYPES]             = "types",
  [PASS_SEPARATE_PERMS]              = "nestperms"
};

static bool show_ast_flags[PASS_COUNT] = { false };

void control(int bits) {
  if (bits & 1) opt_trace = 1; else opt_trace = 0;
}

void diagnose(int bits) {
}

static void parse_showast_arg(const char *arg) {
  char *str = strdup(arg);
  if (!str) return;

  char *token = strtok(str, ",");
  while (token != NULL) {
    if (strcmp(token, "all") == 0) {
      for (int i = 0; i < PASS_COUNT; i++) {
        show_ast_flags[i] = true;
      }
    } else {
      bool matched = false;
      for (int i = 0; i < PASS_COUNT; i++) {
        if (strcmp(token, pass_names[i]) == 0) {
          show_ast_flags[i] = true;
          matched = true;
          break;
        }
      }
      if (!matched) {
        fprintf(stderr, "%s: warning: unknown pass '%s' in --showast\n", progname, token);
      }
    }
    token = strtok(NULL, ",");
  }
  free(str);
}

static void check_and_dump_ast(FilterPass pass, IMPAST *program) {
  if (show_ast_flags[pass]) {
    dump_ast(stdout, program, pass_names[pass]);
  }
}

static void usage(FILE *fp) {
  fprintf(fp,
    "Usage: %s [options] [icode-file.icd | imp-file.imp]\n"
    "\n"
    "Convert an icode file to C or Imp77 source.\n"
    "\n"
    "Options:\n"
    "      --showast           ",
    progname);
  for (int i = 0; i < PASS_COUNT; i++) {
    fprintf(fp, "%s", pass_names[i]);
    if (i < PASS_COUNT-1) fprintf(fp, ",");
  }
  fprintf(fp, "\n                          Display AST after specified passes (comma-separated list)\n");
  fprintf(fp,
    "  -t, --trace             Generate trace of compiler execution\n"
    "  -p, --perms             Display icode for perms\n"
    "  -f, --filter            Filter C output via clang-format\n"
    "  -v, --verbose           Enable verbose diagnostic logging\n"
    "  -h, --help              Display this help and exit\n");
}

int setup_output_filter(const char *filter_command) {
  int pipefd[2];

  if (pipe(pipefd) == -1) {
    perror("pipe failed");
    return -1;
  }

  filter_pid = fork();
  if (filter_pid == -1) {
    perror("fork failed");
    return -1;
  }

  if (filter_pid == 0) {
    // Child process
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    
    if (dup2(pipefd[0], STDIN_FILENO) == -1) {
      perror("child dup2 failed");
      exit(EXIT_FAILURE);
    }

    close(pipefd[0]);
    close(pipefd[1]);

    execl("/bin/bash", "bash", "-c", filter_command, (char *)NULL);
    perror("execl failed");
    exit(EXIT_FAILURE);
  } else {
    // Parent process
    fflush(stdout);

    if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
      perror("parent dup2 failed");
      return -1;
    }

    close(pipefd[0]);
    close(pipefd[1]);

    clearerr(stdout);
    setvbuf(stdout, NULL, _IONBF, 0);
  }

  return 0;
}

int main(int argc, char **argv) {
  IMPAST *Program;
  const char *input_filename = NULL;
  char *icode_filename, *list_filename;
  char *s;
  FILE *devnull = fopen("/dev/null", "w");

  progname = strrchr(argv[0], '/');
  progname = (progname != NULL) ? progname + 1 : argv[0];

  static struct option long_options[] = {
    {"showast", required_argument, 0, 'S'},
    {"imp",     no_argument,       0, 'I'},
    {"trace",   no_argument,       0, 't'},
    {"perms",   no_argument,       0, 'p'},
    {"filter",  no_argument,       0, 'f'},
    {"verbose", no_argument,       0, 'v'},
    {"help",    no_argument,       0, 'h'},
    {0, 0, 0, 0}
  };

  int opt;
  int option_index = 0;

  while ((opt = getopt_long(argc, argv, "tpfhv", long_options, &option_index)) != -1) {
    switch (opt) {
      case 'S':
        parse_showast_arg(optarg);
        break;
      case 'I':
        opt_target_imp = 1;
        break;
      case 't':
        opt_trace = 1;
        break;
      case 'p':
        opt_perms = 1;
        break;
      case 'f':
        opt_filter = 1;
        break;
      case 'v':
        opt_verbose = 1;
        break;
      case 'h':
        usage(stdout);
        exit(EXIT_SUCCESS);
      case '?':
      default:
        usage(stderr);
        exit(EXIT_FAILURE);
    }
  }

  if (optind < argc) {
    input_filename = argv[optind++];
  }

  if (optind >= argc && input_filename == NULL) {
    fprintf(stderr, "%s: missing input filename\n", progname);
    usage(stderr);
    exit(EXIT_FAILURE);
  }

  list_filename = NULL;

  if (strlen(input_filename) > 4 && strcmp(input_filename + strlen(input_filename) - 4, ".imp") == 0) {
    icode_filename = realloc(strdup(input_filename), strlen(input_filename) + 16);
    s = strrchr(icode_filename, '.');
    if (s) sprintf(s, ".icd");

    list_filename = realloc(strdup(input_filename), strlen(input_filename) + 16);
    s = strrchr(list_filename, '.');
    if (s) sprintf(s, ".lis");

    pass1(input_filename, "perms.inc", icode_filename, list_filename);
    input_filename = icode_filename;

    if (freopen(input_filename, "r", stdin) == NULL) {
      fprintf(stderr, "%s: cannot open icode input %s - %s\n",
              progname, input_filename, strerror(errno));
      exit(EXIT_FAILURE);
    }
  } else {
    if (freopen(input_filename, "r", stdin) == NULL) {
      fprintf(stderr, "%s: cannot open icode input %s - %s\n",
              progname, input_filename, strerror(errno));
      exit(EXIT_FAILURE);
    }
  }

  Program = load_icode_file();
  dump_ast(devnull, Program, "load_icode_file");
  check_and_dump_ast(PASS_LOAD, Program);

  Program = apply_aliases(Program);
  check_and_dump_ast(PASS_ALIASES, Program);

  Program = assign_linenos(Program);
  check_and_dump_ast(PASS_LINENOS, Program);

  Program = assign_formal_params(Program);
  check_and_dump_ast(PASS_FORMAL_PARAMS, Program);

  Program = handle_recordformat_declarations(Program);
  check_and_dump_ast(PASS_RECORDFORMAT, Program);

  Program = handle_proc_params(Program);
  check_and_dump_ast(PASS_PROC_PARAMS, Program);

  if (show_ast_flags[PASS_STACK]) stackdebug = 1;
  Program = run_stack(Program);
  check_and_dump_ast(PASS_STACK, Program);

  Program = reverse_array_index_declaration_order(Program);
  check_and_dump_ast(PASS_REVERSE_ARRAY_INDICES, Program);

  {
    IMPAST *rest = NULL;
    Program = fold_blocks(Program, &rest);
    if (rest) fprintf(stderr, "? Warning: 'rest' was not NULL\n");
  }
  check_and_dump_ast(PASS_FOLD_BLOCKS, Program);

  Program = handle_missing_recordformat_specs(Program);
  check_and_dump_ast(PASS_MISSING_RECORDFORMAT_SPECS, Program);

  Program = fixup_procparam_specs(Program);
  check_and_dump_ast(PASS_PROCPARAM_SPECS, Program);

  Program = propagate_types(Program);
  check_and_dump_ast(PASS_PROPAGATE_TYPES, Program);

  if (!opt_perms) {
    Program = separate_perms(Program);
  }
  check_and_dump_ast(PASS_SEPARATE_PERMS, Program);

  if (opt_target_imp) {
    generate_imp_source(Program);
  } else if (opt_filter) {
    if (setup_output_filter(FILTER_COMMAND) != 0) {
      fprintf(stderr, "Failed to initialize output filter \"| %s\".\n", FILTER_COMMAND);
      exit(EXIT_FAILURE);
    } else {
      fprintf(stderr, "Filtering output through \"| %s\".\n", FILTER_COMMAND);
      generate_c_source(Program, list_filename);
    }
  } else {
    //fprintf(stderr, "Outputting C code to standard output.\n");
    generate_c_source(Program, list_filename);
  }

  // Flush and close stdout so filter receives EOF
  fflush(stdout);
  close(STDOUT_FILENO);

  // Wait for child process to complete flushing output
  if (filter_pid > 0) {
    int status;
    waitpid(filter_pid, &status, 0);
  }

  fclose(devnull);
  exit(EXIT_SUCCESS);
  return 0;
}

#endif // !__MAIN_C__
