#!/usr/bin/env python

# Copyright (C) 2012-2013 Aleksey Lim
#
# 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 3 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 errno
import signal
import locale
import logging
from os.path import join, abspath, exists

from gevent import monkey

import sugar_network_webui as webui
from sugar_network import db, toolkit, client, node
from sugar_network.client.routes import CachedClientRoutes
from sugar_network.node import stats_node, stats_user
from sugar_network.model import RESOURCES
from sugar_network.toolkit.router import Router, Request, Response
from sugar_network.toolkit import mountpoints, printf, application
from sugar_network.toolkit import Option, coroutine


class Application(application.Daemon):

    def __init__(self, **kwargs):
        application.Daemon.__init__(self, **kwargs)

        node.sync_layers.value = client.layers.value
        self.jobs = coroutine.Pool()

        new_root = (client.local_root.value != client.local_root.default)
        client.local_root.value = abspath(client.local_root.value)
        if new_root:
            application.logdir.value = join(client.local_root.value, 'log')
        else:
            application.logdir.value = client.profile_path('logs')
        if not exists(toolkit.cachedir.value):
            os.makedirs(toolkit.cachedir.value)
        application.rundir.value = join(client.local_root.value, 'run')

        coroutine.signal(signal.SIGCHLD, self.__SIGCHLD_cb)

    @application.command(
            'index local Sugar Network database')
    def index(self):
        if self.check_for_instance():
            printf.info('%s already started, no need in index', self.name)
            return

        printf.info('Index database in %r', client.local_root.value)

        volume = db.Volume(client.path('db'), RESOURCES)
        try:
            volume.populate()
        finally:
            volume.close()

    @application.command(hidden=True)
    def graceful_start(self):
        """Restart existing process if it was started in different environment.

        This command makes sense only for Sugar Network activity when it is
        required avoid restarting existing sugar-network-client. But, restart
        process if it was started in different Sugar session.
        """
        pid = self.check_for_instance()
        if pid:
            run_environ = _read_environ(pid)
            if os.environ.get('DISPLAY') == run_environ.get('DISPLAY') and \
                    os.environ.get('DBUS_SESSION_BUS_ADDRESS') == \
                    run_environ.get('DBUS_SESSION_BUS_ADDRESS'):
                printf.info('%s already started in current environment',
                        self.name)
                return
            application.replace.value = True
        self.cmd_start()

    @application.command(hidden=True)
    def debug(self):
        printf.info('Use "start --foreground" command instead')
        application.foreground.value = True
        self.cmd_start()

    def run(self):
        volume = db.Volume(client.path('db'), RESOURCES, lazy_open=True)
        routes = CachedClientRoutes(volume,
                client.api_url.value if not client.server_mode.value else None)
        router = Router(routes, allow_spawn=True)

        logging.info('Listening for IPC requests on %s port',
                client.ipc_port.value)
        server = coroutine.WSGIServer(
                ('localhost', client.ipc_port.value), router)
        self.jobs.spawn(server.serve_forever)
        coroutine.dispatch()

        self.accept()

        def final_start():
            volume.populate()

            if webui.webui.value:
                host = (webui.webui_host.value, webui.webui_port.value)
                logging.info('Start Web server on %s:%s', *host)
                webui_app = webui.get_app(
                        lambda **kwargs: router.call(Request(**kwargs),
                            Response()),
                        'http://localhost:%s' % client.ipc_port.value)
                server = coroutine.WSGIServer(host, webui_app)
                self.jobs.spawn(server.serve_forever)

            if client.mounts_root.value:
                mounts_root = abspath(client.mounts_root.value)
                if not exists(mounts_root):
                    os.makedirs(mounts_root)
                self.jobs.spawn(mountpoints.monitor, mounts_root)

            if client.cache_timeout.value:
                self.jobs.spawn(self._recycle_cache, routes)

            routes.connect()

        def delayed_start(event=None):
            for __ in routes.subscribe(event='delayed-start'):
                break
            logging.info('Proceed delayed start')
            final_start()

        if client.delayed_start.value:
            self.jobs.spawn(delayed_start)
        else:
            final_start()

        try:
            self.jobs.join()
        except KeyboardInterrupt:
            toolkit.exception('%s interrupted', self.name)
        finally:
            self.jobs.kill()
            routes.close()

    def shutdown(self):
        self.jobs.kill()

    def _recycle_cache(self, routes):
        while True:
            logging.debug('Start cache recycling in %d seconds',
                    client.cache_timeout.value)
            coroutine.sleep(client.cache_timeout.value)
            routes.recycle()

    def __SIGCHLD_cb(self):
        while True:
            try:
                pid, __ = os.waitpid(-1, os.WNOHANG)
                if pid:
                    continue
            except OSError, error:
                if error.errno != errno.ECHILD:
                    raise
            break


def _read_environ(pid):
    with file('/proc/%s/environ' % pid, 'rb') as f:
        return dict([i.split('=', 1) for i in f.read().split('\0') if i])


# Let toolkit.http work in concurrence
monkey.patch_socket()
monkey.patch_select()
monkey.patch_ssl()
monkey.patch_time()

locale.setlocale(locale.LC_ALL, '')

# New defaults
application.debug.value = client.logger_level()
# If tmpfs is mounted to /tmp, `os.fstat()` will return 0 free space
# and will brake offline synchronization logic
toolkit.cachedir.value = client.profile_path('tmp')

Option.seek('main', application)
Option.seek('main', [toolkit.cachedir])
Option.seek('webui', webui)
Option.seek('client', client)
Option.seek('db', db)
Option.seek('node', [node.port, node.find_limit, node.sync_layers])
Option.seek('node-stats', stats_node)
Option.seek('user-stats', stats_user)

app = Application(
        name='sugar-network-client',
        description='Sugar Network client application.',
        epilog='See http://wiki.sugarlabs.org/go/Sugar_Network '
               'for details.',
        config_files=[
            '/etc/sweets.d',
            '/etc/sweets.conf',
            '~/.config/sweets/config',
            client.profile_path('sweets.conf'),
            ])
app.start()
