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

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires qore >= 0.9

%requires Qdx

%exec-class qdx

const opts = {
    "moddx": "M,module-dx=s",
    "post": "p,post",
    "tag": "t,tag=s@",
    "tex": "x,posttex=s@",          # use TeX syntax for "post" processing
    "dox": "d,dox",
    "nmp": "N,no-mainpage",
    "keepdollar": "k,keep-dollar",
    "src_root": "T,top-srcdir=s",
    "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 postProcess($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 postProcess($1, True), 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
  -T,--top-srcdir=ARG the top source directory
  -h,--help           this help text
", get_script_name());
      exit(1);
    }

    private string getQoreVersion() {
        return qorever ?? sprintf("%d.%d.%d", Qore::VersionMajor, Qore::VersionMinor, Qore::VersionSub);
    }

    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);
        }
        #printf("src: %y trg: %y src_root: %y ofn: %y\n", src, trg, o.src_root, ofn);

        # get module name and version (the easy way)
        string name = basename(src);
        # remove ".qm" suffix for files
        name =~ s/\.qm$//;

        Program p();
        p.define("QORE_QDX_RUN", 1);
        string msrc = sprintf("%%requires %s\nhash sub get() { return get_module_hash().'%s'; }\n", src, name);
        p.parse(msrc, "mod");
        hash<auto> h = p.callFunction("get");

        # issue #2966: if module is a directory, then get all relevant files for trg / {input}
        if (is_dir(src)) {
            string trg_dir = dirname(trg) + DirSep;
            list<string> trg_list;
            foreach string file in (glob(src + DirSep + "*")) {
                if (file =~ /\.qm$/ || file =~ /\.qc$/ || file =~ /\.ql$/) {
                    file = sprintf("%s%s.dox.h", trg_dir, basename(file));
                    trg_list += file;
                }
            }

            # replace target list for {input}
            trg = trg_list.join(" ");
        }

        InputStreamLineIterator i(new FileInputStream(fn), NOTHING, NOTHING, False);

        StreamWriter w(new FileOutputStream(ofn));

        # get tags substitution string
        string tags = o.tag ? o.tag.join(" ") : "";

        while (i.next()) {
            string line = i.getValue();

            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);
            else if (line =~ /{qore_src_dir}/) {
                line = replace(line, "{qore_src_dir}", o.src_root);
            }

            w.write(line);
        }
    }

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

        DocumentTableInputStreamLineIterator i(new FileInputStream(fn), NOTHING, NOTHING, False);

        StreamWriter w(new FileOutputStream(ofn));

        while (i.next()) {
            string line = i.getValue();

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

            w.write(line);
        }
    }

    private postProcess(string ifn, bool tex = False) {
        *hash h = hstat(ifn);
        if (h.type == "DIRECTORY") {
            Dir d();
            d.chdir(ifn);
            list l = d.listFiles("\\.(html|js)$"); #");
            map postProcessIntern(ifn + "/" + $1, tex), l;
        }
        else
            map postProcessIntern($1, tex), glob(ifn);
    }

    postProcessIntern(string ifn, bool tex) {
        DocumentTableInputStreamLineIterator i(new FileInputStream(ifn), NOTHING, NOTHING, False);

        FileInputStream inf(ifn);
        InputStreamLineIterator ins = tex
            ? new QorePostProcessingTexInputStreamLineIterator(inf)
            : new QorePostProcessingInputStreamLineIterator(inf);

        string ofn = ifn + ".new";

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

        StreamWriter w(new FileOutputStream(ofn));

        on_success rename(ofn, ifn);

        map w.write($1), ins;
    }

    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, InputStreamLineIterator ins, bool fix_param = False) {
        comment =~ s/^[ \t]+//g;
        if (fix_param)
            fixParam(\comment);

        DocumentTableHelper dth();

        while (ins.next()) {
            string line = ins.getValue();
            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 output in
            # particular places, for example
            #line =~ s/<!--%.*%-->//g;

            comment += line;
        }
        return comment;
    }

    #! doxygen dislikes qouted strings but accept apostrophed ones
    string adjustQuotedStrings(string line) {
        int flag = 0;
        int i = 0;
        while (i < line.size()) {
            switch (flag) {
                case 0:
                    switch (line[i]) {
                        case '"':
                            splice line, i, 1, "'";
                            flag = 1;
                            break;
                        case "'":
                            flag = 2;
                            break;
                    }
                    break;
                case 1:  # in quoted string
                    switch (line[i]) {
                        case '\':
                            i++;  # skip next char
                            break;
                        case '"':
                            splice line, i, 1, "'";
                            flag = 0;
                            break;
                        case "'":
                            splice line, i, 0, '\';   # prefix apostroph
                            i++;
                            break;
                    }
                    break;
                case 2:  # in apostropted string
                    switch (line[i]) {
                        case '\':
                            i++;  # skip next char
                            break;
                        case "'":
                            flag = 0;
                            break;
                    }
                    break;
            }
            i++;
        }
        return line;
    }

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

        # issue #2966: process module directories
        if (is_dir(fn)) {
            string targ_dir = dirname(nn);
            foreach string file in (glob(fn + DirSep + "*")) {
                if (file =~ /\.qm$/ || file =~ /\.qc$/ || file =~ /\.ql$/) {
                    string file_nn = sprintf("%s/%s.dox.h", targ_dir, basename(file));
                    processQoreFile(file, file_nn);
                }
            }
        } else {
            processQoreFile(fn, nn);
        }
    }

    /* note: ideally the function should process input files to output ones keep line numbering. It would
       allow to use doxygen filters as the line would be referenced to source file. But there are some
       expansions which makes it difficult, i.e. table expansion to html tags
     */
    private processQoreFile(string fn, string nn) {
        FileInputStream inf(fn);
        # need to use an unbuffered StreamReader here
        InputStreamLineIterator ins(new StreamReader(inf), NOTHING, False);

        StreamWriter w(new FileOutputStream(nn));

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

        *string class_name;
        *string ns_name;

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

        # public/private block type
        string pp_type;

        # 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 = ();

        # hashdecl flag
        bool hashdecl_flag;

        while (ins.next()) {
            string line = ins.getValue();
            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;
                }
                w.write(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, ins);
                w.write(line);
                continue;
            }

            if (line =~ /^[[:blank:]]*\/\*/){
                if (line !~ /\*\//) {
                    in_doc = True;
                }
                w.write(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 //;

            # change hashdecl to struct
            if (line =~ /[^a-zA-Z0-9_]hashdecl /) {
                line =~ s/([^a-zA-Z0-9_])?hashdecl /$1struct /g;
                hashdecl_flag = True;
            }

            # 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() && !regex(line, "}") && ins.next()) {
                    line = ins.getValue();
                }
                continue;
            }

            # see if we have a function or method declaration that spans multiple lines
            if (line =~ /\w+\s?(\(|\))/) {
                # copy of line to check it without parens in quotes
                string line_copy = line;
                line_copy =~ s/"(?:[^"\\]|\\.)*"/""/g;
                # count how many open curly brackets
                int ob = (line_copy =~ x/(\()/g).size();
                # count how many close curly brackets
                int cb = (line_copy =~ x/(\))/g).size();
                int tot = (ob - cb);
                if (tot > 0) {
                    trim line;
                    # read until we have an open bracket
                    while (ins.next()) {
                        string new_line = ins.getValue();
                        trim new_line;
                        line += " " + new_line;
                        #printf("FOUND COMPLEX LINE: %s\n", line);
                        if (new_line =~ /{\s?(#.*)?$/) {
                            break;
                        }
                    }
                    line += "\n";
                }
            }

            # see if the line is the start of a method or function declaration
            if (regex(line, '\(.*\)[[:blank:]]*{[[:blank:]]*(#.*)?$')
                && line !~ /const .*=/
                && line !~ /extern .*=.*\(.*\)/
                && !regex(line, '^[[:blank:]]*\"') ) {
                if (line =~ /code.*cve/) { printf("GOT IT: %s\n", line);}
                # printf("method or func: %s", line);

                # remove "$" signs
                line =~ s/\$//g;

                # remove any trailing line comment
                line =~ s/#.*$//;

                # make into a declaration (also remove any parent class constructor calls)
                line =~ s/[[:blank:]]*(?:([^:A-za-z]):[^:].*\(.*)?{[[:blank:]]*$/$1;/;
                #line = regex_subst(line, '[[:blank:]]*(?:([^:]):[^:].*\(.*)?{[[:blank:]]*$', "$1;");

                # modify string default value quoted by quotes to apostroph
                line = adjustQuotedStrings(line);

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

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

                # remove "$" signs
                line =~ s/\$//g;

                # modify string default value quoted by quotes to apostroph
                line = adjustQuotedStrings(line);
            }

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

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

            # temporary list variable
            *list xl;

            # convert class inheritance lists to c++-style declarations
            if (line =~ /inherits / && line !~ /\/(\/|\*)/) {
                trim line;
                xl = regex_extract(line, '(.*) inherits ([^{]+)(.*)');
                xl[1] = split(",", xl[1]);
                foreach string e in (\xl[1]) {
                    if (e !~ /(private:internal|private:hierarchy|private|public)[^A-Za-z0-9_]/)
                        e = "public " + e;
                    else {
                        if (e =~ /private:hierarchy/) {
                            e = replace(e, "private:hierarchy", "protected");
                        } else if (e =~ /private:internal/) {
                            e = replace(e, "private:internal", "private");
                        } else if (e =~ /private/) {
                            e = replace(e, "private", "protected");
                        }
                    }
                }
                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 :(
            }

            # temporary string variable
            (*string xs, *string sc) = (line =~ x/^[[:space:]]*namespace[[:space:]]+(\w+(?:::\w+)?)[[:space:]]*(\;)?/);
            if (xs) {
                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;

                if (sc)
                    line =~ s/;/ {}/;

                w.write(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 (regex(line, "{") ) {
                        if (!regex(line, "}")) {
                            line += "\npublic:\n";
                            ++cbc;
                        }
                        else
                            delete class_name;
                    }

                    w.write(line);
                    continue;
                } else if (class_name) {
                    if (line =~ /({|})/) {
                        # count how many open curly brackets
                        int ob = (line =~ x/({)/g).size();
                        # count how many close curly brackets
                        int cb = (line =~ x/(})/g).size();
                        cbc += (ob - cb);
                        if (!cbc) {
                            line = regex_subst(line, "}", "};");
                            delete class_name;
                        }
                    }

                    if (*list<string> strlist = (line =~ x/^(\s*)(public|private(?::internal|:hierarchy)?)\s+{(.*)}/)) {
                        switch (strlist[1]) {
                            case "private":
                            case "private:hierarchy":
                                strlist[1] = "protected";
                                break;
                            case "private:internal":
                                strlist[1] = "private";
                                break;
                        }
                        if (strlist[1] != "public") {
                            w.printf("%s:\n", strlist[1]);
                        }
                        w.printf("%s%s\n", strlist[0], strlist[2]);
                        if (strlist[1] != "public") {
                            w.printf("public:\n");
                        }
                        continue;
                    }
                    if (exists (xs = (line =~ x/(?:public|private(?::internal|:hierarchy)?)\s*{(.*)}/)[0])) {
                        string mod = getDoxMod(xs);
                        if (mod != "public") {
                            w.printf("%s:\n%s\npublic:\n", mod, xs);
                        } else {
                            w.printf("%s\n", xs);
                        }
                        continue;
                    } else if (!ppc) {
                        if (exists (xs = (line =~ x/\s*(public|private(?::internal|:hierarchy)?)\s*{\s*(#.*)?$/)[0])) {
                            pp_type = xs;
                            xs = getDoxMod(xs);
                            ++ppc;
                            if (xs != "public") {
                                w.printf("%s:\n", xs);
                            }
                            #printf("PP line: %s\n", line);
                            continue;
                        }
                    } else {
                        # count how many open curly brackets
                        int ob = (line =~ x/({)/g).size();
                        # count how many close curly brackets
                        int cb = (line =~ x/(})/g).size();
                        int tot = (ob - cb);
                        if (tot) {
                            ppc += tot;
                            if (!ppc) {
                                if (pp_type != "public") {
                                    w.write("\npublic:\n");
                                }
                                remove pp_type;
                                continue;
                            }
                        }
                    }
                } else if (hashdecl_flag) {
                    if (regex(line, "}") ) {
                        line = regex_subst(line, "}", "};");
                        remove hashdecl_flag;
                    }
                } else if (ns_name) {
                    if (regex(line, "{")) {
                        if (!regex(line, "}"))
                            ++nbc;
                    } else if (regex(line, "}")) {
                        --nbc;
                        if (!nbc) {
                            line = regex_subst(line, "}", "};");
                        }
                        ns_name = pop nss;
                    }
                }
            }

            if (!ppc && line !~ /^[ \t]*\/\//) {
                list<string> mods();

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

                if (!mods.empty()) {
                    trim mods;
                    #printf("mods: %y line: %y\n", mods, line);
                    foreach string mod in (mods) {
                        if (mod == "private" || mod == "private:hierarchy") {
                            if (!mpp) {
                                mpp = True;
                                w.write("\protected:\n");
                            }
                        } else if (mod == "private:internal") {
                            if (!mpp) {
                                mpp = True;
                                w.write("\private:\n");
                            }
                        }
                    }
                    mods = select mods, $1 !~ /^private/ && $1 != "public";
                    if (!mods.empty()) {
                        line = regex_subst(line, "^([[:blank:]]+)(.*)", "$1 " + join(" ", mods) + " $2");
                    }
                }
            }

            w.write(line);

            if (mpp) {
                if (regex(line, "{")) {
                    ++mpc;
                } else if (regex(line, "}")) {
                    --mpc;
                }
                if (!mpc) {
                    w.write("\npublic:\n");
                    mpp = False;
                }
            }
        }
    }

    private string getDoxMod(string mod) {
        switch (mod) {
            case "private":
            case "private:hierarchy":
                return "protected";
            case "private:internal":
                return "private";
            case "public":
                return mod;
            default:
                throw "MOD-ERROR", sprintf("unrecognized protection attribute %y", mod);
        }
    }

    # return number of lines processed
    private int readUntilCloseBracket(FileInputStream inf) {
        int cnt = 1;
        int line_cnt = 0;
        string quote;
        bool need = True;
        int regex = 0;

        StreamReader sr(inf);
        *string fc = sr.readString(1);
        if (!exists fc) {
            return line_cnt;
        }

        string c = fc;
        need = False;

        while (True) {
            if (need) {
                c = sr.readString(1);
            }
            else {
                need = True;
            }

            if (c == "\n") {
                line_cnt++;
            }

            if (regex) {
                if (c == "\\")
                    sr.readString(1);
                if (c == "/") {
                    --regex;
                }
                continue;
            }

            if (c == "'" || c == '"') {
                if (quote.val()) {
                    if (c == quote) {
                        delete quote;
                    }
                }
                else {
                    quote = c;
                }
                continue;
            }
            if (quote.val()) {
                if (c == "\\" && quote != "'")
                    sr.readString(1);
                continue;
            }
            if (c == "!" || c == "=") {
                c = sr.readString(1);
                if (c == "~") {
                    regex = 1;
                    while (True) {
                        c = sr.readString(1);
                        if (c == "s") {
                            ++regex;
                        } else if (c == "/") {
                            break;
                        }
                    }
                }
                continue;
            }

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