#!/usr/bin/env python
#
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2013 YAMAMOTO Takashi <yamamoto at valinux co jp>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# a simple command line msgpack-rpc client
#
# a usage example:
#     % PYTHONPATH=. ./bin/rpc-cli \
#      --peers=echo-server=localhost:9999,hoge=localhost:9998
#     (Cmd) request echo-server echo ["hoge"]
#     RESULT hoge
#     (Cmd) request echo-server notify ["notify-method", ["param1","param2"]]
#     RESULT notify-method
#     (Cmd)
#     NOTIFICATION from echo-server ['notify-method', ['param1', 'param2']]
#     (Cmd)

import ryu.contrib

from ryu import cfg

import cmd
import signal
import socket
import sys
import termios

from ryu.lib import rpc


CONF = cfg.CONF
CONF.register_cli_opts([
    # eg. rpc-cli --peers=hoge=localhost:9998,fuga=localhost:9999
    cfg.ListOpt('peers', default=[], help='list of peers')
])


class Peer(object):
    def __init__(self, name, addr):
        self._name = name
        self._addr = addr
        self.client = None
        try:
            self.connect()
        except:
            pass

    def connect(self):
        self.client = None
        s = socket.create_connection(self._addr)
        self.client = rpc.Client(s, notification_callback=self.notification)

    def try_to_connect(self, verbose=False):
        if self.client:
            return
        try:
            self.connect()
            assert self.client
        except Exception, e:
            if verbose:
                print "connection failure", e
            raise EOFError

    def notification(self, n):
        print "NOTIFICATION from", self._name, n

    def call(self, method, params):
        return self._do(lambda: self.client.call(method, params))

    def send_notification(self, method, params):
        self._do(lambda: self.client.send_notification(method, params))

    def _do(self, f):
        def g():
            try:
                return f()
            except EOFError:
                self.client = None
                raise

        self.try_to_connect(verbose=True)
        try:
            return g()
        except EOFError:
            print "disconnected.  trying to connect..."
            self.try_to_connect(verbose=True)
            print "connected.  retrying the request..."
            return g()


peers = {}


def add_peer(name, host, port):
    peers[name] = Peer(name, (host, port))


class Cmd(cmd.Cmd):
    def __init__(self, *args, **kwargs):
        self._in_onecmd = False
        self._notification_check_interval = 1  # worth to be configurable?
        self._saved_termios = None
        cmd.Cmd.__init__(self, *args, **kwargs)

    def _request(self, line, f):
        args = line.split(None, 2)
        try:
            peer = args[0]
            method = args[1]
            params = eval(args[2])
        except:
            print "argument error"
            return
        try:
            p = peers[peer]
        except KeyError:
            print "unknown peer", peer
            return
        try:
            f(p, method, params)
        except rpc.RPCError, e:
            print "RPC ERROR", e
        except EOFError:
            print "disconnected"

    def _complete_peer(self, text, line, _begidx, _endidx):
        if len((line + 'x').split()) >= 3:
            return []
        return [name for name in peers if name.startswith(text)]

    def do_request(self, line):
        """request <peer> <method> <params>
        send a msgpack-rpc request and print a response.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            result = p.call(method, params)
            print "RESULT", result

        self._request(line, f)

    def do_notify(self, line):
        """notify <peer> <method> <params>
        send a msgpack-rpc notification.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            p.send_notification(method, params)

        self._request(line, f)

    def complete_request(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def complete_notify(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def do_EOF(self, _line):
        sys.exit(0)

    def emptyline(self):
        self._peek_notification()

    def postcmd(self, _stop, _line):
        self._peek_notification()

    def _peek_notification(self):
        for k, p in peers.iteritems():
            if p.client:
                try:
                    p.client.peek_notification()
                except EOFError:
                    p.client = None
                    print "disconnected", k

    @staticmethod
    def _save_termios():
        return termios.tcgetattr(sys.stdin.fileno())

    @staticmethod
    def _restore_termios(t):
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, t)

    def preloop(self):
        self._saved_termios = self._save_termios()
        signal.signal(signal.SIGALRM, self._timeout)
        signal.alarm(1)

    def onecmd(self, string):
        self._in_onecmd = True
        try:
            return cmd.Cmd.onecmd(self, string)
        finally:
            self._in_onecmd = False

    def _timeout(self, _sig, _frame):
        if not self._in_onecmd:
            # restore terminal settings. (cooked/raw, ...)
            # required for pypy at least.
            # this doesn't seem to be needed for cpython readline
            # module but i'm not sure if it's by spec or luck.
            o = self._save_termios()
            self._restore_termios(self._saved_termios)
            self._peek_notification()
            self._restore_termios(o)
        signal.alarm(self._notification_check_interval)


def main(args=None, prog=None):
    CONF(args=args, prog=prog, project='rpc-cli', version='rpc-cli')

    for p_str in CONF.peers:
        name, addr = p_str.split('=')
        host, port = addr.rsplit(':', 1)
        add_peer(name, host, port)

    Cmd().cmdloop()


if __name__ == "__main__":
    main()
