#! /usr/bin/env python3

"""\
Warhol -- Website Assembler Requiring Hardly any Other Libraries
"""

import os, warhol


def init(args):

    ## Make the site directory, usually via a template
    # TODO: what if it already exists? Warn, backup?
    os.makedirs(args.SITEDIR, exist_ok=True)
    if args.USE_TEMPLATE:
        import shutil, importlib.resources as res
        sitetemplate = res.files(warhol).joinpath(os.path.join("data", args.SITETEMPLATE+".site"))
        with res.as_file(sitetemplate) as stpath:
            shutil.copytree(stpath, args.SITEDIR, dirs_exist_ok=True)

    ## Write project-root marker file
    import datetime
    with open(os.path.join(args.SITEDIR, ".warhol"), "w") as f:
        f.write("Warhol site root, init " + str(datetime.datetime.now(tz=None)) + "\n")


def build(args):

    ## Read config file if there is one, and handle option/config conflicts
    import os, tomllib
    CONFIG = {}
    if os.path.exists(args.CONFIGFILE):
        with open(args.CONFIGFILE, "rb") as cf:
            CONFIG = tomllib.load(cf)
    if not args.SITENAME:
        args.SITENAME = CONFIG.get("sitename")
    if not args.CONTENTDIR:
        args.CONTENTDIR = CONFIG.get("contentdir") or "content"
    if not args.OUTPUTDIR:
        args.OUTPUTDIR = CONFIG.get("outputdir") or "public"
    if not args.CACHEDIR:
        args.CACHEDIR = CONFIG.get("builddir") or ".build"

    def is_newer(f1, f2):
        "Is file f1 newer than f2?"
        if not os.path.exists(f1): return False
        if not os.path.exists(f2): return True
        return os.stat(f1).st_mtime > os.stat(f2).st_mtime

    def pagematch(patts, fallback=None):
        "Get the val for the last regex match to the current URL path"
        for regex, val in reversed(patts.items()):
            #print("Regex:", regex, "vs", str(oname))
            if re.match(regex, str(oname)):
                #print("  ", regex, "matched", str(oname))
                return val
        return fallback

    def pagematch_includes(patts, vardict={}):
        "Get cat'd includes for the last regex match to the current URL path"
        rtn = ""
        incfiles = pagematch(patts, [])
        if isinstance(incfiles, str): incfiles = [incfiles]
        for incfile in incfiles:
            with open(incfile) as incf:
                from string import Template
                inc = Template(incf.read())
                rtn += inc.safe_substitute(**vardict)
        return rtn

    def tagwrap(txt, tag, tag_indent=4):
        "Wrap some text in a pair of the given tag"
        # TODO: use textindent
        rtn = tag_indent*" " + f"<{tag}>\n" + txt + tag_indent*" " + f"</{tag}>\n"
        return rtn

    class AttrDict(dict):
        "Dict specialisation to allow object-like access to template vars"
        def __init__(self, *args, **kwargs):
            super(AttrDict, self).__init__(*args, **kwargs)
            self.__dict__ = self


    ## Walk the content tree and convert to HTML partials, writing into the cache dir
    # TODO: force flag to force a clear
    import os, shutil, pathlib, re
    contentroot = pathlib.Path(args.CONTENTDIR)
    cacheroot = pathlib.Path(args.CACHEDIR)
    for path, dirs, files in os.walk(args.CONTENTDIR):
        relpath = pathlib.Path(path).relative_to(args.CONTENTDIR)

        # mdfiles = [f for f in files if f.endswith(".md")]
        # htfiles = [f for f in files if f.endswith(".html")]

        ## Make an output dir for this level in the cache
        # TODO: clean up dirs that have no representation in the content
        #print(path, mdfiles)
        d_cache = cacheroot / relpath
        if not os.path.exists(d_cache):
            os.makedirs(d_cache, exist_ok=True)

        for f in files:

            ## Convert each changed MD file
            if f.endswith(".md"):
                # TODO: use multiprocessing
                mdp = contentroot / relpath / f
                htp = d_cache / f.replace(".md", ".html")
                if is_newer(mdp, htp):
                    if args.VERBOSE:
                        print(f"Rendering {mdp} -> {htp}")
                    try:
                        import markdown
                        with open(mdp, "r") as input_file:
                            mdtxt = input_file.read()
                            html = markdown.markdown(mdtxt)
                            with open(htp, "w", encoding="utf-8", errors="xmlcharrefreplace") as output_file:
                                output_file.write(html)
                    except ImportError:
                        os.system(f"pandoc -i {mdp} -o {htp}")

            ## Copy remaining files
            else:
                p = contentroot / relpath / f
                if is_newer(p, d_cache / f):
                    if args.VERBOSE:
                        print(f"Copying {p} -> {d_cache / f}")
                    shutil.copy(p, d_cache)


    ## Configure includes and templates
    # TODO: load configs from a metadata file
    # patt_hideext = {
    #     "/" : True,
    #     "/doxy/" : False
    # }
    # patt_titles = {
    #     "/" : "Rivet — the particle-physics MC analysis toolkit", #< default
    #     "/releases" : "Rivet releases",
    #     "/analyses" : "Rivet analyses",
    #     "/analyses/anadiff" : "Rivet analysis diffs",
    #     "/analyses/coverage" : "Rivet analysis coverage",
    #     "/tutorials" : "Rivet tutorials",
    #     "/doxy" : "Rivet API documentation",
    #     "/contact" : "Rivet contacts",
    # }
    # patt_heads = {
    #     "/" : ["include/head_base.html"], #< default
    #     "/analyses/coverage" : ["include/head_base.html", "include/head_coverage.html"],
    #     "/doxy" : ["include/head_base.html", "include/head_doxy.html"],
    # }
    # patt_asides = {
    #     "/$" : "include/aside_front.html",
    #     "/releases" : "include/aside_links.html",
    #     "/analyses" : ["include/aside_analyses.html", "include/aside_anasub.html"],
    #     "/analyses/coverage/" : ["include/aside_analyses.html"],
    #     "/tutorials$" : "include/aside_tutorials.html",
    #     "/contact" : "include/aside_links.html",
    #     r"/doxy/[^/]+$" : "include/aside_doxy.html",
    # }
    # patt_headers = {
    #     "/" : "include/navbar.html",
    # }
    # patt_pre = {
    #     "/" : "include/pre_default.html", #< default
    #     "/$" : "include/pre_front.html", #< front page
    #     "/analyses/coverage/" : [],
    #     #"/doxy/.*" : [] #"include/pre_doxy.html",
    # }
    # patt_post = {
    #     #"/doxy/.*" : "include/post_doxy.html",
    # }
    # patt_footers = {
    #     "/" : "include/footer_all.html",
    # }


    ## Loop over cache and impose header and footer, writing to the output dir
    outroot = pathlib.Path(args.OUTPUTDIR)
    for path, dirs, files in os.walk(args.CACHEDIR):
        relpath = pathlib.Path(path).relative_to(args.CACHEDIR)

        ## Make an output dir for this level in the cache
        d_out = outroot / relpath
        if not d_out.exists():
            os.makedirs(d_out, exist_ok=True)

        ## Cat together files
        # TODO: timestamp check (forceable)
        for f in files:

            ## Process HTML into a nice form
            if f.endswith(".html"):
                htp = cacheroot / relpath / f
                with open(htp) as ht:
                    content = ht.read()
                    standalone = ("<html>" in content)

                    ## Filename and URL
                    fname = f.replace(".html", "")
                    oname = "/" / relpath / f.replace("index.html", "").replace(".html", "")

                    ## Make a separate out dir for each .html if extensions are disabled
                    outfile = None
                    if f == "index.html" or not pagematch(CONFIG["HideExtns"]):
                        outfile = d_out / f
                    else:
                        outdir = d_out / fname
                        if not outdir.exists():
                            os.makedirs(outdir, exist_ok=True)
                        outfile = outdir / "index.html"

                    ## Write the file, with template injection
                    with open(outfile, "w") as of:

                        ## Short-circuit standalone
                        # TODO: Use BeautifulSoup or similar to inject??
                        if standalone:
                            of.write(content)
                            break

                        ## Load metadata for template insertion
                        template_vars = AttrDict(PATH=str(oname), TITLE=None)
                        template_vars.TITLE = pagematch(CONFIG["Titles"])

                        ## Start writing the HTML
                        of.write("<!DOCTYPE html>\n")
                        of.write("<html>\n")
                        of.write("  <head>\n")

                        ## Insert possibly multiple fragments into <head>
                        headstr = pagematch_includes(CONFIG["HeadIncludes"], template_vars)
                        if headstr: of.write(headstr)

                        of.write("  </head>\n")
                        of.write("  <body>\n")

                        ## Optional page header
                        phstr = pagematch_includes(CONFIG["HeaderIncludes"], template_vars)
                        if phstr: of.write(tagwrap(phstr, "header"))

                        ## Asides (right-hand boxes)
                        astr = pagematch_includes(CONFIG["AsideIncludes"], template_vars)
                        if astr: of.write(tagwrap(astr, "aside"))

                        ## Main content
                        of.write("    <main>\n")
                        pstr = pagematch_includes(CONFIG["PreIncludes"], template_vars)
                        if pstr: of.write(pstr)
                        of.write(content) #< no substitution since content can be anything
                        pstr = pagematch_includes(CONFIG["PostIncludes"], template_vars)
                        if pstr: of.write(pstr)
                        of.write("    </main>\n")

                        ## Optional page footer
                        pfstr = pagematch_includes(CONFIG["FooterIncludes"], template_vars)
                        if pfstr: of.write(tagwrap(pfstr, "footer"))

                        ## Close
                        from datetime import datetime
                        datestr = datetime.now().strftime("%I:%M%p, %d %B, %Y")
                        of.write(f"    <!-- Page generated at {datestr} -->\n")
                        of.write("  </body>\n")
                        of.write("</html>\n")

            ## Copy remaining files
            else:
                p = cacheroot / relpath / f
                # if args.VERBOSE:
                #     print(f"Copying {p} to {d_out}")
                shutil.copy(p, d_out)


def serve(args):
    import http.server, functools
    Handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=args.SERVEDIR)
    with http.server.HTTPServer(("", args.SERVEPORT), Handler) as httpd:
        print(f"Serving site from {args.SERVEDIR}/ at http://0.0.0.0:{args.SERVEPORT}")
        print("Press Ctrl-C to stop serving...")
        httpd.serve_forever()


## Main dispatcher
import argparse
ap = argparse.ArgumentParser(description=__doc__)
ap_subs = ap.add_subparsers(required=True)

# Arg parser for the "init" command
ap_init = ap_subs.add_parser("init")
ap_init.add_argument("SITEDIR", nargs="?", default=".", help="directory of page templates in site structure [current dir]")
ap_init.add_argument("-t", dest="SITETEMPLATE", default="default", help="name of the site template to initialise [default]")
ap_init.add_argument("-n", dest="USE_TEMPLATE", default=True, action="store_false", help="disable use of a site template")
ap_init.set_defaults(func=init)

# Arg parser for the "build" command
ap_build = ap_subs.add_parser("build")
ap_build.add_argument("-c", "--config", dest="CONFIGFILE", default="config.toml", help="TOML config file for the site build [config.toml]")
ap_build.add_argument("CONTENTDIR", nargs="?", help="directory of page templates in site structure [content/]")
ap_build.add_argument("-n", "--sitename", dest="SITENAME", help="optional name for the site")
ap_build.add_argument("-o", "--outdir", dest="OUTPUTDIR", help="directory to write completed site into [public/]")
ap_build.add_argument("-C", "-B", "--builddir", dest="CACHEDIR", help="cache directory for intermediate site files [.build/]")
ap_build.add_argument("-v", "--verbose", dest="VERBOSE", action="store_true", default=False)
ap_build.set_defaults(func=build)

# Arg parser for the "serve" command
ap_serve = ap_subs.add_parser("serve")
ap_serve.add_argument(dest="SERVEDIR", nargs="?", default="public")
ap_serve.add_argument("-p", dest="SERVEPORT", default=8080)
ap_serve.set_defaults(func=serve)

# Parse the args and call whatever function was selected
args = ap.parse_args()
args.func(args)
