"""

"""

from __future__ import annotations

import builtins
import glob
import importlib
import inspect
import os
import pathlib
import sys

import pkg_resources
from RestrictedPython import compile_restricted, safe_globals
from RestrictedPython.Eval import default_guarded_getitem, default_guarded_getiter
from RestrictedPython.Guards import safe_builtins

from .. import markdown, slrzd
from . import parse

safe_globals["_getiter_"] = default_guarded_getiter
safe_globals["_getitem_"] = default_guarded_getitem

__all__ = [
    "templates",
    "build",
    "TemplatePackage",
    "template",
    "CompiledTemplate",
    "TemplateResult",
]


_builtin_names = (
    "dict",
    "enumerate",
    "float",
    "int",
    "bool",
    "list",
    "long",
    "reversed",
    "set",
    "slice",
    "tuple",
    "xrange",
    "abs",
    "all",
    "any",
    "callable",
    "chr",
    "cmp",
    "divmod",
    "filter",
    "hex",
    "id",
    "isinstance",
    "iter",
    "len",
    "sum",
    "max",
    "min",
    "oct",
    "ord",
    "pow",
    "range",
    "True",
    "False",
    "None",
    "__import__",
    "getattr",
    "object",
    "repr",
    "sorted",
    "basestring",
    "str",
    "bytes",
    "type",
    "zip",
    "round",
    "dir",
    "next",
)


def templates(
    package, *template_paths, restricted=False, base_dir=None, **_globals
) -> TemplatePackage:
    """ """
    # XXX if not template_paths:
    try:
        path = pkg_resources.resource_filename(package, "templates")
    except ModuleNotFoundError:
        path = package
    ns = importlib.import_module(package + ".templates")
    if base_dir:
        path = str(base_dir / path)
    return TemplatePackage(path, ns, restricted=restricted, **_globals)
    # XXX templates = []
    # XXX for template_path in template_paths:
    # XXX     template_path = str(template_path)
    # XXX     path = pkg_resources.resource_filename(package, template_path)
    # XXX     if os.path.isdir(path):
    # XXX         obj = templates(package, template_path)
    # XXX     else:
    # XXX         with open(path) as fp:
    # XXX             template = fp.read()
    # XXX         obj = Template(template, filename=template_path, globals=_globals)
    # XXX     templates.append(obj)
    # XXX return templates


def build(directory):
    """
    compiles directory of templates to python code

    """
    for dirpath, dirnames, filenames in os.walk(directory):
        with open(os.path.join(dirpath, "__init__.py"), "w") as template:
            print("from web.templating.parse import *", file=template)
            print("from web.templating.templating import *", file=template)
            for dirname in dirnames[:]:
                if dirname.startswith("."):
                    dirnames.remove(dirname)
            if dirnames:
                print(file=template)
                for dirname in sorted(dirnames):
                    print("import {}".format(dirname), file=template)
            print(file=template)
            for fn in filenames:
                if fn.startswith((".", "__init__.py")) or fn.endswith("~"):
                    continue
                path = os.path.join(dirpath, fn)
                if "." in fn:
                    name = fn.split(".", 1)[0]
                else:
                    name = fn
                text = open(path).read()
                text = Template.normalize_text(text)
                code = Template.generate_code(text, path)
                code = code.replace("template__", name, 1)
                print(file=template)
                print(code, end="\n\n", file=template)
                print(
                    '{0} = CompiledTemplate({0}, "{1}")'.format(name, path),
                    file=template,
                )
                print("join_ = {}._join".format(name), file=template)
                print("escape_ = {}._escape".format(name), file=template)


def safestr(obj, encoding="utf-8"):
    r"""
    converts any given object to utf-8 encoded string

        >>> safestr("hello")
        'hello'
        >>> safestr(2)
        '2'

    # TODO >>> safestr("\u1234")
    # TODO '\xe1\x88\xb4'

    """
    if isinstance(obj, str):
        return obj
    # TODO elif isinstance(obj, unicode):
    # TODO     return obj.encode(encoding)
    # XXX elif hasattr(obj, "next") and hasattr(obj, "__iter__"):
    # XXX    return itertools.imap(safestr, obj)
    return str(obj)


html_entities = [
    ("&", "amp"),  # must come first/last during quote/unquote
    ("<", "lt"),
    (">", "gt"),
    ("'", "#39"),
    ('"', "quot"),
]


def htmlquote(text):
    r"""
    encode `text` for raw use in HTML

        >>> htmlquote("<'&\">")
        '&lt;&#39;&amp;&quot;&gt;'

    """
    text = str(text)
    for entity, code in html_entities:
        text = text.replace(entity, "&{};".format(code))
    return text


def websafe(val):
    r"""
    return a safe version of text for use in utf-8 encoded HTML, XHTML or XML

        >>> websafe("<'&\">")
        '&lt;&#39;&amp;&quot;&gt;'
        >>> websafe(None)
        ''
        >>> websafe("\u203d")
        '\u203d'

    # TODO >>> websafe("\xe2\x80\xbd")
    # TODO '\u203d'

    """
    if val is None:
        return ""
    # elif isinstance(val, str):
    #     val = val.decode("utf-8")
    # elif not isinstance(val, unicode):
    #     val = unicode(val)
    return htmlquote(val)
    # return htmlquote(str(val))
    # return htmlquote(bytes(val, "utf-8"))


class TemplatePackage:

    """
    a template renderer

        >>> templates = TemplatePackage()  # doctest: +SKIP
        >>> templates.foo()  # doctest: +SKIP
        '<p>bar</p>'

    TemplatePackage are rendered recursively based upon directory layout.

    """

    def __init__(self, directory, ns=None, restricted=False, **kwglobals):
        """
        use the HTML found in in or around given `module`

        """
        self._directory = str(directory)
        if ns:
            self._ns = ns
        else:
            dir_path = pathlib.Path(directory)
            sys.path.insert(0, str(dir_path.parent))
            self._ns = importlib.import_module(dir_path.stem)
        self._globals = {
            k: v
            for k, v in self._ns.__dict__.items()
            if k in getattr(self._ns, "__all__", [])
        }
        self._globals["mkdn"] = markdown.render
        self._globals.update(kwglobals)
        self._cache = {}
        self._restricted = restricted

    def __getattr__(self, name) -> Template:  # TODO | TemplatePackage
        """
        load a template from root module directory

        An `html` subdirectory will be attempted if no template is found.

        """
        try:
            return self._cache[name]
        except KeyError:
            pass
        path = os.path.join(self._directory, name)
        if os.path.isdir(path):
            return TemplatePackage(
                path, self._ns, restricted=self._restricted, **self._globals
            )
        try:
            path = self._get_filename(path)
        except IndexError:
            raise AttributeError("No template named `{}`".format(name))
        with open(path) as f:
            template = f.read()
        compiled = Template(
            template, filename=path, restricted=self._restricted, globals=self._globals
        )
        if os.getenv("WEBCTX") != "dev":
            self._cache[name] = compiled
        return compiled

    def _get_filenames(self, path):
        """"""
        return list(sorted(f for f in os.listdir(path) if not f.endswith("~")))

    def _get_filename(self, path):
        """"""
        return list(
            sorted(
                f
                for f in glob.glob(path + ".*")
                if not (f == "__init__.py" or f.endswith("~"))
            )
        )[0]

    def __dir__(self):
        return [
            os.path.split(p)[-1].split(".")[0]
            for p in self._get_filenames(os.path.join(self._directory))
        ]


class Template:

    """ """

    globals = {}
    builtins = dict(
        (name, getattr(builtins, name))
        for name in _builtin_names
        if name in builtins.__dict__
    )

    def __init__(
        self,
        template,
        filename="<template>",
        restricted=False,
        globals=None,
        builtins=None,
        extensions=None,
    ):
        self._restricted = restricted
        self.extensions = extensions or []
        try:
            with template as fp:
                template = fp.read()
        except (AttributeError, TypeError):
            pass
        self._template = template
        text = Template.normalize_text(str(template))
        code = self.compile_template(text, filename)

        if globals is None:
            globals = self.globals
        if builtins is None:
            builtins = self.builtins

        self.filename = filename
        _globals = {
            "get_obj_name": lambda o: o.__name__,
            "get_obj_docstring": inspect.getdoc,
            "mkdn": markdown.render,
        }

        _globals.update(**globals)
        self._globals = _globals
        self._builtins = builtins
        if code:
            self.template = self._compile(code)  # XXX
        else:
            self.template = lambda: ""

    @staticmethod
    def normalize_text(text):
        """
        normalizes template text by correcting `\r\n`, tabs and BOM chars

        """
        text = text.replace("\r\n", "\n").replace("\r", "\n").expandtabs()
        if not text.endswith("\n"):
            text += "\n"
        BOM = "\xef\xbb\xbf"  # XXX support unicode? u"\ufeff"
        if isinstance(text, str) and text.startswith(BOM):
            text = text[len(BOM) :]
        return text

    def __add__(self, other):
        try:
            new_text = other._template
        except AttributeError:
            new_text = other
        return Template(self._template + new_text, restricted=self._restricted)

    def _repr_html_(self):
        return slrzd.highlight(self._template, ".html")

    def __call__(self, *args, **kwargs):
        """
        return rendered template

        As a side effect the current transaction's headers are updated
        according to content type of template.

        """
        types = {
            "txt": "text/plain",
            "xml": "text/xml",
            "html": "text/html",
            "md": "text/x-markdown",
            "css": "text/css",
            "js": "text/javascript",
            "json": "application/json",
            "py": "text/x-python",
            "svg": "image/svg+xml",
        }
        output = self.template(*args, **kwargs)
        ext = os.path.splitext(self.filename)[1][1:]
        output.content_type = types.get(ext, "text/html")
        # output.content_type = "text/html"
        return output

    @staticmethod
    def generate_code(text, filename, parser=None):
        parser = parser or parse.Parser()
        rootnode = parser.parse(text, filename)
        code = rootnode.emit(indent="").strip()
        return safestr(code)

    def create_parser(self):
        p = parse.Parser()
        for ext in self.extensions:
            p = ext(p)
        return p

    def compile_template(self, template_string, filename):
        code = Template.generate_code(
            template_string, filename, parser=self.create_parser()
        )

        def get_source_line(filename, lineno):
            try:
                lines = open(filename).read().splitlines()
                return lines[lineno]
            except Exception:
                return None

        try:
            if self._restricted:
                compiled_code = compile_restricted(code, filename, "exec")
            else:
                compiled_code = compile(code, filename, "exec")
        except SyntaxError as err:
            if err.lineno:
                # display template line that caused the error along with traceback
                lineno = get_source_line(err.filename, err.lineno - 1)
                try:
                    msg = "\n\nTemplate traceback:\n    File {}, line {}\n    {}"
                    err.msg += msg.format(repr(err.filename), err.lineno - 5, lineno)
                except Exception:
                    pass
            raise
        return compiled_code

    def _compile(self, code):
        if self._restricted:
            env = self.make_env(dict(safe_globals, **self._globals), safe_builtins)
        else:
            env = self.make_env(self._globals or {}, self._builtins)
        exec(code, env)
        return env["template__"]

    def make_env(self, globals, builtins):
        return dict(
            globals,
            __builtins__=builtins,
            ForLoop=parse.ForLoop,
            TemplateResult=TemplateResult,
            escape_=self._escape,
            join_=self._join,
        )

    def _join(self, *items):
        return "".join(items)

    def _escape(self, value, escape=False):
        if value is None:
            value = ""
        value = str(value)
        if escape:
            value = websafe(value)
        return value


template = Template


class CompiledTemplate(Template):

    """ """

    def __init__(self, f, filename):
        super(CompiledTemplate, self).__init__("", filename)
        self.template = f

    def compile_template(self, *a):
        return None

    def _compile(self, *a):
        return None


class TemplateResult(dict):

    """
    a dictionary like object for storing template output

    The result of a template execution is a `body` string and often a col-
    lection of attributes set using `$var: ...`. This class provides both a
    simple dictionary like interface for accessing these attributes as well
    as a string like interface for accessing the text output of the tem-
    plate.

    When the template is in execution, the output is generated part by part
    and those parts are combined at the end. Parts are added to the
    TemplateResult by calling the `extend` method and the parts are combined
    seemlessly when __body__ is accessed.

        >>> template = TemplateResult("hello, world", x="foo")
        >>> template
        <TemplateResult: 'hello, world' {'x': 'foo'}>
        >>> str(template)
        'hello, world'
        >>> template["x"]
        'foo'

        >>> template = TemplateResult()
        >>> template.extend([u"hello", u"world"])
        >>> template
        <TemplateResult: 'helloworld' {}>

    """

    __all__ = ["body"]

    def __init__(self, *a, **kw):
        self._body = None
        super().__init__(**kw)
        self._parts = []
        self.extend = self._parts.extend
        self.extend(a)

    @property
    def body(self):
        if self._body is None:
            self._body = "".join(str(p) if p else "" for p in self._parts)
        return self._body

    def __str__(self):
        return str(self.body).strip()

    # def __bytes__(self):
    #     return bytes(self.body, "utf-8")

    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError("attr `{}` not found".format(name))

    def __add__(self, other):
        self._body = self.body + other.body
        return self

    def __bool__(self):
        return bool(self.body)

    def __repr__(self):
        return "<TemplateResult: '{}' {}>".format(self, super().__repr__())
