#!/usr/bin/python3
# TODO:
#  - remove pages that don't exit anymore from the output directory
#  - rename to genpages.py


import argparse
import datetime
import errno
import gettext
import io
import os
import os.path
import re
import sys
from lxml import etree

import rad.console
import rad.pagelib as pagelib

def mkdir_p(path):
    # source: https://stackoverflow.com/a/600612
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            raise

def needs_rebuild(inpath, has_module, modpath, outpath):
    if not os.path.exists(outpath):
        return True

    reftime = os.path.getmtime(outpath)
    if os.path.getmtime(inpath) > reftime:
        return True
    if has_module and os.path.getmtime(modpath) > reftime:
        return True
    for lang in args.langs:
        mopath = os.path.join(args.locale_dir, lang, "LC_MESSAGES",
                args.gettext_domain + ".mo")
        if os.path.getmtime(mopath) > reftime:
            return True

    return False

def genpage(virtpath, realpath):
    virtpath = virtpath.lstrip("/")
    modpath = os.path.splitext(realpath)[0] + ".php"
    has_module = os.path.exists(modpath)

    outpath = os.path.join(args.output_dir, virtpath) + ".php"
    outdir = os.path.dirname(outpath)

    # don't generate page if output file is newer than input files
    if not needs_rebuild(realpath, has_module, modpath, outpath):
        return

    print("Generating page from '{}'...".format(realpath))

    # read module file
    # TODO: maybe do some basic parsing of the PHP code like verifying the class
    # name or extracting method names
    if has_module:
        with io.open(modpath, "r", encoding="utf-8") as f:
            module_source = f.read()
        module_source = re.sub(r"^\s*<\?php", "", module_source)
        module_source = re.sub(r"\?>\s*$", "", module_source)

    # parse page
    methods = {}
    os.environ["LANGUAGE"] = "C" # first language is always "C"

    root = pagelib.open_page(realpath, translator)
    if root.find(pagelib.RAD + "body") is not None:
        # page has an actual <body>
        methods.update(pagelib.parse_page(root, realpath, translator, None))

        for lang in args.langs:
            os.environ["LANGUAGE"] = lang
            root = pagelib.open_page(realpath, translator)
            methods.update(pagelib.parse_page(root, realpath, translator, lang))

        methods[("public", "build")] = """
            $langId = getenv("LANGUAGE");
            $langId = str_replace(array("-", "."), array("_", "_"), $langId);
            $method = "build_page_$langId";
            $methodDefault = "build_page";
            if (method_exists($this, $method)) {
                return $this->$method($node, $model, $pdo, $res, $lang, $auth, $resolver);
            } else {
                return $this->$methodDefault($node, $model, $pdo, $res, $lang, $auth, $resolver);
            }
        """
    elif root.find(pagelib.RAD + "scripted-body") is not None:
        # page has a scripted body instead of a <body>
        body = root.find(pagelib.RAD + "scripted-body")

        if "callback-data" in body.attrib:
            cb_data = "function() use ($node, $model, $pdo) {{ return $this->{}($pdo, $model, $node); }}".format(body.attrib["callback-data"])
        else:
            cb_data = "null";
        if "callback-render" in body.attrib:
            cb_render = "function() use ($node, $model, $pdo) {{ return $this->{}($pdo, $model, $node); }}".format(body.attrib["callback-render"])
        else:
            cb_render = "null";

        methods[("public", "build")] = """
            $cbData = {};
            $cbRender = {};
            return new class ($cbData, $cbRender)
                        implements \\Cbikt\\Front\\TwoStageRunnable {{
                private $callbackData;
                private $callbackRender;
                public function __construct($callbackData, $callbackRender) {{
                    $this->callbackData = $callbackData;
                    $this->callbackRender = $callbackRender;
                }}
                public function runData() {{
                    return ($this->callbackData !== null)
                            ? call_user_func($this->callbackData)
                            : null;
                }}
                public function runRender() {{
                    if ($this->callbackRender !== null) {{
                        call_user_func($this->callbackRender);
                    }}
                }}
            }};
        """.format(cb_data, cb_render)

    # write out page file
    mkdir_p(outdir)
    with io.open(outpath, "w", encoding="utf-8") as f:
        # If 'methods' is empty, the page has no body. Create an empty PHP file
        # to indicate that the operation succeeded.
        if len(methods) > 0:
            f.write("<?php\n")
            f.write("// file generated by genpages.py on {}\n"
                    .format(datetime.datetime.now()))
            f.write("// complete command line: {}\n".format(" ".join(sys.argv)))
            f.write("\n")
            f.write("namespace Cbikt\Generated\\Page;\n")
            f.write("use Cbikt\\Widgets as W;\n")
            f.write("\n")

            if has_module:
                #print("  Inserting module code from '{}'.".format(modpath))
                f.write("\n")
                f.write(module_source)
                f.write("\n")

            classdef = "class PageFactory"
            if has_module:
                classdef += " extends Module"

            #print("  Generating class PageFactory.")
            f.write("{} {{\n".format(classdef))
            for (visibility, name), code in list(methods.items()):
                #print("  Adding method '{}'.".format(name))
                f.write("    {} function {}(\\Cbikt\\Front\\AbstractNode $node, \\Cbikt\\Front\\PageModel $model, \\PDO $pdo, \\Cbikt\\Views\\ClientResources $res, \\Cbikt\\LanguageSystem $lang, \\Cbikt\\Security\\Authentication $auth, \\Cbikt\\Front\\PathResolver $resolver) {{\n"
                        .format(visibility, name))
                f.write(code)
                f.write("\n")
                f.write("    }\n")
            f.write("}\n")

    #print("Successfully generated '{}'.".format(outpath))

parser = argparse.ArgumentParser(description="Parse XML pages XML_FILE and generate various output files. TODO")

parser.add_argument("-l", "--lang", dest="langs", metavar="LANG", action="append", default=[])
parser.add_argument("-d", "--gettext-domain", metavar="DOMAIN")
parser.add_argument("-m", "--locale-dir", metavar="PATH")
parser.add_argument("-o", "--output-dir", metavar="PATH", required=True)
parser.add_argument("input_dirs", metavar="INPUT_DIR", nargs="+")

args = parser.parse_args()

# validate arguments
if (args.gettext_domain is None) ^ (args.locale_dir is None):
    parser.error("The optional arguments -d/--gettext-domain and -m/--locale-dir must be used together.")
if len(args.langs) > 0 and args.gettext_domain is None:
    parser.error("Argument -l/--lang requires arguments -d/--gettext-domain and -m/--locale-dir.")

# setup gettext and Translator
if args.gettext_domain is not None:
    gettext.bindtextdomain(args.gettext_domain, args.locale_dir)
translator = pagelib.Translator(gettext_domain=args.gettext_domain, is_stub=False)

# traverse page directories
def visit_dir(file_tree, virtbase):
    for (full_name, (is_dir, source_or_children)) in list(file_tree.items()):
        (name, ext) = os.path.splitext(full_name)
        virtpath = os.path.join(virtbase, name)
        if is_dir:
            visit_dir(source_or_children, virtpath)
        else:
            genpage(virtpath, source_or_children)
files = pagelib.list_file_tree(args.input_dirs)


try:
    visit_dir(files, "/")
except:
    rad.console.error()
    print(sys.exc_info()[1])
    sys.exit(1)
