#
# Copyright 2001 by Object Craft P/L, Melbourne, Australia.
#
# LICENCE - see LICENCE file distributed with this software for details.
#
# Application support which all application types inherit from

import os
import sys
import stat
import imp
import urlparse

from albatross.context import NamespaceMixin, ExecuteMixin, ResourceMixin,\
     NameRecorderMixin, HiddenFieldSessionMixin, PickleSignMixin,\
     CachingTemplateLoaderMixin, _caller_globals

from albatross.session import SessionServerContextMixin,\
     SessionServerAppMixin

from albatross import tags

from albatross.common import *


class Redirect:

    def __init__(self, loc):
        self.loc = loc

class ResponseMixin:

    '''Maintain an ordered dictionary of headers.
    '''

    def __init__(self):
        self.__sent_headers = 0
        self.__seqnum = 0
        self.__headers = {}
        self.set_header('Pragma', 'no-cache')
        self.set_header('Content-Type', 'text/html')

    def get_header(self, name):
        try:
            seqnum, value = self.__headers[name]
            return value
        except KeyError:
            pass

    def set_header(self, name, value):
        if self.__sent_headers:
            raise ApplicationError('headers have already been sent')
        try:
            seqnum, old_value = self.__headers[name]
            self.__headers[name] = (seqnum, value)
            return
        except KeyError:
            pass
        self.__headers[name] = (self.__seqnum, value)
        self.__seqnum = self.__seqnum + 1

    def del_header(self, name):
        if self.__sent_headers:
            raise ApplicationError('headers have already been sent')
        try:
            del self.__headers[name]
        except KeyError:
            pass

    def write_headers(self):
        if self.__sent_headers:
            raise ApplicationError('headers have already been sent')
        headers = []
        for name, seqnum_value in self.__headers.items():
            seqnum, value = seqnum_value
            headers.append((seqnum, name, value))
        headers.sort()
        for seqnum, name, value in headers:
            self.request.write_header(name, value)
        self.request.end_headers()
        self.__sent_headers = 1

    def send_content(self, data):
        if not self.__sent_headers:
            self.write_headers()
        self.request.write_content(data)

    def send_redirect(self, loc):
        cookie = self.get_header('Set-Cookie')
        if cookie:
            self.request.write_header('Set-Cookie', cookie)
        return self.request.redirect(loc)


# With an application object the registration of resources is managed
# by the application.
class AppContext(NamespaceMixin, ResponseMixin, ExecuteMixin):

    def __init__(self, app):
        NamespaceMixin.__init__(self)
        ResponseMixin.__init__(self)
        ExecuteMixin.__init__(self)
        self.locals.__page__ = None
        self.locals.__pages__ = []
        self.app = app

    # ----------------------------------------------------
    # Serve all requests for resources via the application

    def get_macro(self, name):
        return self.app.get_macro(name)

    def register_macro(self, name, macro):
        return self.app.register_macro(name, macro)

    def get_lookup(self, name):
        return self.app.get_lookup(name)

    def register_lookup(self, name, lookup):
        return self.app.register_lookup(name, lookup)

    def get_tagclass(self, name):
        return self.app.get_tagclass(name)

    # ----------------------------------------------------
    # Serve all requests for templates via the application

    def load_template(self, name):
        return self.app.load_template(name)

    def load_template_once(self, name):
        return self.app.load_template_once(name)

    def run_template(self, name):
        templ = self.app.load_template(name)
        self.set_globals(_caller_globals('run_template'))
        templ.to_html(self)

    def run_template_once(self, name):
        templ = self.app.load_template_once(name)
        if templ:
            self.set_globals(_caller_globals('run_template_once'))
            templ.to_html(self)

    # ----------------------------------------------------
    # Introduce some application specific methods
    def clear_locals(self):
        page, pages = self.locals.__page__, self.locals.__pages__
        NamespaceMixin.clear_locals(self)
        self.locals.__page__, self.locals.__pages__ = page, pages

    def __enter_page(self, name, args):
        self.locals.__page__ = name
        self.app.load_page(self)
        self.app.page_enter(self, args)

    def __leave_page(self, new_page_name):
        if self.locals.__page__ \
           and self.locals.__page__ != new_page_name:
            self.app.load_page(self)
            self.app.page_leave(self)

    def set_page(self, name, *args):
        self.__leave_page(name)
        self.__enter_page(name, args)

    def push_page(self, name, *args):
        if self.locals.__page__ \
           and self.locals.__page__ != name:
            self.locals.__pages__.append(self.locals.__page__)
        self.__enter_page(name, args)

    def pop_page(self, target_page = None):
        while 1:
            try:
                name = self.locals.__pages__.pop()
            except IndexError:
                raise ApplicationError('pop_page: page stack is empty')
            self.__leave_page(name)
            self.locals.__page__ = name
            self.app.load_page(self)
            if target_page is None or target_page == name:
                break

    def set_request(self, req):
        self.request = req

    def req_equals(self, name):
        try:
            if self.request.field_value(name): 
                return 1
        except KeyError:
            pass
        return 0

    def base_url(self):
        return self.app.base_url()

    def current_url(self):
        uri = self.request.get_uri()
        scheme, netloc, path = urlparse.urlparse(uri)[:3]
        return path

    def absolute_base_url(self):
        this_path = urlparse.urlparse(self.request.get_uri())[2]
        base_path = urlparse.urlparse(self.app.base_url())[2]
        pos = this_path.find(base_path)
        new_base = this_path[:pos + len(base_path)]
        if new_base[-1] != '/':
            new_base = new_base + '/'
        return new_base

    def redirect_url(self, loc):
        new_base = self.absolute_base_url()
        return urlparse.urlunparse(('http', self.request.get_servername(),
                                    new_base + loc, '', '', ''))

    def redirect(self, loc):
        scheme, netloc, path = urlparse.urlparse(loc)[:3]
        if scheme or netloc or (path and path[0] == '/'):
            raise Redirect(loc)
        # Redirect from base url - turn into full URL redirect
        new_loc = self.redirect_url(loc)
        raise Redirect(new_loc)


# Basic application processing sequence is implemented in a class
# which is then subclassed in cgiapp and modapp.
class Application(ResourceMixin):

    def __init__(self, base_url):
        ResourceMixin.__init__(self)
        self.register_tagclasses(*tags.tags)
        # Use a base_url of '/' if not provided
        if base_url:
            self.__base_url = base_url
        else:
            self.__base_url = '/'

    def run(self, req):
        '''Process a single browser request
        '''
        ctx = None
        try:
            ctx = self.create_context()
            ctx.set_request(req)
            self.load_session(ctx)
            self.load_page(ctx)
            if self.validate_request(ctx):
                self.merge_request(ctx)
                self.process_request(ctx)
            self.display_response(ctx)
            self.save_session(ctx)
            ctx.flush_content()
        except Redirect, e:
            self.save_session(ctx)
            return ctx.send_redirect(e.loc)
        except:
            self.handle_exception(ctx, req)
        return req.status()

    def format_exception(self):
        import traceback
        import linecache
        etype, value, tb = sys.exc_info()
        list = traceback.format_exception(etype, value, tb)
        pyexc = ''.join(list)
        list = []
        list.append('Template traceback (most recent call last):')
        for tup in self.template_traceback(tb):
            list.append('  File "%s", line %s, in %s' % tup)
            line = linecache.getline(tup[0], tup[1]).rstrip()
            if line:
                list.append(line)
        htmlexc = '\n'.join(list)
        return pyexc, htmlexc

    def handle_exception(self, ctx, req):
        pyexc, htmlexc = map(tags.escape, self.format_exception())
        req.write_header('Content-Type', 'text/html')
        req.end_headers()
        try:
            tmp_ctx = self.create_context()
            tmp_ctx.set_request(req)
            tmp_ctx.locals.python_exc = pyexc
            tmp_ctx.locals.html_exc = htmlexc
            templ = self.load_template('traceback.html')
            templ.to_html(tmp_ctx)
            tmp_ctx.flush_content()
        except:
            req.write_content('<pre>')
            req.write_content(htmlexc)
            req.write_content('\n\n')
            req.write_content(pyexc)
            req.write_content('</pre>')
        if ctx:
            self.remove_session(ctx)

    def template_traceback(self, tb):
        list = []
        prev = None
        while tb is not None:
            f = tb.tb_frame
            if f.f_locals.has_key('self'):
                obj = f.f_locals['self']
                if hasattr(obj, 'line_num'):
                    tag = (obj.filename, obj.line_num, obj.name)
                    if tag != prev:
                        list.append(tag)
                        prev = tag
            tb = tb.tb_next
        return list

    def load_session(self, ctx):
        ctx.load_session()

    def save_session(self, ctx):
        ctx.save_session()

    def remove_session(self, ctx):
        ctx.remove_session()

    def validate_request(self, ctx):
        return 1

    def base_url(self):
        return self.__base_url

    def merge_request(self, ctx):
        ctx.merge_request()

    def pickle_sign(self, text):
        return ''

    def pickle_unsign(self, text):
        return ''

# ------------------------------------------------------------------
# Page processing handlers
# ------------------------------------------------------------------


# A caching page module loader which only reloads the page module it
# has been modified.
class PageModuleMixin:

    '''Caching module loader
    '''

    def __init__(self, base_dir, start_page):
        self.__base_dir = base_dir
        self.__start_page = start_page
        self.__cache = {}

    def module_path(self):
        return self.__base_dir

    def start_page(self):
        return self.__start_page

    def load_page(self, ctx):
        if ctx.locals.__page__:
            name = ctx.locals.__page__
        else:
            name = self.__start_page
            ctx.set_page(name)
        self.load_page_module(ctx, name)

    def load_page_module(self, ctx, name):
        if self.__base_dir == '.':
            path = name
        else:
            path = os.path.join(self.__base_dir, name)
        page = self.__cache.get(path)
        if page:
            mtime = os.stat(page.__file__)[stat.ST_MTIME]
            if mtime > page.__mtime__:
                reload(page)
                page.__mtime__ = mtime
        if not page:
            dirname, name = os.path.split(path)
            file, filepath, desc = imp.find_module(name, [dirname])
            page = sys.modules.get(name)
            if not page or not page.__file__.startswith(filepath):
                try:
                    page = imp.load_module(name, file, filepath, desc)
                finally:
                    if file:
                        file.close()
            if not (hasattr(page, 'page_enter') or
                    hasattr(page, 'page_leave') or
                    hasattr(page, 'page_process') or
                    hasattr(page, 'page_display')):
                raise ApplicationError('module "%s" does not define one of page_enter, page_leave, page_process or page_display' % (name))
            page.__mtime__ = os.stat(page.__file__)[stat.ST_MTIME]
            self.__cache[path] = page
        ctx.page = page

    def page_enter(self, ctx, args):
        if hasattr(ctx.page, 'page_enter'):
            func = getattr(ctx.page, 'page_enter')
            func(ctx, *args)

    def page_leave(self, ctx):
        if hasattr(ctx.page, 'page_leave'):
            func = getattr(ctx.page, 'page_leave')
            func(ctx)

    def process_request(self, ctx):
        if hasattr(ctx.page, 'page_process'):
            func = getattr(ctx.page, 'page_process')
            func(ctx)

    def display_response(self, ctx):
        if hasattr(ctx.page, 'page_display'):
            func = getattr(ctx.page, 'page_display')
            func(ctx)


# A page loader in which pages are implemented in objects.
class PageObjectMixin:

    def __init__(self, start_page):
        self.__start_page = start_page
        self.__page_objects = {}

    def module_path(self):
        pass

    def start_page(self):
        return self.__start_page

    def register_page(self, name, obj):
        self.__page_objects[name] = obj

    def load_page(self, ctx):
        if ctx.locals.__page__:
            name = ctx.locals.__page__
        else:
            name = self.__start_page
            ctx.set_page(name)
        ctx.page = name

    def page_enter(self, ctx, args):
        obj = self.__page_objects[ctx.page]
        if hasattr(obj, 'page_enter'):
            func = getattr(obj, 'page_enter')
            func(ctx, *args)

    def page_leave(self, ctx):
        obj = self.__page_objects[ctx.page]
        if hasattr(obj, 'page_leave'):
            func = getattr(obj, 'page_leave')
            func(ctx)

    def process_request(self, ctx):
        obj = self.__page_objects[ctx.page]
        if hasattr(obj, 'page_process'):
            func = getattr(obj, 'page_process')
            func(ctx)

    def display_response(self, ctx):
        obj = self.__page_objects[ctx.page]
        if hasattr(obj, 'page_display'):
            func = getattr(obj, 'page_display')
            func(ctx)

# ------------------------------------------------------------------
# Prepackaged application contexts
# ------------------------------------------------------------------


# Simple application context simply stores field names in form to
# allow empty fields to be set to None.  Also saves state in hidden
# form fields.
class SimpleAppContext(AppContext,
                       NameRecorderMixin,
                       HiddenFieldSessionMixin):

    def __init__(self, app):
        AppContext.__init__(self, app)
        NameRecorderMixin.__init__(self)
        HiddenFieldSessionMixin.__init__(self)

    def form_close(self):
        use_multipart_enc = NameRecorderMixin.form_close(self)
        HiddenFieldSessionMixin.form_close(self)
        return use_multipart_enc


# Session state is stored server side
class SessionAppContext(AppContext,
                        NameRecorderMixin,
                        SessionServerContextMixin):

    def __init__(self, app):
        AppContext.__init__(self, app)
        NameRecorderMixin.__init__(self)
        SessionServerContextMixin.__init__(self)

# ------------------------------------------------------------------
# Prepackaged application objects
# ------------------------------------------------------------------


# Page processing is implemented in methods of page objects
class SimpleApp(PickleSignMixin,
                Application,
                CachingTemplateLoaderMixin,
                PageObjectMixin):

    def __init__(self, base_url, template_path, start_page, secret):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageObjectMixin.__init__(self, start_page)

    def create_context(self):
        return SimpleAppContext(self)


# Same as SimpleApp with server-side session support
class SimpleSessionApp(PickleSignMixin,
                       Application,
                       CachingTemplateLoaderMixin,
                       PageObjectMixin,
                       SessionServerAppMixin):

    def __init__(self, base_url, template_path, start_page, secret,
                 session_appid, session_server = 'localhost', server_port = 34343, session_age = 1800):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageObjectMixin.__init__(self, start_page)
        SessionServerAppMixin.__init__(self, session_appid, session_server, server_port, session_age)

    def create_context(self):
        return SessionAppContext(self)


# Page processing is implemented in functions of page modules
class ModularApp(PickleSignMixin,
                 Application,
                 CachingTemplateLoaderMixin,
                 PageModuleMixin):

    def __init__(self, base_url, module_path, template_path, start_page, secret):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageModuleMixin.__init__(self, module_path, start_page)

    def create_context(self):
        return SimpleAppContext(self)


# Same as ModularApp with server-side session support
class ModularSessionApp(PickleSignMixin,
                        Application,
                        CachingTemplateLoaderMixin,
                        PageModuleMixin,
                        SessionServerAppMixin):

    def __init__(self, base_url, module_path, template_path, start_page, secret,
                 session_appid, session_server = 'localhost', server_port = 34343, session_age = 1800):
        Application.__init__(self, base_url)
        PickleSignMixin.__init__(self, secret)
        CachingTemplateLoaderMixin.__init__(self, template_path)
        PageModuleMixin.__init__(self, module_path, start_page)
        SessionServerAppMixin.__init__(self, session_appid, session_server, server_port, session_age)

    def create_context(self):
        return SessionAppContext(self)
