#!/usr/bin/env python

'''
Created on Dec 6, 2013

:author: jzupka, astepano
:contact: Andrei Stepanov <astepano@redhat.com>
'''

from __future__ import division
import sys
import time
import inspect

from virttest.remote_commander import remote_interface
from virttest.remote_commander import messenger


def getsource(obj):
    return inspect.getsource(obj)


def wait_timeout(timeout):
    if timeout is None:
        while 1:
            yield True
    else:
        end_time = time.time() + timeout
        while time.time() < end_time:
            yield True


class CmdMaster(object):

    """
    Representation of BaseCmd on master side.
    """

    def __init__(self, commander, name, *args, **kargs):
        """
        :params commander: Commander from which was command started.
        :params name: Name parsed to string representation
        :type name: [str, str, str]
        :parmas args: list to arguments to cmd.
        :type args: []
        :params kargs: {}
        """
        self._basecmd = remote_interface.BaseCmd(name, *args, **kargs)
        self.commander = commander
        self._stdout = ""
        self._stderr = ""
        self._results_cnt = 0
        self._stdout_cnt = 0
        self._stderr_cnt = 0
        self.timeout = kargs.get('timeout', None)

    def getbasecmd(self):
        """
        Property basecmd getter
        """
        self._results_cnt = 0
        return self._basecmd

    def setbasecmd(self, value):
        """
        Property basecmd setter _resuls_cnt identify if value was change from
        last reading.
        """

        self._basecmd = value
        self._results_cnt += 1

    basecmd = property(getbasecmd, setbasecmd)

    def getstdout(self):
        """
        Property stdout getter
        """
        self._stdout_cnt = 0
        return self._stdout

    def setstdout(self, value):
        """
        Property stdout setter _stdout_cnt identify if value was change from
        last reading.
        """
        self._stdout = value
        self._stdout_cnt += 1

    stdout = property(getstdout, setstdout)

    def getstderr(self):
        """
        Property stderr getter
        """
        self._stderr_cnt = 0
        return self._stderr

    def setstderr(self, value):
        """
        Property stderr setter _stderr_cnt identify if value was change from
        last reading.
        """
        self._stderr = value
        self._stderr_cnt += 1

    stderr = property(getstderr, setstderr)

    def send_stdin(self, msg):
        """
        Send data to stdin
        """
        self.commander.manage.send_msg(msg, self.basecmd.cmd_id)

    def wait(self):
        """
        Wait until command return results.
        """
        return self.commander.wait(self)

    def wait_response(self):
        """
        Wait until command return any cmd.
        """
        self.commander.wait_response(self)

    def __getattr__(self, name):
        """
        Shortcut to encapsulated basecmd.
        """
        if name in ["__getstate__", "__setstate__", "__slots__"]:
            raise AttributeError()
        return getattr(self.basecmd, name)

    def set_commander(self, commander):
        """
        For nohup commands it allows connect cmd to new created commander.

        """
        self.commander = commander
        if self not in commander.cmds:
            commander.cmds[self.cmd_id] = self
        self.commander.manage.register_cmd(self.basecmd,
                                           remote_interface.BaseCmd.single_cmd_id)


class CmdEncapsulation(object):

    """
    Class parse command name   cmd.nohup.shell -> ["nohup", "shell"]
    """

    def __init__(self, master, obj_name, name):
        self.master = master
        if obj_name is None:
            self.name = [name]
        else:
            self.name = obj_name + [name]
        self.cmd = None

    def __getattr__(self, name):
        return CmdEncapsulation(self.master, self.name, name)

    def __call__(self, *args, **kargs):
        """
        Call commander with specific command.
        """
        self.cmd = CmdMaster(self.master, self.name, *args, **kargs)
        return self.master.cmd(self.cmd)


class CmdTimeout(remote_interface.MessengerError):

    """
    Raised when waiting for cmd exceeds time define by timeout.
    """

    def __init__(self, msg):
        super(CmdTimeout, self).__init__(msg)

    def __str__(self):
        return "Commander Timeout %s" % (self.msg)


class Commander(object):

    """
    Commander representation for transfer over network.
    """
    __slots__ = []


class CommanderMaster(messenger.Messenger):

    """
    Class commander master is responsible for communication with commander
    slave. It invoke commands to slave part and receive messages from them.
    For communication is used only stdin and stdout which are streams from
    slave part.
    """

    def __init__(self, stdin, stdout, debug=False):
        """
        :type stdin: IOWrapper with implemented write function.
        :type stout: IOWrapper with implemented read function.
        """
        super(CommanderMaster, self).__init__(stdin, stdout)
        self.cmds = {}
        self.debug = debug
        self.responder = None

        self.flush_stdin()
        self.write_msg("start")
        succ, msg = self.read_msg()
        if not succ or msg != "Started":
            raise remote_interface.CommanderError("Remote commander"
                                                  " not started.")

    def set_responder(self, responder):
        """
        :warning: Users's helper becomes a part of a remote_commander.
            remote_commander is treated as a session. All sessions are part of
            BaseVM object. If you provide unpickable helper then your BaseVM
            also becomes unpickable. You can catch something like::

                File "/usr/lib64/python2.7/copy_reg.py", line 74, in _reduce_ex
                getstate = self.__getstate__
                KeyError: '__getstate__'

            For more info see::

                https://github.com/avocado-framework/avocado-vt/issues/455

        """
        self.responder = responder

    def close(self):
        try:
            self.manage.exit()
        except Exception:
            pass
        super(CommanderMaster, self).close()

    def __getattr__(self, name):
        """
        Start parsing unknown attribute in cmd.
        """
        if name in ["__getstate__", "__setstate__", "__slots__"]:
            raise AttributeError()
        return CmdEncapsulation(self, None, name)

    def __deepcopy__(self, memo):
        """
        Replace deepcopy by substituting by network Commander version.
        """
        result = Commander.__new__(Commander)
        memo[id(self)] = result
        return result

    def listen_streams(self, cmd):
        """
        Listen on all streams included in Commander commands.
        """
        if isinstance(cmd, remote_interface.StdStream):
            if (self.debug):
                print(cmd.msg)
            if cmd.isCmdMsg():
                if isinstance(cmd, remote_interface.StdOut):
                    self.cmds[cmd.cmd_id].stdout += cmd.msg
                elif isinstance(cmd, remote_interface.StdErr):
                    self.cmds[cmd.cmd_id].stderr += cmd.msg
            else:
                if isinstance(cmd, remote_interface.StdOut):
                    sys.stdout.write(cmd.msg)
                elif isinstance(cmd, remote_interface.StdErr):
                    sys.stderr.write(cmd.msg)

    def listen_errors(self, cmd):
        """
        Listen for errors raised from slave part of commander.
        """
        if isinstance(cmd, (Exception, remote_interface.CommanderError,
                            remote_interface.MessengerError)):
            raise cmd

    def listen_queries(self, cmd):
        """Manage queries from slave side.
        """
        if isinstance(cmd, remote_interface.CmdQuery):
            assert self.responder  # Should be set by set_responder()
            data = self.responder(*cmd.args, **cmd.kargs)
            reply = remote_interface.CmdRespond(data)
            self.write_msg(reply)

    def listen_cmds(self, cmd):
        """
        Manage basecmds from slave side.
        """
        if isinstance(cmd, remote_interface.BaseCmd):
            if (self.debug):
                print(cmd.func, cmd.results, cmd._finished)

            if isinstance(cmd.results, Exception):
                raise cmd.results
            if cmd.cmd_id in self.cmds:
                self.cmds[cmd.cmd_id].basecmd.update(cmd)
                self.cmds[cmd.cmd_id].basecmd.update_cmd_hash(cmd)

    def listen_messenger(self, timeout=60):
        """
        Wait for msg from slave side and take care about them.
        """
        succ, r_cmd = self.read_msg(timeout)
        if succ is None:
            return r_cmd
        if not succ:
            raise remote_interface.CommanderError("Remote process died.")

        self.listen_errors(r_cmd)
        self.listen_streams(r_cmd)
        self.listen_cmds(r_cmd)
        self.listen_queries(r_cmd)
        return r_cmd

    def cmd(self, cmd):
        """
        Invoke command on client side.
        """
        self.cmds[cmd.basecmd.cmd_id] = cmd
        self.write_msg(cmd.basecmd)
        while (1):
            if cmd.basecmd.func[0] not in ["async", "nohup"]:
                # If not async wait for finish.
                self.wait(cmd)
            else:
                ancmd = self.wait_response(cmd)
                cmd.update_cmd_hash(ancmd)
            return cmd

    def wait(self, cmd):
        """
        Wait until command return results.
        """
        if cmd.cmd_id not in self.cmds:
            return cmd
        m_cmd = self.cmds[cmd.cmd_id]
        if m_cmd.is_finished():
            return m_cmd

        r_cmd = None

        time_step = None
        timeout = cmd.timeout
        if timeout is not None:
            time_step = timeout / 10.0
        w = wait_timeout(timeout)
        for _ in w:
            r_cmd = self.listen_messenger(time_step)
            if isinstance(r_cmd, remote_interface.BaseCmd):
                if (self.debug):
                    print(m_cmd._stdout)
                if r_cmd is not None and r_cmd == m_cmd.basecmd:
                    # If command which we waiting for.
                    if r_cmd.is_finished():
                        del self.cmds[m_cmd.basecmd.cmd_id]
                        m_cmd.basecmd.update(r_cmd)
                        return m_cmd
                    m_cmd.basecmd.update(r_cmd)
                    m_cmd.basecmd.update_cmd_hash(r_cmd)

        if r_cmd is None:
            raise CmdTimeout("%ss during %s" % (timeout, str(cmd)))

    def wait_response(self, cmd):
        """
        Wait until command return any cmd.
        """
        if cmd.cmd_id not in self.cmds:
            return cmd
        if cmd.is_finished() or cmd._stdout_cnt or cmd._stderr_cnt:
            return cmd
        m_cmd = self.cmds[cmd.cmd_id]

        r_cmd = None

        time_step = None
        timeout = cmd.timeout
        if timeout is not None:
            time_step = timeout / 10.0
        w = wait_timeout(timeout)
        while (next(w)):
            r_cmd = self.listen_messenger(time_step)
            if r_cmd is not None and r_cmd == m_cmd.basecmd:
                return m_cmd

        if r_cmd is None:
            raise CmdTimeout(timeout)
