#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%require-our
%strict-args
%new-style

%exec-class qdx

const opts = (
    "moddx": "M,module-dx=s",
    "post": "p,post",
    "tag": "t,tag=s@",
    "tex": "x,posttex",          # use TeX syntax for "post" processing
    "dox": "d,dox",
    "nmp": "N,no-mainpage",
    "keepdollar": "k,keep-dollar",
    "help": "h,help"
    );

class qdx {
    private {
        bool post;
        bool links;
        bool svc;
        bool help;
        hash o;
        *string sname;
        *string psname;
        string build;
        string qorever;
    }

    public {
        const QoreVer = sprintf("%d.%d.%d", Qore::VersionMajor, Qore::VersionMinor, Qore::VersionSub);
    }

    constructor() {
        GetOpt g(opts);
        o = g.parse3(\ARGV);

        if (o.help || ARGV.empty())
            usage();

        if (o.post) {
            map (map postProcess($1), glob($1)), ARGV;
            return;
        }

        if (o.moddx) {
            if (ARGV.size() < 2)
                usage();
            doModDx(ARGV[0], ARGV[1]);
            return;
        }

        if (o.dox) {
            doDox(ARGV[0], ARGV[1]);
            return;
        }

        if (o.tex) {
            map (map postProcess($1, True), glob($1)), ARGV;
            return;
        }

        if (ARGV.size() < 2)
            usage();
        processQore(ARGV[0], ARGV[1]);
    }

    static usage() {
      printf("usage: %s [options] <infile> <outfile>
  -d,--dox            process doxygen files
  -M,--module-dx=ARG  prepare doxyfile module template; ARG=<src>:<trg>
  -N,--no-mainpage    change @mainpage to @page
  -p,--post           post process files
  -k,--keep-dollar    keep $ signs when post-processing
  -t,--tag=ARG        tag argument for doxyfile
  -h,--help           this help text
", get_script_name());
      exit(1);
    }

    private getQoreVersion() {
        if (!exists qorever)
            qorever = sprintf("%d.%d.%d", Qore::VersionMajor, Qore::VersionMinor, Qore::VersionSub);
        return qorever;
    }

    private checkNames(string fn, string ofn) {
        if (fn === ofn) {
            stderr.printf("OUTPUT-ERROR: input and output files are the same: %y", fn);
            exit(1);
        }
    }

    doModDx(string fn, string ofn) {
        checkNames(fn, ofn);

        (*string src, *string trg) = (o.moddx =~ x/([^:]+):(.*)/);
        if (!exists trg) {
            stderr.printf("MODULE-ERROR: --module-dx argument %y is not in format <src>:<trg>\n", o.moddx);
            exit(1);
        }

        # get module name and version (the easy way)
        string name = substr(basename(src), 0, -3);
        Program p();
        p.parse(sprintf("%requires %s\nhash sub get() { return getModuleHash().'%s'; }\n", src, name), "mod");
        hash h = p.callFunction("get");

        printf("processing %y -> %y (module %s %s src: %y trg: %y)\n", fn, ofn, name, h.version, src, trg);

        File inf();
        inf.open2(fn);

        File of();
        of.open2(ofn, O_CREAT | O_TRUNC | O_WRONLY);
        
        # get tags substitution string
        string tags = o.tag ? o.tag.join(" ") : "";

        while ((*string line = inf.readLine()).val()) {            
            if (line =~ /{module}/)
                line = replace(line, "{module}", name);
            else if (line =~ /{input}/)
                line = replace(line, "{input}", trg);
            else if (line =~ /{version}/)
                line = replace(line, "{version}", h.version);
            else if (line =~ /{tags}/)
                line = replace(line, "{tags}", tags);
            else if (line =~ /{qore_version}/)
                line = replace(line, "{qore_version}", QoreVer);

            of.write(line);
        }
    }

    doDox(string fn, string ofn) {
        printf("processing %y -> %y\n", fn, ofn);

        File inf();
        inf.open2(fn);

        File of();
        of.open2(ofn, O_CREAT | O_TRUNC | O_WRONLY);

        DocumentTableHelper dth();

        while ((*string line = inf.readLine()).val()) {            
            line = dth.process(line);

            if (line =~ /{qore_version}/)
                line = replace(line, "{qore_version}", QoreVer);

            of.write(line);
        }
    }

    postProcess(string ifn, bool tex = False) {
        File inf();
        inf.open2(ifn);

        string ofn = ifn + ".new";

        printf("processing API file %s\n", ifn);

        File of();
        of.open2(ofn, O_CREAT | O_WRONLY | O_TRUNC);

        on_success rename(ofn, ifn);

        DocumentTableHelper dth();

        while (exists (*string line = inf.readLine())) {
            line = dth.process(line);

            if (tex) {
                line =~ s/\\_\\-\\_\\-1\\_\\-/[/g;
                line =~ s/\\_\\-\\_\\-2\\_\\-/]/g;
                line =~ s/\\_\\-\\_\\-4\\_\\-/./g;
                line =~ s/\\_\\-\\_\\-5\\_\\-/-/g;
                line =~ s/\\_\\-\\_\\-6\\_\\-/\$/g;
                line =~ s/\\_\\-\\_\\-7\\_\\-/*/g; #//;
            } 

            line =~ s/__1_/[/g;
            line =~ s/__2_/]/g;
            line =~ s/__3_/*/g;
            line =~ s/__4_/./g;
            line =~ s/__5_/-/g;
            if (o.keepdollar)
                line =~ s/__6_/$/g;
            else
                line =~ s/__6_//g;
            line =~ s/__7_ /*/g; #//;

            # remove "inline" tags
            line =~ s/\[inline\]//g;
            line =~ s/, inline\]/]/g;
            line =~ s/\[inline, /[/g;

            of.print(line);
        }
    }

    fixParam(reference line) {
        if (line =~ /@param/) {
            line =~ s/([^\/\*])\*/1__7_ /g;
            line =~ s/\$/__6_/g;
        }
        if (exists (*string str = regex_extract(line, "(" + sname + "\\.[a-z0-9_]+)", RE_Caseless)[0])) {
            string nstr = str;
            #printf("str=%n nstr=%n\n", str, nstr);
            nstr =~ s/\./__4_/g;
            line = replace(line, str, nstr);
        }
    }

    string getComment(string comment, File inf, bool fix_param = False) {
        comment =~ s/^[ \t]+//g;
        if (fix_param)
            fixParam(\comment);

        DocumentTableHelper dth();

        while (exists (*string line = inf.readLine())) {
            #line =~ s/\$/__6_/g; #/;

            if (fix_param)
                fixParam(\line);

            line = dth.process(line);

            if (line =~ /\*\//) {
                comment += line;
                break;
            }

            # remove <!--% ... %--> comments to allow for invisible spacing, to allow for "*/" to be outout in particular places, for example
            line =~ s/<!--%.*%-->//g;

            comment += line;
        }
        #printf("comment: %s", comment);
        return comment;
    }

    processQore(string fn, string nn) {
        checkNames(fn, nn);

        File inf();
        inf.open2(fn);

        File of();
        of.open2(nn, O_CREAT|O_WRONLY|O_TRUNC);

        printf("processing %y -> %y\n", fn, nn);

        *string class_name;
        *string ns_name;

        # class member public/private bracket count
        int ppc = 0;

        # method private flag
        bool mpp;

        # method private count
        int mpc = 0;

        # class bracket count
        int cbc = 0;

        # namespace bracket count
        int nbc = 0;

        bool in_doc = False;

        # namespace stack
        list nss = ();

        while (exists (*string line = inf.readLine())) {
            #printf("line: %s", line);

            line =~ s/\$\.//g;
            line =~ s/([^\/\*])\*([a-zA-Z])/$1__7_ $2/g;
            #line =~ s/\$/__6_/g;
            #line =~ s/\$//g;

            if (o.nmp)
                line =~ s/@mainpage ([\w]+)/@page $1 $1/;

            if (in_doc) {
                if (line =~ /\*\//)
                    in_doc = False;
                of.print(line);
                continue;
            }

            # skip parse commands
            if (line =~ /^%/)
                continue;

            # see if the line is the start of a doxygen block comment
            if (line =~ /^[[:blank:]]*\/\*\*/) {
                line = getComment(line, inf);
                of.print(line);
                continue;
            }
            
            if (line =~ /^[[:blank:]]*\/\*/){ #/){
                if (line !~ /\*\//)
                    in_doc = True;
                of.print(line);
                continue;
            }

            # take public off sub definitions
            line =~ s/public(.*)[[:space:]]sub([[:space:]]+)/$1$2/g;

            # switch mode: qore to mode: c++
            line =~ s/mode: qore/mode: c++/g;

            line =~ s/\$\.//g;
            #line =~ s/\$//g;
            if (line =~ /our /) {
                line =~ s/our /extern /g;
                line =~ s/\$//g;
            }
            line =~ s/my //g;
            line =~ s/sub //;

            # take public off namespace, class, constant and global variable declarations
            line =~ s/public[[:space:]]+(const|our|namespace|class)/$1/g;

            # remove regular expressions
            line =~ s/[=!]~ *\/.*\//==1/g;        

            # skip module declarations for now
            if (line =~ /^[[:space:]]*module[[:space:]]+/) {
                while (line.val() && line !~ /}/)
                    line = inf.readLine();
                continue;
            }

            # see if the line is the start of a method or function declaration
            if (line =~ /\(.*\)[[:blank:]]*{[[:blank:]]*$/ && line !~ /const .*=/ && line !~ /extern .*=.*\(.*\)/
                && line !~ /^[[:blank:]]*\"/) {
                #printf("method or func: %s", line);
                
                # remove "$" signs
                line =~ s/\$//g;

                # make into a declaration (also remove any parent class constructor calls)
                line =~ s/[[:blank:]]*([^:]:[^:].*\(.*)?{[[:blank:]]*$/;/;

                # read until closing curly bracket '}'
                readUntilCloseBracket(inf);
            }

            if (line =~ /[[:blank:]]*abstract .*;[[:blank:]]*$/) {
                #printf("method or func: %s", line);
                
                # remove "$" signs
                line =~ s/\$//g;

            }

            # convert Qore line comments to C++ line comments
            line =~ s/\#/\/\//;

            # skip lines that are only comments
            if (line =~ /^[[:blank:]]*\/\//) {
                of.write(line);
                continue;
            }

            # temporary list variable
            *list xl;

            # convert class inheritance lists to c++-style declarations
            if (line =~ /inherits / && line !~ /\/(\/|\*)/) {
                trim line;
                xl = (line =~ x/(.*) inherits ([^{]+)(.*)/);
                xl[1] = split(",", xl[1]);                
                foreach string e in (\xl[1]) {
                    if (e !~ /(private|public)[^A-Za-z0-9_]/)
                        e = "public " + e;
                }
                trim(xl[0]);
                line = xl[0] + " : " + join(",", xl[1]) + xl[2] + "\n";

                # add {} to any inline empty class declaration
                if (line =~ /;[ \t]*/) #/)
                    line =~ s/;[ \t]*/ {};/; #/;# this comment is only needed for emacs' broken qore-mode :(

                #printf("x: %y line: %y\n", xl, line);
                #of.print(line);
                #continue;
            }

            # temporary string variable
            *string xs = (line =~ x/^[[:space:]]*namespace[[:space:]]+(\w+(::\w+)?)/)[0];
            if (xs.val()) {
                if (!ns_name.empty()) {
                    nss += ns_name;
                    #throw "NS-ERROR", sprintf("current ns: %s; found nested ns: %s", ns_name, line);
                }

                #printf("namespace %n\n", xs);
                ns_name = xs;

                #if (nbc != 0) throw "ERROR", sprintf("namespace found but nbc: %d\nline: %n\n", nbc, line);

                if (line =~ /{/ && line !~ /}/)
                    ++nbc;
                    
                of.print(line);
                continue;
            }
            else {
                xs = (line =~ x/^[[:space:]]*class[[:space:]]+(\w+(::\w+)?)/)[0];

                if (xs.val()) {
                    if (class_name)
                        throw "CLASS-ERROR", sprintf("current class: %s; found nested class: %s", class_name, line);
                    #printf("class %n\n", xs);
                    class_name = xs;

                    if (cbc)
                        throw "ERROR", sprintf("class found but cbc=%d\nline=%n\n", cbc, line);

                    if (line =~ /{/) {
                        if (line !~ /}/) {
                            line += "\npublic:\n";
                            ++cbc;
                        }
                        else
                            delete class_name;                            
                    }
                    
                    of.print(line);
                    continue;
                }
                else if (class_name) {
                    if (line =~ /{/) {
                        if (line !~ /}/)
                            ++cbc;
                    }
                    else if (line =~ /}/) {
                        --cbc;
                        if (!cbc) {
                            line =~ s/}/};/;
                            delete class_name;
                        }
                    }

                    if (exists (xs = (line =~ x/(public|private)[ \t]+{(.*)}/)[1])) {
                        of.printf("private:\n%s\npublic:\n", xs);
                        continue;
                    }
                    else if (!ppc) {
                        if (line =~ /(public|private)[[:space:]]*{/) {
                            ++ppc;
                            line =~ s/{/:/;
                            #printf("PP line: %s\n", line);
                        }
                    }
                    else {
                        if (line =~ /{/) {
                            if (line !~ /}/)
                                ++ppc;
                        }
                        else if (line =~ /}/) {
                            if (!--ppc)
                                line = "\npublic:\n";
                        }
                    }
                }
                else if (exists ns_name) {
                    if (line =~ /{/) {
                        if (line !~ /}/)
                            ++nbc;
                    }
                    else if (line =~ /}/) {
                        --nbc;
                        if (!nbc) {
                            line =~ s/}/};/;                            
                        }
                        ns_name = pop nss;
                    }
                }
            }

            if (!ppc && line !~ /^[ \t]*\/\//) {
                list mods = ();
                if (line !~ /"/) {
                    while (exists (*list l = (line =~ x/(.*)(deprecated|synchronized|private[^-:]|public[^-:]|static)([^A-Za-z0-9_].*)/))) {
                        mods += l[1];
                        line = l[0] + l[2];
                    }
                }

                if (!mods.empty()) {
                    trim mods;
                    #printf("mods=%n line=%n\n",mods, line);
                    foreach string mod in (mods) {
                        if (mod == "private") {
                            if (!mpp) {
                                mpp = True;
                                of.printf("\nprivate:\n");
                            }
                        }
                        #line = regex_subst(line, mod, "");
                    }
                    mods = select mods, $1 != "private" && $1 != "public";
                    if (!mods.empty()) {
                        line = regex_subst(line, "^([[:blank:]]+)(.*)", "$1 " + join(" ", mods) + " $2");
                    }
                }
            }

            of.print(line);

            if (mpp) {
                if (line =~ /{/)
                    ++mpc;
                else if (line =~ /}/)
                    --mpc;

                if (!mpc) {
                    of.print("\npublic:\n");
                    mpp = False;
                }
            }
        }
    }
    
    private readUntilCloseBracket(File inf) {
        int cnt = 1;
        string quote;
        bool need = True;
        int regex = 0;
        string c;
        while (True) {
            if (need)
                c = inf.read(1);
            else
                need = True;

            if (regex) {
                if (c == "\\")
                    inf.read(1);
                if (c == "/")
                    --regex;
                continue;
            }
            
            #printf("%s", c);
            if (c == "'" || c == '"') {
                if (quote.val()) {
                    if (c == quote)
                        delete quote;
                }
                else
                    quote = c;
                continue;
            }
            if (quote.val()) {
                if (c == "\\")
                    inf.read(1);
                continue;
            }
            if (c == "!" || c == "=") {
                c = inf.read(1);
                if (c == "~") {
                    regex = 1;
                    while (True) {
                        c = inf.read(1);
                        if (c == "s")
                            ++regex;
                        else if (c == "/")
                            break;
                    }
                }
                continue;
            }

            if (c == "{")
                ++cnt;
            else if (c == "}") {
                if (!--cnt)
                    return;
            }
            else if (c == "$") {#"){
                c = inf.read(1);
                if (c != "#")
                    need = False;
            }
            else if (c == "#") {                
                # read until EOL
                inf.readLine();
            }
            else if (c == "/") {
                c = inf.read(1);
                if (c == "*") {
                    # read until close block comment
                    bool star = False;
                    while (True) {
                        c = inf.read(1);
                        if (star) {
                            if (c == "/")
                                break;
                            star = (c == "*");
                            continue;
                        }
                        if (c == "*")
                            star = True;
                    }
                }
                else
                    need = False;
            }
        }
    }
}

class DocumentTableHelper {
    private {
        bool css = False;
        bool inTable = False;
    }

    # no public members
    public {}

    string process(string line) {
        if (line =~ /@page/) {
            #printf("PAGE: css: %n %s", css, line);#exit(1);
            css = False;
        }
        #printf("XXX %s", line);

        if (line !~ /^(\s)*\|/) {
            if (inTable) {
                inTable = False;
                return "    </table>\n" + line;
            }
            return line;
        }

        string str;

        if (!inTable) {
            if (!css) {
                str = "    @htmlonly <style><!-- td.qore { background-color: #5b9409; color: white; } --></style> @endhtmlonly\n";
                css = True;
            }
            str += "    <table>\n";
        }

        inTable = True;

        str += "      <tr>\n";

        trim line;
        splice line, 0, 1;
        foreach *string cell in (split("|", line)) {
            trim cell;
            *string cs;
            if (cell =~ /^!/)
                str += sprintf("        <td class=\"qore\"><b>%s</b></td>\n", substr(cell, 1));
            else
                str += sprintf("        <td>%s</td>\n", cell);
        }        
        str += "      </tr>\n";
        return str;
    }
}
