#!/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 types
import locale
from json import dumps, loads
from os.path import join, exists, isfile

from gevent import monkey

from sugar_network import db, client, toolkit
from sugar_network.model import RESOURCES
from sugar_network.client import IPCConnection, Connection
from sugar_network.client.routes import ClientRoutes
from sugar_network.toolkit.router import Router, Request, Response
from sugar_network.toolkit import application, coroutine
from sugar_network.toolkit import Option, BUFFER_SIZE, enforce


quiet = Option(
        'turn off any output',
        default=False, type_cast=Option.bool_cast, action='store_true',
        name='quiet')

porcelain = Option(
        'give the output in an easy-to-parse format for scripts',
        default=False, type_cast=Option.bool_cast, action='store_true',
        short_option='-P', 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(r'([^\[\]\{\}0-9][^\]\[\{\}]+)')
_LIST_RE = re.compile(r'\s*[;,:]+\s*')


class ClientRouter(Router, ClientRoutes):

    def __init__(self):
        home = db.Volume(client.path('db'), RESOURCES, lazy_open=True)
        Router.__init__(self, self)
        ClientRoutes.__init__(self, home,
                client.api_url.value if not offline.value else None,
                no_subscription=True)
        if not offline.value:
            for __ in self.subscribe(event='inline', state='online'):
                break
            coroutine.dispatch()
        server = coroutine.WSGIServer(
                ('localhost', client.ipc_port.value), self)
        coroutine.spawn(server.serve_forever)
        coroutine.dispatch()


class Application(application.Application):

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

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

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

    @application.command(
            'launch a Sugar activity; the COMMAND-LINE-ARGUMENTS might '
            'include arguments supported by sugar-activity application',
            args='BUNDLE_ID [COMMAND-LINE-ARGUMENTS]',
            interspersed_args=False,
            )
    def launch(self):
        enforce(self.check_for_instance(), 'No sugar-network-client session')
        ipc = IPCConnection()

        enforce(self.args, 'BUNDLE_ID was not specified')
        bundle_id = self.args.pop(0)

        params = {}
        if self.args:
            params['args'] = self.args

        ipc.get(['context', bundle_id], cmd='launch', **params)

    @application.command(
            'upload new implementaion for a context; if BUNDLE_PATH points '
            'not to a .xo bundle, specify all implementaion PROPERTYs for the '
            'new release (at least context and version)',
            args='BUNDLE_PATH [PROPERTY=VALUE]',
            )
    def release(self):
        enforce(self.args, 'BUNDLE_PATH was not specified')
        path = self.args.pop(0)
        enforce(isfile(path), 'Cannot open bundle')

        props = {}
        self._parse_args(props)
        if 'license' in props:
            value = [i for i in _LIST_RE.split(props['license'].strip()) if i]
            props['license'] = value

        if self.check_for_instance():
            conn = IPCConnection()
        else:
            conn = Connection(client.api_url.value)
        guid = conn.upload(['implementation'], path, cmd='submit', **props)

        if porcelain.value:
            self._print(guid, '\n')
        else:
            self._print('-- Uploaded %s implementaion' % guid, '\n')

    @application.command(
            'send raw API POST request; '
            'specifies all ARGUMENTs the particular API call requires',
            args='PATH [ARGUMENT=VALUE]')
    def POST(self):
        self._request('POST', True, Response())

    @application.command(
            'send raw API PUT request; '
            'specifies all ARGUMENTs the particular API call requires',
            args='PATH [ARGUMENT=VALUE]')
    def PUT(self):
        self._request('PUT', True, Response())

    @application.command(
            'send raw API DELETE request',
            args='PATH')
    def DELETE(self):
        self._request('DELETE', False, Response())

    @application.command(
            'send raw API GET request; '
            'specifies all ARGUMENTs the particular API call requires',
            args='PATH [ARGUMENT=VALUE]')
    def GET(self):
        self._request('GET', False, Response())

    @application.command(
            'send raw API HEAD request; '
            'specifies all ARGUMENTs the particular API call requires',
            args='PATH [ARGUMENT=VALUE]')
    def HEAD(self):
        response = Response()
        self._request('HEAD', False, response)
        result = {}
        result.update(response)
        result.update(response.meta)
        self._dump(result)

    def _request(self, method, post, response):
        request = Request(method=method)
        request.allow_redirects = True
        request.accept_encoding = ''

        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

        self._parse_path(request)
        self._parse_args(request)

        pid_path = None
        cp = None
        try:
            if self.check_for_instance():
                cp = IPCConnection()
            else:
                pid_path = self.new_instance()
                cp = ClientRouter()
            result = cp.call(request, response)

            if result is None:
                pass
            elif response.content_type == 'application/json':
                self._dump(result)
            elif isinstance(result, types.GeneratorType):
                for chunk in result:
                    self._dump(chunk)
            elif hasattr(result, 'read'):
                if response.content_type == 'text/event-stream':
                    while True:
                        chunk = toolkit.readline(result)
                        if not chunk:
                            break
                        if chunk.startswith('data: '):
                            self._dump(loads(chunk[6:]))
                else:
                    while True:
                        chunk = result.read(BUFFER_SIZE)
                        if not chunk:
                            break
                        self._print(chunk)
            else:
                self._print(result, '\n')
        finally:
            if cp is not None:
                cp.close()
            if pid_path:
                os.unlink(pid_path)

    def _parse_path(self, request):
        if self.args and self.args[0].startswith('/'):
            request.path = self.args.pop(0).strip('/').split('/')

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

    def _dump(self, result):
        if not porcelain.value:
            self._print(dumps(result, indent=2, ensure_ascii=False), '\n')
            return

        def porcelain_dump(value):
            if type(value) is dict:
                if len(value) == 1:
                    porcelain_dump(value.values()[0])
                else:
                    for i in sorted(value.items()):
                        self._print('%-18s%s' % i, '\n')
            else:
                if type(value) not in (list, tuple):
                    value = [value]
                for n, i in enumerate(value):
                    if n:
                        self._print('\t')
                    if type(i) is dict and len(i) == 1:
                        i = i.values()[0]
                    self._print(i)
                self._print('\n')

        if type(result) in (list, tuple):
            for i in result:
                porcelain_dump(i)
        elif type(result) is dict and \
                'total' in result and 'result' in result:
            for i in result['result']:
                porcelain_dump(i)
        else:
            porcelain_dump(result)

    def _print(self, *data):
        if not quiet.value:
            sys.stdout.write(''.join(data))


# Let toolkit.http work in concurrence
monkey.patch_socket()
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, quiet, porcelain, post_data, post_file, json, offline,
    ])
Option.seek('main', [toolkit.cachedir])
Option.seek('client', client)
Option.seek('db', db)

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.d',
            '/etc/sweets.conf',
            '~/.config/sweets/config',
            client.profile_path('sweets.conf'),
            ],
        )
app.start()
