#!/usr/bin/env python

# Copyright (C) 2012 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 re
import sys
import shlex
import locale
from json import dumps, loads
from os.path import join, exists

from gevent import monkey

from sugar_network import db, client, toolkit
from sugar_network.resources.volume import Volume
from sugar_network.client import IPCRouter
from sugar_network.client.commands import ClientCommands
from sugar_network.toolkit import application, coroutine, util
from sugar_network.toolkit import Option, BUFFER_SIZE, enforce


porcelain = Option(
        'give the output in an easy-to-parse format for scripts',
        default=False, type_cast=Option.bool_cast, action='store_true',
        name='porcelain')

post_data = Option(
        'send content as a string from POST or PUT command',
        name='post_data', short_option='-d')

post_file = Option(
        'send content of the specified file from POST or PUT command',
        name='post_file', short_option='-f')

json = Option(
        'treat POST or PUT command content as a JSON data',
        name='json', short_option='-j', default=False,
        type_cast=Option.bool_cast, action='store_true')

offline = Option(
        'do not connect to Sugar Network server',
        default=False, type_cast=Option.bool_cast, action='store_true',
        name='offline')


_ESCAPE_VALUE_RE = re.compile('([^\\[\\]\\{\\}0-9][^\\]\\[\\{\\}]+)')


class Application(application.Application):

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

        application.rundir.value = join(client.local_root.value, 'run')
        util.init_logging(application.debug.value)

        if not exists(toolkit.cachedir.value):
            os.makedirs(toolkit.cachedir.value)

    @application.command(
            'send POST API request')
    def POST(self):
        self._call('POST', True)

    @application.command(
            'send PUT API request')
    def PUT(self):
        self._call('PUT', True)

    @application.command(
            'send DELETE API request')
    def DELETE(self):
        self._call('DELETE', False)

    @application.command(
            'send GET API request')
    def GET(self):
        self._call('GET', False)

    def _call(self, method, post):
        request = db.Request(method=method)
        request.allow_redirects = True
        response = db.Response()
        reply = []

        if post:
            if post_data.value is None and post_file.value is None:
                json.value = True
                post_data.value = sys.stdin.read()

            if post_data.value:
                request.content = post_data.value.strip()
            elif post_file.value:
                with file(post_file.value, 'rb') as f:
                    # TODO Avoid loading entire file
                    request.content = f.read()

            request.content_type = 'application/octet-stream'
            if json.value:
                try:
                    request.content = loads(request.content)
                    request.content_type = 'application/json'
                except Exception:
                    # TODO
                    pass

        if self.args and self.args[0].startswith('/'):
            path = self.args.pop(0).strip('/').split('/')
            request['document'] = path.pop(0)
            if path:
                request['guid'] = path.pop(0)
            if path:
                request['prop'] = path.pop(0)

        for arg in self.args:
            arg = shlex.split(arg)
            if not arg:
                continue
            arg = arg[0]
            if '=' not in arg:
                reply.append(arg)
                continue
            arg, value = arg.split('=', 1)
            arg = arg.strip()
            enforce(arg, 'No argument name in %r expression', arg)
            if arg in request:
                if isinstance(request[arg], basestring):
                    request[arg] = [request[arg]]
                request[arg].append(value)
            else:
                request[arg] = value

        pid_path = None
        server = None
        cp = None
        try:
            if self.check_for_instance():
                cp = client.IPCClient()
            else:
                pid_path = self.new_instance()
                if not client.anonymous.value:
                    util.ensure_key(client.key_path())
                home = Volume(client.path('db'))
                cp = ClientCommands(home,
                        client.api_url.value if not offline.value else None,
                        no_subscription=True)
                if not offline.value:
                    for __ in cp.subscribe(event='inline', state='online'):
                        break
                    coroutine.dispatch()
                server = coroutine.WSGIServer(
                        ('localhost', client.ipc_port.value), IPCRouter(cp))
                coroutine.spawn(server.serve_forever)
                coroutine.dispatch()
            result = cp.call(request, response)
        finally:
            if server is not None:
                server.close()
            if cp is not None:
                cp.close()
            if pid_path:
                os.unlink(pid_path)

        if result is None:
            return

        if response.content_type == 'application/json':
            if porcelain.value:
                if type(result) in (list, tuple):
                    for i in result:
                        # TODO
                        print i
                else:
                    # TODO
                    print result
            elif reply:
                for key in reply:
                    key = _ESCAPE_VALUE_RE.sub("'\\1'", key)
                    print eval('result%s' % key)
            else:
                print dumps(result, indent=2)
        elif response.content_type == 'text/event-stream':
            while True:
                chunk = util.readline(result)
                if not chunk:
                    break
                sys.stdout.write(chunk)
        elif hasattr(result, 'read'):
            while True:
                chunk = result.read(BUFFER_SIZE)
                if not chunk:
                    break
                sys.stdout.write(chunk)
        else:
            sys.stdout.write(result)

# Let toolkit.http work in concurrence
# XXX No DNS because `toolkit.network.res_init()` doesn't work otherwise
monkey.patch_socket(dns=False)
monkey.patch_select()
monkey.patch_ssl()
monkey.patch_time()

# 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.debug, porcelain, post_data, post_file, json, offline,
    ])
Option.seek('client', [
    client.api_url, client.layers, client.ipc_port, client.local_root,
    client.no_dbus, client.anonymous, client.accept_language,
    ])

locale.setlocale(locale.LC_ALL, '')

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