# w.c.s. - web application for online forms
# Copyright (C) 2005-2010  Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.

import os
import glob
import xml.etree.ElementTree as ET

import django.template
from django.template import (engines,
                             TemplateSyntaxError as DjangoTemplateSyntaxError,
                             VariableDoesNotExist as DjangoVariableDoesNotExist)
from django.template.loader import render_to_string
from django.utils.encoding import force_text, smart_text
from django.utils.safestring import SafeString, SafeText
from django.utils import six
from django.utils.six import StringIO

from quixote import get_session, get_request, get_response, get_publisher
from quixote.directory import Directory
from quixote.util import StaticDirectory, StaticFile
from quixote.html import htmltext, htmlescape, TemplateIO

from . import ezt, force_str


def get_template_from_script(filename):
    local_result = {}
    exec(open(filename).read(), {
        'publisher': get_publisher(),
        'request': get_request(),
        '__file__': filename}, local_result)
    return local_result.get('template_content')


def get_theme_directory(theme_id):
    system_location = os.path.join(get_publisher().data_dir, 'themes', theme_id)
    local_location = os.path.join(get_publisher().app_dir, 'themes', theme_id)
    if os.path.exists(local_location):
        location = local_location
    elif os.path.exists(system_location):
        location = system_location
    else:
        return None

    while os.path.islink(location):
        location = os.path.join(os.path.dirname(location), os.readlink(location))

    if not os.path.exists(location):
        return None

    return location


class ThemesDirectory(Directory):
    def _q_lookup(self, name):
        from . import errors
        if name in ('.', '..'):
            raise errors.TraversalError()

        location = get_theme_directory(name)
        if location is None:
            raise errors.TraversalError()

        if os.path.isdir(location):
            return StaticDirectory(location)
        else:
            return StaticFile(location)

def get_themes_dict():
    system_location = os.path.join(get_publisher().data_dir, 'themes')
    local_location = os.path.join(get_publisher().app_dir, 'themes')

    themes = {}
    for theme_xml in glob.glob(os.path.join(system_location, '*/desc.xml')) + \
                glob.glob(os.path.join(local_location, '*/desc.xml')):
        theme_dict = get_theme_dict(theme_xml)
        if not theme_dict:
            continue
        themes[theme_dict.get('name')] = theme_dict
    return themes

def get_theme_dict(theme_xml):
    try:
        tree = ET.parse(theme_xml).getroot()
    except: # parse error
        return None
    publisher = get_publisher()
    name = force_str(tree.attrib['name'])
    version = force_str(tree.attrib.get('version') or '')
    label = force_str(tree.findtext('label') or '')
    desc = force_str(tree.findtext('desc') or '')
    author = force_str(tree.findtext('author') or '')
    icon = None
    if isinstance(theme_xml, six.string_types):
        icon = os.path.join(os.path.dirname(theme_xml), 'icon.png')
        if not os.path.exists(icon):
            icon = None
    theme = {'name': name, 'label': label, 'desc': desc, 'author': author,
             'icon': icon, 'version': version}
    theme['keywords'] = []
    for keyword in tree.findall('keywords/keyword'):
        theme['keywords'].append(keyword.text)
    return theme

def get_themes():
    # backward compatibility function, it returns a tuple with theme info,
    # newer code should use get_themes_dict()
    themes = {}
    for k, v in get_themes_dict().items():
        themes[k] = (v['label'], v['desc'], v['author'], v['icon'])
    return themes

def get_current_theme():
    from .publisher import get_cfg
    current_theme = get_cfg('branding', {}).get('theme', 'default')
    system_location = os.path.join(get_publisher().data_dir, 'themes', current_theme)
    local_location = os.path.join(get_publisher().app_dir, 'themes', current_theme)
    for location in (local_location, system_location):
        if os.path.exists(location):
            return get_theme_dict(os.path.join(location, 'desc.xml'))
    default_theme_location = os.path.join(get_publisher().data_dir, 'themes', 'default')
    return get_theme_dict(os.path.join(default_theme_location, 'desc.xml'))

DEFAULT_TEMPLATE_EZT = """<!DOCTYPE html>
<html lang="[site_lang]">
  <head>
    <title>[page_title]</title>
    <link rel="stylesheet" type="text/css" href="[css]"/>
    [script]
  </head>
  <body[if-any onload] onload="[onload]"[end]>
    <div id="page">
    <div id="top"> <h1>[if-any title][title][else][site_name][end]</h1> </div>
    <div id="main-content">
    [if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
    [body]
    </div>
    <div id="footer">[if-any footer][footer][end]</div>
    </div>
  </body>
</html>"""

DEFAULT_IFRAME_EZT = """<!DOCTYPE html>
<html lang="[site_lang]">
  <head>
    <title>[page_title]</title>
    <link rel="stylesheet" type="text/css" href="[css]"/>
    [script]
  </head>
  <body[if-any onload] onload="[onload]"[end]>
    <div id="main-content">
    [if-any breadcrumb]<p id="breadcrumb">[breadcrumb]</p>[end]
    [body]
    </div>
  </body>
</html>"""

default_template = ezt.Template()
default_template.parse(DEFAULT_TEMPLATE_EZT)

default_iframe_template = ezt.Template()
default_iframe_template.parse(DEFAULT_IFRAME_EZT)


def html_top(title=None, default_org=None):
    if not hasattr(get_response(), 'filter'):
        get_response().filter = {}
    get_response().filter['title'] = title
    get_response().filter['default_org'] = default_org

def error_page(error_message, error_title = None, exception = None, continue_to = None,
            location_hint = None):
    from . import _
    if not error_title:
        error_title = _('Error')
    if exception:
        root_url = get_publisher().get_root_url()
        get_response().add_javascript(['jquery.js', 'exception.js'])

    kwargs = {'title': error_title}
    if location_hint == 'backoffice':
        from .backoffice.menu import html_top as error_html_top
        kwargs[str('section')]  = None
    else:
        error_html_top = html_top

    r = TemplateIO(html=True)
    error_html_top(**kwargs)
    r += htmltext('<div class="error-page">')
    r += htmltext('<p>%s</p>') % error_message

    if exception and get_publisher().logger.error_email:
        r += htmltext('<p>%s</p>') % _('It has been sent to the site administrator for analyse.')

    if continue_to:
        continue_link = htmltext('<a href="%s">%s</a>') % continue_to
        r += htmltext('<p>%s</p>') % htmltext(_('Continue to %s')) % continue_link

    if exception:
        r += htmltext('<p><a id="display-exception">%s</a></p>') % _('View Error Details')
        r += htmltext('<pre id="exception"><code>%s</code></pre>') % force_str(exception)
    r += htmltext('</div>')
    return htmltext(r.getvalue())

def get_default_ezt_template():
    from .publisher import get_cfg
    current_theme = get_cfg('branding', {}).get('theme', 'default')

    filename = os.path.join(get_publisher().app_dir, 'themes',
            current_theme, 'template.%s.ezt' % get_publisher().APP_NAME)
    if os.path.exists(filename):
        return open(filename).read()

    filename = os.path.join(get_publisher().data_dir, 'themes',
            current_theme, 'template.%s.ezt' % get_publisher().APP_NAME)
    if os.path.exists(filename):
        return open(filename).read()

    filename = os.path.join(get_publisher().app_dir, 'themes', current_theme, 'template.ezt')
    if os.path.exists(filename):
        return open(filename).read()

    filename = os.path.join(get_publisher().data_dir, 'themes', current_theme, 'template.ezt')
    if os.path.exists(filename):
        return open(filename).read()

    return DEFAULT_TEMPLATE_EZT


def get_decorate_vars(body, response, generate_breadcrumb=True, **kwargs):
    from .publisher import get_cfg

    if response.content_type != 'text/html':
        return {'body': body}

    body = str(body)
    if get_request().get_header('x-popup') == 'true':
        return {'body': body}

    kwargs = {}
    for k, v in response.filter.items():
        if v:
            kwargs[k] = str(v)
    if 'lang' not in kwargs and hasattr(get_request(), 'language'):
        response.filter['lang'] = get_request().language

    if 'rel="popup"' in body or 'rel="popup"' in kwargs.get('sidebar', ''):
        response.add_javascript(['jquery.js', 'jquery-ui.js', 'popup.js', 'widget_list.js'])

    onload = kwargs.get('onload')
    org_name = get_cfg('sp', {}).get('organization_name',
            kwargs.get('default_org', get_publisher().APP_NAME))
    site_name = get_cfg('misc', {}).get('sitename', org_name)

    current_theme = get_cfg('branding', {}).get('theme', get_publisher().default_theme)

    if kwargs.get('title'):
        title = kwargs.get('title')
        page_title = '%s - %s' % (site_name, title)
        title_or_orgname = title
    else:
        page_title = site_name
        title = None
        title_or_orgname = site_name
    script = kwargs.get('script') or ''
    script += response.get_css_includes_for_header()
    script += response.get_javascript_for_header()

    try:
        user = get_request().user
    except:
        user = None

    if type(user) in (int, str) and get_session():
        try:
            user = get_session().get_user_object()
        except KeyError:
            pass

    root_url = get_publisher().get_application_static_files_root_url()

    theme_url = '%sthemes/%s' % (root_url, current_theme)

    is_in_backoffice = response.filter.get('admin_ezt')

    if is_in_backoffice:
        header_menu = kwargs.get('header_menu')
        user_info = kwargs.get('user_info')
        page_title = kwargs.get('sitetitle', '') + kwargs.get('title', '')
        subtitle = kwargs.get('subtitle')
        sidebar = kwargs.get('sidebar')
        css = root_url + get_publisher().qommon_static_dir + get_publisher().qommon_admin_css

        app_dir_filename = os.path.join(get_publisher().app_dir, 'themes',
                        current_theme, 'admin.css')
        data_dir_filename = os.path.join(get_publisher().data_dir, 'themes',
                        current_theme, 'admin.css')
        for filename in (app_dir_filename, data_dir_filename):
            if os.path.exists(filename):
                extra_css = root_url + 'themes/%s/admin.css' % current_theme
                break
        extra_head = get_publisher().get_site_option('backoffice_extra_head')
        app_label = get_publisher().get_site_option('app_label') or 'w.c.s.'
    else:
        if current_theme == 'default':
            css = root_url + 'static/css/%s.css' % get_publisher().APP_NAME
        else:
            css = root_url + 'themes/%s/%s.css' % (current_theme, get_publisher().APP_NAME)

        # this variable is kept in locals() as it was once part of the default
        # template and existing installations may have template changes that
        # still have it.
        prelude = ''

    if generate_breadcrumb:
        breadcrumb = ''
        if hasattr(response, 'breadcrumb') and response.breadcrumb:
            s = []
            path = root_url
            if is_in_backoffice:
                path += response.breadcrumb[0][0]
                response.breadcrumb = response.breadcrumb[1:]
            total_len = sum([len(str(x[1])) for x in response.breadcrumb if x[1] is not None])
            for component, label in response.breadcrumb:
                if component.startswith('http:') or component.startswith('https:'):
                    s.append('<a href="%s">%s</a>' % (component, label))
                    continue
                if label is not None:
                    if type(label) is str:
                        label = htmlescape(label)
                    if not is_in_backoffice and (total_len > 80 and len(label) > 10 and
                            response.breadcrumb[-1] != (component, label)):
                        s.append('<a href="%s%s" title="%s">%s</a>' % (
                                    path, component, label, '...'))
                    else:
                        s.append('<a href="%s%s">%s</a>' % (path, component, label))
                path += component
            breadcrumb = ' <span class="separator">&gt;</span> '.join(s)

    vars = response.filter.copy()
    vars.update(get_publisher().substitutions.get_context_variables())
    vars.update(locals())
    return vars


def decorate(body, response):
    if get_request().get_header('x-popup') == 'true':
        return '''<div class="popup-content"> %s </div>''' % body

    from .publisher import get_cfg
    generate_breadcrumb = True
    if True:
        template_ezt = get_cfg('branding', {}).get('template')
        current_theme = get_cfg('branding', {}).get('theme', 'default')
        if not template_ezt:
            # the theme can provide a default template
            possible_filenames = []
            possible_filenames.append('template.%s.ezt' % get_publisher().APP_NAME)
            possible_filenames.append('template.ezt')

            possible_dirnames = [
                os.path.join(get_publisher().app_dir, 'themes', current_theme),
                os.path.join(get_publisher().data_dir, 'themes', current_theme),
                os.path.join(get_publisher().data_dir, 'themes', 'default'),
            ]

            for fname in possible_filenames:
                for dname in possible_dirnames:
                    filename = os.path.join(dname, fname)
                    if os.path.exists(filename):
                        template_ezt = open(filename).read()
                        break
                else:
                    continue
                break

        if template_ezt:
            generate_breadcrumb = ('[breadcrumb]' in template_ezt)

            template = ezt.Template()
            template.parse(template_ezt)
        else:
            template = default_template

    fd = StringIO()
    vars = get_decorate_vars(body, response,
            generate_breadcrumb=generate_breadcrumb)

    template.generate(fd, vars)
    return fd.getvalue()


def render(template_name, context):
    request = getattr(get_request(), 'django_request', None)
    result = render_to_string(template_name, context, request=request)
    return htmltext(force_str(result))


class QommonTemplateResponse(object):
    def __init__(self, templates, context):
        self.templates = templates
        self.context = context

    def add_media(self):
        if 'form' in self.context:
            # run add_media so we get them in the page <head>
            self.context['form'].add_media()


class TemplateError(Exception):
    def __init__(self, msg, params=()):
        self.msg = msg
        self.params = params

    def __str__(self):
        from . import misc
        return misc.site_encode(smart_text(self.msg) % self.params)


def ezt_raises(exception, on_parse=False):
    from . import _
    parts = []
    parts.append({
            ezt.ArgCountSyntaxError: _('wrong number of arguments'),
            ezt.UnknownReference: _('unknown reference'),
            ezt.NeedSequenceError: _('sequence required'),
            ezt.UnclosedBlocksError: _('unclosed block'),
            ezt.UnmatchedEndError: _('unmatched [end]'),
            ezt.UnmatchedElseError: _('unmatched [else]'),
            ezt.BaseUnavailableError: _('unavailable base location'),
            ezt.BadFormatConstantError: _('bad format constant'),
            ezt.UnknownFormatConstantError: _('unknown format constant'),
        }.get(exception.__class__, _('unknown error')))
    if exception.line is not None:
        parts.append(_('at line %(line)d and column %(column)d') % {
            'line': exception.line + 1, 'column': exception.column + 1})
    if on_parse:
        message = _('syntax error in ezt template: %s')
    else:
        message = _('failure to render ezt template: %s')
    raise TemplateError(message % ' '.join(parts))


class Template(object):
    def __init__(self, value, raises=False, ezt_format=ezt.FORMAT_RAW,
                 ezt_only=False, autoescape=True):
        '''Guess kind of template (Django or ezt), and parse it'''
        self.value = value
        self.raises = raises

        if ('{{' in value or '{%' in value) and not ezt_only:  # Django template
            self.render = self.django_render
            if autoescape is False:
                value = '{%% autoescape off %%}%s{%% endautoescape %%}' % value
            value = '{%% load %s %%}%s' % (
                    get_publisher().get_default_templatetags_libraries(), value)
            try:
                self.template = engines['django'].from_string(value)
            except DjangoTemplateSyntaxError as e:
                if raises:
                    from . import _
                    raise TemplateError(_('syntax error in Django template: %s'), e)
                self.render = self.null_render

        elif '[' in value:  # ezt template
            self.render = self.ezt_render
            self.template = ezt.Template(compress_whitespace=False)
            try:
                self.template.parse(value, base_format=ezt_format)
            except ezt.EZTException as e:
                if raises:
                    ezt_raises(e, on_parse=True)
                self.render = self.null_render

        else:
            self.render = self.null_render

    def null_render(self, context={}):
        return str(self.value)

    def django_render(self, context={}):
        try:
            rendered = self.template.render(context)
        except (DjangoTemplateSyntaxError, DjangoVariableDoesNotExist) as e:
            if self.raises:
                from . import _
                raise TemplateError(_('failure to render Django template: %s'), e)
            else:
                return self.value
        return force_str(rendered)

    def ezt_render(self, context={}):
        fd = StringIO()
        try:
            self.template.generate(fd, context)
        except ezt.EZTException as e:
            if self.raises:
                ezt_raises(e)
            else:
                return self.value
        return force_str(fd.getvalue())

    @classmethod
    def is_template_string(cls, string):
        return string and ('{{' in string or '{%' in string or '[' in string)


# monkey patch django template Variable resolution to convert legacy
# strings to unicode
variable_resolve_orig = django.template.base.Variable.resolve

def variable_resolve(self, context):
    try:
        value = variable_resolve_orig(self, context)
    except UnicodeEncodeError:
        # don't crash on non-ascii variable names
        return context.template.engine.string_if_invalid
    if six.PY2 and isinstance(value, SafeString):
        return SafeText(value, 'utf-8')
    if isinstance(value, str):
        return force_text(value, 'utf-8')
    return value

if not getattr(django.template.base.Variable, 'monkey_patched', False):
    django.template.base.Variable.resolve = variable_resolve
    django.template.base.Variable.monkey_patched = True
