#!/usr/bin/env python3
#
#    build-command-template: converts old style commands definitions to XML
#
#    Copyright (C) 2019 VyOS maintainers <maintainers@vyos.net>
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
#    USA


import os
import re
import sys

from lxml import etree


# Node types
NODE = 0
LEAF_NODE = 1
TAG_NODE = 2

def parse_command_data(t):
    regs = {
        'help': r'\bhelp:(.*)(?:\n|$)',
        'priority': r'\bpriority:(.*)(?:\n|$)',
        'type': r'\btype:(.*)(?:\n|$)',
        'syntax_expression_var': r'\bsyntax:expression: \$VAR\(\@\) in (.*)'
    }

    data = {'multi': False, 'help': ""}

    for r in regs:
        try:
            data[r] = re.search(regs[r], t).group(1).strip()
        except:
            data[r] = None

    # val_help is special: there can be multiple instances
    val_help_strings = re.findall(r'\bval_help:(.*)(?:\n|$)', t)
    val_help = []
    for v in val_help_strings:
        try:
            fmt, msg = re.match(r'\s*(.*)\s*;\s*(.*)\s*(?:\n|$)', v).groups()
        except:
            fmt = "<text>"
            msg = v
        val_help.append((fmt, msg))
    data['val_help'] = val_help

    # multi is on/off
    if re.match(r'\bmulti:', t):
        data['multi'] = True

    return(data)

def walk(tree, base_path, name):
    path = os.path.join(base_path, name)

    contents = os.listdir(path)

    # Determine node type and create XML element for the node
    # Tag node dirs will always have 'node.tag' subdir and 'node.def' file
    # Leaf node dirs have nothing but a 'node.def' file
    # Everything that doesn't match either of these patterns is a normal node
    if 'node.tag' in contents:
        print("Creating a tag node from {0}".format(path))
        elem = etree.Element('tagNode')
        node_type = TAG_NODE
    elif contents == ['node.def']:
        print("Creating a leaf node from {0}".format(path))
        elem = etree.Element('leafNode')
        node_type = LEAF_NODE
    else:
        print("Creating a node from {0}".format(path))
        elem = etree.Element('node')
        node_type = NODE

    # Read and parse the command definition data (the 'node.def' file)
    with open(os.path.join(path, 'node.def'), 'r') as f:
        node_def = f.read()
    data = parse_command_data(node_def)

    # Import the data into the properties element
    props_elem = etree.Element('properties')

    if data['priority']:
        # Priority values sometimes come with comments that explain the value choice
        try:
            prio, prio_comment = re.match(r'\s*(\d+)\s*#(.*)', data['priority']).groups()
        except:
            prio = data['priority'].strip()
            prio_comment = None
        prio_elem = etree.Element('priority')
        prio_elem.text = prio
        props_elem.append(prio_elem)
        if prio_comment:
            prio_comment_elem = etree.Comment(prio_comment)
            props_elem.append(prio_comment_elem)

    if data['multi']:
        multi_elem = etree.Element('multi')
        props_elem.append(multi_elem)

    if data['help']:
        help_elem = etree.Element('help')
        help_elem.text = data['help']
        props_elem.append(help_elem)

    # For leaf nodes, absense of a type: tag means they take no values
    # For any other nodes, it doesn't mean anything
    if not data['type'] and (node_type == LEAF_NODE):
        valueless = etree.Element('valueless')
        props_elem.append(valueless)

    # There can be only one constraint element in the definition
    # Create it now, we'll modify it in the next two cases, then append
    constraint_elem = etree.Element('constraint')
    has_constraint = False

    # Add regexp field for multiple options
    if data['syntax_expression_var']:
        regex = etree.Element('regex')
        constraint_error=etree.Element('constraintErrorMessage')
        values = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(1)
        message = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(2)
        values = re.findall(r'\"(.+?)\"', values)
        regex.text = '|'.join(values)
        constraint_error.text = re.sub('\".*?VAR.*?\"', '', message)
        constraint_error.text = re.sub(r'[\"|\\]', '', message)
        constraint_elem.append(regex)
        props_elem.append(constraint_elem)
        props_elem.append(constraint_error)

    if data['val_help']:
        for vh in data['val_help']:
            vh_elem = etree.Element('valueHelp')

            vh_fmt_elem = etree.Element('format')
            # Many commands use special "u32:<start>-<end>" format for ranges
            if re.match(r'u32:', vh[0]):
                vh_fmt = re.match(r'u32:(.*)', vh[0]).group(1).strip()

                # If valid range of values is specified in val_help, we can automatically
                # create a constraint for it
                # Extracting it from syntax:expression: would be much more complicated
                vh_validator = etree.Element('validator')
                vh_validator.set("name", "numeric")
                vh_validator.set("argument", "--range {0}".format(vh_fmt))
                constraint_elem.append(vh_validator)
                has_constraint = True
            else:
                vh_fmt = vh[0]
            vh_fmt_elem.text = vh_fmt

            vh_help_elem = etree.Element('description')
            vh_help_elem.text = vh[1]

            vh_elem.append(vh_fmt_elem)
            vh_elem.append(vh_help_elem)
            props_elem.append(vh_elem)

    # Translate the "type:" to the new validator system
    if data['type']:
        t = data['type']
        if t == 'txt':
            # Can't infer anything from the generic "txt" type
            pass
        else:
            validator = etree.Element('validator')
            if t == 'u32':
                validator.set('name', 'numeric')
                validator.set('argument', '--non-negative')
            elif t == 'ipv4':
                validator.set('name', 'ipv4-address')
            elif t == 'ipv4net':
                validator.set('name', 'ipv4-prefix')
            elif t == 'ipv6':
                validator.set('name', 'ipv6-address')
            elif t == 'ipv6net':
                validator.set('name', 'ipv6-prefix')
            elif t == 'macaddr':
                validator.set('name', 'mac-address')
            else:
                print("Warning: unsupported type \'{0}\'".format(t))
                validator = None

            if (validator is not None) and (not has_constraint):
                # If has_constraint is true, it means a more specific validator
                # was already extracted from another option
                constraint_elem.append(validator)
                has_constraint = True

    if has_constraint:
        props_elem.append(constraint_elem)

    elem.append(props_elem)

    elem.set("name", name)

    if node_type != LEAF_NODE:
        children = etree.Element('children')

        # Create the next level dir path,
        # accounting for the "virtual" node.tag subdir for tag nodes
        next_level = path
        if node_type == TAG_NODE:
            next_level = os.path.join(path, 'node.tag')

        # Walk the subdirs of the next level
        for d in os.listdir(next_level):
            dp = os.path.join(next_level, d)
            if os.path.isdir(dp):
                walk(children, next_level, d)

        elem.append(children)

    tree.append(elem)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: {0} <base path>".format(sys.argv[0]))
        sys.exit(1)
    else:
        base_path = sys.argv[1]

    root = etree.Element('interfaceDefinition')
    contents = os.listdir(base_path)
    elem = etree.Element('node')
    elem.set('name', os.path.basename(base_path))
    children = etree.Element('children')

    for c in contents:
        path = os.path.join(base_path, c)
        if os.path.isdir(path):
            walk(children, base_path, c)

    elem.append(children)
    root.append(elem)

    xml_data = etree.tostring(root, pretty_print=True).decode()
    with open('output.xml', 'w') as f:
        f.write(xml_data)