# -*- encoding: utf-8 -*-
"""
So far:
 * Reads directory structure
 * manages overriding parent templates
 * importing macros seems to work
 * can reference other pre-renedered templates in process.
 * works in both main view and detail

TODO: this destroys entry sorting and mg sorting

TODO: how do local vs. global macros work exactly?

TODO: template for rendering remainder of analysis with no entry.

TODO: local CSS files

TODO: reprocess template directories on save doesn't seem to work,
      figure this out somehow

TODO: once we're actually only using these, can do a lot of code cleanup
      of views, soon we will be doing more with less.

TODO:  mobile width - detail - paradigm disappears, was problematic on
    old templates, doublecheck here.

TODO: generated context accessible with new templates? is the way it's
      done now ideal?

TODO: sme pregenerated forms don't really work without sme.py

"""

import os, sys
import yaml

from lxml import etree

__all__ = ['TemplateConfig', 'LanguageNotFound']


cwd = lambda x: os.path.join(os.path.dirname(__file__), x)

class LanguageNotFound(Exception):
    """ Language not found for this project. """
    pass

# A collection for tracking compiled templates. This may provide more
# complexity than necessary: need to check, 
parsed_template_cache = {}

# TODO: read from user defined file elsewhere
# TODO: see following, consider constructing a template loader for all
# this stuff, will help to implement live reloading of project stuff.

#   https://github.com/pallets/jinja/blob/master/jinja2/loaders.py

class TemplateConfig(object):
    """ A class for providing directory-based paradigm definitions.
    This class reads and parses the configs for the sets of languages

    paradigm from dictionary entry nodes and morphological analyses. """

    errorable_templates = [
        'analyses.template',
        'entry.template',
        'paradigm.template',
    ]

    """ Templates in this list will not be rendered on every other page load
    """
    no_subview_rendering = [
        'variant_search.template',
        'detail_search_form.template',
    ]

    def __init__(self, app=None, debug=False, cache=True):
        self.debug = debug
        self._app = app
        self.cache = cache

        self.template_dir = os.path.join( app.config.language_specific_rules_path
                                        , 'templates/'
                                        )

        self.instance = app.config.short_name
        self.render_template_errors = app.config.render_template_errors
        self.languages_available = app.config.languages.keys()

        # Use a plain jinja environment if none exists.
        if self._app is None:
            from jinja2 import Environment
            self.jinja_env = Environment()
        else:
            self.jinja_env = self._app.jinja_env

        self.read_templates_directory()

        self.process_template_paths()

        if self.debug:
            self.print_debug_tree()

    def process_template_paths(self):
        from jinja2 import ChoiceLoader, FileSystemLoader

        # A choice loader for the deepest potential directory first,
        # so when the template loader is used to select a template
        # (generally only when rendering full page templates), the
        # intended option will appear.

        # NB: if this becomes insufficient, PrefixLoader might be good.
        # Can bind options to a prefix, so, sanit/blah.template would
        # resolve to the right thing. Could thus make a loader on top of
        # that to try with a prefix, and then return the unprefixed
        # variant

        reversed_priority = self.template_loader_dirs[::-1]

        self.jinja_env.loader = ChoiceLoader([FileSystemLoader(cwd('templates'))] + [
            FileSystemLoader(p) for p in reversed_priority
        ])

        def process_template_set(ts):
            _ts = {}
            for k, path in ts.iteritems():
                _ts[k] = self.read_template_file(path)
            return _ts

        # Process self.default_templates
        self.default_templates = process_template_set(self.default_templates)

        # Process self.project_templates
        self.project_templates = process_template_set(self.project_templates)

        # Process self.language_templates
        _l_ts = self.language_templates.copy()

        for l, temps in _l_ts.iteritems():
            _l_ts[l] = process_template_set(temps)

        self.language_templates = _l_ts

        return

    def has_template(self, language, template):
        """ Returns a boolean value for the source language iso
        and the template name.
        """
        try:
            tpl = self.get_template(language, template)
        except:
            tpl = False

        if tpl:
            return True
        else:
            return False

    def has_local_override(self, language, template):
        """ Returns a boolean value for the source language iso and teh
        template name, but only if the project short name is found in
        the template path (meaning a local override exists).
        """

        try:
            tpl = self.get_template(language, template)
        except:
            tpl = False

        # TODO: partition path based on
        # app.config.language/specific_rules
        if tpl:
            _, _, _path = tpl.path.partition('language_specific_rules')
            return self.instance in _path

        return tpl

    def get_template(self, language, template):
        """ .. py:function:: get_template(language, template)

        Render a paradigm if one exists for language.

        :param str language: The 3-character ISO for the language.
        :param str template: The  template name

        :return Template: Parsed template object

        """
        from jinja2.exceptions import TemplateNotFound

        # TODO: what exception works best if template doesn't exist
        if language not in self.language_templates:
            raise LanguageNotFound("Missing language <%s>" % language)
        if template not in self.language_templates[language]:
            raise TemplateNotFound("Missing template <%s>" % template)
        return self.language_templates[language][template]

    def render_individual_template(self, language, template, **kwargs):

        tpl = self.get_template(language, template)

        is_still_renderable = template in self.errorable_templates

        # Add default values
        context = {
        }

        context.update(**kwargs)

        # Return the rendered main template.
        try:
            rendered = tpl.render(**context)
        except Exception, e:
            if is_still_renderable:
                rendered = self.render_individual_template(language, 'template_error.template', **{
                    'exception': e.__class__,
                    'message': repr(e),
                    'render_template_errors': self.render_template_errors,
                    'template_name': tpl.path.partition('language_specific_rules')[2],
                })
            else:
                raise e

        return rendered

    def render_template(self, language, template, **extra_kwargs):
        """ Do the actual rendering. This is run for each entry in a lookup.

        Here we apply some things to the context that the user probably
        needs: access to lookup parameters, individual templates, and
        already rendered templates.

        Then at the end, a fully rendered result is returned.

        """

        from flask import g

        tpl = self.get_template(language, template)
        is_still_renderable = template in self.errorable_templates
        error_tpl = self.get_template(language, 'template_error.template')

        # add default things
        dict_opts = self._app.config.dictionary_options.get((g._from,
                                                             g._to))

        context = {
            'lexicon_entry': False,
            # Provide access to lexicon options, xpath statements, etc
            'dictionary_options': dict_opts,
            'analyses': [],

            'user_input': False,
            'word_searches': False,
            'errors': False,
            'show_info': False,
            'successful_entry_exists': False,
            'paradigm': [],
        }

        context['template_root'] = os.path.dirname(tpl.path) + '/'

        # Add templates to the context
        context['templates'] = dict(
            (k.replace('.template', ''), v)
            for k, v in self.language_templates[language].iteritems()
            if k.endswith('.template')
        )

        context['rendered_templates'] = {}

        try:
            lookup_params = extra_kwargs.pop('lookup_parameters')
        except:
            lookup_params = {}

        context['lookup_parameters'] = lookup_params

        context.update(extra_kwargs)

        # Now render the templates for each entry. If there's an error,
        # then we consider it a failure for everything and raise an
        # exception.

        rendered = {}
        for k, t in self.language_templates[language].iteritems():
            if k != template and k.endswith('.template') and k not in self.no_subview_rendering:
                try:
                    rendered[k.replace('.template', '')] = t.render(**context)
                except Exception, e:
                    msg = e.message
                    msg += " in template <%s>" % t.path.partition('language_specific_rules')[2]
                    e_context = {
                        'exception': e.__class__,
                        'message': e.__class__(msg),
                        'template_name': t.path.partition('language_specific_rules')[2],
                        'render_template_errors': self.render_template_errors
                    }

                    if is_still_renderable:
                        rendered[k.replace('.template', '')] = error_tpl.render(**e_context)
                    else:
                        raise e.__class__(msg)

        context['rendered_templates'] = rendered

        # Return the rendered main template.
        return tpl.render(**context)

    def read_templates_directory(self):
        """ .. py:method:: read_paradigm_directory()

        Read through the paradigm directory, and read .paradigm files

        In running contexts, this expects a Flask app instance to be
        passed. For testing purposes, None may be passed.

        Constructs self.default_templates, self.project_templates,
        self.language_templates

        """
        from collections import defaultdict
        from functools import partial

        if self.debug:
            print >> sys.stderr, "* Reading template directory."

        # Path relative to working directory
        _path = self.template_dir

        self.language_templates = {}
        self.template_loader_dirs = [
            os.path.join(os.getcwd(), 'templates/'),
        ]

        def _dirs(p):
            """ Is the path a directory? (TODO: can use different walker) """
            return not p.endswith('.template') and \
                   not p.endswith('.macros') and \
                   not p.startswith('.')

        def _templates(p):
            return p.endswith('.template') or p.endswith('.macros')

        def join_path(p, f):
            return (f, os.path.join(p, f))

        def scan_path_dirs(_p):
            return filter(_dirs, os.listdir(_p))

        def template_dict_for_path(p):
            _join_path = partial(join_path, p)
            return dict( map( _join_path
                            , filter( _templates
                                    , os.listdir(p)
                                    )
                            )
                       )

        # We only want the ones that exist for this instance.
        proj_directories = scan_path_dirs(_path)

        # This holds the root templates, which we'll copy for each
        # project, and language within that project, and then override
        # with the local files.

        # A dictionary of root templates: 
        # {'name.template': '/path/to/name.template'}

        self.template_loader_dirs.append(_path)

        root_templates = template_dict_for_path(_path)
        self.default_templates = root_templates.copy()

        # Find project directories belonging to the instance.
        if self.instance:
            proj_directories = [ p for p in proj_directories
                                 if p == self.instance ]

        # project is not defined in directory structure, so, we just
        # need to copy defaults.
        if self.instance not in proj_directories:
            project_templates = root_templates.copy()
            self.project_templates = project_templates

            for lang in self.languages_available:
                self.language_templates[lang] = project_templates

            # And we're done here.
            return

        # get all the .template files that belong to a project

        # NB, it may be tempting to rewrite this as a recursive
        # strategy, but there's no need yet.

        for project in proj_directories:
            project_templates = root_templates.copy()

            _proj_path = os.path.join( _path
                                     , project
                                     )

            # Add the path for the template loader
            self.template_loader_dirs.append(_proj_path)

            # Construct the template path dict for the project
            local_project_templates = template_dict_for_path(_proj_path)

            # Override the default templates with the local changes
            project_templates.update(local_project_templates)
            self.project_templates = project_templates.copy()

            # Now we roughly repeat the process on language directories.
            for lang in scan_path_dirs(_proj_path):

                lang_project_templates = project_templates.copy()

                _lang_proj_path = os.path.join( os.path.join( _path, project)
                                              , lang
                                              )

                # Template loader
                self.template_loader_dirs.append(_lang_proj_path)

                # Construct the dict for the language
                local_lang_project_templates = template_dict_for_path(_lang_proj_path)

                # Override the previous level's templates with the ones
                # found here.
                lang_project_templates.update(local_lang_project_templates)

                # Add to language template paths
                self.language_templates[lang] = lang_project_templates

        # Now populate the default project settings for the languages
        # that are not defined in the directory structure.

        for lang in self.languages_available:
            if lang not in self.language_templates:
                self.language_templates[lang] = self.project_templates.copy()

    def print_debug_tree(self):
        """ Here we print the handy tree of overridden things
        """
        print
        print 'templates/ '
        for t in self.default_templates.keys():
            print '   + ' + t

        print 
        print '  %s/ ' % self.instance

        for k, f in self.project_templates.iteritems():
            if f.path not in [p.path for p in self.default_templates.values()]:
                print u'    + ' + k
            else:
                print u'      ' + k
        print

        for lang, temps in self.language_templates.iteritems():
            print u'      %s/' % lang

            for k, f in temps.iteritems():
                if f.path not in [p.path for p in self.project_templates.values()]:
                    print u'      + ' + k
                else:
                    print u'        ' + k

            print

        print ' + - overridden here.'
        print
        print

    def read_template_file(self, path):
        if path not in parsed_template_cache:
            with open(path, 'r') as F:
                _raw = F.read().decode('utf-8')
            return self.parse_template_string(_raw, path)
        else:
            return parsed_template_cache.get(path)

    def _template_parse_error_msg(self, exception, path):
        print
        print '--'
        print >> sys.stderr, "Error parsing template at <%s>" % path
        print >> sys.stderr, exception
        print '--'
        print

    def parse_template_string(self, template_string, path):
        parsed_condition = False

        try:
            parsed_template = self.jinja_env.from_string(template_string)
            parsed_template.path = path
        except Exception, e:
            self._template_parse_error_msg(e, path)
            sys.exit()

        return parsed_template
