import sys
import logging
import re
import threading

from pkg_resources import iter_entry_points, load_entry_point

import tw
from tw.core.exceptions import WidgetException
from tw.core.util import LRUCache


__all__ = ["EngineManager", "Renderable", "EngineException", "display",
           "render"]


log = logging.getLogger(__name__)


class EngineException(WidgetException):
    pass

#XXX: This make_renderer/get_render_method/etc... is a mess which should be
#     cleaned sometime
def make_renderer(method, doc=None):
    def _renderer(self, renderable, **kw):
        template = kw.pop('template', renderable.template)
        if template is None:
            return
        origin = dynamic_best_engine(renderable, kw)
        destination = kw.get('displays_on', renderable.displays_on)
        if origin != 'cheetah':
            template = self.load_template(template, origin)
        renderer = self.get_render_method(origin, destination, method)
        output = renderer(info=kw, template=template)
        if method == 'display':
            output = self.adapt_output(output, destination)
        # Make sure string output is always unicode
        if isinstance(output, str):
            output = unicode(output, 'utf-8')
        return output
    _renderer.func_name = method
    _renderer.__doc__ = doc
    return _renderer

class EngineManager(dict):
    """
    Manages availble templating engines.
    """
    default_view = 'toscawidgets'

    def __init__(self, extra_vars_func=None, options=None, load_all=False):
        self.extra_vars_func = extra_vars_func
        self.options = options
        self._cache = LRUCache(50)
        self._lock = threading.Lock()
        if load_all:
            self.load_all()
            

    def __repr__(self):
        return "< %s >" % self.__class__.__name__


    def load_engine(self, name, options=None, extra_vars_func=None, 
                    distribution=None):
        factory = None
        if distribution:
            factory = load_entry_point(
                distribution, "python.templating.engines", name)
        else:
            for entrypoint in iter_entry_points("python.templating.engines"):
                if entrypoint.name == name:
                    factory = entrypoint.load()

        if factory is None:
            raise EngineException("No plugin available for engine '%s'" % name)

        options = options or self.options or {}
        options = options.copy()
        # emulate Kid and Genshi's dotted-path notation lookup
        options.setdefault('mako.directories', []).extend(sys.path)
        # make sure mako produces utf-8 output so we can decode it and use
        # unicode internally
        options['mako.output_encoding'] = 'utf-8'
        extra_vars_func = extra_vars_func or self.extra_vars_func

        self._lock.acquire()
        try:
            self[name] = factory(extra_vars_func, options)
        finally:
            self._lock.release()

    def __getitem__(self, name):
        """
        Return a Buffet plugin by name. If the plugin is not loaded it
        will try to load it with default arguments.
        """
        try:
            return dict.__getitem__(self, name)
        except KeyError:
            self.load_engine(name)
            return dict.__getitem__(self, name)


    def load_all(self, engine_options=None, stdvars=None, raise_error=False):
        for ep in iter_entry_points("python.templating.engines"):
            try:
                self.load_engine(ep.name, engine_options, stdvars)
            except:
                log.warn("Failed to load '%s' template engine: %r",
                         ep.name, sys.exc_info())
                if raise_error:
                    raise


    def load_template(self, template, engine_name):
        """Return's a compiled template and it's enginename"""
        output = None
        if isinstance(template, basestring) and _is_inline_template(template):
            # Assume inline template, try to fetch from cache
            key = (template, engine_name)
            try:
                output = self._cache[key]
                #log.debug("Cache hit for: %s", `key`)
            except KeyError:
                #log.debug("Cache miss for: %s", `key`)
                output = self._cache[key] = self[engine_name].load_template(
                    None, template_string = template
                    )
        elif isinstance(template, basestring):
            # Assume template path
            output = self[engine_name].load_template(template)
        else:
            # Assume compiled template
            output = template
        return output

    display = make_renderer('display', doc=\
        """
        Displays the renderable. Returns appropiate output for target template
        engine
        """)

    render = make_renderer('render', doc=\
        """
        Returns the serialized output in a string.
        Useful for debugging or to return to the browser as-is.
        """)

    def get_render_method(self, origin, destination, method):
        engine = self[origin]
        # Only pass-through Element/Stream output if rendering on the same
        # template engine as the one producing output
        if method == 'display' and \
          origin == destination and origin in ['kid', 'genshi']:
            return engine.transform
        # In any other case render as a string
        def _render_xhtml(**kw):
            kw.update(format='xhtml')
            return engine.render(**kw)
        return _render_xhtml


    def adapt_output(self, output, destination):
        # Handle rendering strings on Genshi so they're not escaped
        if isinstance(output, basestring) and destination == 'genshi':
            from genshi.input import HTML
            output = HTML(output)
        # Handle rendering strings on Kid so they're not escaped
        elif isinstance(output, basestring) and destination == 'kid':
            from kid import XML
            output = XML(output)
        return output
    
# Available as a public symbol for easier extension with PEAK-Rules for
# backwards compatibility
display = EngineManager.display.im_func
render = EngineManager.render.im_func

def choose_engine(obj, engine_name=None):
    tpl = obj.template
    if isinstance(tpl, basestring) and not _is_inline_template(tpl):
        colon = tpl.find(":")
        if colon > -1:
            engine_name = tpl[:colon]
            tpl = tpl[colon+1:]
    return engine_name, tpl

def dynamic_best_engine(renderable, params):
    try:
        ideal_engine = params['displays_on']
    except KeyError:
        ideal_engine = renderable.displays_on
    
    if ideal_engine not in renderable.available_engines:
        try:
            best_engine = params['engine_name']
        except KeyError:
            best_engine = renderable.engine_name
    else:
        best_engine = ideal_engine
    assert best_engine is not None
    return best_engine
    
_is_inline_template = re.compile(r'(<|\n|\$)').search

class Renderable(object):
    """Base class for all objects that the EngineManager can render"""

    engine_name = 'toscawidgets'
    available_engines = []
    template = None


    def displays_on(self):
        return tw.framework.default_view
    displays_on = property(displays_on, doc="""\
        Where the Renderable is being displayed on
        """)

    def __new__(cls, *args, **kw):
        obj = object.__new__(cls, *args, **kw)
        obj.template = kw.pop("template", obj.template)
        engine_name = kw.pop("engine_name", None)
        if obj.template is not None:
            colon_based_engine_name, obj.template = choose_engine(obj)
            # if there is a colon in the engine name, the available engines should be narrowed
            # to that engine.
            if colon_based_engine_name:
                obj.available_engines = [colon_based_engine_name,]
            engine_name = colon_based_engine_name
        if engine_name:
            obj.engine_name = engine_name
        return obj

    def render(self, **kw):
        kw.setdefault('_', tw.framework.translator)
        return tw.framework.engines.render(self, **kw)

    def display(self, **kw):
        kw.setdefault('_', tw.framework.translator)
        return tw.framework.engines.display(self, **kw)
