// docs.cpp: ingame command documentation system

#include "cube.h"

void renderdocsection(void *menu, bool init);

extern hashtable<const char *, ident> *idents;

struct docargument
{
    char *token, *desc, *values;
    bool vararg;

    docargument() : token(NULL), desc(NULL), values(NULL) {};
    ~docargument()
    {
        DELETEA(token);
        DELETEA(desc);
        DELETEA(values);
    }
};

struct docref
{
    char *name, *ident, *url, *article;

    docref() : name(NULL), ident(NULL), url(NULL), article(NULL) {}
    ~docref()
    {
        DELETEA(name);
        DELETEA(ident);
        DELETEA(url);
        DELETEA(article);
    }
};

struct docexample
{
    char *code, *explanation;

    docexample() : code(NULL), explanation(NULL) {}
    ~docexample()
    {
        DELETEA(code);
        DELETEA(explanation);
    }
};

struct dockey
{
    char *alias, *name, *desc;

    dockey() : alias(NULL), name(NULL), desc(NULL) {}
    ~dockey()
    {
        DELETEA(alias);
        DELETEA(name);
        DELETEA(desc);
    }
};

struct docident
{
    char *name, *desc;
    vector<docargument> arguments;
    vector<char *> remarks;
    vector<docref> references;
    vector<docexample> examples;
    vector<dockey> keys;

    docident() : name(NULL), desc(NULL) {}
    ~docident()
    {
        DELETEA(name);
        DELETEA(desc);
    }
};

struct docsection
{
    char *name;
    vector<docident *> idents;
    void *menu;

    docsection() : name(NULL) {};
    ~docsection()
    {
        DELETEA(name);
    }
};

vector<docsection> sections;
hashtable<const char *, docident> docidents; // manage globally instead of a section tree to ensure uniqueness
docsection *lastsection = NULL;
docident *lastident = NULL;

void adddocsection(char *name)
{
    if(!name) return;
    docsection &s = sections.add();
    s.name = newstring(name);
    s.menu = addmenu(s.name, NULL, true, renderdocsection);
    lastsection = &s;
}

void adddocident(char *name, char *desc)
{
    if(!name || !desc || !lastsection) return;
    name = newstring(name);
    docident &c = docidents[name];
    lastsection->idents.add(&c);
    c.name = name;
    c.desc = newstring(desc);
    lastident = &c;
}

void adddocargument(char *token, char *desc, char *values, char *vararg)
{
    if(!lastident || !token || !desc) return;
    docargument &a = lastident->arguments.add();
    a.token = newstring(token);
    a.desc = newstring(desc);
    a.values = values && strlen(values) ? newstring(values) : NULL;
    a.vararg = vararg && atoi(vararg) == 1 ? true : false;
}

void adddocremark(char *remark)
{
    if(!lastident || !remark) return;
    lastident->remarks.add(newstring(remark));
}

void adddocref(char *name, char *ident, char *url, char *article) // FIXME... someone, please :P
{
    if(!lastident || !name) return;
    docref &r = lastident->references.add();
    r.name = newstring(name);
    r.ident = ident && strlen(ident) ? newstring(ident) : NULL;
    r.url = url && strlen(url) ? newstring(url) : NULL;
    r.article = article && strlen(article) ? newstring(article) : NULL;
}

void adddocexample(char *code, char *explanation)
{
    if(!lastident || !code) return;
    docexample &e = lastident->examples.add();
    e.code = newstring(code);
    e.explanation = explanation && strlen(explanation) ? newstring(explanation) : NULL;
}

void adddockey(char *alias, char *name, char *desc)
{
    if(!lastident || !alias) return;
    dockey &k = lastident->keys.add();
    k.alias = newstring(alias);
    k.name = name && strlen(name) ? newstring(name) : NULL;
    k.desc = desc && strlen(desc) ? newstring(desc) : NULL;
}

COMMANDN(docsection, adddocsection, ARG_1STR);
COMMANDN(docident, adddocident, ARG_2STR);
COMMANDN(docargument, adddocargument, ARG_4STR);
COMMANDN(docremark, adddocremark, ARG_1STR);
COMMANDN(docref, adddocref, ARG_4STR);
COMMANDN(docexample, adddocexample, ARG_2STR);
COMMANDN(dockey, adddockey, ARG_3STR);

int stringsort(const char **a, const char **b) { return strcmp(*a, *b); }

char *cvecstr(vector<char *> &cvec, const char *substr, int *rline = NULL)
{
    char *r = NULL;
    loopv(cvec) if(cvec[i]) if((r = strstr(cvec[i], substr)) != NULL)
    {
        if(rline) *rline = i;
        break;
    }
    return r;
}

void listundoneidents(vector<const char *> &inames, int allidents)
{
    enumeratekt(*idents, const char *, name, ident, id,
    {
        if((allidents > 0) || id.type != ID_ALIAS)
        {
            docident *id = docidents.access(name);
            if(id) // search for substrings that indicate undoneness
            {
                vector<char *> srch;
                srch.add(id->name);
                srch.add(id->desc);
                loopvj(id->remarks) srch.add(id->remarks[j]);
                loopvj(id->arguments)
                {
                    srch.add(id->arguments[j].token);
                    srch.add(id->arguments[j].desc);
                    srch.add(id->arguments[j].values);
                }
                loopvj(id->references)
                {
                    srch.add(id->references[j].ident);
                    srch.add(id->references[j].name);
                    srch.add(id->references[j].url);
                }
                if(cvecstr(srch, "TODO") || cvecstr(srch, "UNDONE")) id = NULL;
            }
            if(!id) inames.add(name);
        }
    });
    inames.sort(stringsort);
}

void docundone(int allidents)
{
    vector<const char *> inames;
    listundoneidents(inames, allidents);
    inames.sort(stringsort);
    loopv(inames) conoutf("%s", inames[i]);
}

void docinvalid()
{
    vector<const char *> inames;
    identnames(inames, true);
    inames.sort(stringsort);
    enumerate(docidents, docident, d,
    {
        if(!strchr(d.name, ' ') && !identexists(d.name))
            conoutf("%s", d.name);
    });
}

void docfind(char *search)
{
    enumerate(docidents, docident, i,
    {
        vector<char *> srch;
        srch.add(i.name);
        srch.add(i.desc);
        loopvk(i.remarks) srch.add(i.remarks[k]);

        char *r;
        int rline;
        if((r = cvecstr(srch, search, &rline)))
        {
            const int matchchars = 200;
            string match;
            copystring(match, r-srch[rline] > matchchars/2 ? r-matchchars/2 : srch[rline], matchchars/2);
            conoutf("%-20s%s", i.name, match);
        }
    });
}

char *xmlstringenc(char *d, const char *s, size_t len)
{
    if(!d || !s) return NULL;
    struct spchar { char c; char repl[8]; } const spchars[] = { {'&', "&amp;"}, {'<', "&lt;"}, {'>', "gt;"}, {'"', "&quot;"}, {'\'', "&apos;"}};

    char *dc = d;
    const char *sc = s;

    while(*sc && (size_t)(dc - d) < len - 1)
    {
        bool specialc = false;
        loopi(sizeof(spchars)/sizeof(spchar)) if(spchars[i].c == *sc)
        {
            specialc = true;
            size_t rlen = strlen(spchars[i].repl);
            if(dc - d + rlen <= len - 1)
            {
                memcpy(dc, spchars[i].repl, rlen);
                dc += rlen;
                break;
            }
        }
        if(!specialc) memcpy(dc++, sc, 1);
        *dc = 0;
        sc++;
    }
    return d;
}

void docwriteref(int allidents, const char *ref, const char *schemalocation, const char *transformation)
{
    stream *f = openfile(path(allidents < 0 ? "docs/autogenerated_base_reference.xml" : "docs/autogenerated_todo_reference.xml", true), "w");
    if(!f) return;
    char desc[] = "<description>TODO: Description</description>";

    vector<const char *> inames;
    if(allidents < 0) identnames(inames, true);   // -1: all builtin
    else listundoneidents(inames, allidents);      // 0: todo builtin, 1: todo builtin + alias
    inames.sort(stringsort);

    if(allidents < 0)
    {
        f->printf("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        f->printf("<?xml-stylesheet type=\"text/xsl\" href=\"%s\"?>\n", transformation && strlen(transformation) ? transformation : "transformations/cuberef2xhtml.xslt");
        f->printf("<cuberef xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" name=\"%s\" version=\"v0.1\" xsi:schemaLocation=\"%s\" xmlns=\"http://cubers.net/Schemas/CubeRef\">\n", ref && strlen(ref) ? ref : "Unnamed Reference", schemalocation && strlen(schemalocation) ? schemalocation : "http://cubers.net/Schemas/CubeRef schemas/cuberef.xsd");
        f->printf("\t%s\n", desc);
        f->printf("\t<sections>\n");
        f->printf("\t\t<section name=\"Main\">\n");
        f->printf("\t\t\t%s\n", desc);
        f->printf("\t\t\t<identifiers>\n");
    }

    static const int bases[] = { ARG_1INT, ARG_1STR, ARG_1EXP, ARG_1EXPF, ARG_1EST };
    string name;
    loopv(inames)
    {
        ident *id = idents->access(inames[i]);
        if(!id || id->type != ID_COMMAND) continue;
        f->printf("\t\t\t\t<command name=\"%s\">\n", xmlstringenc(name, id->name, MAXSTRLEN));
        f->printf("\t\t\t\t\t%s\n", desc);
        if(id->narg != ARG_NONE && id->narg != ARG_DOWN && id->narg != ARG_IVAL && id->narg != ARG_FVAL && id->narg != ARG_SVAL)
        {
            f->printf("\t\t\t\t\t<arguments>\n");
            if(id->narg == ARG_CONC || id->narg == ARG_CONCW || id->narg == ARG_VARI) f->printf("\t\t\t\t\t\t<variableArgument token=\"...\" description=\"TODO\"/>\n");
            else
            {
                int base = -1;
                loopj(sizeof(bases)/sizeof(bases[0]))
                {
                    if(id->narg < bases[j]) { if(j) base = bases[j-1]; break; }
                }
                if(base >= 0) loopj(id->narg-base+1) f->printf("\t\t\t\t\t\t<argument token=\"%c\" description=\"TODO\"/>\n", (char)(*"A")+j);
            }
            f->printf("\t\t\t\t\t</arguments>\n");
        }
        f->printf("\t\t\t\t</command>\n");
    }
    loopv(inames)
    {
        ident *id = idents->access(inames[i]);
        if(!id || (id->type != ID_VAR && id->type != ID_FVAR)) continue;
        f->printf("\t\t\t\t<variable name=\"%s\">\n", xmlstringenc(name, id->name, MAXSTRLEN));
        f->printf("\t\t\t\t\t<description>TODO</description>\n");
        switch(id->type)
        {
            case ID_VAR:
                f->printf("\t\t\t\t\t<value %s description=\"TODO\" minValue=\"%i\" maxValue=\"%i\" defaultValue=\"%i\" %s/>\n", id->minval>id->maxval ? "" : "token=\"N\"", id->minval, id->maxval, *id->storage.i, id->minval>id->maxval ? "readOnly=\"true\"" : "");
                break;
            case ID_FVAR:
                f->printf("\t\t\t\t\t<value %s description=\"TODO\" minValue=\"%s\" maxValue=\"%s\" defaultValue=\"%s\" %s/>\n", id->minvalf>id->maxvalf ? "" : "token=\"N\"",
                    floatstr(id->minvalf), floatstr(id->maxvalf), floatstr(*id->storage.f),
                    id->minvalf>id->maxvalf ? "readOnly=\"true\"" : "");
                break;
        }
        f->printf("\t\t\t\t</variable>\n");
    }
    loopv(inames)
    {
        ident *id = idents->access(inames[i]);
        if(!id || id->type != ID_ALIAS) continue;
        f->printf("\t\t\t\t<command name=\"%s\">\n", xmlstringenc(name, id->name, MAXSTRLEN));
        f->printf("\t\t\t\t\t%s\n", desc);
        f->printf("\t\t\t\t</command>\n");
    }

    if(allidents < 0) f->printf("\t\t\t</identifiers>\n\t\t</section>\n\t</sections>\n</cuberef>\n");
    delete f;
}

void docwritebaseref(char *ref, char *schemalocation, char *transformation)
{
    docwriteref(-1, ref, schemalocation, transformation);
}

void docwritetodoref(int allidents)
{
    docwriteref(allidents ? 1 : 0, "", "", "");
}

COMMAND(docundone, ARG_1INT);
COMMAND(docinvalid, ARG_NONE);
COMMAND(docfind, ARG_1STR);
COMMAND(docwritebaseref, ARG_3STR);
COMMAND(docwritetodoref, ARG_1INT);
VAR(docvisible, 0, 1, 1);
VAR(docskip, 0, 0, 1000);

void toggledoc() { docvisible = !docvisible; }
void scrolldoc(int i) { docskip += i; if(docskip < 0) docskip = 0; }

int numargs(char *args)
{
    if(!args || !strlen(args)) return -1;

    int argidx = -1;
    char *argstart = NULL;

    for(char *t = args; *t; t++)
    {
        if(!argstart && *t != ' ') { argstart = t; argidx++; }
        else if(argstart && *t == ' ') if(t-1 >= args)
        {
            switch(*argstart)
            {
                case '[': if(*(t-1) != ']') continue; break;
                case '"': if(*(t-1) != '"') continue; break;
                default: break;
            }
            argstart = NULL;
        }
    }
    return argidx;
}

void renderdoc(int x, int y, int doch)
{
    if(!docvisible) return;

    char *exp = getcurcommand();
    if(!exp || *exp != '/' || strlen(exp) < 2) return;

    char *c = exp+1;
    size_t clen = strlen(c);
    for(size_t i = 0; i < clen; i++) // search first matching cmd doc by stripping arguments of exp from right to left
    {
        char *end = c+clen-i;
        if(!*end || *end == ' ')
        {
            string cmd;
            copystring(cmd, c, clen-i+1);
            docident *ident = docidents.access(cmd);
            if(ident)
            {
                vector<const char *> doclines;

                char *label = newstringbuf(); // label
                doclines.add(label);
                formatstring(label)("~%s", ident->name);
                loopvj(ident->arguments)
                {
                    concatstring(label, " ");
                    concatstring(label, ident->arguments[j].token);
                }
                doclines.add(NULL);

                doclines.add(ident->desc);
                doclines.add(NULL);

                if(ident->arguments.length() > 0) // args
                {
                    extern textinputbuffer cmdline;
                    char *args = strchr(c, ' ');
                    int arg = -1;

                    if(args)
                    {
                        args++;
                        if(cmdline.pos >= 0)
                        {
                            if(cmdline.pos >= args-c)
                            {
                                string a;
                                copystring(a, args, cmdline.pos-(args-c)+1);
                                args = a;
                                arg = numargs(args);
                            }
                        }
                        else arg = numargs(args);

                        if(arg >= 0) // multipart idents need a fixed argument offset
                        {
                            char *c = cmd;
                            while((c = strchr(c, ' ')) && c++) arg--;
                        }

                        // fixes offset for var args
                        if(arg >= ident->arguments.length() && ident->arguments.last().vararg) arg = ident->arguments.length() - 1;
                    }

                    loopvj(ident->arguments)
                    {
                        docargument *a = &ident->arguments[j];
                        if(!a) continue;
                        formatstring(doclines.add(newstringbuf()))("~\f%d%-8s%s %s%s%s", j == arg ? 4 : 5, a->token, a->desc,
                            a->values ? "(" : "", a->values ? a->values : "", a->values ? ")" : "");
                    }

                    doclines.add(NULL);
                }

                if(ident->remarks.length()) // remarks
                {
                    loopvj(ident->remarks) doclines.add(ident->remarks[j]);
                    doclines.add(NULL);
                }

                if(ident->examples.length()) // examples
                {
                    doclines.add(ident->examples.length() == 1 ? "Example:" : "Examples:");
                    loopvj(ident->examples)
                    {
                        doclines.add(ident->examples[j].code);
                        doclines.add(ident->examples[j].explanation);
                    }
                    doclines.add(NULL);
                }

                if(ident->keys.length()) // default keys
                {
                    doclines.add(ident->keys.length() == 1 ? "Default key:" : "Default keys:");
                    loopvj(ident->keys)
                    {
                        dockey &k = ident->keys[j];
                        defformatstring(line)("~%-10s %s", k.name ? k.name : k.alias, k.desc ? k.desc : "");
                        doclines.add(newstring(line));
                    }
                    doclines.add(NULL);
                }

                if(ident->references.length()) // references
                {
                    struct category { string label; string refs; }
                    categories[] = {{"related identifiers", ""} , {"web resources", ""}, {"wiki articles", ""}, {"other", ""}};
                    loopvj(ident->references)
                    {
                        docref &r = ident->references[j];
                        char *ref = r.ident ? categories[0].refs : (r.url ? categories[1].refs : (r.article ? categories[2].refs : categories[3].refs));
                        concatstring(ref, r.name);
                        if(j < ident->references.length()-1) concatstring(ref, ", ");
                    }
                    loopj(sizeof(categories)/sizeof(category))
                    {
                        if(!strlen(categories[j].refs)) continue;
                        formatstring(doclines.add(newstringbuf()))("~%s: %s", categories[j].label, categories[j].refs);
                    }
                }

                while(doclines.length() && !doclines.last()) doclines.pop();

                doch -= 3*FONTH + FONTH/2;

                int offset = min(docskip, doclines.length()-1), maxl = offset, cury = 0;
                for(int j = offset; j < doclines.length(); j++)
                {
                    const char *str = doclines[j];
                    int width = 0, height = FONTH;
                    if(str) text_bounds(*str=='~' ? str+1 : str, width, height, *str=='~' ? -1 : VIRTW*4/3);
                    if(cury + height > doch) break;
                    cury += height;
                    maxl = j+1;
                }
                if(offset > 0 && maxl >= doclines.length())
                {
                    for(int j = offset-1; j >= 0; j--)
                    {
                        const char *str = doclines[j];
                        int width = 0, height = FONTH;
                        if(str) text_bounds(*str=='~' ? str+1 : str, width, height, *str=='~' ? -1 : VIRTW*4/3);
                        if(cury + height > doch) break;
                        cury += height;
                        offset = j;
                    }
                }


                cury = y;
                for(int j = offset; j < maxl; j++)
                {
                    const char *str = doclines[j];
                    if(str)
                    {
                        const char *text = *str=='~' ? str+1 : str;
                        draw_text(text, x, cury, 0xFF, 0xFF, 0xFF, 0xFF, -1, str==text ? VIRTW*4/3 : -1);
                        int width, height;
                        text_bounds(text, width, height, str==text ? VIRTW*4/3 : -1);
                        cury += height;
                        if(str!=text) delete[] str;
                    }
                    else cury += FONTH;
                }

                if(maxl < doclines.length()) draw_text("\f4more (F3)", x, y+doch); // footer
                if(offset > 0) draw_text("\f4less (F2)", x, y+doch+FONTH);
                draw_text("\f4disable doc reference (F1)", x, y+doch+2*FONTH);
                return;
            }
        }
    }
}

void *docmenu = NULL;

struct msection { char *name; string cmd; };

int msectionsort(const msection *a, const msection *b)
{
    return strcmp(a->name, b->name);
}

void renderdocsection(void *menu, bool init)
{
    static vector<msection> msections;
    msections.shrink(0);

    loopv(sections)
    {
        if(sections[i].menu != menu) continue;
        loopvj(sections[i].idents)
        {
            docident &id = *sections[i].idents[j];
            msection &s = msections.add();
            s.name = id.name;
            formatstring(s.cmd)("saycommand [/%s ]", id.name);
        }
        msections.sort(msectionsort);
        menureset(menu);
        loopv(msections) { menumanual(menu, msections[i].name, msections[i].cmd); }
        return;
    }
}

struct maction { string cmd; };

void renderdocmenu(void *menu, bool init)
{
    static vector<maction> actions;
    actions.shrink(0);
    menureset(menu);
    loopv(sections)
    {
        maction &a = actions.add();
        formatstring(a.cmd)("showmenu [%s]", sections[i].name);
        menumanual(menu, sections[i].name, a.cmd);
    }
}
