//===-- ldmd.cpp - Drop-in DMD replacement wrapper for LDC ----------------===//
//
//                         LDC – the LLVM D compiler
//
// This file is distributed under the BSD-style LDC license, except for the
// command line handling code, which originated from DMD. See the LICENSE
// file for details.
//
//===----------------------------------------------------------------------===//
//
// Wrapper allowing use of LDC as drop-in replacement for DMD.
//
// The reason why full command line parsing is required instead of just
// rewriting the names of a few switches is an annoying impedance mismatch
// between the way how DMD handles arguments and the LLVM command line library:
// DMD allows all switches to be specified multiple times – in case of
// conflicts, the last one takes precedence. There is no easy way to replicate
// this behavior with LLVM, save parsing the switches and re-emitting a cleaned
// up string.
//
// DMD also reads switches from the DFLAGS enviroment variable, if present. This
// is contrary to what C compilers do, where CFLAGS is usually handled by the
// build system. Thus, conflicts like mentioned above occur quite frequently in
// practice in makefiles written for DMD, as DFLAGS is also a natural name for
// handling flags there.
//
// In order to maintain backwards compatibility with earlier versions of LDMD,
// unknown switches are passed through verbatim to LDC. Finding a better
// solution for this is tricky, as some of the LLVM arguments can be
// intentionally specified multiple times to get a certain effect (e.g. pass,
// linker options).
//
// Just as with the old LDMD script, arguments can be passed through unmodified
// to LDC by using -Csomearg.
//
// If maintaining this wrapper is deemed too messy at some point, an alternative
// would be to either extend the LLVM command line library to support the DMD
// semantics (unlikely to happen), or to abandon it altogether (except for
// passing the LLVM-defined flags to the various passes).
//
// Note: This program inherited ugly C-style string handling and memory leaks
// from DMD, but this should not be a problem due to the short-livedness of
// the process.
//
//===----------------------------------------------------------------------===//

#ifndef LDC_EXE_NAME
#error "Please define LDC_EXE_NAME to the name of the LDC executable to use."
#endif

#include "driver/exe_path.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Program.h"
#include "llvm/Support/SystemUtils.h"
#include "llvm/Support/raw_ostream.h"
#if _WIN32
#include "Windows.h"
#else
#include <sys/stat.h>
#endif
#include <cassert>
#include <cerrno>
#include <climits>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <numeric>
#include <vector>

#ifdef HAVE_SC_ARG_MAX
#include <unistd.h>
#endif

namespace ls = llvm::sys;

// We reuse DMD's response file parsing routine for maximum compatibilty - it
// handles quotes in a very peculiar way.
int response_expand(size_t *pargc, char ***pargv);
void browse(const char *url);

/**
 * Prints a formatted error message to stderr and exits the program.
 */
void error(const char *fmt, ...) {
  va_list argp;
  va_start(argp, fmt);
  fprintf(stderr, "Error: ");
  vfprintf(stderr, fmt, argp);
  fprintf(stderr, "\n");
  exit(EXIT_FAILURE);
  va_end(argp);
}

/**
 * Prints a formatted warning message to stderr.
 */
void warning(const char *fmt, ...) {
  va_list argp;
  va_start(argp, fmt);
  fprintf(stderr, "Warning: ");
  vfprintf(stderr, fmt, argp);
  fprintf(stderr, "\n");
  va_end(argp);
}

char *concat(const char *a, const char *b) {
  size_t na = strlen(a);
  size_t nb = strlen(b);
  char *result = static_cast<char *>(malloc(na + nb + 1));
  assert(result);
  memcpy(result, a, na);
  memcpy(result + na, b, nb + 1);
  return result;
}

char *concat(const char *a, int b) {
  char bStr[14];
#if defined(_MSC_VER)
  _snprintf_s(bStr, _countof(bStr), sizeof(bStr), "%d", b);
#else
  snprintf(bStr, sizeof(bStr), "%d", b);
#endif
  return concat(a, bStr);
}

/**
 * Runs the given executable, returning its error code.
 */
int execute(const std::string &exePath, const char **args) {
  std::string errorMsg;
  int rc = ls::ExecuteAndWait(exePath, args, nullptr, nullptr, 0, 0, &errorMsg);
  if (!errorMsg.empty()) {
    error("Error executing %s: %s", exePath.c_str(), errorMsg.c_str());
  }
  return rc;
}

/**
 * Prints usage information to stdout.
 */
void printUsage(const char *argv0, const std::string &ldcPath) {
  // Print version information by actually invoking ldc -version.
  const char *args[] = {ldcPath.c_str(), "-version", nullptr};
  execute(ldcPath, args);

  printf(
      "\n\
Usage:\n\
  %s [<option>...] <file>...\n\
  %s [<option>...] -run <file> [<arg>...]\n\
\n\
Where:\n\
  <file>           D source file\n\
  <arg>            Argument to pass when running the resulting program\n\
\n\
<option>:\n\
  @<cmdfile>       read arguments from cmdfile\n\
  -allinst         generate code for all template instantiations\n\
  -betterC         omit generating some runtime information and helper functions\n\
  -boundscheck=[on|safeonly|off]   bounds checks on, in @safe only, or off\n\
  -c               do not link\n\
  -color           turn colored console output on\n\
  -color=[on|off]  force colored console output on or off\n\
  -conf=<filename> use config file at filename\n\
  -cov             do code coverage analysis\n\
  -cov=<nnn>       require at least nnn%% code coverage\n\
  -D               generate documentation\n\
  -Dd<directory>   write documentation file to directory\n\
  -Df<filename>    write documentation file to filename\n\
  -d               silently allow deprecated features\n\
  -dw              show use of deprecated features as warnings (default)\n\
  -de              show use of deprecated features as errors (halt compilation)\n\
  -debug           compile in debug code\n\
  -debug=<level>   compile in debug code <= level\n\
  -debug=<ident>   compile in debug code identified by ident\n\
  -debuglib=<name> set symbolic debug library to name\n\
  -defaultlib=<name>\n\
                   set default library to name\n\
  -deps            print module dependencies (imports/file/version/debug/lib)\n\
  -deps=<filename> write module dependencies to filename (only imports)\n\
  -fPIC            generate position independent code\n\
  -dip25           implement http://wiki.dlang.org/DIP25 (experimental)\n\
  -dip1000         implement http://wiki.dlang.org/DIP1000 (experimental)\n\
  -g               add symbolic debug info\n\
  -gc              add symbolic debug info, optimize for non D debuggers\n\
  -gs              always emit stack frame\n"
#if 0
"  -gx              add stack stomp code\n"
#endif
"  -H               generate 'header' file\n\
  -Hd=<directory>  write 'header' file to directory\n\
  -Hf=<filename>   write 'header' file to filename\n\
  --help           print help and exit\n\
  -I=<directory>   look for imports also in directory\n\
  -ignore          ignore unsupported pragmas\n\
  -inline          do function inlining\n\
  -J=<directory>   look for string imports also in directory\n\
  -L=<linkerflag>  pass linkerflag to link\n\
  -lib             generate library rather than object files\n\
  -m32             generate 32 bit code\n"
#if 0
"  -m32mscoff       generate 32 bit code and write MS-COFF object files\n"
#endif
"  -m64             generate 64 bit code\n\
  -main            add default main() (e.g. for unittesting)\n\
  -man             open web browser on manual page\n"
#if 0
"  -map             generate linker .map file\n"
#endif
"  -mcpu=<id>       generate instructions for architecture identified by 'id'\n\
  -mcpu=?          list all architecture options\n\
  -mscrtlib=<name> MS C runtime library to reference from main/WinMain/DllMain\n\
  -mv=<package.module>=<filespec>  use <filespec> as source file for <package.module>\n\
  -noboundscheck   no array bounds checking (deprecated, use -boundscheck=off)\n\
  -O               optimize\n\
  -o-              do not write object file\n\
  -od=<directory>  write object & library files to directory\n\
  -of=<filename>   name output file to filename\n\
  -op              preserve source path for output files\n"
#if 0
"  -profile         profile runtime performance of generated code\n\
  -profile=gc      profile runtime allocations\n"
#endif
"  -release         compile release version\n\
  -shared          generate shared library (DLL)\n\
  -transition=<id> help with language change identified by 'id'\n\
  -transition=?    list all language changes\n\
  -unittest        compile in unit tests\n\
  -v               verbose\n\
  -vcolumns        print character (column) numbers in diagnostics\n\
  -vdmd            print the command used to invoke the underlying compiler\n\
  -verrors=<num>   limit the number of error messages (0 means unlimited)\n\
  -verrors=spec    show errors from speculative compiles such as __traits(compiles,...)\n\
  -vgc             list all gc allocations including hidden ones\n\
  -vtls            list all variables going into thread local storage\n\
  --version        print compiler version and exit\n\
  -version=<level> compile in version code >= level\n\
  -version=<ident> compile in version code identified by ident\n\
  -w               warnings as errors (compilation will halt)\n\
  -wi              warnings as messages (compilation will continue)\n\
  -X               generate JSON file\n\
  -Xf=<filename>   write JSON file to filename\n\n",
      argv0, argv0);
}

/**
 * Parses an enviroment variable for flags and appends them to given list of
 * arguments.
 *
 * This corresponds to getenv_setargv() in DMD, but we need to duplicate it
 * here since it is defined in mars.c.
 */
void appendEnvVar(const char *envVarName, std::vector<char *> &args) {
  char *env = getenv(envVarName);
  if (!env) {
    return;
  }

  env = strdup(env); // create our own writable copy

  size_t j = 1; // leave argv[0] alone
  while (1) {
    switch (*env) {
    case ' ':
    case '\t':
      env++;
      break;

    case 0:
      return;

    default:
      args.push_back(env); // append
      j++;
      char *p = env;
      int slash = 0;
      int instring = 0;
      char c = 0;

      while (1) {
        c = *env++;
        switch (c) {
        case '"':
          p -= (slash >> 1);
          if (slash & 1) {
            p--;
            goto Laddc;
          }
          instring ^= 1;
          slash = 0;
          continue;

        case ' ':
        case '\t':
          if (instring) {
            goto Laddc;
          }
          *p = 0;
          break;

        case '\\':
          slash++;
          *p++ = c;
          continue;

        case 0:
          *p = 0;
          return;

        default:
        Laddc:
          slash = 0;
          *p++ = c;
          continue;
        }
        break;
      }
    }
  }
}

/**
 * Translates the LDMD command-line args (incl. DFLAGS environment variable)
 * to LDC args.
 * `ldcArgs` needs to be initialized with the path to the LDC executable.
 */
void translateArgs(size_t originalArgc, char **originalArgv,
                   std::vector<const char *> &ldcArgs) {
  // Expand any response files present into the list of arguments.
  size_t argc = originalArgc;
  char **argv = originalArgv;
  if (response_expand(&argc, &argv)) {
    error("Could not read response file.");
  }

  std::vector<char *> args(argv, argv + argc);

  std::vector<char *> dflags;
  appendEnvVar("DFLAGS", dflags);
  if (!dflags.empty()) {
    // append, but before a first potential '-run'
    size_t runIndex = 0;
    for (size_t i = 1; i < args.size(); ++i) {
      if (strcmp(args[i], "-run") == 0) {
        runIndex = i;
        break;
      }
    }
    args.insert(runIndex == 0 ? args.end() : args.begin() + runIndex,
                dflags.begin(), dflags.end());
  }

  assert(ldcArgs.size() == 1);
  const std::string ldcPath = ldcArgs[0];

  ldcArgs.push_back("-ldmd");

  bool vdmd = false;
  bool noFiles = true;

  for (size_t i = 1; i < args.size(); i++) {
    char *p = args[i];
    if (*p == '-') {
      if (strcmp(p + 1, "vdmd") == 0) {
        vdmd = true;
      }
      /* Most args are handled directly by LDC.
       * Order corresponds to parsing order in dmd's mars.d.
       *
       * -allinst
       * -de
       * -d
       * -dw
       * -c
       */
      else if (strncmp(p + 1, "color", 5) == 0) {
        bool color = true;
        // Parse:
        //      -color
        //      -color=on|off
        if (p[6] == '=') {
          if (strcmp(p + 7, "off") == 0) {
            color = false;
          } else if (strcmp(p + 7, "on") != 0) {
            goto Lerror;
          }
        } else if (p[6]) {
          goto Lerror;
        }
        ldcArgs.push_back(color ? "-enable-color" : "-disable-color");
      }
      /* -conf
       * -cov
       * -shared
       */
      else if (strcmp(p + 1, "dylib") == 0) {
        ldcArgs.push_back("-shared");
      } else if (strcmp(p + 1, "fPIC") == 0) {
        ldcArgs.push_back("-relocation-model=pic");
      } else if (strcmp(p + 1, "map") == 0) {
        goto Lnot_in_ldc;
      } else if (strcmp(p + 1, "multiobj") == 0) {
        goto Lnot_in_ldc;
      }
      /* -g
       * -gc
       */
      else if (strcmp(p + 1, "gs") == 0) {
        ldcArgs.push_back("-disable-fp-elim");
      } else if (strcmp(p + 1, "gx") == 0) {
        goto Lnot_in_ldc;
      } else if (strcmp(p + 1, "gt") == 0) {
        error("use -profile instead of -gt\n");
      }
      /* -m32
       * -m64
       */
      else if (strcmp(p + 1, "m32mscoff") == 0) {
        ldcArgs.push_back("-m32");
      }
      /* -mscrtlib
       */
      else if (strcmp(p + 1, "profile") == 0) {
        goto Lnot_in_ldc;
      }
      /* -v
       */
      else if (strcmp(p + 1, "vtls") == 0) {
        ldcArgs.push_back("-transition=tls");
      }
      /* -vcolumns
       * -vgc
       */
      else if (memcmp(p + 1, "verrors", 7) == 0) {
        if (p[8] == '=' && isdigit(static_cast<unsigned char>(p[9]))) {
          ldcArgs.push_back(p);
        } else if (memcmp(p + 9, "spec", 4) == 0) {
          ldcArgs.push_back("-verrors-spec");
        } else {
          goto Lerror;
        }
      } else if (strcmp(p + 1, "mcpu=?") == 0) {
        const char *mcpuargs[] = {ldcPath.c_str(), "-mcpu=help", nullptr};
        execute(ldcPath, mcpuargs);
        exit(EXIT_SUCCESS);
      } else if (memcmp(p + 1, "mcpu=", 5) == 0) {
        if (strcmp(p + 6, "baseline") == 0) {
          // ignore
        } else if (strcmp(p + 6, "avx") == 0) {
          ldcArgs.push_back("-mattr=+avx");
        } else if (strcmp(p + 6, "native") == 0) {
          ldcArgs.push_back(p);
        } else {
          goto Lerror;
        }
      } else if (strcmp(p + 1, "transition=?") == 0) {
        const char *transitionargs[] = {ldcPath.c_str(), p, nullptr};
        execute(ldcPath, transitionargs);
        exit(EXIT_SUCCESS);
      }
      /* -transition=<id>
       * -w
       * -wi
       * -O
       * -o-
       * -od
       * -of
       * -op
       */
      else if (strcmp(p + 1, "o") == 0) {
        error("-o no longer supported, use -of or -od");
      }
      /* -D
       * -Dd
       * -Df
       * -H
       * -Hd
       * -Hf
       * -X
       * -Xf
       * -ignore
       * -property
       */
      else if (strcmp(p + 1, "inline") == 0) {
        ldcArgs.push_back("-enable-inlining");
        ldcArgs.push_back("-Hkeep-all-bodies");
      }
      /* -dip25
       * -dip1000
       */
      else if (strcmp(p + 1, "lib") == 0) {
        ldcArgs.push_back(p);
        // DMD seems to emit objects directly into the static lib being
        // generated. No object files are created and therefore they never
        // collide due to duplicate .d filenames (in different dirs).
        // Approximate that behavior by naming the object files uniquely via -oq
        // and instructing LDC to remove the object files on success.
        ldcArgs.push_back("-oq");
        ldcArgs.push_back("-cleanup-obj");
      } else if (strcmp(p + 1, "nofloat") == 0) {
        goto Lnot_in_ldc;
      } else if (strcmp(p + 1, "quiet") == 0) {
        // ignore
      }
      /* -release
       * -betterC
       */
      else if (strcmp(p + 1, "noboundscheck") == 0) {
        ldcArgs.push_back("-boundscheck=off");
      }
      /* -boundscheck
       * -unittest
       * -I
       * -J
       */
      else if (memcmp(p + 1, "debug", 5) == 0 && p[6] != 'l') {
        // Parse:
        //      -debug
        //      -debug=number
        //      -debug=identifier
        if (p[6] == '=') {
          if (isdigit(static_cast<unsigned char>(p[7]))) {
            long level;
            errno = 0;
            level = strtol(p + 7, &p, 10);
            if (*p || errno || level > INT_MAX) {
              goto Lerror;
            }
            ldcArgs.push_back(concat("-d-debug=", static_cast<int>(level)));
          } else {
            ldcArgs.push_back(concat("-d-debug=", p + 7));
          }
        } else if (p[6]) {
          goto Lerror;
        } else {
          ldcArgs.push_back("-d-debug");
        }
      } else if (memcmp(p + 1, "version", 7) == 0) {
        // Parse:
        //      -version=number
        //      -version=identifier
        if (p[8] == '=') {
          if (isdigit(static_cast<unsigned char>(p[9]))) {
            long level;
            errno = 0;
            level = strtol(p + 9, &p, 10);
            if (*p || errno || level > INT_MAX) {
              goto Lerror;
            }
            ldcArgs.push_back(concat("-d-version=", static_cast<int>(level)));
          } else {
            ldcArgs.push_back(concat("-d-version=", p + 9));
          }
        } else {
          goto Lerror;
        }
      } else if (strcmp(p + 1, "-b") == 0 || strcmp(p + 1, "-c") == 0 ||
                 strcmp(p + 1, "-f") == 0 || strcmp(p + 1, "-r") == 0 ||
                 strcmp(p + 1, "-x") == 0 || strcmp(p + 1, "-y") == 0) {
        ldcArgs.push_back(concat("-hidden-debug-", p + 2));
      } else if (strcmp(p + 1, "-help") == 0 || strcmp(p + 1, "h") == 0) {
        printUsage(originalArgv[0], ldcPath);
        exit(EXIT_SUCCESS);
      } else if (strcmp(p + 1, "-version") == 0) {
        // Print version information by actually invoking ldc -version.
        const char *versionargs[] = {ldcPath.c_str(), "-version", nullptr};
        execute(ldcPath, versionargs);
        exit(EXIT_SUCCESS);
      }
      /* -L
       * -defaultlib
       * -debuglib
       * -deps
       * -main
       */
      else if (memcmp(p + 1, "man", 3) == 0) {
        browse("http://wiki.dlang.org/LDC");
        exit(EXIT_SUCCESS);
      } else if (strcmp(p + 1, "run") == 0) {
        ldcArgs.insert(ldcArgs.end(), args.begin() + i, args.end());
        noFiles = (i == args.size() - 1);
        break;
      } else if (p[1] == 'C') {
        ldcArgs.push_back(concat("-", p + 2));
      } else {
      Lerror:
        ldcArgs.push_back(p);
        continue;

      Lnot_in_ldc:
        warning("command-line option '%s' not yet supported by LDC.", p);
        continue;
      }
    } else {
      const auto ext = ls::path::extension(p);
      if (ext.equals_lower(".exe")) {
        // should be for Windows targets only
        ldcArgs.push_back(concat("-of=", p));
        continue;
      }
#ifdef _WIN32
      else if (strcmp(p, "/?") == 0) {
        printUsage(originalArgv[0], ldcPath);
        exit(EXIT_SUCCESS);
      }
#endif
      ldcArgs.push_back(p);
      noFiles = false;
    }
  }

  if (noFiles) {
    printUsage(originalArgv[0], ldcPath);
    if (originalArgc == 1)
      exit(EXIT_FAILURE); // compatible with DMD
    else
      error("No source file specified.");
  }

  if (vdmd) {
    printf(" -- Invoking:");
    for (const auto &arg : ldcArgs) {
      printf(" %s", arg);
    }
    puts("");
  }
}

/**
 * Returns the OS-dependent length limit for the command line when invoking
 * subprocesses.
 */
size_t maxCommandLineLen() {
#if defined(HAVE_SC_ARG_MAX)
  // http://www.in-ulm.de/~mascheck/various/argmax – the factor 2 is just
  // a wild guess to account for the enviroment.
  return sysconf(_SC_ARG_MAX) / 2;
#elif defined(_WIN32)
  // http://blogs.msdn.com/b/oldnewthing/archive/2003/12/10/56028.aspx
  return 32767;
#else
#error "Do not know how to determine maximum command line length."
#endif
}

/**
 * Tries to locate an executable with the given name, or an invalid path if
 * nothing was found. Search paths: 1. Directory where this binary resides.
 * 2. System PATH.
 */
std::string locateBinary(std::string exeName) {
  std::string path = exe_path::prependBinDir(exeName.c_str());
  if (ls::fs::can_execute(path)) {
    return path;
  }

#if LDC_LLVM_VER >= 306
  llvm::ErrorOr<std::string> res = ls::findProgramByName(exeName);
  path = res ? res.get() : std::string();
#else
  path = ls::FindProgramByName(exeName);
#endif
  if (ls::fs::can_execute(path)) {
    return path;
  }

  return "";
}

// In driver/main.d
int main(int argc, char **argv);

int cppmain(int argc, char **argv) {
  exe_path::initialize(argv[0]);

  std::string ldcExeName = LDC_EXE_NAME;
#ifdef _WIN32
  ldcExeName += ".exe";
#endif
  const std::string ldcPath = locateBinary(ldcExeName);
  if (ldcPath.empty()) {
    error("Could not locate " LDC_EXE_NAME " executable.");
  }

  // We need to manually set up argv[0] and the terminating NULL.
  std::vector<const char *> args;
  args.push_back(ldcPath.c_str());

  translateArgs(argc, argv, args);

  args.push_back(nullptr);

  // Check if we can get away without a response file.
  const size_t totalLen = std::accumulate(
      args.begin(), args.end() - 1,
      args.size() * 3, // quotes + space
      [](size_t acc, const char *arg) { return acc + strlen(arg); });
  if (totalLen <= maxCommandLineLen()) {
    return execute(ldcPath, args.data());
  }

  int rspFd;
  llvm::SmallString<128> rspPath;
  if (ls::fs::createUniqueFile("ldmd-%%-%%-%%-%%.rsp", rspFd, rspPath)) {
    error("Could not open temporary response file.");
  }

  {
    llvm::raw_fd_ostream rspOut(rspFd, /*shouldClose=*/true);
    // skip argv[0] and terminating NULL
    for (auto it = args.begin() + 1, end = args.end() - 1; it != end; ++it) {
      rspOut << *it << '\n';
    }
  }

  std::string rspArg = "@";
  rspArg += rspPath.str();

  args.resize(1);
  args.push_back(rspArg.c_str());
  args.push_back(nullptr);

  int rc = execute(ldcPath, args.data());

  if (ls::fs::remove(rspPath.str())) {
    warning("Could not remove response file.");
  }

  return rc;
}
