

import collections
import datetime
import gettext # TODO
import os
import os.path
import polib
import random
import sys
from lxml import etree

from rad.php import PHPArray, PHPDict, PHPLiteral, PHPFunctionCall

# gen_file_tree

RAD_NAMESPACE = "http://cbikt.de/rad-framework/page"
RAD = "{" + RAD_NAMESPACE + "}"
SCHEMA_FILE = "page.xsd"
TABLE_DEFAULT_PAGE_SIZE = 20

class PageParserException(Exception):
    pass

# TODO: split class into StubTranslator and regular Translator
# TODO: maybe move call to gettext() into a callback
class Translator:
    def __init__(self, gettext_domain=None, is_stub=False):
        self._gettext_domain = gettext_domain
        self._is_stub = is_stub
        if not is_stub:
            self._po = polib.POFile()
            timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M%z")
            self._po.metadata = {
                    "POT-Creation-Date": timestamp,
                    "MIME-Version": "1.0",
                    "Content-Type": "text/plain; charset=utf-8",
                    "Content-Transfer-Encoding": "8bit",
            }
            self._current_source_path = None

    def differs_from(self, path):
        po_other = polib.pofile(path)
        for entry in self._po:
            m = po_other.find(entry.msgid)
            if m is None or m != entry:
                return True
        return False

    def write_out(self, path):
        assert not self._is_stub, "Cannot write out in stub mode."
        self._po.save(path)

    def set_source_path(self, path):
        if self._is_stub:
            return
        self._current_source_path = path

    def feed_node(self, node):
        if node.text is not None:
            (left, mid, right) = self._split_string(node.text)
            self._add_string(mid, node.sourceline)
            node.text = left + self._translate(mid) + right
        if node.tail is not None:
            (left, mid, right) = self._split_string(node.tail)
            self._add_string(mid, node.sourceline)
            node.tail = left + self._translate(mid) + right

    def feed_attr(self, node, aname):
        if aname in node.attrib and isinstance(node.attrib[aname], str):
            val = node.attrib[aname]
            (left, mid, right) = self._split_string(val)
            self._add_string(mid, node.sourceline)
            node.attrib[aname] = left + self._translate(mid) + right

    def _split_string(self, s):
        u = s.lstrip()
        v = u.rstrip()
        i = len(s) - len(u)
        j = len(u) - len(v)
        return (s[0:i], s[i:len(s) - j], s[len(s)-j:])

    def _add_string(self, msgid, line=None):
        if self._is_stub or msgid == "":
            return
        args = { "msgid": msgid }
        if self._current_source_path is not None and line is not None:
            args["occurrences"] = [(self._current_source_path, str(line))]
        entry = polib.POEntry(**args)
        existing = self._po.find(msgid)
        if existing is not None:
            entry.occurrences = existing.occurrences + entry.occurrences
            existing.merge(entry)
        else:
            self._po.append(entry)

    def _translate(self, msgid):
        if self._is_stub or self._gettext_domain is None or msgid == "":
            return msgid
        msg = gettext.dgettext(self._gettext_domain, msgid)
        #python 3.7 fix
        #if isinstance(msg, str):
            # assume gettext returned a utf-8 encoded binary string
            #msg = str(msg, "utf-8")
        return msg

def get_schema():
    schema_path = os.path.join(os.path.dirname(sys.argv[0]), SCHEMA_FILE)
    if not os.path.isfile(schema_path):
        raise PageParserException("Could not find schema file '{}'. Make sure it is in the same directory as '{}'."
                .format(schema_path, sys.argv[0]))
        return None

    with open(schema_path, "r") as f:
        return etree.XMLSchema(file=f)

def xmlbool(val):
    return val == "yes"

def list_file_tree(sources, virtdir="/"):
    dirs = {}
    files = {}
    for source in sources:
        for full_name in os.listdir(source):
            abspath = os.path.join(source, full_name)
            if os.path.isdir(abspath):
                if "." in full_name:
                    raise PageParserException("Page directory '{}' must not contain dots."
                            .format(abspath))
                if full_name + ".xml" in files:
                    raise PageParserException("A file and a directory have the same name '{}' in virtual path '{}'.".
                        format(full_name, virtdir))
                if full_name not in dirs:
                    dirs[full_name] = []
                dirs[full_name].append(abspath)
                name = full_name
            elif os.path.isfile(abspath):
                (name, suffix) = os.path.splitext(full_name)
                if suffix != ".xml":
                    continue # we only care for potential page files

                if "." in name:
                    raise PageParserException("Page path '{}' must not contain dots.".format(abspath))
                if name in dirs:
                    raise PageParserException("A file and a directory have the same name '{}' in virtual path '{}'.".
                        format(name, virtdir))
                if full_name in files:
                    raise PageParserException("Files '{}' and '{}' point to the same page in the page tree."
                            .format(files[full_name], abspath))
                files[full_name] = abspath
            else:
                raise PageParserException("Unknown file type for '{}', aborting."
                        .format(abspath))

    file_tree = []
    for name, sources in list(dirs.items()):
        virtchild = os.path.join(virtdir, name)
        children = list_file_tree(sources, virtchild)
        file_tree.append((name, (True, children)))
    file_tree += [(name, (False, source)) for (name, source) in list(files.items())]
    #python3 fix lambda (name, _)
    file_tree = sorted(file_tree, key=lambda name: name)

    return collections.OrderedDict(file_tree)


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

def _parse_body(root, body, php, page_attribs, path, model, translator):
    var_cnt = [0]
    def allocvar():
        var = "$v" + str(var_cnt[0])
        var_cnt[0] += 1
        return var

    def freevar(var):
        pass

    def error(node, msg):
        raise PageParserException("{}:{}: {}".format(path, node.sourceline, msg))

    def find_record(model, name):
        if model is None:
            return None
        for rec in model.findall(RAD + "record"):
            if rec.attrib["name"] == name:
                return rec

    def find_query(node, name):
        if node.tag == RAD + "model":
            elems = node.findall(RAD + "query") + node.findall(RAD + "emptyset")
        elif node.tag == RAD + "query" or node.tag == RAD + "subquery":
            elems = node.findall(RAD + "subquery")
        elif node.tag == RAD + "emptyset":
            elems = node.findall(RAD + "emptyset")
        else:
            return None

        for el in elems:
            if el.attrib["name"] == name:
                return el

        return None

    def parse_source_record(node, return_remainder=False):
        php = None
        path = node.attrib["source"].split("/")
        if path[0] == "$":
            # case 1: source begins with $
            path.pop(0)
            if model is None or model.find(RAD + "primary") is None:
                error(node, "Invalid 'source' attribute: Reference to primary record ($) requires a primary record to be defined in the model.")
            php = "$model->getPrimaryRecord()"
        elif path[0] == "..":
            # case 2: source begins with ../<name>
            if len(path) < 2:
                error(node, "Invalid 'source' attribute: Incomplete path.")
            path.pop(0)
            name = path.pop(0)
            php = "$model->getReadOnlyRecord({})".format(PHPLiteral(name))
        elif find_record(model, path[0]) is not None:
            # case 3: source begins with <name>
            name = path.pop(0)
            php = "$model->getNamedRecord({})".format(PHPLiteral(name))
        elif not return_remainder:
            error(node, "Invalid 'source' attribute: Record '{}' does not exist.".format(path[0]))

        if return_remainder:
            return (php, path)

        if len(path) != 0:
            error(node, "Invalid 'source' attribute: Unknown trailing components.")

        return php

    def parse_source_recordset(node, widget_attribs):
        source = node.attrib["source"]
        (php_record, parts) = parse_source_record(node, return_remainder=True)

        if php_record is not None:
            # case 1: source has format <record>/<subquery>
            if len(parts) != 1:
                error(node, "Invalid 'source' attribute: There must be exactly one subquery following a reference to a record.")
            widget_attribs["recordset"] = PHPLiteral(parts[0])
            widget_attribs["fix-record"] = PHPLiteral(True)
            return php_record
        elif parts[0] == ".":
            # case 2: source has format ./<subset>:
            if len(parts) < 2:
                error(node, "Invalid 'source' attribute: Incomplete path.")
            if len(parts) > 2:
                error(node, "Invalid 'source' attribute: Too many components in relative path.")
            widget_attribs["recordset"] = PHPLiteral(parts[1])
            return "null"
        else:
            # case 3: source has format <query>[/<subquery[/<subquery> ...]]
            if model is None:
                error(node, "Invalid 'source' attribute: No model specified.")
            query = find_query(model, parts[0])
            if query is None:
                error(node, "Query '{}' specified in 'source' attribute does not"
                        " exist.".format(parts[0]))
            expr = ["$model->getRecordSet({})".format(PHPLiteral(parts[0]))]
            for name in parts[1:]:
                query = find_query(query, name)
                if query is None:
                    error(node, "Subquery '{}' specified in 'source' attribute"
                            " does not exist.".format(name))
                expr.append("getSubSet({})".format(PHPLiteral(name)))

            widget_attribs["recordset"] = "->".join(expr)
            widget_attribs["fix-record"] = PHPLiteral(True)
            return "null"

    all_ids = set()
    def parse_attribs_widget(node, attribs=[]):
        # attributes of the Widget base class as defined in the
        # validateAttrib() method of Cbikt\Widgets\Widget
        base_attribs = [("id", str), ("hidden", bool),
                ("fix-record", bool), ("editable", str),
                ("class", str), ("style", str), ("inline", bool)]
        # copy value of 'id' attrib to 'name' (if necessary)
        if "name" not in node.attrib:
            if "id" not in node.attrib:
                error(node, "Widget has neither 'name' nor 'id' attribute.")
            node.attrib["name"] = node.attrib["id"]
        if "id" in node.attrib:
            if node.attrib["id"] in all_ids:
                error(node, "ID already in use.")
            all_ids.add(node.attrib["id"])
        # fill in widget attributes
        widget_attribs = PHPDict()
        _read_widget_attribs(node, base_attribs + attribs, widget_attribs)

        return (PHPLiteral(node.attrib["name"]), widget_attribs)

    def parse_attribs_composite_widget(node, attribs=[]):
        translator.feed_attr(node, "caption")
        composite_attribs = [("outer-class", str), ("caption", str),
                ("help", str)]
        (name, widget_attribs) = parse_attribs_widget(node,
                composite_attribs + attribs)

        actions = []
        i = 0
        while (isinstance(node[0].tag, str)
                and node[0].tag == RAD + "action"):
            childvar = compose(node[0], "_action{}".format(i))
            actions.append(childvar)
            del node[0]
            i += 1

        return (name, widget_attribs, actions)

    def parse_attribs_table_widget(node):
        (name, widget_attribs, actions) = parse_attribs_composite_widget(node,
                [("allow-select", bool), ("allow-add", bool),
                ("allow-delete", bool), ("page-size", int),
                ("auto-filter", bool), ("sortable", bool),
                ("custom-add-link", str), ("custom-select-link", str)])
        if "page-size" not in node.attrib:
            widget_attribs["page-size"] = PHPLiteral(TABLE_DEFAULT_PAGE_SIZE)
        record = parse_source_recordset(node, widget_attribs)
        return (name, widget_attribs, record, actions)

    def parse_attribs_repeat_widget(node):
        (name, widget_attribs, actions) = parse_attribs_composite_widget(node,
                [("separator", str)])
        record = parse_source_recordset(node, widget_attribs)
        return (name, widget_attribs, record, actions)

    def parse_attribs_wizard_widget(node):
        (name, widget_attribs, _) = parse_attribs_composite_widget(node)
        record = parse

    def parse_attribs_record_widget(node, require_source=True):
        (name, widget_attribs, actions) = parse_attribs_composite_widget(node)
        if "source" not in node.attrib and not require_source:
            record = "null"
        else:
            record = parse_source_record(node)
        return (name, widget_attribs, record, actions)

    def parse_attribs_value_widget(node, attr=[]):
        return parse_attribs_widget(node, [("source", str)] + attr)

    def parse_attribs_link_widget(node):
        translator.feed_node(node)
        if "id" not in node.attrib and "name" not in node.attrib:
            # auto-generate a name if the user didn't specify one
            node.attrib["name"] = "_linkconfirm{}".format(
                    parse_attribs_link_widget.counter)
            parse_attribs_link_widget.counter += 1
        (name, widget_attribs) = parse_attribs_widget(node)

        label = node.text if node.text is not None else ""
        widget_attribs["label"] = PHPLiteral(label)
        return (name, widget_attribs)
    parse_attribs_link_widget.counter = 0

    def parse_attribs_linkconfirm_widget(node):
        translator.feed_node(node)
        if "id" not in node.attrib and "name" not in node.attrib:
            # auto-generate a name if the user didn't specify one
            node.attrib["name"] = "_link{}".format(
                    parse_attribs_linkconfirm_widget.counter)
            parse_attribs_linkconfirm_widget.counter += 1
        (name, widget_attribs) = parse_attribs_widget(node)

        label = node.text if node.text is not None else ""
        widget_attribs["label"] = PHPLiteral(label)
        return (name, widget_attribs)
    parse_attribs_linkconfirm_widget.counter = 0

    def parse_attribs_button_widget(node):
        translator.feed_node(node)
        (name, widget_attribs) = parse_attribs_widget(node,
                attribs=[("icon", str), ("requires-write", bool)])
        if node.text is not None:
            label = node.text
        else:
            label = ""
        widget_attribs["label"] = PHPLiteral(label)
        if "requires-write" not in widget_attribs:
            widget_attribs["requires_write"] = PHPLiteral(True)
        return (name, widget_attribs)

    def parse_attribs_select_widget(node):
        translator.feed_attr(node, "null-label")
        translator.feed_attr(node, "new-label")

        return parse_attribs_value_widget(node, [("sql", str),
            ("allow-null", bool), ("null-label", str),("allow-new", bool),
            ("new-label", str), ("ajax-url", str),
            ("null-display", str)])

    def visit_composite_widget(node, parent, idx, cls,
            attr_func=parse_attribs_record_widget,
            require_source=True, attribs=[], events=[]):
        (name, widget_attribs, record, actions) \
                = attr_func(node, require_source=require_source)
        _read_widget_attribs(node, attribs, widget_attribs)
        child_count = descend(node)
        var = allocvar()
        rend = renderer(node)
        php.append("{} = new W\\{}({}, $page, $auth, {}, {}, {});"
                .format(var, cls, name, rend, widget_attribs, record))
        add_events(var, node, events)
        add_actions(var, actions)
        add_children(var, child_count)
        replace_with_pi(node, parent, idx, name)
        stack.append(var)

    def visit_value_widget(node, parent, idx, cls, attribs=[], events=[],
            attr_func=parse_attribs_value_widget):
        (name, widget_attribs) = attr_func(node)
        _read_widget_attribs(node, attribs, widget_attribs)
        var = allocvar()
        php.append("{} = new W\\{}({}, $page, $auth, {});".format(var, cls, name, widget_attribs))
        add_events(var, node, events)
        replace_with_pi(node, parent, idx, name)
        stack.append(var)

    def add_events(var, node, events):
        for event in events:
            eattr = "on" + event
            if eattr in node.attrib:
                fn_name = node.attrib[eattr]
                php.append("{}->listen({}, function(W\\Event $e){{$this->{}($e);}});"
                        .format(var, PHPLiteral(event), fn_name))

    def add_actions(var, actions):
        for actvar in actions:
            php.append("{}->addAction({});".format(var, actvar))

    def add_children(var, child_count):
        for _ in range(child_count):
            childvar = stack.pop()
            php.append("{}->add({});".format(var, childvar))
            freevar(childvar)

    def descend(node):
        stack_ptr = len(stack)
        for (i, child) in enumerate(node):
            visit(child, node, i)
        return len(stack) - stack_ptr

    def renderer(node):
        template = etree.tostring(node, method="xml", encoding="utf-8")
        template = str(template, "utf-8")
        # cut off root tag:
        start = template.index(">") + 1
        end = template.rindex("<")
        template = template[start:end]

        # substitute rad:attributes
        for uid, (aname, afn) in list(add_attribs.items()):
            if template.find(uid) != -1:
                attr_php = "<?= htmlspecialchars({}($w)); ?>".format(afn)
                template = template.replace(uid, attr_php)
            del add_attribs[uid]

        # create PHP object
        return ("new W\\CallbackCompositeRenderer(function("
                "W\\CompositeWidget $w) {{ ?>{}<?php }})".format(template))

    def compose(node, name):
        child_count = descend(node)
        if (child_count == 1
                and len(node) == 1
                and node[0].tag is etree.PI  # child element is a parsed rad: widget
                and (node.text is None or node.text.strip() == "")
                and (node[0].tail is None or node[0].tail.strip() == "")):
            return stack.pop()
        else:
            var = allocvar()
            php.append("{} = new W\\CompositeWidget({}, $page, $auth, {}, array());"
                    .format(var, PHPLiteral(name), renderer(node)))
            add_children(var, child_count)
            return var

    def replace_with_pi(node, parent, idx, name):
        code = "$w->child({})->render(); ".format(name)
        pi = etree.ProcessingInstruction("php", code)
        pi.tail = parent[idx].tail
        parent[idx] = pi

    def visit(node, parent, idx):
        if node.tag is etree.PI and node.target == "php":
            sys.stderr.write("Encountered PHP processing instruction in {}:{}."
                " I hope you know what you are doing.\n".format(path, node.sourceline))
        if not isinstance(node.tag, str):
            return

        if node.tag == RAD + "body":
            if parent is not None:
                error(node, "'body' tag must not be nested.")
            begin_idx = len(php)
            var = "$page"
            add_events(var, root, ["preparerecords", "beforerun",
                "beforerender"])
            php.append("{}->setModel($model, $node);"
                    .format(var))
            child_count = descend(node)
            php.insert(begin_idx, "{} = new W\\Page({}, $auth, {}, $pdo, $res, $lang);"
                    .format(var, renderer(node), page_attribs))
            add_children(var, child_count)
            stack.append(var)
        elif node.tag == RAD + "single-form":
            visit_composite_widget(node, parent, idx, "SingleForm",
                    events=["submit", "validate", "render"])
        elif node.tag == RAD + "group":
            visit_composite_widget(node, parent, idx, "CompositeWidget",
                    attribs=[("table-new-required", str)],
                    events=["render"],
                    require_source=False)
        elif node.tag == RAD + "sql-group":
            visit_composite_widget(node, parent, idx, "SqlGroupWidget",
                    attribs=[("sql", str)],
                    require_source=False)
        elif node.tag == RAD + "table":
            (name, attribs, record, actions) = parse_attribs_table_widget(node)
            var = allocvar()
            php.append("{} = new W\\TableWidget({}, $page, $auth, {}, {});"
                    .format(var, name, attribs, record))
            add_events(var, node, ["newrecord", "recordsaved", "renderrow"])
            add_actions(var, actions)
            for (i, elem) in enumerate(node):
                if not isinstance(elem.tag, str):
                    continue
                elif elem.tag == RAD + "td":
                    translator.feed_attr(elem, "title")
                    attr = PHPDict({k: PHPLiteral(v)
                                    for (k, v) in list(elem.attrib.items())})
                    colvar = compose(elem, "_col{}".format(i))
                    php.append("{}->addColumn({}, {});".format(var, colvar, attr))
                elif elem.tag == RAD + "action-column":
                    colvar = compose(elem, "_action{}".format(i))
                    php.append("{}->addActionColumn({});".format(var, colvar))
                elif elem.tag == RAD + "editing-area":
                    childvar = compose(elem, "_editing_area")
                    php.append("{}->setEditingArea({});".format(var, childvar))
                else:
                    assert False
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "repeat":
            (name, attribs, record, actions) = parse_attribs_repeat_widget(node)
            var = allocvar()
            php.append("{} = new W\\RepeatWidget({}, $page, $auth, {}, {});"
                    .format(var, name, attribs, record))
            add_events(var, node, ["renderrow", "rendernotfound"])
            add_actions(var, actions)
            childvar = compose(node[0], "_content")
            php.append("{}->setContentWidget({});".format(var, childvar))
            if len(node) > 1:
                childvar = compose(node[1], "_notfound")
                php.append("{}->setNotFoundWidget({});".format(var, childvar))
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "switch":
            (name, attribs) = parse_attribs_value_widget(node)
            var = allocvar()
            php.append("{} = new W\\SwitchWidget({}, $page, $auth, {});"
                    .format(var, name, attribs))
            for (i, elem) in enumerate(node.findall(RAD + "case")):
                value = PHPLiteral(elem.attrib["value"])
                childvar = compose(elem, "_case{}".format(i))
                php.append("{}->setCase({}, {});".format(var, value, childvar))
            for elem in node.findall(RAD + "default"):
                childvar = compose(elem, "_default")
                php.append("{}->setDefault({});".format(var, childvar))
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "tabs":
            (name, attribs) = parse_attribs_widget(node,
                    attribs=[("use-url", bool), ("icon", str)])
            var = allocvar()
            php.append("{} = new W\\TabsWidget({}, $page, $auth, {});"
                    .format(var, name, attribs))
            first_tab = None
            for (i, elem) in enumerate(node):
                translator.feed_attr(elem, "title")
                title = PHPLiteral(elem.attrib["title"])
                if "first" in elem.attrib and xmlbool(elem.attrib["first"]):
                    if first_tab is not None:
                        error(node, "Cannot have multiple tabs declared as 'first'.")
                    first_tab = i
                childvar = compose(elem, "_tab{}".format(i))
                if "icon" not in elem.attrib:
                    php.append("{}->addTab({}, {});" .format(var, title, childvar))
                else:
                    icon = PHPLiteral(elem.attrib["icon"])
                    php.append("{}->addTab({}, {}, {});" .format(var, title, childvar, icon))
            if first_tab is not None:
                php.append("{}->setDefaultSelection({});"
                        .format(var, PHPLiteral(first_tab)))
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "wizard":
            (name, attribs, _) = parse_attribs_composite_widget(node)
            if "source" in node.attrib:
                record = parse_source_record(node)
            else:
                record = "null"
            var = allocvar()
            php.append("{} = new W\\WizardWidget({}, $page, $auth, {}, {});"
                    .format(var, name, attribs, record))
            add_events(var, node, ["change", "finish"])
            for (i, elem) in enumerate(node.findall(RAD + "pane")):
                translator.feed_attr(elem, "title")
                title = PHPLiteral(elem.attrib["title"])
                if "subtitle" in elem.attrib:
                    translator.feed_attr(elem, "subtitle")
                    subtitle = PHPLiteral(elem.attrib["subtitle"])
                else:
                    subtitle = "null"
                childvar = compose(elem, "_pane{}".format(i))
                php.append("{}->addPane({}, {}, {});"
                        .format(var, childvar, title, subtitle))
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "select":
            (name, attribs) = parse_attribs_select_widget(node)
            var = allocvar()
            php.append("{} = new W\\SelectWidget({}, $page, $auth, {});".format(var, name, attribs))
            for option in node.findall(RAD + "option"):
                if "sql" in node.attrib:
                    error(node, "Select widget can't have an 'sql' attribute and 'option' child elements at the same time.")
                php.append("{}->addOption({}, {});".format(var,
                    PHPLiteral(option.attrib["value"]), PHPLiteral(option.text)))
            replace_with_pi(node, parent, idx, name)
            stack.append(var)
        elif node.tag == RAD + "text":
            translator.feed_attr(node, "placeholder")
            visit_value_widget(node, parent, idx, "TextWidget",
                    attribs=[("placeholder", str), ("truncate", int)])
        elif node.tag == RAD + "HTML":
            translator.feed_attr(node, "placeholder")
            visit_value_widget(node, parent, idx, "HTMLWidget",
                    attribs=[("placeholder", str), ("truncate", int)])
        elif node.tag == RAD + "currency":
            translator.feed_attr(node, "placeholder")
            visit_value_widget(node, parent, idx, "CurrencyWidget",
                    attribs=[("placeholder", str)])
        elif node.tag == RAD + "textarea":
            translator.feed_attr(node, "placeholder")
            visit_value_widget(node, parent, idx, "TextareaWidget",
                    attribs=[("rows", int), ("cols", int),
                        ("placeholder", str), ("truncate", int)])
        elif node.tag == RAD + "color":
            visit_value_widget(node, parent, idx, "ColorWidget")
        elif node.tag == RAD + "password":
            translator.feed_attr(node, "show-string")
            visit_value_widget(node, parent, idx, "PasswordWidget",
                    attribs=[("show-string", str)])
        elif node.tag == RAD + "date":
            visit_value_widget(node, parent, idx, "DateWidget",
                    attribs=[("format", str)])
        elif node.tag == RAD + "checkbox":
            translator.feed_attr(node, "label")
            visit_value_widget(node, parent, idx, "CheckboxWidget",
                    attribs=[("label", str)])
        elif node.tag == RAD + "enum":
            visit_value_widget(node, parent, idx, "EnumWidget",
                    attr_func=parse_attribs_select_widget)
        elif node.tag == RAD + "link":
            visit_value_widget(node, parent, idx, "LinkWidget",
                    attr_func=parse_attribs_link_widget,
                    attribs=[("href", str), ("target", str), ("icon", str)])
        elif node.tag == RAD + "linkconfirm":
            visit_value_widget(node, parent, idx, "LinkconfirmWidget",
                    attr_func=parse_attribs_linkconfirm_widget,
                    attribs=[("href", str), ("target", str), ("icon", str)])
        elif node.tag == RAD + "button":
            visit_value_widget(node, parent, idx, "ButtonWidget",
                    attr_func=parse_attribs_button_widget, events=["click"])
        elif node.tag == RAD + "file":
            visit_value_widget(node, parent, idx, "FileWidget")
        elif node.tag == RAD + "picture":
            visit_value_widget(node, parent, idx, "FilePictureWidget",
                    attribs=[("width", int), ("height", int), ("resize", bool), ("thumbnail", bool), ("fitwidth", bool), ("fitheight", bool), ("fitbest", bool), ("link", bool), ("cropper", bool)])
        elif node.tag == RAD + "picture-svg":
            visit_value_widget(node, parent, idx, "FilePictureSvgWidget",
                    attribs=[("width", int), ("height", int), ("link", bool)])
        elif node.tag == RAD + "attribute":
            error(node, "Element 'attribute' must be the first child of a non-RAD element.")
        elif etree.QName(node.tag).namespace == RAD_NAMESPACE:
            error(node, "Unknown or unexpected tag '{}'.".format(node.tag))
        else:
            while len(node) > 0 and node[0].tag == RAD + "attribute":
                aname = node[0].attrib["name"]
                afn = node[0].attrib["function"]
                if aname in node.attrib:
                    error(node, "Cannot overwrite existing attribute '{}'."
                            .format(aname))
                uid = str(random.randint(10**15, 10**16))
                node.attrib[aname] = uid
                add_attribs[uid] = (aname, afn)
                if node[0].tail is not None:
                    if node.text is None:
                        node.text = ""
                    node.text += node[0].tail
                del node[0]
            translator.feed_node(node)
            descend(node)

    stack = []
    add_attribs = {}
    visit(body, None, 0)

    return stack[0]

def open_page(path, translator):
    if open_page.parser is None:
        open_page.parser = etree.XMLParser(schema=get_schema(), remove_comments=True,
                attribute_defaults=True)
    parser = open_page.parser

    with open(path, "r") as f:
        tree = etree.parse(f, parser=parser)
    root = tree.getroot()
    translator.set_source_path(path)

    if root.tag != RAD + "page":
        raise PageParserException("Root node must be 'page'.")

    translator.feed_attr(root, "title")
    translator.feed_attr(root, "title-new")
    translator.feed_attr(root, "title-breadcrumb")

    return root
open_page.parser = None

def parse_page(root, path, translator, lang):
    page_attribs = PHPDict()
    _read_widget_attribs(root, [("unsaved-notice", bool)], page_attribs)

    php = []
    body = root.find(RAD + "body")
    if body is None:
        return {}

    model = root.find(RAD + "model")
    var = _parse_body(root, body, php, page_attribs, path, model, translator)
    php.append("return {};".format(var))

    if lang is None:
        method = "build_page"
    else:
        lang_sane = lang.replace("-", "_").replace(".", "_")
        method = "build_page_" + lang_sane

    return {("private", method): "\n".join(php)}

