/*
 * ddepcheck.d
 * Version 0.9.6
 * Last modified 18th of April 2004
 *
 * Dependency walker for D source files.
 * Copyright 2003-2004 Lars Ivar Igesund <larsivar'at'igesund.net>
 *
 * Permission to use, copy, modify, distribute and sell this software
 * and its documentation for any purpose is hereby granted without fee,
 * provided that the above copyright notice appear in all copies and
 * that both that copyright notice and this permission notice appear
 * in supporting documentation. It is provided "as is" without express 
 * or implied warranty.
 */

import std.file;
import std.path;
import std.stream;
import std.string;
import std.c.stdio;

version (Win32) {
import std.c.windows.windows;
}

bool helpcalled = false;
bool versioncalled = false;
bool write = false;
bool makesyntax = false;
bool comment = false;
bool runtime = false;
bool checkprivate = false;
int curopt = 1;
int numpaths = 0;
int allpaths = 10;
int depthlimit = -1;
int depth = 0;
int numdeps = 0;
int alldeps = 10;
int firstfile = 1;
char [][] depmods;
char [][] depfiles;
char [][] depdepths;
char [][] paths;
char [][] argslist;

/*
 * Function called when the "help" option is specified.
 */

void optHelp()
{
  optVersion();
  if (!helpcalled) {
    printf(toStringz("\n" ~
           "Syntax: ddepcheck [options] [src ...]\n" ~
           "  -I[path]               Add a path to be searched.\n" ~
           "  -h/--help              Prints this help table.\n" ~
           "  -d=[num]/--depth=[num] Limit the depth searched \n" ~
           "                           for dependencies.\n" ~
           "  -m/--make-syntax       Print the dependencies using the make\n" ~
           "                           syntax. \"objectfile : src deps\"\n" ~
           "  -V/--version           Prints the version.\n" ~
           "  -w/--writetofile       Prints the dependencies to the file\n" ~
           "                           'depfile' instead of to the console.\n" ~ 
           "  -l/--checkruntimelib   Tries to add runtimelib dependencies.\n" ~
           "                           Note that the path must be added \n" ~
           "                           explicitly."));
  }
  helpcalled = true;
}

/*
 * Function called when the "checkruntimelib" option is specified.
 */

void optCheckRuntime()
{
  runtime = true;
}

/*
 * Function called when the "checkprivate" option is specified.
 */

void optCheckPrivate()
{
  checkprivate = true;
}

/*
 * Function called if the "version" option is used.
 */

void optVersion()
{
  if (!versioncalled) {
    printf(toStringz("ddepcheck\n" ~ 
           "Dependency walker for D source files.\n" ~
           "Version 0.9.4 Copyright Lars Ivar Igesund 2003\n"));
  }
  versioncalled = true;
}

/*
 * Function called if the "writetofile" option is used.
 */

void optWrite()
{
  write = true;
}

/*
 * Function called if the "depth" option is used to decide the maximum
 * depth for recursion.
 */

void optDepth(int arg, bool doubledash)
{
  if (doubledash) {
    depthlimit = (int)atoi(argslist[arg][8..argslist[arg].length]);
  }
  else {
    depthlimit = (int)atoi(argslist[arg][3..argslist[arg].length]);
  }
}

/*
 * Function called for all the "-I" import options used.
 */

void optImport(int arg)
{
  if (argslist[arg].length == 2) {
    return;
  }
  addPath(argslist[arg][2..argslist[arg].length]);
}

/*
 * Function called when the "-m"/"--make-syntax" option is used.
 */

void optMakeSyntax()
{
  makesyntax = true;
}

/*
 * Function that checks if an argument is an option.
 */

bool checkOption(int arg)
{
  switch (argslist[arg]) {
  case "--version":
  case "-V":
    optVersion();
    return true;
    break;
  case "--help":
  case "-h":
    optHelp();
    return true;
    break;
  case "--writetofile":
  case "-w":
    optWrite();
    return true;
    break;
  case "--make-syntax":
  case "-m":
    optMakeSyntax();
    return true;
    break;
  case "--checkruntimelib":
  case "-l":
    optCheckRuntime();
    return true;
    break;
  case "--checkprivate":
  case "-p":
    optCheckPrivate();
    return true;
    break;
  default:
    break;
  }
  if (cmp(argslist[arg][0..3], "-d=") == 0) {
    optDepth(arg, false);
    return true;
  }
  else if (argslist[arg].length > 8 && 
           cmp(argslist[arg][0..8], "--depth=") == 0) {
    optDepth(arg, true);
    return true;
  }
  if (cmp(argslist[arg][0..2], "-I") == 0) {
    optImport(arg);
    return true;
  }
  return false;
}

/*
 * Function that starts the dependency walking for base source files.
 */

void depBase(int arg)
{
  addNewBaseFile(argslist[arg]);
  depWalk(argslist[arg], "", true);
}

/*
 * Function that do the main dependency walking recursively. If it find
 * files that has been walked before, it skips it to avoid infinite
 * cyclic dependencies.
 */

void depWalk(char [] file, char [] mod, bool base)
{
  char [] filepath = file;
  int i = 0;
  while (i < numpaths && !fileExist(filepath)) {
    filepath = std.path.join(paths[i], file);
    i++;
  }
  if (!base) {
    if (!addDep(filepath, mod, depth)) {
      return;
    }
  }
  File src = new File(filepath);
  while (!src.eof()) {
    char [] line = src.readLine();
    if (stripComment(line)) {
      continue;
    }
    char [][] statements = split(line, ";");
    for (int i = 0; i < statements.length; i++ ) {
      int pos = find(statements[i], "import");
      if (pos == -1) {
        continue;
      }
      else {
        if (!base && !checkprivate) {
          int privpos = rfind(statements[i], "private");
          if (privpos > -1 && privpos < pos) {
            char [] impstmt = statements[i][privpos..pos+7]; 
            char [][] words = split(impstmt);
            if (words.length == 2) {
              continue;
            }
          }
        }
        char [] mod = strip(statements[i][pos+7..statements[i].length]);
        if (statements[i][pos+6..pos+7] == r" ") {
          if (!checkIfModule(mod)) {
            continue;
          }
        }
        else {
          continue;
        }
        char [] fp;
        if (!runtime) {
          if (checkPhobos(mod)) {
            continue;
          }
        }
        fp = createFilePath(mod);
        depth++;
        if (depthlimit == -1 || depth <= depthlimit) {  
          depWalk(fp, mod, false);
        }
        depth--;
      }
    }
  }
}

/*
 * Function that adds a dependency to the list when it is verified.
 * A path and the depth where it was found is also added. If the
 * dependency has been found before, the new depth is added to the
 * same entry.
 */

bool addDep(char [] filepath, char [] mod, int depth)
{
  for (int i = 0; i < numdeps; i++) {
    if (cmp(depmods[i], mod) == 0) {
      depdepths[i] ~= ",";
      depdepths[i] ~= toString(depth);
      return false;
    }
  }

  numdeps++;
  if (numdeps > alldeps) {
    alldeps *= 2;
    depmods.length = alldeps;
    depfiles.length = alldeps;
    depdepths.length = alldeps;
  }
  depmods[numdeps-1] = mod;
  depfiles[numdeps-1] = filepath;
  depdepths[numdeps-1] = toString(depth);

  return true;
}

/*
 * Function that adds the filename of the current checked file to
 * the printout.
 */

void addNewBaseFile(char [] file)
{
  numdeps += 3;
  if (numdeps > alldeps) {
    alldeps *= 2;
    depmods.length = alldeps;
    depfiles.length = alldeps;
    depdepths.length = alldeps;
  }
  depmods[numdeps-3] = "#";
  depmods[numdeps-2] = file;
  depmods[numdeps-1] = "##";
  depfiles[numdeps-3] = depfiles[numdeps-2] = depfiles[numdeps-1] = "";
  depdepths[numdeps-3] = depdepths[numdeps-2] = depdepths[numdeps-1] = "";
}

/*
 * Function that takes a module name and creates a real path.
 */
 
char [] createFilePath(char [] mod)
{
  char [] tempmode;
  tempmode = replace(mod, ".", sep);
  tempmode ~= ".d";
  return tempmode;
}

/*
 * Checks if the found dependency is a part of phobos in which case
 * it is ignored.
 */

bool checkPhobos(char [] mod)
{
  if (cmp(mod, "Object") == 0) return true;
  else if (cmp(mod[0..4], "std.") == 0) return true;
  else return false;
}

/*
 * Checks if the found module really can be a module since the parsing
 * is a bit hackish.
 */

bool checkIfModule(char [] mod)
{
  char [] illchars = ")([]/,+?";
  if (find(letters, mod[0]) == -1) {
    return false;
  }
  for (int i = 1; i < mod.length; i++) {
    if (find(illchars, mod[i]) > -1) {
      return false;
    }
  }
  return true;
}

/*
 * Adds a path given with "-I" to the path list.
 */

void addPath(char [] path)
{
  numpaths++;
  if (numpaths > allpaths) {
    allpaths *= 2;
    paths.length = allpaths;
  }
  paths[numpaths - 1] = path;
}

/*
 * Checks if a file exist.
 */

bool fileExist(char [] file)
{
version (Win32) {
  return (GetFileAttributesA(toStringz(file)) != 0xFFFFFFFF);
}
version (Linux) {
  // FIXME: I guess there is a better way to do this. larsivi 18042004
  bool result = true;
  try {
    read(file);
  }
  catch (FileException e) {
    result = false;
  }
  return result;
}
}

/*
 * Strip of the comments.
 * Returns true if the whole line is a comment.
 */

bool stripComment(inout char[] line)
{
  int res = -1;
  bool startascomment = comment;

  void toWhiteSpace(inout char[] line, int start, int stop)
  {   
    for (int i = start; i < stop; i++) {
      line[i] = ' ';
    }
  }

  void endStarComment(inout char[] line)
  {
    if (comment) {
      res = find(line, "*/");
      if (res > -1) {
        comment = false;
        toWhiteSpace(line, 0, res);
      }
    }
  }

  endStarComment(line);

  res = find(line, "//");
  if (res > - 1) {
    toWhiteSpace(line, res, line.length);
    if (res < 1) {
      return false;
    }
  }

  res = find(line, "/*");
  if (res > -1) {
    comment = true;
    toWhiteSpace(line, res, line.length);
  }

  endStarComment(line);

  return startascomment && comment;
}

/*
 * Writes dependencies to file.
 */

void printToFile()
{
  File depfile = new File("depfile", FileMode.Out);
  for (int i = 0; i < numdeps; i++) {
    depfile.printf(depmods[i] ~ " " ~ depfiles[i] ~ " " ~ depdepths[i] ~ "\n");
  }
  depfile.close();
}

/*
 * Prints dependencies to stdout.
 */

void printToStdout()
{
  if (makesyntax) {
    char [] objsuf = "";
    version (Win32) {
      objsuf = ".obj";
    }
    version (linux) {
      objsuf = ".o";
    }
    printf(toStringz(replace(argslist[firstfile], ".d", objsuf) ~ " : "));
    printf(toStringz(argslist[firstfile]));
    if (numdeps > 3) {
      printf(toStringz(" "));
      for (int i = 3; i < numdeps; i++) {
        printf(toStringz(depfiles[i] ~ " "));
      }
    }
  }
  else {
    for (int i = 0; i < numdeps; i++) {
      printf(toStringz(depmods[i] ~ " " ~ depfiles[i] ~ " " ~ 
             depdepths[i] ~ "\n"));
    }
  }
}

int main(char[][] args)
{
  if (args.length == 1) {
    optHelp();
    return 0;
  }
  
  depmods.length = 10;
  depfiles.length = 10;
  depdepths.length = 10;
  paths.length = 10;

  argslist = args;
  while (curopt < args.length && checkOption(curopt)) {
    curopt++;
  }

  if (curopt < args.length) {
    firstfile = curopt;
    for (int i = curopt; i < args.length; i++) {
      depBase(i);
    }
  }

  if (write) printToFile();
  else printToStdout(); 

  return 0;
}
