#!/usr/bin/python
#
# $Id: SocketIO.py,v 1.11 2010-07-28 18:10:20 jloden Exp $
#

import telnetlib
import socket
import string
import re

# Traverse module local imports
from pytraverse.debug import tvdebug
from pytraverse.error import *

class SocketIO(object):
    """SocketIO is used as a wrapper around telnetlib's socket input and output."""

    def __init__(self, host, port, timeout):
        """initialize the socket object, raise NotConnectedError exception on failure."""

        self.TIMEOUT = timeout
        self.ONLINE = False
        try:
            tvdebug.debug("connecting to %s on port %s" % (host, port))
            self.sock = telnetlib.Telnet(host, port)
            self.ONLINE = True
            isready = self.sock.read_until('eady\n', timeout).strip()
            if isready == '':
                raise NotConnectedError("BVE API server failed to return OK 200 ready response")
            else:
                tvdebug.debug(isready)
        except socket.error, (errno, errmsg):
            # tvdebug.error("failed to establish connection to remote host. reason: %s" % (errmsg))
            raise NotConnectedError("unable to create BVE API socket: %s" %(errmsg))

    def __getTimeout(self):
        """Returns the value of the socket timeout."""
        return self.TIMEOUT

    def isConnected(self):
        """Boolean, returns connection state as True/False."""
        return self.ONLINE

    def api_send(self, cmd):
        """
        send a string across the socket, raise NotConnectedError or SocketSendError
        exceptions on error.
        """

        if not self.isConnected():
            # tvdebug.error("no socket instance found. You must call Login() to initiate a BVE API session first")
            raise NotConnectedError("no open socket instance was found")

        else:
            try:
                tvdebug.debug("api_send(): %s" % (cmd))
                self.sock.write(cmd + '\n')
            except socket.error, (errno, errmsg):
                # tvdebug.error("socket error; unable to send data. reason: %s" % (errmsg))
                raise SocketSendError("unable to send data: %s" % (errmsg))

    def api_get_line(self):
        """
        fetch a single line from the socket, raise NotConnectedError or
        SocketReadError exceptions on error.
        """

        if not self.isConnected():
            # tvdebug.error("No socket instance found. You must call Login() to initiate a BVE API session first")
            raise NotConnectedError("no open socket instance was found")

        else:
            try:
                line = self.sock.read_until('\n', self.__getTimeout()).strip()
                if(line not in string.whitespace) and (line):
                    tvdebug.debug("api_get_line(): returning \"%s\"" % line)
                    return line
                else:
                    line = self.sock.read_until('\n', self.__getTimeout()).strip()
                    tvdebug.debug("api_get_line(): returning \"%s\"" % line)
                    return line
            except socket.error, (errno, errmsg):
                # tvdebug.error("socket error; unable to read data. reason: %s" % (errmsg))
                raise SocketReadError("unable to read data: %s" % (errmsg))

    def api_get_multiline(self):
        """
        fetch multi-line response from the socket as a list, raise
        NotConnectedError or SocketReadError exceptions on error.
        """

        response = ""
        retlist = []
        if not self.isConnected():
            # tvdebug.error("No socket instance found. You must call Login() to initiate a BVE API session first")
            raise NotConnectedError("no open socket instance was found")

        try:
            # have to expect \n\n or \r\n\r\n for Windows for result.list
            response = self.sock.expect( [r'\n\n', r'\r\n\r\n', r'\n\r\n'], self.__getTimeout() )
        except EOFError:
            raise SocketReadError("EOF returned before reaching expected end of data. Read so far: \"%r\"" % response[2])

        except socket.error, (errno, errmsg):
            # tvdebug.error("socket error; unable to read data. reason: %s" % (errmsg))
            raise SocketReadError("socket error; unable to read data. Reason: %s" % (errmsg))

        # could be timeout or didn't match the expected patterns
        if response[0] == -1:
            raise SocketReadError("timeout or read error before expected data. Read so far: \"%r\"" % response[2])

        else:
            response = response[2]
            tvdebug.debug("api_get_multiline(): read string \"%r\"" % response)

            # result.list format (pipe separated values, no key/value pairs)
            if re.match(r'.+?\|', response):
                retlist = response.strip().splitlines()
                tvdebug.debug("api_get_multiline(): found pipe-delimited format, returning %s" % retlist)
                return retlist

            # key=value format
            # remove all the whitespace and blank newlines then return a list of response lines
            # regex is needed to account for corner cases such as newlines in a device comment etc
            reobj = re.compile(r'(\".+?=.+?\")\n', re.S)
            tvdebug.debug("api_get_multiline(): attempting to parse key=value format for response string")
            retlist = [x.group(1).replace('\n', '') for x in reobj.finditer(response)]
            tvdebug.debug("api_get_multiline(): returning %s" % retlist)
            return retlist


    def close(self):
        """close the socket and set isConnected() to false."""
        self.sock.close()
        self.ONLINE = False

