#!/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)

from __future__ import print_function

import ast
import cmd
import signal
import socket
import sys
import termios

from ryu import cfg
from ryu.lib import rpc


CONF = cfg.CONF
CONF.register_cli_opts([
    cfg.ListOpt('peers', default=[],
                help='List of peers, separated by commas. '
                     '(e.g., "hoge=localhost:9998,fuga=localhost:9999")'),
    cfg.StrOpt('command', short='c', default=None,
               help='Command to be executed as single command. '
                    'The default is None and opens interactive console.'),
])


class Peer(object):
    def __init__(self, name, addr):
        self._name = name
        self._addr = addr
        self.socket = None
        self.client = None
        try:
            self.connect()
        except ConnectionError as e:
            print('Exception when connecting to peer "%s": %s' % (name, e))
            raise e

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

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

    def notification(self, n):
        print("NOTIFICATION from %s %s" % (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()

    def close(self):
        self.socket.close()


peers = {}


def add_peer(name, host, port):
    try:
        peer = Peer(name, (host, port))
    except ConnectionError:
        return

    peers[name] = peer


def close_peers():
    for peer in peers.values():
        peer.socket.close()


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 = ast.literal_eval(args[2])
        except (IndexError, ValueError) as e:
            print("argument error: %s" % e)
            return
        try:
            p = peers[peer]
        except KeyError:
            print("unknown peer %s" % peer)
            return
        try:
            f(p, method, params)
        except rpc.RPCError as e:
            print("RPC ERROR %s" % 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 %s" % 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=None):
        close_peers()
        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.items():
            if p.client:
                try:
                    p.client.peek_notification()
                except EOFError:
                    p.client = None
                    print("disconnected %s" % 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 postloop(self):
        close_peers()

    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)

    if CONF.command:
        command = Cmd()
        command.onecmd(CONF.command)
        command.do_EOF()

    Cmd().cmdloop()


if __name__ == "__main__":
    main()
