"""
jinja2-cli
==========

License: BSD, see LICENSE for more details.
"""

import warnings

warnings.filterwarnings("ignore")

import os
import sys
from optparse import Option, OptionParser

sys.path.insert(0, os.getcwd())

PY3 = sys.version_info[0] == 3

if PY3:
    text_type = str
    bytes_type = bytes
else:
    text_type = unicode  # NOQA
    bytes_type = str  # NOQA


def force_text(data):
    if isinstance(data, text_type):
        return data
    if isinstance(data, bytes_type):
        return data.decode("utf8")
    return data


class InvalidDataFormat(Exception):
    pass


class InvalidInputData(Exception):
    pass


class MalformedJSON(InvalidInputData):
    pass


class MalformedINI(InvalidInputData):
    pass


class MalformedYAML(InvalidInputData):
    pass


class MalformedQuerystring(InvalidInputData):
    pass


class MalformedToml(InvalidDataFormat):
    pass


class MalformedXML(InvalidDataFormat):
    pass


class MalformedEnv(InvalidDataFormat):
    pass


def get_format(fmt):
    try:
        return formats[fmt]()
    except ImportError:
        raise InvalidDataFormat(fmt)


def has_format(fmt):
    try:
        get_format(fmt)
        return True
    except InvalidDataFormat:
        return False


def get_available_formats():
    for fmt in formats.keys():
        if has_format(fmt):
            yield fmt
    yield "auto"


def _load_json():
    try:
        import json

        return json.loads, ValueError, MalformedJSON
    except ImportError:
        import simplejson

        return simplejson.loads, simplejson.decoder.JSONDecodeError, MalformedJSON


def _load_ini():
    try:
        import ConfigParser
    except ImportError:
        import configparser as ConfigParser

    def _parse_ini(data):
        try:
            from StringIO import StringIO
        except ImportError:
            from io import StringIO

        class MyConfigParser(ConfigParser.ConfigParser):
            def as_dict(self):
                d = dict(self._sections)
                for k in d:
                    d[k] = dict(self._defaults, **d[k])
                    d[k].pop("__name__", None)
                return d

        p = MyConfigParser()
        p.readfp(StringIO(data))
        return p.as_dict()

    return _parse_ini, ConfigParser.Error, MalformedINI


def _load_yaml():
    from yaml import load, YAMLError

    try:
        from yaml import CSafeLoader as SafeLoader
    except ImportError:
        from yaml import SafeLoader

    def yaml_loader(stream):
        return load(stream, Loader=SafeLoader)

    return yaml_loader, YAMLError, MalformedYAML


def _load_querystring():
    try:
        import urlparse
    except ImportError:
        import urllib.parse as urlparse

    def _parse_qs(data):
        """Extend urlparse to allow objects in dot syntax.

        >>> _parse_qs('user.first_name=Matt&user.last_name=Robenolt')
        {'user': {'first_name': 'Matt', 'last_name': 'Robenolt'}}
        """
        dict_ = {}
        for k, v in urlparse.parse_qs(data).items():
            v = map(lambda x: x.strip(), v)
            v = v[0] if len(v) == 1 else v
            if "." in k:
                pieces = k.split(".")
                cur = dict_
                for idx, piece in enumerate(pieces):
                    if piece not in cur:
                        cur[piece] = {}
                    if idx == len(pieces) - 1:
                        cur[piece] = v
                    cur = cur[piece]
            else:
                dict_[force_text(k)] = force_text(v)
        return dict_

    return _parse_qs, Exception, MalformedQuerystring


def _load_toml():
    import toml

    return toml.loads, Exception, MalformedToml


def _load_xml():
    import xml
    import xmltodict

    return xmltodict.parse, xml.parsers.expat.ExpatError, MalformedXML


def _load_env():
    def _parse_env(data):
        """
        Parse an envfile format of key=value pairs that are newline separated
        """
        dict_ = {}
        for line in data.splitlines():
            line = line.lstrip()
            # ignore empty or commented lines
            if not line or line[:1] == "#":
                continue
            k, v = line.split("=", 1)
            dict_[force_text(k)] = force_text(v)
        return dict_

    return _parse_env, Exception, MalformedEnv


# Global list of available format parsers on your system
# mapped to the callable/Exception to parse a string into a dict
formats = {
    "json": _load_json,
    "ini": _load_ini,
    "yaml": _load_yaml,
    "yml": _load_yaml,
    "querystring": _load_querystring,
    "toml": _load_toml,
    "xml": _load_xml,
    "env": _load_env,
}


def render(template_path, data, extensions, strict=False):
    from jinja2 import (
        __version__ as jinja_version,
        Environment,
        FileSystemLoader,
        StrictUndefined,
    )

    # Starting with jinja2 3.1, `with_` and `autoescape` are no longer
    # able to be imported, but since they were default, let's stub them back
    # in implicitly for older versions.
    # We also don't track any lower bounds on jinja2 as a dependency, so
    # it's not easily safe to know it's included by default either.
    if tuple(jinja_version.split(".", 2)) < ("3", "1"):
        for ext in "with_", "autoescape":
            ext = "jinja2.ext." + ext
            if ext not in extensions:
                extensions.append(ext)

    env = Environment(
        loader=FileSystemLoader(os.path.dirname(template_path)),
        extensions=extensions,
        keep_trailing_newline=True,
    )
    if strict:
        env.undefined = StrictUndefined

    # Add environ global
    env.globals["environ"] = lambda key: force_text(os.environ.get(key))
    env.globals["get_context"] = lambda: data

    return env.get_template(os.path.basename(template_path)).render(data)


def is_fd_alive(fd):
    if os.name == "nt":
        return not os.isatty(fd.fileno())
    import select

    return bool(select.select([fd], [], [], 0)[0])


def cli(opts, args):
    template_path, data = args
    format = opts.format
    if data in ("-", ""):
        if data == "-" or (data == "" and is_fd_alive(sys.stdin)):
            data = sys.stdin.read()
        if format == "auto":
            # default to yaml first if available since yaml
            # is a superset of json
            if has_format("yaml"):
                format = "yaml"
            else:
                format = "json"
    else:
        path = os.path.join(os.getcwd(), os.path.expanduser(data))
        if format == "auto":
            ext = os.path.splitext(path)[1][1:]
            if has_format(ext):
                format = ext
            else:
                raise InvalidDataFormat(ext)

        with open(path) as fp:
            data = fp.read()

    template_path = os.path.abspath(template_path)

    if data:
        try:
            fn, except_exc, raise_exc = get_format(format)
        except InvalidDataFormat:
            if format in ("yml", "yaml"):
                raise InvalidDataFormat("%s: install pyyaml to fix" % format)
            if format == "toml":
                raise InvalidDataFormat("toml: install toml to fix")
            if format == "xml":
                raise InvalidDataFormat("xml: install xmltodict to fix")
            raise
        try:
            data = fn(data) or {}
        except except_exc:
            raise raise_exc("%s ..." % data[:60])
    else:
        data = {}

    extensions = []
    for ext in opts.extensions:
        # Allow shorthand and assume if it's not a module
        # path, it's probably trying to use builtin from jinja2
        if "." not in ext:
            ext = "jinja2.ext." + ext
        extensions.append(ext)

    # Use only a specific section if needed
    if opts.section:
        section = opts.section
        if section in data:
            data = data[section]
        else:
            sys.stderr.write("ERROR: unknown section. Exiting.")
            return 1

    data.update(parse_kv_string(opts.D or []))

    if opts.outfile is None:
        out = sys.stdout
    else:
        out = open(opts.outfile, "w")

    if not PY3:
        import codecs

        out = codecs.getwriter("utf8")(out)

    out.write(render(template_path, data, extensions, opts.strict))
    out.flush()
    return 0


def parse_kv_string(pairs):
    dict_ = {}
    for pair in pairs:
        k, v = pair.split("=", 1)
        dict_[force_text(k)] = force_text(v)
    return dict_


class LazyHelpOption(Option):
    "An Option class that resolves help from a callable"

    def __setattr__(self, attr, value):
        if attr == "help":
            attr = "_help"
        self.__dict__[attr] = value

    @property
    def help(self):
        h = self._help
        if callable(h):
            h = h()
        # Cache on the class to get rid of the @property
        self.help = h
        return h


class LazyOptionParser(OptionParser):
    def __init__(self, **kwargs):
        # Fake a version so we can lazy load it later.
        # This is due to internals of OptionParser, but it's
        # fine
        kwargs["version"] = 1
        kwargs["option_class"] = LazyHelpOption
        OptionParser.__init__(self, **kwargs)

    def get_version(self):
        from jinja2 import __version__ as jinja_version
        from jinja2cli import __version__

        return "jinja2-cli v%s\n - Jinja2 v%s" % (__version__, jinja_version)


def main():
    parser = LazyOptionParser(
        usage="usage: %prog [options] <input template> <input data>"
    )
    parser.add_option(
        "--format",
        help=lambda: "format of input variables: %s"
        % ", ".join(sorted(list(get_available_formats()))),
        dest="format",
        action="store",
        default="auto",
    )
    parser.add_option(
        "-e",
        "--extension",
        help="extra jinja2 extensions to load",
        dest="extensions",
        action="append",
        default=["do", "loopcontrols"],
    )
    parser.add_option(
        "-D",
        help="Define template variable in the form of key=value",
        action="append",
        metavar="key=value",
    )
    parser.add_option(
        "-s",
        "--section",
        help="Use only this section from the configuration",
        dest="section",
        action="store",
    )
    parser.add_option(
        "--strict",
        help="Disallow undefined variables to be used within the template",
        dest="strict",
        action="store_true",
    )
    parser.add_option(
        "-o",
        "--outfile",
        help="File to use for output. Default is stdout.",
        dest="outfile",
        metavar="FILE",
        action="store",
    )
    opts, args = parser.parse_args()

    # Dedupe list
    opts.extensions = set(opts.extensions)

    if len(args) == 0:
        parser.print_help()
        sys.exit(1)

    # Without the second argv, assume they maybe want to read from stdin
    if len(args) == 1:
        args.append("")

    if opts.format not in formats and opts.format != "auto":
        raise InvalidDataFormat(opts.format)

    sys.exit(cli(opts, args))


if __name__ == "__main__":
    main()
