#!/usr/bin/env python3
#
#    build-command-template: converts new style command definitions in XML
#      to the old style (bunch of dirs and node.def's) command templates
#
#    Copyright (C) 2017 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 sys
import os
import argparse
import copy
import functools

from lxml import etree as ET
from textwrap import fill

# Defaults
validator_dir = "/opt/vyatta/libexec/validators"
default_constraint_err_msg = "Invalid value"

## Get arguments
parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates')
parser.add_argument('--debug', help='Enable debug information output', action='store_true')
parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file")
parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file")
parser.add_argument('OUTPUT_DIR', type=str, help="Output directory")

args = parser.parse_args()

input_file = args.INPUT_FILE
schema_file = args.SCHEMA_FILE
output_dir = args.OUTPUT_DIR
debug = args.debug

## Load and validate the inputs
try:
    xml = ET.parse(input_file)
except Exception as e:
    print(f"Failed to load interface definition file {input_file}")
    print(e)
    sys.exit(1)

try:
    relaxng_xml = ET.parse(schema_file)
    validator = ET.RelaxNG(relaxng_xml)

    if not validator.validate(xml):
        print(validator.error_log)
        print(f"Interface definition file {input_file} does not match the schema!")
        sys.exit(1)
except Exception as e:
    print(f"Failed to load the XML schema {schema_file}")
    print(e)
    sys.exit(1)

if not os.access(output_dir, os.W_OK):
    print(f"The output directory {output_dir} is not writeable")
    sys.exit(1)

## If we got this far, everything must be ok and we can convert the file
def make_path(l):
    path = functools.reduce(os.path.join, l)
    if debug:
        print(path)
    return path

def get_properties(p):
    props = {}

    if p is None:
        return props

    # Get the help string
    try:
        props["help"] = p.find("help").text
    except:
        props["help"] = "No help available"


    # Get the completion help strings
    try:
        che = p.findall("completionHelp")
        ch = ""
        for c in che:
            scripts = c.findall("script")
            paths = c.findall("path")
            lists = c.findall("list")

            # Current backend doesn't support multiple allowed: tags
            # so we get to emulate it
            comp_exprs = []
            for i in lists:
                comp_exprs.append("echo \"{0}\"".format(i.text))
            for i in paths:
                comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text))
            for i in scripts:
                comp_exprs.append("{0}".format(i.text))
            comp_help = " && ".join(comp_exprs)
            props["comp_help"] = comp_help
    except:
        props["comp_help"] = []

    return props


def make_node_def(props, command):
    # XXX: replace with a template processor if it grows
    #      out of control
    node_def = ""

    if "help" in props:
        help = props["help"]
        help = fill(help, width=64, subsequent_indent='\t\t\t')
        node_def += f'help: {help}\n'
    if "comp_help" in props:
        node_def += f'allowed: {props["comp_help"]}\n'
    if command is not None:
        node_def += f'run: {command.text}\n'
    if debug:
        print('Contents of the node.def file:\n', node_def)

    return node_def

def process_node(n, tmpl_dir):
    # Avoid mangling the path from the outer call
    my_tmpl_dir = copy.copy(tmpl_dir)

    props_elem = n.find("properties")
    children = n.find("children")
    command = n.find("command")
    name = n.get("name")

    node_type = n.tag

    my_tmpl_dir.append(name)

    if debug:
        print(f"Name of the node: {name};\n Created directory: ", end="")
    os.makedirs(make_path(my_tmpl_dir), exist_ok=True)

    props = get_properties(props_elem)

    nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
    if node_type == "node":
        if debug:
          print(f"Processing node {name}")

        # Only create the "node.def" file if it exists but is empty, or if it
        # does not exist at all.
        if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0:
            with open(nodedef_path, "w") as f:
                f.write(make_node_def(props, command))

        if children is not None:
            inner_nodes = children.iterfind("*")
            for inner_n in inner_nodes:
                process_node(inner_n, my_tmpl_dir)
    elif node_type == "tagNode":
        if debug:
          print(f"Processing tagNode {name}")

        os.makedirs(make_path(my_tmpl_dir), exist_ok=True)

        # Only create the "node.def" file if it exists but is empty, or if it
        # does not exist at all.
        if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0:
            with open(nodedef_path, "w") as f:
                f.write('help: {0}\n'.format(props['help']))

        # Create the inner node.tag part
        my_tmpl_dir.append("node.tag")
        os.makedirs(make_path(my_tmpl_dir), exist_ok=True)
        if debug:
            print("Created path for the tagNode: {}".format(make_path(my_tmpl_dir)), end="")

        # Not sure if we want partially defined tag nodes, write the file unconditionally
        nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
        # Only create the "node.def" file if it exists but is empty, or if it
        # does not exist at all.
        if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0:
            with open(nodedef_path, "w") as f:
                f.write(make_node_def(props, command))

        if children is not None:
            inner_nodes = children.iterfind("*")
            for inner_n in inner_nodes:
                process_node(inner_n, my_tmpl_dir)
    elif node_type == "leafNode":
        # This is a leaf node
        if debug:
            print(f"Processing leaf node {name}")

        if not os.path.exists(nodedef_path) or os.path.getsize(nodedef_path) == 0:
            with open(nodedef_path, "w") as f:
                f.write(make_node_def(props, command))
    else:
        print(f"Unknown node_type: {node_type}")


def get_node_key(node, attr=None):
    """ Return the sorting key of an xml node using tag and attributes """
    if attr is None:
        return '%s' % node.tag + ':'.join([node.get(attr)
                                        for attr in sorted(node.attrib)])
    if attr in node.attrib:
        return '%s:%s' % (node.tag, node.get(attr))
    return '%s' % node.tag


def sort_children(node, attr=None):
    """ Sort children along tag and given attribute. if attr is None, sort
    along all attributes """
    if not isinstance(node.tag, str):  # PYTHON 2: use basestring instead
        # not a TAG, it is comment or DATA
        # no need to sort
        return
    # sort child along attr
    node[:] = sorted(node, key=lambda child: get_node_key(child, attr))
    # and recurse
    for child in node:
        sort_children(child, attr)

root = xml.getroot()

# process_node() processes the XML tree in a fixed order, "node" before "tagNode"
# before "leafNode". If the generator created a "node.def" file, it can no longer
# be overwritten - else we would have some stale "node.def" files with an empty
# help string (T2555). Without the fixed order this would resulted in a case
# where we get a node and a tagNode with the same name, e.g. "show interfaces
# ethernet" and "show interfaces ethernet eth0" that the node implementation
# was not callable from the CLI, rendering this command useless (T3807).
#
# This can be fixed by forcing the "node", "tagNode", "leafNode" order by sorting
# the input XML file automatically (sorting from https://stackoverflow.com/a/46128043)
# thus adding no additional overhead to the user.
sort_children(root, 'name')

nodes = root.iterfind("*")
for n in nodes:
    process_node(n, [output_dir])