#!python

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

Use `warhol init` to initialise a project directory with the basic
structure and a simple template.

The `warhol build` command merges pages from the `content/` directory with
headers, footers, and box "asides" loaded from the `include/`
directory. Any Markdown `.md` files in the content directory tree are
converted to HTML, and staged in the `.build` directory. The final stage
is injection of includes and template parameters, which augment the
cache contents and write out to the `public/` directory.

Parameters are mostly defined as URL-regex maps in the `warhol.toml`
config file. The default behaviour is for the build step to create a
directory and `index.html` file in place of each (non-index) HTML
file, so the webpage URLs do not have `.html` suffixes; this can be
disabled via a regex match in the config file.

The generated site in the `public/` directory can be viewed directly
with a web browser, but as URLs are internally specified relative to
the HTTP server root and `index.html` pages are assumed to be
implicitly loaded, it's best to run a local web server. The `warhol
serve` command does this, from the `public/` dir by default.
"""

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)

    ## Initialise with a template (of at least the config file)
    import shutil, importlib.resources as res
    if args.SITE_TEMPLATE:
        sitetemplate = res.files(warhol).joinpath(os.path.join("data", args.SITE_TEMPLATE+".site"))
        with res.as_file(sitetemplate) as stpath:
            shutil.copytree(stpath, args.SITEDIR, dirs_exist_ok=True)
    else:
        configfile = res.files(warhol).joinpath(os.path.join("data", "minimal.site", "warhol.toml"))
        shutil.copy(configfile, args.SITEDIR)

    ## 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"
        # NOTE: requires global 'oname' to be defined
        if patts:
            for regex, val in reversed(patts.items()):
                #print("Regex:", regex, "vs", str(oname))
                if re.match(regex, str(oname)):
                    #print("  ", regex, "matched", str(oname))
                    if args.VERBOSE: print(f"{str(oname)} matches pattern {regex} with value {val}")
                    return val
        if args.VERBOSE: print(f"{str(oname)} matches no pattern, returning default {fallback}")
        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:
            try:
                with open(incfile) as incf:
                    from string import Template
                    inc = Template(incf.read())
                    rtn += inc.safe_substitute(**vardict)
            except FileNotFoundError as err:
                print(f"Include file '{incfile}' not found... exiting")
                exit(2)
        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:
            ## Don't process hidden, backup, or underscore-prefixed files
            if f.startswith(".") or f.startswith("_") or f.endswith("~"):
                continue

            ## 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)


    ## 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.get("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.get("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.get("HeadIncludes"), template_vars)
                        if headstr: of.write(headstr)

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

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

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

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

                        ## Optional page footer
                        pfstr = pagematch_includes(CONFIG.get("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__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--version", action="version", version=warhol.__version__)
ap.add_argument("--verbose", dest="VERBOSE", action="store_true", default=False, help="print verbose debugging information while processing")
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", "--template", dest="SITE_TEMPLATE", nargs="?", const="default", default=None, help="name of a site template to initialise [none, 'default' if no arg]")
ap_init.add_argument("-C", "-n", "--no-config", dest="NO_CONFIG", action="store_true", default=False, help="don't even write a config-file 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="warhol.toml", help="TOML config file for the site build [warhol.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)
