#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai

__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os
import logging
from logging.handlers import RotatingFileHandler

import cherrypy
from cherrypy.process.plugins import SimplePlugin

from calibre.constants import __appname__, __version__
from calibre.utils.date import fromtimestamp
from calibre.library.server import listen_on, log_access_file, log_error_file
from calibre.library.server.utils import expose
from calibre.utils.mdns import publish as publish_zeroconf, \
            stop_server as stop_zeroconf, get_external_ip
from calibre.library.server.content import ContentServer
from calibre.library.server.mobile import MobileServer
from calibre.library.server.xml import XMLServer
from calibre.library.server.opds import OPDSServer
from calibre.library.server.cache import Cache
from calibre.library.server.browse import BrowseServer
from calibre.utils.search_query_parser import saved_searches
from calibre import prints


class DispatchController(object): # {{{

    def __init__(self, prefix, wsgi=False):
        self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
        self.funcs = []
        self.seen = set([])
        self.prefix = prefix if prefix else ''
        if wsgi:
            self.prefix = ''

    def __call__(self, name, route, func, **kwargs):
        if name in self.seen:
            raise NameError('Route name: '+ repr(name) + ' already used')
        self.seen.add(name)
        kwargs['action'] = 'f_%d'%len(self.funcs)
        if route != '/':
            route = self.prefix + route
        elif self.prefix:
            self.dispatcher.connect(name+'prefix_extra', self.prefix, self,
                    **kwargs)
        self.dispatcher.connect(name, route, self, **kwargs)
        self.funcs.append(expose(func))

    def __getattr__(self, attr):
        if not attr.startswith('f_'):
            raise AttributeError(attr + ' not found')
        num = attr.rpartition('_')[-1]
        try:
            num = int(num)
        except:
            raise AttributeError(attr + ' not found')
        if num < 0 or num >= len(self.funcs):
            raise AttributeError(attr + ' not found')
        return self.funcs[num]

# }}}

class BonJour(SimplePlugin): # {{{

    def __init__(self, engine, port=8080, prefix=''):
        SimplePlugin.__init__(self, engine)
        self.port = port
        self.prefix = prefix

    def start(self):
        try:
            publish_zeroconf('Books in calibre', '_stanza._tcp',
                            self.port, {'path':self.prefix+'/stanza'})
        except:
            import traceback
            cherrypy.log.error('Failed to start BonJour:')
            cherrypy.log.error(traceback.format_exc())

    start.priority = 90

    def stop(self):
        try:
            stop_zeroconf()
        except:
            import traceback
            cherrypy.log.error('Failed to stop BonJour:')
            cherrypy.log.error(traceback.format_exc())


    stop.priority = 10

cherrypy.engine.bonjour = BonJour(cherrypy.engine)

# }}}

class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
        BrowseServer):

    server_name = __appname__ + '/' + __version__

    def __init__(self, db, opts, embedded=False, show_tracebacks=True,
            wsgi=False):
        self.is_wsgi = bool(wsgi)
        self.opts = opts
        self.embedded = embedded
        self.state_callback = None
        self.max_cover_width, self.max_cover_height = \
                        map(int, self.opts.max_cover.split('x'))
        path = P('content_server')
        self.build_time = fromtimestamp(os.stat(path).st_mtime)
        self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()

        cherrypy.engine.bonjour.port = opts.port
        cherrypy.engine.bonjour.prefix = opts.url_prefix

        Cache.__init__(self)

        self.set_database(db)

        st = 0.1 if opts.develop else 1

        cherrypy.config.update({
            'log.screen'             : opts.develop,
            'engine.autoreload_on'   : getattr(opts,
                                        'auto_reload', False),
            'tools.log_headers.on'   : opts.develop,
            'checker.on'             : opts.develop,
            'request.show_tracebacks': show_tracebacks,
            'server.socket_host'     : listen_on,
            'server.socket_port'     : opts.port,
            'server.socket_timeout'  : opts.timeout, #seconds
            'server.thread_pool'     : opts.thread_pool, # number of threads
            'server.shutdown_timeout': st, # minutes
        })
        if embedded or wsgi:
            cherrypy.config.update({'engine.SIGHUP'          : None,
                                    'engine.SIGTERM'         : None,})
        self.config = {}
        self.is_running = False
        self.exception = None
        if not wsgi:
            self.setup_loggers()
            cherrypy.engine.bonjour.subscribe()
            self.config['global'] = {
                'tools.gzip.on'        : True,
                'tools.gzip.mime_types': ['text/html', 'text/plain',
                    'text/xml', 'text/javascript', 'text/css'],
            }
            if opts.password:
                self.config['/'] = {
                        'tools.digest_auth.on'    : True,
                        'tools.digest_auth.realm' : (
                            'Password to access your calibre library. Username is '
                            + opts.username.strip()),
                        'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
                }

        self.__dispatcher__ = DispatchController(self.opts.url_prefix, wsgi)
        for x in self.__class__.__bases__:
            if hasattr(x, 'add_routes'):
                x.add_routes(self, self.__dispatcher__)
        root_conf = self.config.get('/', {})
        root_conf['request.dispatch'] = self.__dispatcher__.dispatcher
        self.config['/'] = root_conf

    def set_database(self, db):
        self.db = db
        sr = getattr(self.opts, 'restriction', None)
        sr = db.prefs.get('cs_restriction', '') if sr is None else sr
        self.set_search_restriction(sr)

    def graceful(self):
        cherrypy.engine.graceful()

    def set_search_restriction(self, restriction):
        self.search_restriction_name = restriction
        if restriction:
            if restriction not in saved_searches().names():
                prints('WARNING: Content server: search restriction ',
                       restriction, ' does not exist')
                self.search_restriction = ''
            else:
                self.search_restriction = 'search:"%s"'%restriction
        else:
            self.search_restriction = ''
        self.reset_caches()

    def setup_loggers(self):
        access_file = log_access_file
        error_file  = log_error_file
        log = cherrypy.log

        maxBytes = getattr(log, "rot_maxBytes", 10000000)
        backupCount = getattr(log, "rot_backupCount", 1000)

        # Make a new RotatingFileHandler for the error log.
        h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
        h.setLevel(logging.DEBUG)
        h.setFormatter(cherrypy._cplogging.logfmt)
        log.error_log.addHandler(h)

        # Make a new RotatingFileHandler for the access log.
        h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
        h.setLevel(logging.DEBUG)
        h.setFormatter(cherrypy._cplogging.logfmt)
        log.access_log.addHandler(h)

    def start(self):
        self.is_running = False
        cherrypy.tree.mount(root=None, config=self.config)
        try:
            try:
                cherrypy.engine.start()
            except:
                ip = get_external_ip()
                if not ip or ip.startswith('127.'):
                    raise
                cherrypy.log('Trying to bind to single interface: '+ip)
                # Change the host we listen on
                cherrypy.config.update({'server.socket_host' : ip})
                # This ensures that the change is actually applied
                cherrypy.server.socket_host = ip
                cherrypy.server.httpserver = cherrypy.server.instance = None

                cherrypy.engine.start()

            self.is_running = True
            #if hasattr(cherrypy.engine, 'signal_handler'):
            #    cherrypy.engine.signal_handler.subscribe()

            cherrypy.engine.block()
        except Exception as e:
            self.exception = e
            import traceback
            traceback.print_exc()
        finally:
            self.is_running = False
            try:
                if callable(self.state_callback):
                    self.state_callback(self.is_running)
            except:
                pass

    def exit(self):
        try:
            cherrypy.engine.exit()
        finally:
            cherrypy.server.httpserver = None
            self.is_running = False
            try:
                if callable(self.state_callback):
                    self.state_callback(self.is_running)
            except:
                pass

    def threaded_exit(self):
        from threading import Thread
        t = Thread(target=self.exit)
        t.daemon = True
        t.start()

