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

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

%requires qore >= 0.8.13

%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",
    "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
  -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);
        }

        # get module name and version (the easy way)
        string name = substr(basename(src), 0, -3);
        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 h = p.callFunction("get");

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

        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);

            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 QorePostProcessingInputStreamLineIterator(inf)
            : new QorePostProcessingTexInputStreamLineIterator(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;
    }

    processQore(string fn, string nn) {
        checkNames(fn, 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;

        # 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() && line !~ /}/ && ins.next()) {
                    line = ins.getValue();
                }
                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;

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

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

                # 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:]]*\/\//) {
                w.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 :(
            }

            # 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 (line =~ /{/) {
                        if (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 =~ s/}/};/;
                            delete class_name;
                        }
                    }

                    if (exists (xs = (line =~ x/(public|private)[ \t]+{(.*)}/)[1])) {
                        w.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 (hashdecl_flag) {
                    if (line =~ /}/) {
                        line =~ s/}/};/;
                        remove hashdecl_flag;
                    }
                }
                else if (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 =~ /\(.*\)/) {
                #if (line !~ /"/) {
                    while (exists (*list l = (line =~ x/(.*)(deprecated|synchronized|static|private:internal|private:hierarchy)([^A-Za-z0-9_].*)/))) {
                        l[1] =~ s/:.*//;
                        mods += l[1];
                        line = l[0] + l[2];
                    }
                    while (exists (*list l = (line =~ x/(.*)(private|public)([^-: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;
                                w.write("\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");
                    }
                }
            }

            w.write(line);

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

                if (!mpc) {
                    w.write("\npublic:\n");
                    mpp = False;
                }
            }
        }
    }

    private readUntilCloseBracket(FileInputStream inf) {
        int cnt = 1;
        string quote;
        bool need = True;
        int regex = 0;

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

        string c = fc;
        need = False;

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

            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;
                }
            }
            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;
            }
        }
    }
}
