#!/usr/bin/python3


import argparse
import datetime
import io
import os.path
import re
import sys
from lxml import etree
from phply import phplex

import rad.console
import rad.pagelib as pagelib
from rad.php import PHPArray, PHPDict, PHPLiteral, PHPFunctionCall

SCHEMA_FILE_ROUTES = "routes.xsd"

class RouteParserException(Exception):
    pass

def gentree_pages(basedirs):
    class Node(object):
        def __init__(self, name):
            self._name = name
            self._params = PHPDict()
        def get_name(self):
            return self._name
        def get_params(self):
            return self._params
        def set_weight(self, weight):
            self._weight = weight
        def get_weight(self):
            return self._weight

    class LeafNode(Node):
        def __init__(self, name):
            super(LeafNode, self).__init__(name)
        def to_php(self):
            return PHPFunctionCall("new LeafNode",
                    PHPLiteral(self.get_name()), self.get_params())

    class DirectoryNode(Node):
        def __init__(self, name, children):
            super(DirectoryNode, self).__init__(name)
            self._children = children
        def to_php(self):
            return PHPFunctionCall("new DirectoryNode",
                    PHPLiteral(self.get_name()), self.get_params(),
                               self._children)

    def fetch_attribs(node, attribs, outdict):
        for (aname, atype) in attribs:
            if aname in node.attrib:
                val = node.attrib[aname]
                if atype is bool:
                    val = pagelib.xmlbool(val)
                elif atype is int:
                    val = int(val)
                outdict[aname] = PHPLiteral(val)

    def parse_model(model, params):
        def parse_query_args(node, qdef):
            # TODO: validation

            # parse <rad:foreign> args
            args = PHPArray()
            for el in node.findall(pagelib.RAD + "foreign"):
                adef = PHPDict()
                fetch_attribs(el, [("field", str), ("to-record", str),
                    ("to-field", str), ("required", bool)], adef)
                args.append(adef)
            qdef["args-foreign"] = args

            # parse <rad:var> args
            args = PHPArray()
            for el in node.findall(pagelib.RAD + "var"):
                adef = PHPDict()
                fetch_attribs(el, [("name", str), ("field", str),
                    ("required", bool)], adef)
                args.append(adef)
            qdef["args-var"] = args

            # parse <rad:urlparam> args
            args = PHPArray()
            for el in node.findall(pagelib.RAD + "urlparam"):
                adef = PHPDict()
                fetch_attribs(el, [("name", str), ("field", str),
                    ("required", bool)], adef)
                args.append(adef)
            qdef["args-urlparam"] = args

        def parse_subquery(node):
            sdef = PHPDict()
            sdef["sql"] = PHPLiteral(node.find(pagelib.RAD + "sql").text)

            binds = PHPDict()
            for bind in node.findall(pagelib.RAD + "bind"):
                binds[bind.attrib["child"]] = PHPLiteral(bind.attrib["parent"])
            sdef["binds"] = binds

            subqueries = PHPDict()
            for subq in node.findall(pagelib.RAD + "subquery"):
                (name, subqdict) = parse_subquery(subq)
                subqueries[name] = subqdict
            sdef["subqueries"] = subqueries

            return (node.attrib["name"], sdef)

        def parse_emptyset(node):
            edef = PHPDict({"required": PHPLiteral(True)})
            fetch_attribs(node, [("name", str)], edef)
            subsets = PHPDict()
            for subset in node.findall(pagelib.RAD + "emptyset"):
                (name, subdict) = parse_emptyset(subset)
                subsets[name] = subdict
            edef["subsets"] = subsets

            return (node.attrib["name"], edef)

        definition = PHPDict()
        queries = PHPDict()

        for query in model.findall(pagelib.RAD + "query"):
            qdef = PHPDict({"type": PHPLiteral("sql")})
            fetch_attribs(query, [("name", str), ("required", bool)], qdef)
            qdef["sql"] = PHPLiteral(query.find(pagelib.RAD + "sql").text)
            parse_query_args(query, qdef)

            subqueries = PHPDict()
            for subq in query.findall(pagelib.RAD + "subquery"):
                (name, subqdict) = parse_subquery(subq)
                subqueries[name] = subqdict
            qdef["subqueries"] = subqueries

            queries[query.attrib["name"]] = qdef

        for emptyset in model.findall(pagelib.RAD + "emptyset"):
            (name, edef) = parse_emptyset(emptyset)
            edef["type"] = PHPLiteral("emptyset")
            queries[name] = edef

        definition["queries"] = queries

        primary = model.find(pagelib.RAD + "primary")
        if primary is not None:
            pdef = PHPDict()
            fetch_attribs(primary, [("query", str), ("field", str),
                ("allow-add", str)], pdef)
            if pdef["allow-add"].value == "only" and "field" in pdef:
                raise pagelib.PageParserException("Attribute allow-add='only' of primary record cannot be used in combination with the 'field' attribute.")
            if ("field" in pdef) != params["requires-arguments"].value:
                raise pagelib.PageParserException("Attribute 'field' of primary record must be present iff the page's 'requires-arguments' attribute is set.")
            if not queries[primary.attrib["query"]]["required"].value:
                raise pagelib.PageParserException("Primary record must point to a query that is marked as 'required'.")
            definition["primary"] = pdef
            params["allow-add"] = pdef["allow-add"]
            params["has-primary"] = PHPLiteral(True)

        params["model"] = definition

    def parse_page_impl(realpath, virtpath, node):
        if parse_page_impl.parser is None:
            schema = pagelib.get_schema()
            parse_page_impl.parser = etree.XMLParser(schema=schema,
                    remove_comments=True, attribute_defaults=True)
        parser = parse_page_impl.parser

        #print("Parsing file '{}'...".format(realpath))
        with open(realpath, "r") as f:
            tree = etree.parse(f, parser=parser)
        root = tree.getroot()

        node.set_weight(float(root.attrib["menu-weight"]))

        params = node.get_params()
        params["full-name"] = PHPLiteral(virtpath)
        fetch_attribs(root, [("title", str), ("title-new", str),
            ("title-breadcrumb", str), ("requires-arguments", bool),
            ("layout", str), ("show-in-menu", bool),
            ("menu-icon", str), ("menu-icon-color", str),
            ("menu-max-depth", int), ("menu-new-window", bool)], params)

        if root.find(pagelib.RAD + "body") is not None:
            params["has-body"] = PHPLiteral(True)
        elif root.find(pagelib.RAD + "scripted-body") is not None:
            params["has-scripted-body"] = PHPLiteral(True)
        else:
            params["has-body"] = PHPLiteral(False)

        model = root.find(pagelib.RAD + "model")
        if model is not None:
            parse_model(model, params)

        primary_exists = (model is not None and "primary" in params["model"])
        if params["requires-arguments"].value and not primary_exists:
            raise pagelib.PageParserException("Page attribute 'requires-arguments' is set but no <primary> element in model defined.")
    parse_page_impl.parser = None

    def parse_page(realpath, virtpath, node):
        try:
            parse_page_impl(realpath, virtpath, node)
        except etree.LxmlError as e:
            raise pagelib.PageParserException("{}: XML error: {}"
                    .format(realpath, e.message))
        except pagelib.PageParserException as e:
            raise pagelib.PageParserException("{}: {}".format(realpath, e.message))

    def create_leaf_node(name, realpath, virtpath):
        node = LeafNode(name)
        parse_page(realpath, virtpath, node)
        node.get_params()["compiled-path"] = PHPLiteral(virtpath + ".php")
        return node

    def create_dir_node(name, realpath, virtpath, children):
        node = DirectoryNode(name, children)
        parse_page(realpath, virtpath, node)
        node.get_params()["compiled-path"] = PHPLiteral(virtpath + "index.php")
        return node

    def visit_dir(file_tree, virtbase, is_root=False):
        if not "index.xml" in file_tree:
            print(("Omitting '{}': No index.xml.".format(virtbase)))
            return None

        children = []
        for (full_name, (is_dir, source_or_children)) in list(file_tree.items()):
            if full_name == "index.xml":
                continue
            (name, ext) = os.path.splitext(full_name)
            virtpath = os.path.join(virtbase, name)
            if is_dir:
                node = visit_dir(source_or_children, virtpath)
                if node is not None:
                    children.append(node)
            elif ext == ".xml":
                node = create_leaf_node(name, source_or_children, virtpath)
                children.append(node)

        children_sorted = PHPDict()
        for node in sorted(children,
                key=lambda node: (node.get_weight(), node.get_name())):
            children_sorted[node.get_name()] = node.to_php()

        if is_root:
            node_name = "home"
        else:
            node_name = os.path.basename(virtbase)

        return create_dir_node(node_name, file_tree["index.xml"][1],
                os.path.join(virtbase, ""), children_sorted)

    print(("Reading pages from '{}'...".format("', '".join(basedirs))))
    file_tree = pagelib.list_file_tree(basedirs)
    root = visit_dir(file_tree, "/", is_root=True)

    return {"root": root.to_php()}

def gentree_routes(route_files):
    def process_class_definition(cls, path):
        requires_login_class = pagelib.xmlbool(cls.attrib["requires-login"])

        for method in cls.findall("method"):
            if "requires-login" in method.attrib:
                requires_login = pagelib.xmlbool(method.attrib["requires-login"])
            else:
                requires_login = requires_login_class

            if requires_login < requires_login_class:
                raise RouteParserException("Method {}::{}: Cannot override 'requires-login' with 'no'.".format(cls.attrib["name"], method.attrib["name"]))

            for pattern in method.findall("pattern"):
                route = pattern.text

                ent = PHPDict({
                    "route": PHPLiteral(route),
                    "class": PHPLiteral(cls.attrib["name"]),
                    "method": PHPLiteral(method.attrib["name"]),
                    "requires-login": PHPLiteral(requires_login),
                    "output-buffering": PHPLiteral(pagelib.xmlbool(method.attrib["output-buffering"])),
                })
                if "http-methods" in method.attrib:
                    ent["http-methods"] = PHPArray(list(map(PHPLiteral, set(method.attrib["http-methods"].split()))))

                if route in routes:
                    rad.console.notice()
                    print(("{}: Overriding existing route '{}'.".format(
                        path, route)))

                routes[route] = ent

    def process_routes_definition(root, path):
        for cls in root.findall("class"):
            # verify class exists
            components = cls.attrib["name"].lstrip("\\").split("\\")
            srcdir = os.path.join(os.path.dirname(path), "..", "src")
            classfile = os.path.join(srcdir, *components) + ".php"
            if not os.path.exists(classfile):
                raise RouteParserException("Class file '{}' not found.".format(classfile))

            # parse routes
            process_class_definition(cls, path)

    routes = PHPDict()
    with open(SCHEMA_PATH_ROUTES, "r") as f:
        schema = etree.XMLSchema(file=f)
    parser = etree.XMLParser(schema=schema, remove_comments=True,
            attribute_defaults=True)

    for path in route_files:
        try:
            print(("Reading routes from '{}'...".format(path)))
            with open(path, "r") as f:
                tree = etree.parse(f, parser=parser)
            root = tree.getroot()

            process_routes_definition(root, path)
        except RouteParserException as e:
            raise RouteParserException("{}: {}".format(path, e.message))
        except IOError as e:
            raise RouteParserException("{}: {}".format(path, e))
        except etree.LxmlError as e:
            raise RouteParserException("{}: XML error: {}".format(path, e))

    return {"controllers": PHPFunctionCall("new RoutingTable", routes)}

def main():
    global SCHEMA_PATH_ROUTES

    # make sure schema files exist
    mydir = os.path.dirname(__file__)
    SCHEMA_PATH_ROUTES = os.path.join(mydir, SCHEMA_FILE_ROUTES)
    if not os.path.isfile(SCHEMA_PATH_ROUTES):
        sys.stderr.write("Could not find schema file '{}', aborting. Make sure it is in the same directory as '{}'.\n".format(SCHEMA_PATH_ROUTES, sys.argv[0]))
        sys.exit(2)

    # parse arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("output_file")
    parser.add_argument("-p", "--pages-dir", dest="pages_dirs",
            action="append", default=[])
    parser.add_argument("-r", "--routes-file", dest="routes_files",
            action="append", default=[])
    args = parser.parse_args()

    if len(args.pages_dirs) + len(args.routes_files) == 0:
        parser.error("Must have at least one -p or -r argument.")

    for path in args.pages_dirs:
        if not os.path.isdir(path):
            parser.error("Cannot access directory '{}'.".format(path))

    # generate tree
    outvars = {}
    try:
        outvars.update(gentree_pages(args.pages_dirs))
        outvars.update(gentree_routes(args.routes_files))
    except RouteParserException as e:
        rad.console.error(sys.stderr)
        sys.stderr.write("Could not parse routes file, aborting.\n")
        sys.stderr.write("{}\n".format(e.message))
        sys.exit(1)
    except pagelib.PageParserException as e:
        rad.console.error(sys.stderr)
        sys.stderr.write("Could not build page tree, aborting.\n")
        sys.stderr.write("{}\n".format(e.message))
        sys.exit(1)

    # write output file
    print(("Writing output to {}...".format(args.output_file)))
    with io.open(args.output_file, "w", encoding="utf-8") as f:
        f.write("<?php\n")
        f.write("// file generated by gentree.py on {}\n"
                .format(datetime.datetime.now()))
        f.write("// complete command line: {}\n".format(" ".join(sys.argv)))
        f.write("\n")
        f.write("use Cbikt\\Front\\DirectoryNode;\n")
        f.write("use Cbikt\\Front\\LeafNode;\n")
        f.write("use Cbikt\\Front\\RoutingTable;\n")
        f.write("\n")
        for name, obj in list(outvars.items()):
            f.write("${} = {};\n".format(name, obj))

if __name__ == "__main__":
    main()
