"""mems.instrument.client.tk.scope_connection

Class that implements the microscope's wire protocol and handles the
connection.
"""


__revision__ = "$Id: scope_connection.py,v 1.27 2002/03/29 17:00:35 akuchlin Exp $"

import socket, string, sys, time, errno
import asyncore, Tkinter
import select, threading

from mems.instrument import protocols
from mems.instrument.server import codecs, util

DEBUG = 0                               # Display debugging information
TIMING = 0                              # Display timing information

import os
if hasattr(os, 'uname'):
    if os.uname()[0] == 'Linux':
	# Get maximum size of TCP send and receive queues
	data = open('/proc/sys/net/core/wmem_max', 'r').readline()
	data = string.strip(data)
	max_send_queue = int(data)

	data = open('/proc/sys/net/core/rmem_max', 'r').readline()
	data = string.strip(data)
	max_recv_queue = int(data)
    else:
	# Otherwise, assume a default value
	# XXX add Solaris support for this!
	max_send_queue = max_recv_queue = 65535
	
else:
    # XXX Windows -- who knows what these values should be?
    max_send_queue = max_recv_queue = 65535



class CommandLineConnection(asyncore.dispatcher_with_send):
    def __init__ (self, connection, host, port):
        self.connection = connection
        self.host, self.port = host, port

        self._line = ""
        
        sock = self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((self.host, self.port))
        sock.setblocking(0)
        asyncore.dispatcher_with_send.__init__(self, sock)

        if sys.platform == 'win32':
            t1 = threading.Thread(target=self.threaded_read, args=())
            t1.setDaemon(1)
            t1.start()
        else:
            # On Unix, create a Tk filehandler that will just call
            # asyncore's poll().

            def poll (*args):
                asyncore.poll()
            Tkinter.tkinter.createfilehandler(sock, Tkinter.tkinter.READABLE,
                                              poll)

    def threaded_read (self):
        """threaded_read()
        Spin in an endless loop polling for data from the socket.
        This version is only used on Windows, where it's executed
        in a separate thread (so polling is OK).
        """

        while 1:
            try:
                select.select([self.sock], [], [])
                self.handle_read()
            except socket.error:
                self.handle_close()
                return


    def handle_read (self):
        data = self.recv(80)
        if data == "":
            # asyncore signals a closed connection by signalling a read
            # and returning an empty string.  This therefore means
            # the connection to the server has been broken.
            self.connection.report_broken_connection()
            return

        # Add available data to the command line being built up.
        self._line += data
        
        # If the end of a line is reached, perform the command.
        while self._line:
            index = self._line.find('\n')
            if index == -1:
                break
            
            cmd = self._line[:index]
            self._line = self._line[index+1:]
            self.connection.process_command(cmd)

        # Guard against insanely long commands; when the line grows to
        # be more than 4K, throw it away and log the problem.
        if len(self._line) > 4096:
            msg = ("%s: %r sent overly long line\n"
                   "       first 25 bytes = %r" % (__name__, self,
                                                   self._line[:25]))
            util.log(msg)
            self._line = ""


    def handle_write (self):
        if self.out_buffer:
            sent = self.socket.send(self.out_buffer)
            self.out_buffer = self.out_buffer[sent:]


    def handle_close (self):
        self.connection.report_broken_connection()




        
class ScopeImageConnection(CommandLineConnection):
    def __init__ (self, connection, host, port):
        CommandLineConnection.__init__(self, connection, host, port)

        self._packet_size = None
        
        # Lengthen the TCP send and receive queues for this socket
        self.sock.setsockopt(socket.SOL_SOCKET,
                        socket.SO_SNDBUF, max_send_queue)
        self.sock.setsockopt(socket.SOL_SOCKET,
                        socket.SO_RCVBUF, max_recv_queue)  


    def handle_read (self):
        if DEBUG:
            start = time.time()
            util.log('read of line started')
        data = self.recv(self._packet_size or 1024)
        if DEBUG:
            util.log('read of line ended (%f sec) %s bytes from %s '
                      % (time.time() - start, len(data), self._packet_size))
        self._line += data
        
        if self._packet_size is None:
            if '\0' in self._line:
                index = self._line.find('\0')
                self._packet_size = (index +
                                         int(self._line[:index].strip()) + 1)

        elif len(self._line) >= self._packet_size:
            packet, self._line = (self._line[:self._packet_size],
                                  self._line[self._packet_size:])
            self._packet_size = None
            if DEBUG:
                util.log('packet received')
            self.connection.process_packet(packet)

    def handle_close (self):
        # Do nothing when the connection closes -- the broken command
        # connection will worry about presenting a dialog box.
        pass
            

class ScopeConnection:
    """Class representing a microscope connection.
    """
    
    def __init__ (self, client, host, port):
        self._command = CommandLineConnection(self, host, port)
        self._image = ScopeImageConnection(self, host, port+2)
        self.client = client
        self.host, self.port = host, port
        
        self.handlers = []
        self.num_cmds = 0

        self.buffer = ""
        self.partial_line = ""

        self.add_handler(self)


    def get_image_port (self):
        image_addr, image_port = self._image.sock.getsockname()
        return image_port
    

    def cmd_image (self, sock, args):
        self._image.read_image(args[0])
        
    def add_handler (self, handler_obj):
        self.handlers.append(handler_obj)
                              
    def send_message (self, message):
        if DEBUG:
            print 'ScopeConnection', id(self), ': Sending:', message
        self._command.send(message.strip() + '\n')

    def process_command (self, cmd):
        if DEBUG:
            print '%i: %r' % (self.num_cmds, cmd)
            self.num_cmds += 1
            
        cmd, args = protocols.parse_message_line(cmd)
        if len(cmd) == 0: return
        method_name = "cmd_" + string.lower(cmd)
        for obj in self.handlers:
            if hasattr(obj, method_name):
                meth = getattr(obj, method_name)
                if TIMING:
                    now = time.time()
                meth(self,**args)
                if TIMING:
                    print 'Time for command', method_name, time.time()-now
                break
        else:
            sys.stderr.write('Unknown command: '+repr(cmd)+'\n')


    def process_packet (self, packet):
        if TIMING:
            t1 = time.time()

        fmt, data, keywords = protocols.parse_packet(packet)
        # Create a codec if none exists, or if the server seems to have
        # changed its output format.
        codec = self.client.image_display.codec
        if codec is None or codec.codec_format != fmt:
            if DEBUG:
                print 'Creating new codec object'
            codec_class = codecs.lookup(fmt)
            if codec_class is None:
                raise RuntimeError, "Unknown codec: %r" % fmt
            
            self.client.image_display.codec = codec = codec_class()
        
        codec.process(data, **keywords)
        
        if TIMING:
            t2 = time.time()
            print 'Time in codec.process(): ', t2-t1
        

    def report_broken_connection (self):
        self.client.report_broken_connection()

    def close (self):
        self._command.close()
        self._image.close()
