#!/usr/bin/env python
# encoding: iso-8859-15
#
#Minë. Mundo Interactivo-Narrativo en Español [Tierra-Media]
#Copyright (C) 2002  Pablo Ruiz Múzquiz
#
#
#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
#Fichero: connection.py

"""Module to handle communication via sockets at high level.

It contains these classes:
 - Connection
 - ConnectionHandler
"""

from socket import *
from select import *
from string import *

class Connection(object):
    """An accepted connection.

    It allows to send and receive lines of text messages from a client.

    It does 'page waiting': it counts the lines sent to the client, and
    when the count exceeds the page size, it sends a prompt, stops
    sending lines, and store subsequent ones in a buffer, until a
    message is received from the client.

    It also does line wrapping: lines longer than the screen width (by
    default 79 lines) are cut.

    Do not directly instantiate it, but rather use instances returned
    by the accept() method of ConnectionHandler.

    The Connection has two 'callback' function that the user of this
    class must provide:
     - One will be called every time a line of text arrives.
     - The other will be called if there is an error or disconnection,
       and the connection has been closed.
    """
    
    def __init__(self, handler, socket, address, receive_callback, error_callback):
        """Only to be called from ConnectionHandler"""
        self.handler = handler
        self.socket = socket
        self.ip = address[0]

        #This string collects incoming data, for the benefit of clients that pass
        #all typed information as it arrives as opposed to waiting for a \n.        
        self.dataqueue = ""

        #This string holds a complete line of received data. Is not modified
        #until a "\n" is stored into dataqueue.
        self.input = ""

        #This is the function that will be called to notify that incoming
        #data is available to read with receive(). One parameter will be
        #passed to the function: this connection.
        self.receive_callback = receive_callback

        #This is the function that will be called to notify that an error
        #has occured and the connection is closed. One parameter will be
        #passed to the function: this connection.
        self.error_callback = error_callback

	#The line width in characters
	self.line_width = 79

	#The page length in lines
	self.page_length = 0
	#%%self.page_length = 21

	#The buffer to store lines until the client has accepted
	self.page_wait_buffer = []

	#The number of lines sent in the current page
	self.line_count = 0

	#The prompt to send when page full
	self.page_prompt = "+++"

	self.is_open = 1


    def set_line_width(self, line_width):
	"""Change the width of the lines from now on.
	
	If 0, no line wrapping is done.
	"""
	self.line_width = line_width


    def set_page_length(self, page_length):
	"""Change the length of the page from now on.
	
	If 0, no page waiting is done.
	"""
	self.page_length = page_length


    def set_page_prompt(self, page_prompt):
	"""Change the prompt to send when page full, from now on."""
	self.page_prompt = page_prompt


    def receive(self):
        """Return the input data received from the client.

        Use this only when the callback has been called to notify
        that a line of text has been received. The returned data
        will be ended with a \\n."""
        return self.input


    def send(self, string, end_of_line = 1):
        """Send a string of text to the client.

	The string may contain multiple lines separated by "\n".

        If end_of_line == 1, an end of line is appended to the string.

	If line_width != 0, if the text is longer than line_width, it is cut to
	this width, breaking in spaces.

	If page_length != 0, if the page has been filled, the line is not
	sent to the client, but instead is stored in a buffer until the
	client sends an acknowledge message.
	"""
	lines_to_send = []

	# Split the string in one or more lines
	for line in string.split("\n"):

	    # Split the line in pieces of length less than or equal than line_width.
	    start = 0
	    end = self.line_width
	    while start < len(line):
		
		if end > len(line):
		    # We have reached the end of the line.
		    end = len(line)
		else:
		    # Loop back searching the first blank character.
		    while (end > start+1) and (line[end-1] != ' '):
			end -= 1

		    # If no blanks in the line, split just at the
		    # given width.
		    if end == start+1:
			end = start + self.line_width

		# Add the piece to the lines to send list.
		if start == 0:
		    piece = line[start:end]
		else:
		    piece = "\n" + line[start:end]
		lines_to_send.append(piece)

		# Advance.
		start = end
		end = start + self.line_width

	    # Add the last new line, if asked to do so.
	    if (end_of_line):
		lines_to_send.append("\n")

	# Send the lines, or store in the buffer, wether the screen is filled out or not.
	for piece in lines_to_send:
	    if self.page_length == 0 or self.line_count < self.page_length:
		self.socket.send(piece)
		if len(piece) > 0 and piece[0] == "\n":
		    self.line_count += 1
	    else:
		if len(self.page_wait_buffer) == 0:
		    self.socket.send(self.page_prompt + "\n")
		self.page_wait_buffer.append(piece)


    def close(self, with_error = 0):
        """Disconnect and release the connection.

	If with_error is set, the disconnection is abnormal and the error callback
	is called.
	"""
	self.is_open = 0
        self.socket.close()
        self.handler.connected.remove(self)	#remove the connection from the array
	if with_error:
            self.error_callback(self)


    def fileno(self):
        """Only to be called from ConnectionHandler.

        Return the fileno of the socket. This is required to use select,
        which is used to choose which sockets have incoming data later.
        """
        return self.socket.fileno()


    def data_available(self):
        """Only to be called from ConnectionHandler.

        This function is called when any data is available to read
        in the socket.
        """
        try:
            # Read the pending data
            data = self.socket.recv(1024)
        except:
            #There has been an error, drop the connection.
            self.close(1)
        else:
            if not data:
                #There is somehow no data. This shouldn't happen unless there is an
                #error of some kind, so we are going to disconnect them.
                self.close(1)
            else:

		if len(data) > 0:
		    if not data[-1] == '\n':
			#If the last character recieved is not a return, then we save the incoming
			#data in the buffer. Some clients like to send fragments.
			self._enqueue(data) 
		    else:
			#The player pressed return, so we are going to process their input.

			#Add the new data to the end of the queue.
			self._enqueue(data)

			#Two cases: a) if the output was blocked by page full, this input
			#unblocks the next page of output, and is ignored otherwise.

			if len(self.page_wait_buffer) != 0:

			    self.line_count = 0
			    while self.line_count < self.page_length \
			      and len(self.page_wait_buffer) > 0:

				  piece = self.page_wait_buffer[0]
				  del self.page_wait_buffer[0]
				  self.socket.send(piece)
				  if len(piece) > 0 and piece[0] == "\n":
				      self.line_count += 1

			    if len(self.page_wait_buffer) > 0:
				self.socket.send(self.page_prompt + "\n")

			else:

			    # b) This is a normal input. Resets the page counting.
			    self.line_count = 0

			    #Get the entire string of input for processing from the queue.
			    self.input = self._queue()

			    #Clear the queue.                                               
			    self._resetqueue()

			    #Notify to the client that there is available data.
			    self.receive_callback(self)


    def _enqueue(self, string):
        """Add an incoming string to the input buffer.
        
        This is used to collect stray input until we recieve a '\\n'
        carriage return. Some clients are nice, they don't send the data
        until it is all together, but some send it by keystroke.

        It also processes backspace key and suppress \\r characters.
        """
        self.dataqueue = self.dataqueue + string

        i = 0
        while i < len(self.dataqueue):
            if self.dataqueue[i] == '\b':
                if i == 0:
                    self.dataqueue = self.dataqueue[1:]
                else:
                    self.dataqueue = self.dataqueue[:i-1] + \
                                     self.dataqueue[i+1:]
                    i = i - 1
            elif self.dataqueue[i] == '\r':
                if i == 0:
                    self.dataqueue = self.dataqueue[1:]
                else:
                    self.dataqueue = self.dataqueue[:i] + \
                                     self.dataqueue[i+1:]
            else:
                i = i + 1


    def _queue(self):
        """Simply return the contents of the input buffer."""
        return self.dataqueue


    def _resetqueue(self):
        """Clear the input buffer."""
        self.dataqueue = ""


class ConnectionHandler(object):
    """A server that listens to a TCP port and accepts connection
    requests from clients.

    The ConnectionHandler has one 'callback' function that the user
    of this class must provide. It will be called every time a new
    client requests a connection.
    """
    
    def __init__(self, host, port, new_connection_callback):
        """Creation of a new ConnectionHandler.

        host -- the hostname or IP address of this host (optional)
        port -- the TCP port to listen in
        new_connection_callback -- a function or method to call
                                   on new connection requests
        """
        
        #Set up an array to contain the connections.
        self.connected = [ ]

        #Set up the listening socket.  All incoming connections are picked up here,
        #then passed off to a new connection.
        self.listener = socket(AF_INET, SOCK_STREAM)
        self.listener.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.listener.bind((host, port))
        self.listener.listen(32)

        #This is the function that will be called to notify that a new
        #connection has been requested, and may be accepted with accept()
        self.new_connection_callback = new_connection_callback


    def accept(self, receive_callback, error_callback):
        """Accept a request and return a new connection with a client.

        Use this only when the callback has been called to notify
        that a client is requesting connection."""
        
        #We have a new connection, add it to connected[]
        socket, address = self.listener.accept()
        conn = Connection(self, socket, address, receive_callback, error_callback)
        self.connected.append(conn)
        
        return conn


    def run(self):
        """Cycle once through each connection, calling all the callbacks.

        This is run from a loop within the main program.
        """

        #We want to check both the list of active connections, and the listening socket.
        #	...so we create a variable that combines the two.  Lets call it 'ready'.
        ready = self.connected[:]
        ready.append(self.listener)

        #We only want to check the ones that have incoming data, however.
        #   ...which is what select() does.  In this case, it returns the members
        #      of ready that have data waiting.
        #
        #      The 0.1 is important.  It is an optional argument, if it is omitted the
        #      process will block, meaning it will wait until there is data to be processed
        #      before it continues.  Specifying a number other than 0 will block for that many
        #      seconds, to see if any data comes in.
        ready = select(ready, [], [], 0.1)[0]

	#Cycle through each connection once.
        for conn in ready:
            if conn is self.listener:
                #We have a new connection request, notify to the client
                self.new_connection_callback(self)
            else:
                #Activate the connection
                conn.data_available()


    def close_all(self):
	"""Close and destroy all connections."""
	for conn in self.connected[:]:
	    conn.close(1)
        
        

# Code for module unitary test.

if (__name__ == '__main__'):

    from random import randint
    
    clients = []

    class Client(object):
        def data_received(self, conn):
            print "Received message from " + conn.ip + ":", conn.receive(),
            if conn.receive() == "quit\n":
                print "User from " + conn.ip + " has quit."
                conn.close()
                clients.remove(self)

        def error(self, conn):
            print "Connection closed to " + conn.ip + "."
            clients.remove(self)
        
    def new_client(handler):
        cli = Client()
        conn = handler.accept(cli.data_received, cli.error)
        cli.conn = conn
        clients.append(cli)
        print "Connection accepted from " + conn.ip + "."
        
    handler = ConnectionHandler('', 4000, new_client)
    print "Connection handler active and listening on port 4000." 

    while 1:
        handler.run()
        if len(clients) > 0:
            if randint(1, 50) == 1:
                clients[randint(0, len(clients)) - 1].conn.send("hello\r\n")
