/*
** dmake utility v0.21 for Windows and Linux
**   analyzes import statements to build a D program from its main sourcefile
**   dmake calls dmd to do the dirty work
**
** Copyright (C) 2004 Helmut Leitner
** substancial code from Burton Radons OpenSource digc utility was used
** Released under the GNU GPL, see http://www.gnu.org/copyleft/gpl.html
*/

module util.dmake;

private import std.c.stdio;
private import std.ctype;
private import std.outbuffer;
private import std.file;
private import std.path;
private import std.string;
private import std.stream;

extern (C) int putenv(char *assignment);

alias char [] String;

alias uint _dev_t;
alias ushort _ino_t;
alias int _off_t;
alias int time_t;

String [] importroots;

bit [String] versions;
int versionLevel = 0;

Source [String] SourceHash;

version(Windows) {
    private import std.c.windows.windows;
    String configfile="c:\\dmd\\bin\\sc.ini";
    String ExeExt="exe";
    String ObjExt="obj";
}

version(linux) {
    private import std.c.linux.linux;
    String configfile="/etc/dmd.conf";
    String ExeExt="";
    String ObjExt="o";
}

struct sstat
{
    _dev_t st_dev;
    _ino_t st_ino;
    ushort st_mode;
    short st_nlink;
    short st_uid;
    short st_gid;
    _dev_t st_rdev;
    _off_t st_size;
    time_t st_atime;
    time_t st_mtime;
    time_t st_ctime;
}

extern (C)
{
    void exit(int code);
    void mkdir(char *path);
    int stat(char *path, sstat *buffer);
    int rmdir(char *path);
    int system (char *args);
}

String ProgPath = "dmake";
String ProgVersion = "0.21";

String ExeName;            /* Output name. */

bit verbose = false;
bit compileonly = false;  /* don't linking the program. */
bit linkforgui = false;  /* WinMain mode, as opposed to console mode. */

String [] sourcefiles;  /* File list. growing from dependencies */
String [] linkfiles;    /* File list. obj lib from commandline */

String [] dmake_args; /* Arguments passed to dmake */
String [] dmd_args; /* Arguments passed to dmake */

char [100] env_LIB=r"LIB=c:\dm\lib;c:\dmd\lib";

void ProgramHelp()
{
    printf(
        "dmake v%.*s\n"
        "Usage: dmake mainfile [options objectfiles libraries]\n"
        ,ProgVersion
    );
    printf(
        "  mainfile   main D source file, containing main or WinMain\n"
        "  -c         just compile, don't link executable (also passed through)\n"
        "  -v         verbose (also passed through)\n"
        "  -build     do a complete build, compile and link\n"
        "  -gui       link to gui executable rather than console\n"
        "  [...]      all other options, objectfiles and libraries are passed to dmd\n"
    );
}

int ProgramExec(String cmd)
{
    return system(toStringz(cmd));
}

void StringStripChar(inout String s,char c)
{
    if(s.length>0 && s[s.length-1]==c) {
        s.length=s.length-1;
    }
}

void StdoutFlush()
{
    fflush(std.c.stdio.stdout);
}

int ProgramExecVerbose(String cmd)
{
    if(verbose) {
        printf("%.*s",cmd);
        StdoutFlush();
    }
    int ret=system(toStringz(cmd));
    if(verbose) {
        printf("return status=%d\n",ret);
        StdoutFlush();
    }
    return ret;
}

String FileRetString(String file)
{
    return cast(String) std.file.read(file);
}

String [] FileRetLines(String file)
{
    String [] lines=split(FileRetString(file),"\n");
    if(lines.length) {
        if(lines[lines.length-1].length==0) {
            lines.length=lines.length-1;
        }
    }
    return lines;
}

bit FileExist(String filename) {
    return std.file.exists(filename) ? 1 : 0;
}

version(Windows) {

/* seconds between timebase and last modification - zero if it doesn't exist. */
/* Windows and Linux timebases are different */
ulong FileRetTime (String filename)
{
    ulong ret;

    WIN32_FIND_DATA data;
    HANDLE handle=FindFirstFileA (toStringz (std.string.replace (filename, "/", "\\")), &data);
    FindClose(handle);
    if(!handle) {
      goto do_ret;
    }
    ret=data.ftLastWriteTime.dwLowDateTime | (cast(ulong) data.ftLastWriteTime.dwHighDateTime << 32);
    ret/=10000000; /* 1E7 reduce to seconds */


do_ret:
    if(verbose) {
       printf("File %.*s time=%llu\n",filename,ret);
    }
    return ret;
}

}

version(linux) {

/* seconds between timebase and last modification - zero if it doesn't exist. */
/* Windows and Linux timebases are different */
ulong FileRetTime(String filename)
{
    ulong ret;

    int fd;
    struct_stat statbuf;
    char *namez;

    namez = toStringz(filename);
    fd = std.c.linux.linux.open(namez, O_RDONLY);
    if (fd == -1) {
        goto do_ret;
    }
    if(std.c.linux.linux.fstat(fd, &statbuf)==0) {
        ret = statbuf.st_mtime;
    }
    std.c.linux.linux.close(fd);

do_ret:
    if(verbose) {
        printf("File %.*s time=%llu\n",filename,ret);
    }

    return ret;
}

}

/** Split a path into head and tail components, where the head contains
  * everything up to the final component and tail is the final component.
  * For example, pathTail("a/b/c", head, tail) puts "a/b" in head and "c" in
  * tail.  If there is no head and/or no tail, there is none given.
  *
  * @param path The path to split.
  * @param head The path before the final component.
  * @param tail The final component of the path.
  */

void PathGetHeadTail(String path, out String  head, out String  tail)
{
    for (int c = path.length - 1; c >= 0; c --) {
        if (path[c] == '/' || path[c] == '\\') {
            head = path[0..c];
            tail = path[c+1..path.length];
            return;
        }
    }

    head = null;
    tail = path;
}

/** Returns whether the file or directory exists.
  * @param path The path to the resource to test.
  * @return Returns true if this file or directory exists or null otherwise.
  */

bit exists(String  path)
{
    sstat st;
    return stat(path, &st) == 0;
}

void StringRepChar(inout String s,char c1,char c2)
{
    for(int i; i<s.length; i++) {
        if(s[i]==c1) {
            s[i]=c2;
        }
    }
}

void FileSetString(String fnam,String content)
{
    byte [] data=cast(byte [])content[0..content.length];
    std.file.write(fnam,data);
}

String [] FileRetModules_File(String file)
{
    String [] files;
    String s = FileRetString(file);
    char* i = s;
    StrRangeRetModules_File(i, i+s.length-1, files);
    return files;
}

void SourcesFileAddModules(Source source,String file)
{
    String [] files=FileRetModules_File(file);

    Source source2;
    bit lookat;

    foreach(String f2; files) {
        lookat=true;
        if(SourceHash[f2]!==null) {
            source2=SourceHash[f2];
        } else {
            lookat=true;
            if(std.string.find(f2,std.path.sep ~ "phobos" ~ std.path.sep)>=0) {
                lookat=false;
            }
            if(lookat) {
               source2=new Source(f2);
            }
        }
        if(lookat) {
            if(source.modstime<source2.modstime) {
                source.modstime=source2.modstime;
            }
        }
    }
}

bit SourceSearchFlag=false;

class Source {
    String file; /* absolute */
    ulong objtime;     /* time of object file, must be newest, otherwise compile */
    ulong modstime;   /* time of newest of all dependencies */
    ulong filetime;
    bit searched=false;

    this(String file) {
        this.file=file;
        modstime=filetime=FileRetTime(file);
        objtime=FileRetTime(addExt(file,ObjExt));
        SourceHash[file]=this;
        sourcefiles ~= file;
        if(verbose) {
          printf("SourceHash += %.*s sourcefiles=%d\n",file,sourcefiles.length);
        }
        if(SourceSearchFlag) {
            search();
        }
    }

    void search() {
       if(searched) {
           return;
       }
       SourcesFileAddModules(this,file);
       searched=true;
    }

}

void SourcesAddModulesAll(inout ulong modstime)
{
    Source source;
    String file;
    for(int i=0; i<sourcefiles.length; i++) {
        file=sourcefiles[i];
        if(verbose) {
            printf("SourcesAddModulesAll sourcefile[%d]=%.*s\n",i,file);
        }
        source=SourceHash[file];
        source.search();
        if(modstime<source.modstime) {
            modstime=source.modstime;
        }
    }
}

/** Build the program.  Abort and return an error code if there was a problem, else return zero. */
int DmakeWork(String [] args,bit buildflag)
{

    bit progmodified;
    bit compilefile;
    String filelist;
    Source source;
    int result;

    String appname=addExt(ExeName,ExeExt);
    StringStripChar(appname,'.'); /* hack for "" */

    ulong programtime = FileRetTime (appname);


    for (int c; c < args.length; c ++) {
        String arg = args [c];
        if (arg.length <= 9 || arg [0 .. 9] != "-version=") {
            continue;
        }
        versions [arg [9 .. arg.length]] = true;
    }

    SourceSearchFlag=true;

    if(verbose) {
        printf ("Program %.*s\n", ExeName);
    }

    if (sourcefiles.length == 0) {
        return 0;
    }

    versions = null;

    ulong modstime;
    SourcesAddModulesAll(modstime);

    if(modstime > programtime) {
        progmodified = true;
    }
    int i=0;
    foreach (String file; sourcefiles) {
        source=SourceHash[file];
        compilefile=false;
        if(i==0) {
           if(progmodified) {
              compilefile=true;
           }
        }
        i++;
        if(buildflag) {
            compilefile=true;
        } else if(source.filetime>source.objtime) {
            compilefile=true;
        } else if(source.modstime>source.objtime) {
            compilefile=true;
        }

        if(compilefile) {
            progmodified = true;
            filelist ~= addExt(file,"d")  ~ std.path.linesep;
        } else {
            filelist ~= addExt(file,ObjExt)   ~ std.path.linesep;
        }

    }

    dmd_args ~= "-op ";
    if (linkforgui) {
        version(Windows) {
            dmd_args ~= "-L/exet:nt/su:windows";
        }
    }

    String rspfile=addExt(ExeName,"rsp");
    String content;

    foreach (String arg; dmd_args) {
        content ~= arg ~ " ";
    }
    content ~=  std.path.linesep;
    content ~= filelist;
    foreach (String file; linkfiles) {
        content ~= file ~ " ";
    }
    if(linkforgui) {
        version(Windows) {
            content ~= "gdi32.lib" ~ std.path.linesep;
        }
    }

    version(Windows) {
        FileSetString(rspfile,content);
        String command = "dmd @" ~ rspfile;
    }
    version(linux) { // using commandline, may run into limits
        content=std.string.replace(content,std.path.linesep," ");
        String command = "dmd " ~ content;
    }

    if(buildflag==false) {
        if (!progmodified) {
            printf ("Files are not modified, skipping compilation.\n");
            return 0;
        }
    }

    result = ProgramExecVerbose(command);

    return result;
}

/* Quickly get string or single-symbol tokens. */
String token (inout char *i, char *e)
{
    while (i < e) {
        if (i [0] == '/' && i [1]=='*') {
            i += 2;
            while (i < e) {
                if (i [0] == '*' && i [1] == '/') {
                    i += 2;
                    break;
                }
                i ++;
            }
        } else if (i [0] == '/' && i [1] == '/') {
            i += 2;
            while (i < e && *i != '\n')
                i ++;
        } else if (i [0] == '/' && i [1] == '+') {
            int depth = 1;
            i += 2;
            while (i < e) {
                if (i [0] == '/' && i [1] == '+') {
                    depth ++, i += 2;
                } else if (i [0] == '+' && i [1] == '/') {
                    i += 2;
                    depth --;
                    if (depth == 0) {
                        break;
                    }
                } else {
                    i ++;
                }
            }
        } else if (isalpha (i [0])) {
            char *s = i;
            i ++;
            while (i < e && (isalnum (i [0]) || i [0] == '_')) {
                i ++;
            }
            return s [0 .. cast(int) (i - s)];
        } else if (isspace (i [0])) {
            i ++;
        } else {
            return (i ++) [0 .. 1];
        }
    }

    return null;
}

void skipContent (inout char *i, char *e)
{
    String t;

    while ((t = token (i, e)) !== null)
    {
        if (t == "}")
            return;
        if (t == "{")
            skipContent (i, e);
    }
}

void StrRangeRetModules_File (inout char *i, char *e, inout String [] files)
{
    String t, name;

    while ((t = token (i, e)) !== null) {
        if (t == "}")
            return;
        if (t == "{") {
            StrRangeRetModules_File (i, e, files);
            continue;
        }

        if (t == "\\") {
            if (i < e)
                i ++;
            continue;
        }
        if (t == "\"") {
            while (i < e && *i != '\"')
                if (*i == '\\')
                    i += 2;
                else
                    i ++;
            i ++;
            continue;
        }
        if (t == "\'") {
            while (i < e && *i != '\'')
                if (*i == '\\')
                    i += 2;
                else
                    i ++;
            i ++;
            continue;
        }
        if (t == "`") {
            do i ++;
            while (i < e && i [-1] != '`');
            continue;
        }
        if (t == "r" && i < e && *i == '\"') {
            i ++;
            do i ++;
            while (i < e && i [-1] == '\"');
            continue;
        }


        if (t == "version") {
do_restart:
            t = token (i, e);
            if (t == "=") {
                t = token (i, e);
                versions [t] = true;
                continue;
            }

            if (t != "(")
                continue;

            if (token (i, e) in versions) {
                token (i, e); // skip the closing parenthesis.

                if (token (i, e) == "{") // read a block statement
                    StrRangeRetModules_File (i, e, files);
                else while ((t = token (i, e)) != ";" && t !== null) {
                    if (t == "{") {
                        StrRangeRetModules_File (i, e, files);
                        break;
                    }
                }

                char *r = i;

                if (token (i, e) != "else") {
                    i = r;
                } else {
                    if (token (i, e) == "{") {
                        skipContent (i, e);
                    } else while ((t = token (i, e)) != ";" && t !== null) {
                        if (t == "{") {
                            skipContent (i, e);
                            break;
                        }
                    }
                }
            } else {
                skipContent (i, e);
                char *r = i;

                if (token (i, e) != "else") {
                    i = r;
                } else {
                    r = i;
                    if ((t = token (i, e)) == "{") {
                        StrRangeRetModules_File (i, e, files);
                    } else if (t == "version") {
                        goto do_restart;
                    }
                    continue;
                }
            }
        } else if (t == "private") {
            t=token(i,e);
            if(t == "import") {
               goto do_import;
            }
        } else if (t == "import") {
do_import:
            while ((t = token (i, e)) !== null) {
                name = t;
                while ((t = token (i, e)) == ".") {
                    name ~= "." ~ token (i, e);
                }
                if (t == ";" || t != ",") {
                    break;
                }
            }
            files ~= ModuleRetFile(name);
        }
    }
}

String FileRetExt(String filename)
{
    return std.path.getExt(filename);
}

bit [String] RootHash;

void DmakeAddRoot(String root)
{
    if(root.length<1) {
        return;
    }
    String uroot=std.string.toupper(root);

    if(RootHash[uroot]==false) {
        importroots ~= root;
        RootHash[uroot]=true;
    }
}

void ProgramReadConfig() {
    String [] lines=FileRetLines(configfile);
    String root;
    int pos,pos2;
    foreach(String line; lines) {
        while(line.length>0 && line[line.length-1]<=' ') {
            line.length=line.length-1;
        }
        pos=std.string.find(line,"DFLAGS=");
        if(pos>=0) {
            line=" " ~ line[pos+7..length] ~ " ";
            while((pos=std.string.find(line," -I"))>=0) {
                pos=pos+3;
                pos2=pos;
                while(line[pos2]!=' ') {
                    pos2++;
                }
                root=line[pos..pos2];
                DmakeAddRoot(root);
                if(verbose) {
                    printf("add root from config=%.*s\n",root);
                }
                line=line[pos2..length];
            }
        }
    }
}

void StringArrayPrintTitle(String [] sar,String title)
{
    int i;
    printf("%.*s\n",title);
    foreach(String s; sar) {
        printf("  [%d]: %.*s\n",i++,s);
    }
}

String FileRelRetFile(String rfile)
{
    String test;
    String file=rfile;
    foreach(String i; importroots) {
        test= ( i ~ std.path.sep ~ rfile );
        if(FileExist(test)) {
            file=test;
            return file;
        }
    }
    if(file[0]!=std.path.sep[0] && file[1]!=':') {
        test=std.file.getcwd() ~ std.path.sep ~ file;
        if(FileExist(test)) {
            file=test;
        }
    }
    return file;
}

String ModuleRetFile(String mod)
{
    String rfile=mod.dup;
    StringRepChar(rfile,'.',std.path.sep[0]);
    String file=FileRelRetFile(addExt(rfile,"d"));
    if(verbose) {
        printf("  module %.*s => file %.*s\n",mod,file);
    }
    return file;
}

void DmakeCheckVersions()
{
    version(DigitalMars) {
        versions ["DigitalMars"] = true;
    }
    version(X86) {
        versions ["X86"] = true;
    }
    version(AMD64) {
        versions ["AMD64"] = true;
    }
    version(Windows) {
        versions ["Windows"] = true;
    }
    version(Win32) {
        versions ["Win32"] = true;
    }
    version(Win64) {
        versions ["Win64"] = true;
    }
    version(linux) {
        versions ["linux"] = true;
    }
    version(LittleEndian) {
        versions ["LittleEndian"] = true;
    }
    version(BigEndian) {
        versions ["BigEndian"] = true;
    }
    version(D_InlineAsm) {
        versions ["D_InlineAsm"] = true;
    }
}

int main (String [] args)
{
    bit buildflag;
    int result;
    int errors;
    String root;

    ProgPath=args[0];

    args=args[1..args.length];
    if (args.length == 0) {
        ProgramHelp ();
        return 0;
    }

    version(Windows) {
        putenv(env_LIB);
    }

    DmakeCheckVersions();
    foreach(String arg; args) {
        if(arg.length>9) {
            if(arg[0..9]=="-version=") {
                arg=arg [9 .. arg.length];
                if (std.ctype.isdigit (arg [0])) {
                   versionLevel = atoi (arg);
                } else {
                   versions [arg] = true;
                }
            }
        }
        if(arg=="-v") {
            verbose = true;
            dmd_args ~= arg;
        }
    }
    foreach(String arg; args) {
        switch(arg) {
            case "-c":
                compileonly = true;
                dmd_args ~= arg;
                break;
            case "-gui":
                linkforgui = true;
                dmake_args ~= arg;
                break;
            case "-build":
                buildflag=true;
                dmake_args ~= arg;
                break;
            case "-v": /* we need verbose status earlier */
                break;

            default:
                if(arg[0]=='-') {
                    if(arg[1]=='I') {
                        root=arg[2..arg.length];
                        DmakeAddRoot(root);
                        if(verbose) {
                            printf("add root from command line=%.*s\n",root);
                        }
                    }
                    dmd_args ~= arg;
                } else {
                    switch(FileRetExt(arg)) {
                       case "":
                           arg ~= ".d";
                       case "d":
                           new Source(FileRelRetFile(arg));
                           break;
                       default:
                           linkfiles ~= FileRelRetFile(arg);
                    }
                    if(ExeName === null) {
                       ExeName=arg;
                    }
                }
                break;
        }
    }

    ProgramReadConfig();

    if(ExeName === null) {
        printf("error - no program name given.\n");
        return 1;
    }
    if (errors) {
        printf("%s errors - program aborted.\n");
        return 1;
    }

    if(verbose) {
        StringArrayPrintTitle(dmake_args,"dmake args:");
        StringArrayPrintTitle(dmd_args,"dmd args:");
        StringArrayPrintTitle(sourcefiles,"declared sourcefiles:");
        StringArrayPrintTitle(importroots,"import roots:");
    }

    result = DmakeWork(dmake_args,buildflag);

    StdoutFlush();

do_ret:
    return result;
}
