#!/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;
    }
}
