#!/usr/bin/python
#
# $Id: SocketServer.py,v 1.3 2010-07-02 17:40:37 jloden Exp $
#

import telnetlib
import re

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

class SocketServer(object):
    """
    Base class for Traverse.SimpleServerClient class, implements socket server
    higher level interface for sending data, parsing responses, login
    operations etc. that are common to Traverse's Socket Server APIs (BVE API,
    EDF, ISM).

    This class should not be utilized directly. Instead use one of the
    subclassed socket API interfaces, such as Traverse.Provisioning, which
    extends SimpleServerClient with API-specific operations.
    """

    def __init__(self, host="localhost", port=7661, timeout=30, debug=False):
        """Intializes the SocketServer object. No connections are made at this point."""

        self.HOST = host
        self.PORT = port
        self.TIMEOUT = timeout
        self.mysock = None
        self.RETCODE = None
        self.RETNUM = None
        self.RESPONSE = ''
        self.FULL_RESPONSE = ''
        self.COUNT = 0
        self.SUCCESS = False
        self.ONLINE = False
        self.NEED_RESULTSET = False
        self.RESULTSET = []
        tvdebug.setDebug(debug)

        # tvdebug.debug("initialized SocketServer object ...")

    def isConnected(self):
        """Return True if connected to a BVE API server, or False if not."""
        return self.ONLINE

    def GetErrorMsg(self):
        """
        Return the last error string received from the BVE API. Typically used
        in conjunction with GetSuccess().
        """
        return self.ERRMSG


    def GetFullResponseText(self):
        """return the entire API response without parsing or translation."""
        return self.FULL_RESPONSE


    def GetResponseCode(self):
        """Return the response code from the API, e.g. "ERR" or "OK"."""
        return self.RETCODE


    def GetResponseText(self):
        """Return the API response text following the response code."""
        return self.RESPONSE


    def GetResponseNumCode(self):
        """Return the numeric API response code, e.g. 200 for OK responses."""
        return self.RETNUM


    def GetResultCount(self):
        """Return the number of items affected by a query."""
        return self.COUNT


    def GetSuccess(self):
        """Returns True/False based on the response code of the last query."""
        return self.SUCCESS


    def GetResultSet(self):
        """
        return a list of objects resulting from the previous BVE API command.
        Note that SocketIO.api_get_multiline() which is called here can throw
        SocketReadError if problems are encountered reading from the socket.
        Returns and empty list if no items are able to be parsed from the API
        response text.
        """

        self.RESULTSET = []
        if self.GetResultCount() > 0 and self.NEED_RESULTSET:
            self.NEED_RESULTSET = False
            api_lines = self.mysock.api_get_multiline()
            self.RESULTSET = [self._parse_params(line) for line in api_lines]

        return self.RESULTSET


    def _parse_params(self, line):
        """
        Parse a response line for key/value pairs and return them as a
        dictionary indexed by each key as a string. All keys are converted to
        lowercase for consistency.

        Example:
            mydict = _parse_params("deviceName=localhost")

            # prints "localhost"
            print mydict[devicename]

        """

        # check for pipe-separated values for result.list commands and ignore
        if re.match("(.+?\|)+", line):
            return line
            
        linedict = {}
        parser = re.compile(r'\"([^\"]+?)=([^\"]*?)\"', re.I|re.S)
        for param in parser.finditer(line):
            linedict[param.group(1).lower()] = param.group(2)
        return linedict


    def Login(self, user, password, timeout=None):
        """
        Log in to the BVE API. Creates a SocketIO object, which will raise an
        exception if socket creation fails or data cannot be sent. The socket
        timeout can be set here and will override the value set at
        initialization of the SocketServer instance. Raises LoginFailure if
        login is unsuccessful.
        """

        if timeout is None:
            timeout = self.TIMEOUT
        tvdebug.debug("creating socket object with host: %s, port: %s, timeout: %s" % (self.HOST, self.PORT, timeout))
        self.mysock = SocketIO(self.HOST, self.PORT, timeout)
        self.mysock.api_send("login %s/%s" % (user,password))
        self._parse_response()
        self.ONLINE = self.GetSuccess()
        if not self.GetSuccess():
            raise LoginFailure(self.GetFullResponseText())
        return self.GetSuccess()


    def Logout(self):
        """
        Log out from the BVE API and return True/False based on the response
        code. Sets isConnected() to False and closes the socket.
        """

        if self.mysock is not None:
            self.mysock.api_send("logout")
            self._parse_response()
            self.mysock.close()
            self.ONLINE = False
        return self.GetSuccess()


    def _parse_response(self):
        """Read the response line from the last command sent, and parse out the
        response code, result count, and response text to be accessed through
        the class methods GetResponseCode(), GetResultCount() and
        GetResponseText().
        """

        # clear out our instance variables
        self.RETNUM = ''
        self.ERRMSG = ''
        self.RESPONSE = ''
        self.FULL_RESPONSE = ''
        self.COUNT = 0
        self.SUCCESS = True
        self.NEED_RESULTSET = False

        # set a few local vars
        errtype = ''
        errtxt  = ''
        errcode = None

        response = self.mysock.api_get_line()
        tvdebug.debug("_parse_response(): response: \"%s\"" % response)
        self.FULL_RESPONSE = response

        # ISM API uses slightly different format of "ERR: failure reason" or
        # simply "OK", with no numeric code or other information. We check for
        # that format first then move on to the more standardized BVE API style
        if response == "OK":
            self.RETCODE = response
            self.RESPONSE = response
            return

        if response.startswith("ERR:"):
            self.RETCODE = "ERR"
            self.SUCCESS = False
            self.RESPONSE = response[4:]
            self.ERRMSG = self.RESPONSE
            return

        # try to split the errcode from the reason text for "ERR failure reason"
        # formatted messages
        try:
            errtype, errtxt = response.split(None,1)
            self.RETCODE = errtype
        except:
            tvdebug.error("unable to parse response code")
            tvdebug.debug("full response text: \"%s\"" % (response))
            self.RETCODE = None
            self.SUCCESS = False
            tvdebug.debug("unable to parse response text")
            raise ResponseError("unable to parse response text: %s" % (response))

        # try parsing a numeric response code if there is one, e.g.
        #   "ERR 501 failure reason"
        try:
            errtxt = errtxt.strip()
            errcode = errtxt.split()[0]
            self.RETNUM = int(errcode.strip())
        except:
            self.RETNUM = None

        # if there's a numeric return code, don't include it in the text
        # returned by GetErrorMsg()
        if errcode is None:
            self.RESPONSE = errtxt
        else:
            self.RESPONSE = errtxt[errtxt.find(' ')+1:]

        # if we are an error, set error message
        if errtype == 'ERR':
            self.ERRMSG = self.RESPONSE
            self.SUCCESS = False

        # if the response contains number of records returned or affected,
        # set the result count
        if self.RESPONSE.find('records returned') > -1:
            self.COUNT = int(self.RESPONSE[self.RESPONSE.rfind(' ')+1:])
            self.NEED_RESULTSET = True

