#! /usr/bin/python3

import getopt
import sys
import os.path
import xml.dom.minidom

from ovs_build_helpers import nroff
from ovs_build_helpers.extract_ofp_fields import (
    extract_ofp_fields,
    PREREQS,
    OXM_CLASSES,
    VERSION,
    fatal,
    n_errors,
)

VERSION_REVERSE = dict((v, k) for k, v in VERSION.items())


def oxm_name_to_class(name):
    prefix = ""
    class_ = None
    for p, c in OXM_CLASSES.items():
        if name.startswith(p) and len(p) > len(prefix):
            prefix = p
            class_ = c
    return class_


def is_standard_oxm(name):
    oxm_vendor, oxm_class, oxm_class_type = oxm_name_to_class(name)
    return oxm_class_type == "standard"


def usage():
    argv0 = os.path.basename(sys.argv[0])
    print(
        """\
%(argv0)s, for extracting OpenFlow field properties from meta-flow.h
usage: %(argv0)s INPUT [--meta-flow | --nx-match]
  where INPUT points to lib/meta-flow.h in the source directory.
Depending on the option given, the output written to stdout is intended to be
saved either as lib/meta-flow.inc or lib/nx-match.inc for the respective C
file to #include.\
"""
        % {"argv0": argv0}
    )
    sys.exit(0)


def protocols_to_c(protocols):
    if protocols == set(["of10", "of11", "oxm"]):
        return "OFPUTIL_P_ANY"
    elif protocols == set(["of11", "oxm"]):
        return "OFPUTIL_P_NXM_OF11_UP"
    elif protocols == set(["oxm"]):
        return "OFPUTIL_P_NXM_OXM_ANY"
    elif protocols == set([]):
        return "OFPUTIL_P_NONE"
    else:
        assert False


def autogen_c_comment():
    return [
        "/* Generated automatically; do not modify!    "
        "-*- buffer-read-only: t -*- */",
        "",
    ]


def make_meta_flow(meta_flow_h):
    fields = extract_ofp_fields(meta_flow_h)
    output = autogen_c_comment()
    for f in fields:
        output += ["{"]
        output += ["    %s," % f["mff"]]
        if f["extra_name"]:
            output += ['    "%s", "%s",' % (f["name"], f["extra_name"])]
        else:
            output += ['    "%s", NULL,' % f["name"]]

        if f["variable"]:
            variable = "true"
        else:
            variable = "false"
        output += ["    %d, %d, %s," % (f["n_bytes"], f["n_bits"], variable)]

        if f["writable"]:
            rw = "true"
        else:
            rw = "false"
        output += [
            "    %s, %s, %s, %s, false,"
            % (f["mask"], f["string"], PREREQS[f["prereqs"]], rw)
        ]

        oxm = f["OXM"]
        of10 = f["OF1.0"]
        of11 = f["OF1.1"]
        if f["mff"] in ("MFF_DL_VLAN", "MFF_DL_VLAN_PCP"):
            # MFF_DL_VLAN and MFF_DL_VLAN_PCP don't exactly correspond to
            # OF1.1, nor do they have NXM or OXM assignments, but their
            # meanings can be expressed in every protocol, which is the goal of
            # this member.
            protocols = set(["of10", "of11", "oxm"])
        else:
            protocols = set([])
            if of10:
                protocols |= set(["of10"])
            if of11:
                protocols |= set(["of11"])
            if oxm:
                protocols |= set(["oxm"])

        if f["mask"] == "MFM_FULLY":
            cidr_protocols = protocols.copy()
            bitwise_protocols = protocols.copy()

            if of10 == "exact match":
                bitwise_protocols -= set(["of10"])
                cidr_protocols -= set(["of10"])
            elif of10 == "CIDR mask":
                bitwise_protocols -= set(["of10"])
            else:
                assert of10 is None

            if of11 == "exact match":
                bitwise_protocols -= set(["of11"])
                cidr_protocols -= set(["of11"])
            else:
                assert of11 in (None, "bitwise mask")
        else:
            assert f["mask"] == "MFM_NONE"
            cidr_protocols = set([])
            bitwise_protocols = set([])

        output += ["    %s," % protocols_to_c(protocols)]
        output += ["    %s," % protocols_to_c(cidr_protocols)]
        output += ["    %s," % protocols_to_c(bitwise_protocols)]

        if f["prefix"]:
            output += ["    FLOW_U32OFS(%s)," % f["prefix"]]
        else:
            output += ["    -1, /* not usable for prefix lookup */"]

        output += ["},"]
    for oline in output:
        print(oline)


def make_nx_match(meta_flow_h):
    fields = extract_ofp_fields(meta_flow_h)
    output = autogen_c_comment()
    print("static struct nxm_field_index all_nxm_fields[] = {")
    for f in fields:
        # Sort by OpenFlow version number (nx-match.c depends on this).
        for oxm in sorted(f["OXM"], key=lambda x: x[2]):
            header = "NXM_HEADER(0x%x,0x%x,%s,0,%d)" % oxm[0]
            print(
                """{ .nf = { %s, %d, "%s", %s } },"""
                % (header, oxm[2], oxm[1], f["mff"])
            )
    print("};")
    for oline in output:
        print(oline)


# ------------------------ #
# Documentation Generation #
# ------------------------ #


def field_to_xml(field_node, f, body, summary):
    f["used"] = True

    # Summary.
    if field_node.hasAttribute("internal"):
        return

    min_of_version = None
    min_ovs_version = None
    for header, name, of_version_nr, ovs_version_s in f["OXM"]:
        if is_standard_oxm(name) and (
            min_ovs_version is None or of_version_nr < min_of_version
        ):
            min_of_version = of_version_nr
        ovs_version = [int(x) for x in ovs_version_s.split(".")]
        if min_ovs_version is None or ovs_version < min_ovs_version:
            min_ovs_version = ovs_version
    summary += [r"\fB%s\fR" % f["name"]]
    if f["extra_name"]:
        summary += [r" aka \fB%s\fR" % f["extra_name"]]
    summary += [";%d" % f["n_bytes"]]
    if f["n_bits"] != 8 * f["n_bytes"]:
        summary += [" (low %d bits)" % f["n_bits"]]
    summary += [";%s;" % {"MFM_NONE": "no", "MFM_FULLY": "yes"}[f["mask"]]]
    summary += ["%s;" % {True: "yes", False: "no"}[f["writable"]]]
    summary += ["%s;" % f["prereqs"]]
    support = []
    if min_of_version is not None:
        support += ["OF %s+" % VERSION_REVERSE[min_of_version]]
    if min_ovs_version is not None:
        support += ["OVS %s+" % ".".join([str(x) for x in min_ovs_version])]
    summary += " and ".join(support)
    summary += ["\n"]

    # Full description.
    if field_node.hasAttribute("hidden"):
        return

    title = field_node.attributes["title"].nodeValue

    body += [
        r""".PP
\fB%s Field\fR
.TS
tab(;),nowarn;
l lx.
"""
        % title
    ]

    body += [r"Name:;\fB%s\fR" % f["name"]]
    if f["extra_name"]:
        body += [r" (aka \fB%s\fR)" % f["extra_name"]]
    body += ["\n"]

    body += ["Width:;"]
    if f["n_bits"] != 8 * f["n_bytes"]:
        body += [
            "%d bits (only the least-significant %d bits "
            "may be nonzero)" % (f["n_bytes"] * 8, f["n_bits"])
        ]
    elif f["n_bits"] <= 128:
        body += ["%d bits" % f["n_bits"]]
    else:
        body += ["%d bits (%d bytes)" % (f["n_bits"], f["n_bits"] / 8)]
    body += ["\n"]

    body += ["Format:;%s\n" % f["formatting"]]

    masks = {
        "MFM_NONE": "not maskable",
        "MFM_FULLY": "arbitrary bitwise masks",
    }
    body += ["Masking:;%s\n" % masks[f["mask"]]]
    body += ["Prerequisites:;%s\n" % f["prereqs"]]

    access = {True: "read/write", False: "read-only"}[f["writable"]]
    body += ["Access:;%s\n" % access]

    of10 = {
        None: "not supported",
        "exact match": "yes (exact match only)",
        "CIDR mask": "yes (CIDR match only)",
    }
    body += ["OpenFlow 1.0:;%s\n" % of10[f["OF1.0"]]]

    of11 = {
        None: "not supported",
        "exact match": "yes (exact match only)",
        "bitwise mask": "yes",
    }
    body += ["OpenFlow 1.1:;%s\n" % of11[f["OF1.1"]]]

    oxms = []
    for header, name, of_version_nr, ovs_version in [
        x
        for x in sorted(f["OXM"], key=lambda x: x[2])
        if is_standard_oxm(x[1])
    ]:
        of_version = VERSION_REVERSE[of_version_nr]
        oxms += [
            r"\fB%s\fR (%d) since OpenFlow %s and Open vSwitch %s"
            % (name, header[2], of_version, ovs_version)
        ]
    if not oxms:
        oxms = ["none"]
    body += ["OXM:;T{\n%s\nT}\n" % r"\[char59] ".join(oxms)]

    nxms = []
    for header, name, of_version_nr, ovs_version in [
        x
        for x in sorted(f["OXM"], key=lambda x: x[2])
        if not is_standard_oxm(x[1])
    ]:
        nxms += [
            r"\fB%s\fR (%d) since Open vSwitch %s"
            % (name, header[2], ovs_version)
        ]
    if not nxms:
        nxms = ["none"]
    body += ["NXM:;T{\n%s\nT}\n" % r"\[char59] ".join(nxms)]

    body += [".TE\n"]

    body += [".PP\n"]
    body += [nroff.block_xml_to_nroff(field_node.childNodes)]


def group_xml_to_nroff(group_node, fields):
    title = group_node.attributes["title"].nodeValue

    summary = []
    body = []
    for node in group_node.childNodes:
        if node.nodeType == node.ELEMENT_NODE and node.tagName == "field":
            id_ = node.attributes["id"].nodeValue
            field_to_xml(node, fields[id_], body, summary)
        else:
            body += [nroff.block_xml_to_nroff([node])]

    content = [
        ".bp\n",
        '.SH "%s"\n' % nroff.text_to_nroff(title.upper() + " FIELDS"),
        '.SS "Summary:"\n',
        ".TS\n",
        "tab(;),nowarn;\n",
        "l l l l l l.\n",
        "Name;Bytes;Mask;RW?;Prereqs;NXM/OXM Support\n",
        r"\_;\_;\_;\_;\_;\_",
        "\n",
    ]
    content += summary
    content += [".TE\n"]
    content += body
    return "".join(content)


def make_oxm_classes_xml(document):
    s = r"""tab(;),nowarn;
l l l.
Prefix;Vendor;Class
\_;\_;\_
"""
    for key in sorted(OXM_CLASSES, key=OXM_CLASSES.get):
        vendor, class_, class_type = OXM_CLASSES.get(key)
        s += r"\fB%s\fR;" % key.rstrip("_")
        if vendor:
            s += r"\fL0x%08x\fR;" % vendor
        else:
            s += "(none);"
        s += r"\fL0x%04x\fR;" % class_
        s += "\n"
    e = document.createElement("tbl")
    e.appendChild(document.createTextNode(s))
    return e


def recursively_replace(node, name, replacement):
    for child in node.childNodes:
        if child.nodeType == node.ELEMENT_NODE:
            if child.tagName == name:
                node.replaceChild(replacement, child)
            else:
                recursively_replace(child, name, replacement)


def make_ovs_fields(meta_flow_h, meta_flow_xml):
    fields = extract_ofp_fields(meta_flow_h)
    fields_map = {}
    for f in fields:
        fields_map[f["mff"]] = f

    document = xml.dom.minidom.parse(meta_flow_xml)
    doc = document.documentElement

    global version
    if version is None:
        version = "UNKNOWN"

    print(
        r"""'\" tp
.\" -*- mode: troff; coding: utf-8 -*-
.TH "ovs\-fields" 7 "%s" "Open vSwitch" "Open vSwitch Manual"
.fp 5 L CR              \" Make fixed-width font available as \fL.
.de ST
.  PP
.  RS -0.15in
.  I "\\$1"
.  RE
..

.de SU
.  PP
.  I "\\$1"
..

.de IQ
.  br
.  ns
.  IP "\\$1"
..

.de TQ
.  br
.  ns
.  TP "\\$1"
..
.de URL
\\$2 \(laURL: \\$1 \(ra\\$3
..
.if \n[.g] .mso www.tmac
.SH NAME
ovs\-fields \- protocol header fields in OpenFlow and Open vSwitch
.
.PP
"""
        % version
    )

    recursively_replace(doc, "oxm_classes", make_oxm_classes_xml(document))

    s = ""
    for node in doc.childNodes:
        if node.nodeType == node.ELEMENT_NODE and node.tagName == "group":
            s += group_xml_to_nroff(node, fields_map)
        elif node.nodeType == node.TEXT_NODE:
            assert node.data.isspace()
        elif node.nodeType == node.COMMENT_NODE:
            pass
        else:
            s += nroff.block_xml_to_nroff([node])

    for f in fields:
        if "used" not in f:
            fatal(
                "%s: field not documented "
                "(please add documentation in lib/meta-flow.xml)" % f["mff"]
            )
    if n_errors:
        sys.exit(1)

    output = []
    for oline in s.split("\n"):
        oline = oline.strip()

        # Life is easier with nroff if we don't try to feed it Unicode.
        # Fortunately, we only use a few characters outside the ASCII range.
        oline = oline.replace(u"\u2208", r"\[mo]")
        oline = oline.replace(u"\u2260", r"\[!=]")
        oline = oline.replace(u"\u2264", r"\[<=]")
        oline = oline.replace(u"\u2265", r"\[>=]")
        oline = oline.replace(u"\u00d7", r"\[mu]")
        if len(oline):
            output += [oline]

    # nroff tends to ignore .bp requests if they come after .PP requests,
    # so remove .PPs that precede .bp.
    for i in range(len(output)):
        if output[i] == ".bp":
            j = i - 1
            while j >= 0 and output[j] == ".PP":
                output[j] = None
                j -= 1
    for i in range(len(output)):
        if output[i] is not None:
            print(output[i])


# ------------ #
# Main Program #
# ------------ #

if __name__ == "__main__":
    argv0 = sys.argv[0]
    try:
        options, args = getopt.gnu_getopt(
            sys.argv[1:], "h", ["help", "ovs-version="]
        )
    except getopt.GetoptError as geo:
        sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
        sys.exit(1)

    global version
    version = None
    for key, value in options:
        if key in ["-h", "--help"]:
            usage()
        elif key == "--ovs-version":
            version = value
        else:
            sys.exit(0)

    if not args:
        sys.stderr.write(
            "%s: missing command argument " "(use --help for help)\n" % argv0
        )
        sys.exit(1)

    commands = {
        "meta-flow": (make_meta_flow, 1),
        "nx-match": (make_nx_match, 1),
        "ovs-fields": (make_ovs_fields, 2),
    }

    if not args[0] in commands:
        sys.stderr.write(
            '%s: unknown command "%s" '
            "(use --help for help)\n" % (argv0, args[0])
        )
        sys.exit(1)

    func, n_args = commands[args[0]]
    if len(args) - 1 != n_args:
        sys.stderr.write(
            '%s: "%s" requires %d arguments but %d '
            "provided\n" % (argv0, args[0], n_args, len(args) - 1)
        )
        sys.exit(1)

    func(*args[1:])
