#!/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

# 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("Failed to load interface definition file {0}".format(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("Interface definition file {0} does not match the schema!".format(input_file))
        sys.exit(1)
except Exception as e:
    print("Failed to load the XML schema {0}".format(schema_file))
    print(e)
    sys.exit(1)

if not os.access(output_dir, os.W_OK):
    print("The output directory {0} is not writeable".format(output_dir))
    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 listNodes {0}".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:
        node_def += "help: {0}\n".format(props["help"])


    if "comp_help" in props:
        node_def += "allowed: {0}\n".format(props["comp_help"])


    if command is not None:
        node_def += "run: {0}\n".format(command.text)


    if debug:
        print("The 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("Name of the node: {};\n Created directory: ".format(name), end="")
    os.makedirs(make_path(my_tmpl_dir), exist_ok=True)

    props = get_properties(props_elem)

    if node_type == "node":
        if debug:
          print("Processing node {}".format(name))

        nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
        if not os.path.exists(nodedef_path):
            with open(nodedef_path, "w") as f:
                f.write(make_node_def(props, command))
        else:
           # Something has already generated this file
           pass

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

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

        nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def")
        if not os.path.exists(nodedef_path):
            with open(nodedef_path, "w") as f:
                f.write('help: {0}\n'.format(props['help']))
        else:
            # Something has already generated this file
            pass

        # 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
        with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "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)
    else:
        # This is a leaf node
        if debug:
            print("Processing leaf node {}".format(name))

        with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f:
            f.write(make_node_def(props, command))


root = xml.getroot()

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