#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
#
# Copyright (c) 2017 Varnish Software AS
# All rights reserved.
#
# Author: Poul-Henning Kamp <phk@phk.freebsd.dk>
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

'''
This program compiles a `.vsc` file to `.c`, `.h` and `.rst` formats.

Note: A `.vsc` file is *almost* a `.rst` file, or at last *almost*
the same general syntax as a `.rst` file, but for now we process
it with this program to get a *real* `.rst` file.
'''

import getopt
import json
import sys
import collections
import codecs

# Parameters of 'vinyl_vsc_begin', first element is default
TYPES = ["counter", "gauge", "bitmap"]
CTYPES = ["uint64_t"]
LEVELS = ["info", "diag", "debug"]
FORMATS = ["integer", "bytes", "bitmap", "duration"]

PARAMS = {
    "type": TYPES,
    "ctype": CTYPES,
    "level": LEVELS,
    "oneliner": None,
    "group": None,
    "format": FORMATS,
}

def genhdr(fo, name):

    '''Emit .[ch] file boiler-plate warning'''

    fo.write('/*\n')
    fo.write(' * NB:  This file is machine generated, DO NOT EDIT!\n')
    fo.write(' *\n')
    fo.write(' * Edit ' + name +
             '.vsc and run lib/libvsc/vsctool.py instead.\n')
    fo.write(' */\n')
    fo.write('\n')

#######################################################################

class CounterSet(object):

    '''
        A set of counters

        In the `.vsc` file a CounterSet is everything between a

            .. vinyl_vsc_begin::

        and the subsequent

            .. vinyl_vsc_end::
    '''

    def __init__(self, name, m):
        self.name = name
        self.struct = "struct VSC_" + name
        self.mbrs = []
        self.groups = {}
        self.head = m
        self.completed = False
        self.off = 0
        self.gnames = None

    def addmbr(self, m, g):
        '''Add a counter'''
        assert not self.completed
        self.mbrs.append(m)
        retval = self.off
        self.off += 8
        if g is not None:
            if g not in self.groups:
                self.groups[g] = []
            self.groups[g].append(m)
        return retval

    def complete(self, arg):
        '''Mark set completed'''
        assert arg == self.name
        self.completed = True
        self.gnames = list(self.groups.keys())
        self.gnames.sort()


    def emit_json(self, fo):
        '''Emit JSON as compact C byte-array and as readable C-comments'''
        assert self.completed
        dd = collections.OrderedDict()
        dd["version"] = "1"
        dd["name"] = self.name
        dd["oneliner"] = self.head.param["oneliner"].strip()
        dd["order"] = int(self.head.param["order"])
        dd["docs"] = "\n".join(self.head.getdoc())
        dd["elements"] = len(self.mbrs)
        el = collections.OrderedDict()
        dd["elem"] = el
        for i in self.mbrs:
            ed = collections.OrderedDict()
            el[i.arg] = ed
            for j in PARAMS:
                if j in i.param:
                    ed[j] = i.param[j]
            ed["index"] = i.param["index"]
            ed["name"] = i.arg
            ed["docs"] = "\n".join(i.getdoc())
        s = json.dumps(dd, separators=(",", ":")) + "\0"
        fo.write("\nstatic const unsigned char")
        fo.write(" vsc_%s_json[%d] = {\n" % (self.name, len(s)))
        bz = bytearray(s, encoding="ascii")
        t = "\t"
        for i in bz:
            t += "%d," % i
            if len(t) >= 69:
                fo.write(t + "\n")
                t = "\t"
        if len(t) > 1:
            fo.write(t[:-1])
        fo.write("\n};\n")
        s = json.dumps(dd, indent=2, separators=(',', ': '))
        fo.write("\n")
        for i in s.split("\n"):
            j = "// " + i
            if len(j) > 72:
                fo.write(j[:72] + "[...]\n")
            else:
                fo.write(j + "\n")
        fo.write("\n")

    def emit_h(self):
        '''Emit .h file'''
        assert self.completed

        fon = "VSC_" + self.name + ".h"
        try:
            # Python3
            fo = open(fon, "w", encoding="UTF-8")
        except TypeError:
            # Python2
            fo = open(fon, "w")
        genhdr(fo, self.name)

        fo.write(self.struct + " {\n")
        for i in self.mbrs:
            s = "\tuint64_t\t%s;" % i.arg
            g = i.param.get("group")
            if g is not None:
                while len(s.expandtabs()) < 64:
                    s += "\t"
                s += "/* %s */" % g
            fo.write(s + "\n")
        fo.write("};\n")
        fo.write("\n")

        for i in self.gnames:
            fo.write(self.struct + "_" + i + " {\n")
            for j in self.groups[i]:
                fo.write("\tuint64_t\t%s;\n" % j.arg)
            fo.write("};\n")
            fo.write("\n")

        fo.write("#define VSC_" + self.name +
                 "_size PRNDUP(sizeof(" + self.struct + "))\n\n")

        fo.write("struct vsmw_cluster;\n");
        fo.write("struct vsc_seg;\n");
        fo.write("\n");

        fo.write(self.struct + " *VSC_" + self.name + "_New")
        fo.write("(struct vsmw_cluster *,\n")
        fo.write("    struct vsc_seg **, const char *fmt, ...);\n")

        fo.write("void VSC_" + self.name + "_Destroy")
        fo.write("(struct vsc_seg **);\n")

        sf = self.head.param.get('sumfunction')
        if sf is not None:
            for i in sf.split():
                j = i.split("_")
                assert len(j) <= 2
                if len(j) == 1:
                    fo.write("void VSC_" + self.name + "_Summ_" + i)
                    fo.write("(" + self.struct + " *, ")
                    fo.write("const " + self.struct + "_" + i + " *);\n")
                else:
                    fo.write("void VSC_" + self.name + "_Summ_" + i)
                    fo.write("(" + self.struct + "_" + j[0] + " *, ")
                    fo.write("const " + self.struct + "_" + j[1] + " *);\n")

    def emit_c_paranoia(self, fo):
        '''Emit asserts to make sure compiler gets same byte index'''
        fo.write("#define PARANOIA(a,n)\t\t\t\t\\\n")
        fo.write("    v_static_assert(\t\t\t\t\\\n")
        fo.write("\toffsetof(" + self.struct + ", a) == n,\t\\\n")
        fo.write("\t\"VSC element '\" #a \"' at wrong offset\")\n\n")

        for i in self.mbrs:
            fo.write("PARANOIA(" + i.arg)
            fo.write(", %d);\n" % (i.param["index"]))

        fo.write("#undef PARANOIA\n")

    def emit_c_sumfunc(self, fo, tgt):
        '''Emit a function summ up countersets'''
        fo.write("\n")
        fo.write("void\n")
        fo.write("VSC_" + self.name + "_Summ")
        fo.write("_" + tgt[0])
        if len(tgt) > 1:
            fo.write("_" + tgt[1])
            fo.write("(" + self.struct + "_" + tgt[1])
        else:
            fo.write("(" + self.struct)
        fo.write(" *dst, const " + self.struct + "_" + tgt[0] + " *src)\n")
        fo.write("{\n")
        fo.write("\n")
        fo.write("\tAN(dst);\n")
        fo.write("\tAN(src);\n")
        for i in self.groups[tgt[0]]:
            s1 = "\tdst->" + i.arg + " +="
            s2 = "src->" + i.arg + ";"
            if len((s1 + " " + s2).expandtabs()) < 79:
                fo.write(s1 + " " + s2 + "\n")
            else:
                fo.write(s1 + "\n\t    " + s2 + "\n")
        fo.write("}\n")

    def emit_c_newfunc(self, fo):
        '''Emit New function'''
        fo.write("\n")
        fo.write(self.struct + "*\n")
        fo.write("VSC_" + self.name + "_New")
        fo.write("(struct vsmw_cluster *vc,\n")
        fo.write("    struct vsc_seg **sg, const char *fmt, ...)\n")
        fo.write("{\n")
        fo.write("\tva_list ap;\n")
        fo.write("\t" + self.struct + " *retval;\n")
        fo.write("\n")
        fo.write("\tva_start(ap, fmt);\n")
        fo.write("\tretval = VRT_VSC_Allocv")
        fo.write("(vc, sg, vsc_" + self.name + "_name, ")
        fo.write("VSC_" + self.name + "_size,\n")
        fo.write("\t    vsc_" + self.name + "_json, ")
        fo.write("sizeof vsc_" + self.name + "_json, fmt, ap);\n")
        fo.write("\tva_end(ap);\n")
        fo.write("\treturn(retval);\n")
        fo.write("}\n")

    def emit_c_destroyfunc(self, fo):
        '''Emit Destroy function'''
        fo.write("\n")
        fo.write("void\n")
        fo.write("VSC_" + self.name + "_Destroy")
        fo.write("(struct vsc_seg **sg)\n")
        fo.write("{\n")
        fo.write("\tstruct vsc_seg *p;\n")
        fo.write("\n")
        fo.write("\tAN(sg);\n")
        fo.write("\tp = *sg;\n")
        fo.write("\t*sg = NULL;\n")
        fo.write('\tVRT_VSC_Destroy(vsc_%s_name, p);\n' % self.name)
        fo.write("}\n")

    def emit_c(self):
        '''Emit .c file'''
        assert self.completed
        fon = "VSC_" + self.name + ".c"
        try:
            # Python3
            fo = open(fon, "w", encoding="UTF-8")
        except TypeError:
            # Python2
            fo = open(fon, "w")
        genhdr(fo, self.name)
        fo.write('#include "config.h"\n')
        fo.write('#include <stdio.h>\n')
        fo.write('#include <stdarg.h>\n')
        fo.write('#include "vdef.h"\n')
        fo.write('#include "vas.h"\n')
        fo.write('#include "vrt.h"\n')
        fo.write('#include "VSC_%s.h"\n' % self.name)

        fo.write("\n")
        fo.write('static const char vsc_%s_name[] = "%s";\n' %
                 (self.name, self.name.upper()))

        fo.write("\n")

        self.emit_c_paranoia(fo)
        self.emit_json(fo)
        self.emit_c_newfunc(fo)
        self.emit_c_destroyfunc(fo)
        sf = self.head.param.get('sumfunction')
        if sf is not None:
            for i in sf.split():
                self.emit_c_sumfunc(fo, i.split("_"))

#######################################################################

class OurDirective(object):

    '''
        One of our `.. blablabla::` directives in the source file
    '''

    def __init__(self, s):
        ll = s.split("\n")
        i = ll.pop(0).split("::", 2)
        self.cmd = i[0]
        self.arg = i[1].strip()
        assert len(self.arg.split()) == 1

        self.param = {}
        while ll:
            j = ll[0].split(":", 2)
            if len(j) != 3 or not j[0].isspace():
                break
            self.param[j[1]] = j[2].strip()
            ll.pop(0)
        self.ldoc = ll

    def getdoc(self):
        '''
        Get docs for JSON

        Note that these docs end with the first '\n.. ' sequence
        in the .vsc file, so that we can put a longer and more
        complex description into the .RST docs than the "long"
        explanation vinylstat(1) and similar programs provide.
        '''
        while self.ldoc and self.ldoc[0].strip() == "":
            self.ldoc.pop(0)
        while self.ldoc and self.ldoc[-1].strip() == "":
            self.ldoc.pop(-1)
        return self.ldoc

    def emit_rst(self, fo):
        '''Emit the documentation as .rst'''
        assert False

class RstVscDirectiveBegin(OurDirective):

    '''
        `vinyl_vsc_begin` directive
    '''

    def __init__(self, s, vsc_set, fo):
        super(RstVscDirectiveBegin, self).__init__(s)
        vsc_set.append(CounterSet(self.arg, self))
        if fo:
            fo.write("\n..\n\t" + self.cmd + ":: " + self.arg + "\n")

            s = self.arg.upper() + " – " + self.param["oneliner"]
            fo.write("\n")
            fo.write(s + "\n")
            fo.write("=" * len(s) + "\n")
            fo.write("\n".join(self.ldoc))

class RstVscDirective(OurDirective):

    '''
        `vinyl_vsc` directive - one counter
    '''

    def __init__(self, s, vsc_set, fo):
        assert not vsc_set or vsc_set[-1].complete
        super(RstVscDirective, self).__init__(s)

        for i, v in PARAMS.items():
            if v is not None:
                if i not in self.param:
                    self.param[i] = v[0]
                if self.param[i] not in v:
                    sys.stderr.write("Wrong " + i + " '" + self.param[i])
                    sys.stderr.write("' on field '" + self.arg + "'\n")
                    exit(2)

        for p in self.param:
            if p in PARAMS:
                continue
            sys.stderr.write("Unknown parameter ")
            sys.stderr.write("'" + p + "'")
            sys.stderr.write(" on field '" + self.arg + "'\n")
            exit(2)
        self.param["index"] = vsc_set[-1].addmbr(self, self.param.get("group"))
        if fo:
            fo.write("\n``%s`` – " % self.arg)
            fo.write("`%s` - " % self.param["type"])
            fo.write("%s\n\n" % self.param["level"])

            fo.write("\t" + self.param["oneliner"] + "\n")
            fo.write("\n".join(self.ldoc))

class RstVscDirectiveEnd(OurDirective):

    '''
        `vinyl_vsc_end` directive
    '''

    def __init__(self, s, vsc_set, fo):
        super(RstVscDirectiveEnd, self).__init__(s)
        vsc_set[-1].complete(self.arg)
        if fo:
            fo.write("\n..\n\t" + self.cmd + ":: " + self.arg + "\n")
            fo.write("\n".join(self.ldoc))

#######################################################################

def mainfunc(argv):

    '''Process a .vsc file'''

    optlist, args = getopt.getopt(argv[1:], "chr")

    if len(args) != 1:
        sys.stderr.write("Need exactly one filename argument\n")
        exit(2)

    rstfile = None
    for f, v in optlist:
        if f == '-r':
            try:
                # Python3
                sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
            except AttributeError:
                # Python2
                pass
            rstfile = sys.stdout

    vscset = []
    try:
        # Python3
        f = open(args[0], encoding="UTF-8")
    except TypeError:
        # Python2
        f = open(args[0])
    scs = f.read().split("\n.. ")
    if rstfile:
        rstfile.write(scs[0])
    for i in scs[1:]:
        j = i.split(None, 1)
        f = {
            "vinyl_vsc_begin::":  RstVscDirectiveBegin,
            "vinyl_vsc::":        RstVscDirective,
            "vinyl_vsc_end::":    RstVscDirectiveEnd,
        }.get(j[0])
        if f is not None:
            f(i, vscset, rstfile)
        elif rstfile:
            rstfile.write("\n.. " + i)

    for i in vscset:
        for f, v in optlist:
            if f == '-h':
                i.emit_h()
            if f == '-c':
                i.emit_c()

if __name__ == "__main__":

    mainfunc(sys.argv)
