#include "../adlib/lib.h"
#include "parser.h"

void Error(const char *message) {
  printf("ppcc: %s\n", message);
  exit(1);
}

#define LINK 1
#define COMP 2
#define CPP  4
#define DEP  8
#define ALL  (LINK | COMP | CPP)

typedef enum {
  NoArg,
  IntegralArg,
  FollowArg,
  IntegralOrFollowArg
} OptArgType;

typedef void (*OptFunc)(Str *, const char *);

struct OptSpec {
  const char *name;
  int mode;
  OptArgType argtype;
  OptFunc handler;
};

int debug = 0;
int mode = LINK;
bool depend = false;
StrArr *CPPArgs, *CompArgs, *LinkArgs, *DepArgs, *ExtraArgs;
Str *Language = NULL;
Str *Compiler = NULL;
Str *Output = NULL;
StrArr *CInput = NULL;
StrArr *CCInput = NULL;
StrArr *ObjInput = NULL;
StrArr *AsmInput = NULL;

void AddArg(int mode, Str *arg) {
  if (mode & CPP) CPPArgs->add(arg);
  if (mode & COMP) CompArgs->add(arg);
  if (mode & LINK) LinkArgs->add(arg);
}

void ModeCPP(Str *opt, const char *arg) {
  mode = CPP;
}

void ModeComp(Str *opt, const char *arg) {
  mode = COMP;
}

void ModeDepend(Str *opt, const char *arg) {
  depend = true;
}

void SelectLang(Str *opt, const char *arg) {
  Language = new Str(arg);
  AddArg(ALL, opt);
  AddArg(ALL, Language);
}

void SaveOutput(Str *opt, const char *arg) {
  Output = new Str(arg);
  AddArg(COMP|LINK, opt);
  AddArg(COMP|LINK, Output);
}


OptSpec optspecs[] = {
  { "-E", 0, NoArg, ModeCPP },
  { "-c", 0, NoArg, ModeComp },
  { "-S", 0, NoArg, ModeComp },
  { "-x", ALL, FollowArg, SelectLang },
  { "-f", ALL, IntegralArg },
  { "-O", COMP, IntegralArg },
  { "-g", COMP|LINK, IntegralArg },
  { "-W", ALL, IntegralArg },
  { "-X", ALL, IntegralArg },
  { "-w", ALL, IntegralArg },
  { "-D", CPP, IntegralArg },
  { "-U", CPP, IntegralArg },
  { "-MM", CPP, NoArg },
  { "-MD", CPP, NoArg },
  { "-MMD", CPP, NoArg },
  { "-MG", CPP, NoArg },
  { "-MP", CPP, NoArg },
  { "-MF", CPP, FollowArg },
  { "-MT", CPP, FollowArg },
  { "-MQ", CPP, FollowArg },
  { "-M", CPP, NoArg },
  { "-o", COMP|LINK, FollowArg, SaveOutput },
  { "-I", CPP, IntegralOrFollowArg },
  { "-I-", CPP, IntegralOrFollowArg },
  { "-I", CPP, IntegralOrFollowArg },
  { "-include", CPP, FollowArg },
  { "-idirafter", CPP, FollowArg },
  { "-imacros", CPP, FollowArg },
  { "-iquote", CPP, FollowArg },
  { "-isystem", CPP, FollowArg },
  { NULL }
};

int ParseOpt(Str *opt, char *arg) {
  for (OptSpec *spec = optspecs; spec->name; spec++) {
    Word len = strlen(spec->name);
    if (len <= opt->len()) {
      if (strncmp(spec->name, opt->c_str(), len) == 0) {
        OptFunc handler = spec->handler;
        switch (spec->argtype) {
          case NoArg:
          case IntegralArg:
            if (handler) {
              handler(opt, NULL);
            } else {
              AddArg(spec->mode, opt);
            }
            return 0;
          case FollowArg:
            if (!arg)
              Error("missing argument");
            if (handler) {
              handler(opt, arg);
            } else {
              AddArg(spec->mode, opt);
              AddArg(spec->mode, new Str(arg));
            }
            return 1;
          case IntegralOrFollowArg:
            if (len == opt->len()) {
              if (!arg)
                Error("missing argument");
              if (handler) {
                handler(opt, arg);
              } else {
                AddArg(spec->mode, opt);
                AddArg(spec->mode, new Str(arg));
              }
              return 1;
            } else {
              if (handler) {
                handler(opt, arg);
              } else {
                AddArg(spec->mode, opt);
              }
              return 0;
            }
        }
      }
    }
  }
  AddArg(ALL, opt);
  return 0;
}

void TestOutputExecutable() {
  if (mode == LINK) {
    if (Output && FileExtension(Output)->len() != 0) {
      return;
    }
    Str *path = DirName(ProgramPath());
    if (path) {
      path->add("/../Singular/threadsupport.cc");
      path = NormalizePath(path);
    }
    if (path && FileStat(path)) {
      ExtraArgs->add(path);
    } else {
      PrintLn(path);
      Error("cannot locate ppcc directory");
    }
  }
}

void ParseArgs() {
  if (ArgC <= 1) {
    Error("no arguments");
  }
  Compiler = new Str(ArgV[1]);
  for (int i = 2; i < ArgC; i++) {
    char *p = ArgV[i];
    if (p[0] == '-') {
      i += ParseOpt(new Str(p), ArgV[i+1]);
    } else {
      Str *arg = new Str(p);
      if (arg->ends_with(".c")) {
        CInput->add(arg);
      } else if (arg->ends_with(".cc") || arg->ends_with(".cpp")) {
        CCInput->add(arg);
      } else if (arg->ends_with(".o")) {
        ObjInput->add(arg);
      } else if (arg->ends_with(".s")) {
        AsmInput->add(arg);
      } else {
        AddArg(ALL, arg);
      }
    }
  }
}

Str *RunCPP() {
  Str *input;
  if (CInput->len())
    input = CInput->at(0);
  else
    input = CCInput->at(0);
  StrArr *args = A("-E", "-DPSINGULAR");
  args->add(CPPArgs)->add(input);
  if (debug) {
    printf("Preprocessor: %s %s\n",
      Compiler->c_str(),
      StrJoin(args, " ")->c_str());
  }
  Str *result = ReadProcess(Compiler, args);
  if (!result)
    Error("preprocessor failed");
  if (debug >= 3)
    Print(result);
  result = RunPreProcessor(input, result);
  return result;
}

void PassThroughCompiler() {
  if (debug)
    PrintLn(S("Pass through: ")->
      add(StrJoin(Args->clone()->add(ExtraArgs), S(" "))));
  if (ArgC <= 1) {
    Error("no arguments");
  }
  Compiler = new Str(ArgV[1]);
  StrArr *args = new StrArr(ArgC);
  for (int i = 2; i < ArgC; i++) {
    args->add(S(ArgV[i]));
  }
  args->add(ExtraArgs);
  int exitcode = System(Compiler, args);
  exit(exitcode);
}

void ExecCompiler() {
  int exitcode;
  Int inputs;
  StrArr *args = new StrArr(1);
  switch (inputs = CInput->len() + CCInput->len()) {
    case 0:
      if (mode == COMP)
        Error("no input files");;
      break;
    case 1:
      break;
    default:
      Error("too many C/C++ input files");
      break;
  }
  switch (mode) {
    case CPP:
      PassThroughCompiler();
      break;
    case COMP:
      args->add(S("-c"))->add(CompArgs);
      break;
    case LINK:
      args->add(LinkArgs);
      break;
  }
  if (inputs) {
    Str *cppOutput = RunCPP();
    const char *lang = CInput->len() == 1 ? "c" : "c++";
    args->add(S("-x"))->add(S(lang))->add(S("-"));
    if (debug) {
      printf("%s: %s %s\n",
        mode == COMP ? "Compiler" : "Linker",
        Compiler->c_str(),
        StrJoin(args, " ")->c_str());
      if (debug >= 2)
        Print(cppOutput);
      fflush(stdout);
    }
    int success = WriteProcess(Compiler, args, cppOutput);
    if (!success)
      exit(1);
  } else {
    TestOutputExecutable();
    PassThroughCompiler();
  }
}

INIT(Args, {
  GCVar(Language);
  GCVar(Output);
  GCVar(Compiler);

  GCVar(CPPArgs, new StrArr());
  GCVar(CompArgs, new StrArr());
  GCVar(LinkArgs, new StrArr());
  GCVar(DepArgs, new StrArr());
  GCVar(ExtraArgs, new StrArr());

  GCVar(CInput, new StrArr());
  GCVar(CCInput, new StrArr());
  GCVar(ObjInput, new StrArr());
  GCVar(AsmInput, new StrArr());
});

void Main() {
  char *envDebug = getenv("DEBUG_PPCC");
  if (envDebug) {
    switch (envDebug[0]) {
      case '0': debug = 0; break;
      case '1': debug = 1; break;
      case '2': debug = 2; break;
      default:
        Error("invalid DEBUG_PPCC value, must be 0, 1, or 2");
        break;
    }
  }
  if (Args->len() >= 1 && Args->at(0)->eq(S("--test"))) {
    for (Int i = 1; i < Args->len(); i++) {
      Str *filename = Args->at(i);
      PrintLn(S("=== ")->add(filename));
      Print(TestPreProcessor(filename));
    }
    return;
  }
  if (getenv("RUN_PPCC")) {
    if (debug) {
      PrintLn(S("Args: ")->add(StrJoin(Args, " ")));
    }
    ParseArgs();
    ExecCompiler();
  } else {
    PassThroughCompiler();
  }
}
