""" Classes to create a multiplexed asynchronous connection to an
    FCP server.

    Copyright (C) 2008 Darrell Karbott

    This library 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.0 of the License, or (at your option) any later version.

    This library 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 library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks

    OVERVIEW:
    IAsyncSocket is an abstract interface to an asynchronous
    socket.  The intent is that client code can plug in a
    framework appropriate implementation. i.e. for Twisted,
    asyncore, Tkinter, pyQt, pyGtk, etc.  A platform agnostic
    implementation, PolledSocket is supplied.

    FCPConnection uses an IAsyncSocket delegate to run the
    FCP 2.0 protocol over a single socket connection to an FCP server.

    FCPConnection fully(*) supports multiplexing multiple requests.
    Client code runs requests by passing an instance of MinimalClient
    into FCPConnection.start_request().  The FCPClient MinimalClient
    subclass provides convenience wrapper functions for common requests.

    Both blocking and non-blocking client requests are supported.
    If MinimalClient.in_params.async == True, FCPConnection.start_connection()
    returns a request id string immediately.  This is the same request
    id which appears in the 'Identifier' field of subsequent incoming.
    FCP messages. The MinimalClient.message_callback(client, msg)
    callback function is called for every incoming client message for
    the request. Async client code can detect the request has finished
    by checking client.is_finished() from this callback.

    (*) GOTCHA: If you start a request which writes trailing data
    to the FCP server, the FCPConnection will transition into the
    UPLOADING state and you won't be able to start new requests until
    uploading finishes and it transitions back to the CONNECTED state.
    It is recommended that you use a dedicated FCPConnection instance
    for file uploads. You don't have to worry about this if you use
    blocking requests exclusively.
"""
# REDFLAG: get pylint to acknowledge inherited doc strings from ABCs?

import os, os.path, random, select, socket, time

try:
    from hashlib import sha1
    def sha1_hexdigest(bytes):
        """ Return the SHA1 hexdigest of bytes using the hashlib module. """
        return sha1(bytes).hexdigest()
except ImportError:
    # Fall back so that code still runs on pre 2.6 systems.
    import sha
    def sha1_hexdigest(bytes):
        """ Return the SHA1 hexdigest of bytes using the sha module. """
        return sha.new(bytes).hexdigest()

from fcpmessage import make_request, FCPParser, HELLO_DEF, REMOVE_REQUEST_DEF

FCP_VERSION = '2.0' # Expected version value sent in ClientHello

RECV_BLOCK = 4096 # socket recv
SEND_BLOCK = 4096 # socket send
READ_BLOCK = 16 * 1024  # disk read

MAX_SOCKET_READ = 33 * 1024 # approx. max bytes read during IAsyncSocket.poll()

POLL_TIME_SECS = 0.25 # hmmmm...

# FCPConnection states.
CONNECTING = 1
CONNECTED  = 2
CLOSED     = 3
UPLOADING  = 4

CONNECTION_STATES = {CONNECTING:'CONNECTING',
                     CONNECTED:'CONNECTED',
                     CLOSED:'CLOSED',
                     UPLOADING:'UPLOADING'}

def example_state_callback(dummy, state):
    """ Example FCPConnection.state_callback function. """

    value = CONNECTION_STATES.get(state)
    if not value:
        value = "UNKNOWN"
    print "FCPConnection State -> [%s]" % value

def make_id():
    """ INTERNAL: Make a unique id string. """
    return sha1_hexdigest(str(random.random()) + str(time.time()))

#-----------------------------------------------------------#
# Byte level socket handling
#-----------------------------------------------------------#

class IAsyncSocket:
    """ Abstract interface for an asynchronous socket. """
    def __init__(self):
        # lambda's prevent pylint E1102 warning

        # Data arrived on socket
        self.recv_callback = lambda x:None
        # Socket closed
        self.closed_callback = lambda :None
        # Socket wants data to write. This can be None.
        self.writable_callback = None

    def write_bytes(self, bytes):
        """ Write bytes to the socket. """
        pass

    def close(self):
        """ Release all resources associated with the socket. """
        pass

    # HACK to implement waiting on messages.
    def poll(self):
        """ Do whatever is required to check for new activity
            on the socket.

            e.g. run gui framework message pump, explictly poll, etc.
            MUST call recv_callback, writable_callback
        """
        pass

class NonBlockingSocket(IAsyncSocket):
    """ Base class used for IAsyncSocket implementations based on
        non-blocking BSD style sockets.
    """
    def __init__(self, connected_socket):
        """ REQUIRES: connected_socket is non-blocking and fully connected. """
        IAsyncSocket.__init__(self)
        self.buffer = ""
        self.socket = connected_socket

    def write_bytes(self, bytes):
        """ IAsyncSocket implementation. """
        assert bytes
        self.buffer += bytes
        #print "write_bytes: ", self.buffer

    def close(self):
        """ IAsyncSocket implementation. """
        if self.socket:
            self.socket.close() # sync?
            self.closed_callback()
        self.socket = None

    def do_write(self):
        """ INTERNAL: Write to the socket.

            Returns True if data was written, false otherwise.

            REQUIRES: buffer has data or the writable_callback is set.
        """

        assert self.buffer or self.writable_callback
        if not self.buffer:
            # pylint doesn't infer that this must be set.
            # pylint: disable-msg=E1102
            self.writable_callback()
        if self.buffer:
            chunk = self.buffer[:SEND_BLOCK]
            sent = self.socket.send(chunk)
            #print "WRITING:", self.buffer[:sent]
            assert sent >= 0
            #print "TO_WIRE:"
            #print repr(self.buffer[:sent])
            self.buffer = self.buffer[sent:]
            return True
        assert not self.writable_callback # Hmmmm... This is a client error.
        return False

    def do_read(self):
        """ INTERNAL: Read from the socket.

            Returns the data read from the socket or None
            on EOF.

            Closes on EOF as a side effect.
        """
        data = self.socket.recv(RECV_BLOCK)
        if not data:
            return None

        #print "FROM_WIRE:"
        #print  repr(data)
        return data


class PolledSocket(NonBlockingSocket):
    """ Sucky polled IAsyncSocket implementation which should
        work everywhere. i.e. *nix, Windows, OSX. """

    def __init__(self, host, port):
        connected_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # REDFLAG: Can block here.
        connected_socket.connect((host, port))
        connected_socket.setblocking(0)
        NonBlockingSocket.__init__(self, connected_socket)

    def poll(self):
        """ IAsyncSocket implementation. """
        #print "PolledSocket.poll -- called"
        if not self.socket:
            #print "PolledSocket.poll -- CLOSED"
            raise IOError("The socket is closed")
        # Why? Because we don't want to call the recv_callback while
        # reading... wacky re-entrance issues....
        read = ''
        ret = True
        while len(read) < MAX_SOCKET_READ: # bound read length
            check_writable  = []
            if self.buffer or self.writable_callback:
                check_writable = [self.socket]
            readable, writable, errs = \
                      select.select([self.socket], check_writable,
                                    [self.socket], 0)

            #print "result:", readable, writable, errs

            stop = True
            if errs:
                #print "GOT AN ERROR"
                # Hack. Force an IO exception.
                self.socket.sendall(RECV_BLOCK)
                # Err... should never get here.
                raise IOError("Unknown socket error")

            if readable:
                data = self.do_read()
                if not data:
                    ret = False
                    break

                read += data
                stop = False

            if writable:
                if self.do_write():
                    stop = False

            if stop:
                break

        if read:
            self.recv_callback(read)
        #print "PolledSocket.poll -- exited"
        return ret

#-----------------------------------------------------------#
# Message level FCP protocol handling.
#-----------------------------------------------------------#

# NOTE:
# 'DataFound' is sometimes terminal. See msg_is_terminal().
#
# NOTE:
# This list is not complete.  It only lists
# messages generated by supported FCP commands.
# Messages which always indicate that an FCP request ended in success.
SUCCESS_MSGS = frozenset([ \
    'NodeHello', 'SSKKeypair', 'AllData', 'PutSuccessful', 'NodeData',
    ])

# Messages which always indicate that an FCP request ended in failure.
FAILURE_MSGS = frozenset([ \
    'CloseConnectionDuplicateClientName', 'PutFailed', 'GetFailed',
    'ProtocolError', 'IdentifierCollision', 'UnknownNodeIdentifier',
    'UnknownPeerNoteType'
    ])

# Messages which always indicate that an FCP request ended.
TERMINAL_MSGS = SUCCESS_MSGS.union(FAILURE_MSGS)

def msg_is_terminal(msg, params):
    """ INTERNAL: Return True if the message ends an FCP request,
    False otherwise.
    """

    if msg[0] in TERMINAL_MSGS:
        return True

    # Special cases
    if msg[0] == 'DataFound' and 'ReturnType' in params and \
           params['ReturnType'] == 'none':
        return True

    #print "msg_is_terminal: False"
    #print "MSG:", msg
    #print "PARAMS:", params
    
    return False

def get_code(msg):
    """ Returns integer error code if msg has a 'Code' field
        None otherwise.
    """

    # Hmmmm... does 'Code' ever appear in non-error messages?
    #if not msg[0] in FAILURE_MSGS:
    #    # Message is not an error.
    #    return None

    if not 'Code' in msg[1]:
        if msg[0] in FAILURE_MSGS:
            print "WARNING: get_code(msg, code) couldn't read 'Code'."
        return None

    return int(msg[1]['Code'])

def is_code(msg, error_code):
    """ Returns True if msg has a 'Code' field and it is
        equal to error_code, False, otherwise.
    """

    code = get_code(msg)
    if code is None:
        return False
    return code == error_code

def is_fatal_error(msg):
    """ Returns True if msg has a 'Fatal' field and it
        indicates a non-recoverable error, False otherwise.
    """

    value = msg[1].get('Fatal')
    if value is None:
        return False # hmmm...
    return bool(value.lower() == 'true')

class FCPError(Exception):
    """ An Exception raised when an FCP command fails. """

    def __init__(self, msg):
        Exception.__init__(self, msg[0])
        self.fcp_msg = msg
        self.last_uri = None

    def __str__(self):
        text = "FCPError: " + self.fcp_msg[0]
        if self.fcp_msg[1].has_key('CodeDescription'):
            text += " -- " + self.fcp_msg[1]['CodeDescription']
        return text

    def is_code(self, error_code):
        """ Returns True if the 'Code' field in the FCP error message
            is equal to error_code, False, otherwise.
        """

        if not self.fcp_msg or not 'Code' in self.fcp_msg[1]:
            # YES. This does happen.
            # Hmmmm... just assert?  Can this really happen.
            print "WARNING: FCPError.is_code() couldn't read 'Code'."
            return False

        return is_code(self.fcp_msg, error_code)

def raise_on_error(msg):
    """ INTERNAL: raise an FCPError if msg indicates an error. """

    assert msg
    if msg[0] in FAILURE_MSGS:
        raise FCPError(msg)

class IDataSource:
    """ Abstract interface which provides data written up to
        the FCP Server as part of an FCP request. """
    def __init__(self):
        pass

    def initialize(self):
        """ Initialize. """
        raise NotImplementedError()

    def data_length(self):
        """ Returns the total length of the data which will be
            returned by read(). """
        raise NotImplementedError()

    def release(self):
        """ Release all resources associated with the IDataSource
            implementation. """
        raise NotImplementedError()

    def read(self):
        """ Returns a raw byte block or None if no more data
            is available. """
        raise NotImplementedError()

class FileDataSource(IDataSource):
    """ IDataSource implementation which get's its data from a single
        file.
    """
    def __init__(self, file_name):
        IDataSource.__init__(self)
        self.file_name = file_name
        self.file = None

    def initialize(self):
        """ IDataSource implementation. """
        self.file = open(self.file_name, 'rb')

    def data_length(self):
        """ IDataSource implementation. """
        return os.path.getsize(self.file_name)

    def release(self):
        """ IDataSource implementation. """
        if self.file:
            self.file.close()
            self.file = None

    def read(self):
        """ IDataSource implementation. """
        assert self.file
        return self.file.read(READ_BLOCK)

# MESSAGE LEVEL

class FCPConnection:
    """Class for a single persistent socket connection
       to an FCP server.

       Socket level IO is handled by the IAsyncSocket delegate.

       The connection is multiplexed (i.e. it can handle multiple
       concurrent client requests).

    """

    def __init__(self, socket_, wait_for_connect = False,
                 state_callback = None):
        """ Create an FCPConnection from an open IAsyncSocket instance.

            REQUIRES: socket_ ready for writing.
        """
        self.running_clients = {}
        # Delegate handles parsing FCP protocol off the wire.
        self.parser = FCPParser()
        self.parser.msg_callback = self.msg_handler
        self.parser.context_callback = self.get_context

        self.socket = socket_
        if state_callback:
            self.state_callback = state_callback
        else:
            self.state_callback = lambda x, y: None
        self.socket.recv_callback = self.parser.parse_bytes
        self.socket.closed_callback = self.closed_handler

        self.node_hello = None

        # Only used for uploads.
        self.data_source = None

        # Tell the client code that we are trying to connect.
        self.state_callback(self, CONNECTING)

        # Send a ClientHello
        params = {'Name':'FCPConnection[%s]' % make_id(),
                  'ExpectedVersion': FCP_VERSION}
        self.socket.write_bytes(make_request(HELLO_DEF, params))
        if wait_for_connect:
            # Wait for the reply
            while not self.is_connected():
                if not self.socket.poll():
                    raise IOError("Socket closed")
                time.sleep(POLL_TIME_SECS)

    def is_connected(self):
        """ Returns True if the instance is fully connected to the
            FCP Server and ready to process requests, False otherwise.
        """
        return not self.node_hello is None

    def is_uploading(self):
        """ Returns True if the instance is uploading data, False
            otherwise.
        """
        return (self.data_source or
                self.socket.writable_callback)

    def close(self):
        """ Close the connection and the underlying IAsyncSocket
            delegate.
        """
        if self.socket:
            self.socket.close()

    # set_data_length only applies if data_source is set
    def start_request(self, client, data_source = None, set_data_length = True):
        """ Start an FCP request.

            If in_params.async is True this returns immediately, otherwise
            it blocks until the request finishes.

            If client.in_params.send_data is set, trailing data is sent
            after the request message.  If data_source is not None, then
            the data in it is sent.  Otherwise if client.in_params.file is
            not None, the data in the file is sent. Finally if neither of
            the other sources are not None the contents of
            client.in_params.send_data are sent.

            If set_data_length is True the 'DataLength' field is set in the
            requests FCP message.

            If in_params.async it True, this method returns the identifier
            for the request, otherwise, returns the FCP message which
            terminated the request.
        """
        assert not self.is_uploading()
        assert not client.context
        assert not client.response
        assert not 'Identifier' in client.in_params.fcp_params
        identifier = make_id()
        client.in_params.fcp_params['Identifier'] = identifier
        write_string = False
        if client.in_params.send_data:
            assert not self.data_source
            if data_source:
                data_source.initialize()
                if set_data_length:
                    client.in_params.fcp_params['DataLength'] = (data_source.
                                                                 data_length())
                self.data_source = data_source
                self.socket.writable_callback = self.writable_handler
            elif client.in_params.file_name:
                self.data_source = FileDataSource(client.in_params.file_name)
                self.data_source.initialize()
                client.in_params.fcp_params['DataLength'] = (self.
                                                             data_source.
                                                             data_length())
                self.socket.writable_callback = self.writable_handler
            else:
                client.in_params.fcp_params['DataLength'] = len(client.
                                                                in_params.
                                                                send_data)
                write_string = True

        self.socket.write_bytes(make_request(client.in_params.definition,
                                             client.in_params.fcp_params,
                                             client.in_params.
                                             default_fcp_params))

        if write_string:
            self.socket.write_bytes(client.in_params.send_data)

        assert not client.context
        client.context = RequestContext(client.in_params.allowed_redirects,
                                        identifier,
                                        client.in_params.fcp_params.get('URI'))
        if not client.in_params.send_data:
            client.context.file_name = client.in_params.file_name

        #print "MAPPED [%s]->[%s]" % (identifier, str(client))
        self.running_clients[identifier] = client

        if self.data_source:
            self.state_callback(self, UPLOADING)

        if client.in_params.async:
            return identifier

        resp = self.wait_for_terminal(client)
        raise_on_error(resp)
        return client.response

    def remove_request(self, identifier, is_global = False):
        """ Cancel a running request.
            NOT ALLOWED WHILE UPLOADING DATA.
        """
        if self.is_uploading():
            raise Exception("Can't remove while uploading. Sorry :-(")

        if not identifier in self.running_clients:
            print "FCPConnection.remove_request -- unknown identifier: ", \
                  identifier
        params = {'Identifier': identifier,
                  'Global': is_global}
        self.socket.write_bytes(make_request(REMOVE_REQUEST_DEF, params))

    def wait_for_terminal(self, client):
        """ Wait until the request running on client finishes. """
        while not client.is_finished():
            if not self.socket.poll():
                break
            time.sleep(POLL_TIME_SECS)

        # Doh saw this trip 20080124. Regression from NonBlockingSocket changes?
        # assert client.response
        if not client.response:
            raise IOError("No response. Maybe the socket dropped?")

        return client.response

    def handled_redirect(self, msg, client):
        """ INTERNAL: Handle code 27 redirects. """

        # BITCH: This is a design flaw in the FCP 2.0 protocol.
        #        They should have used unique numbers for all error
        #        codes so that client coders don't need to keep track
        #        of the initiating request in order to interpret the
        #        error code.
        if client.in_params.definition[0] == 'ClientGet' and is_code(msg, 27):
            #print "Checking for allowed redirect"
            if client.context.allowed_redirects:
                #print "Handling redirect"
                client.context.allowed_redirects -= 1
                assert client.context.initiating_id
                assert client.context.initiating_id in self.running_clients
                assert client.context.running_id
                if client.context.running_id != client.context.initiating_id:
                    # Remove the context for the intermediate redirect.
                    #print "DELETED: ", client.context.running_id
                    del self.running_clients[client.context.running_id]

                client.context.running_id = make_id()
                client.context.last_uri = msg[1]['RedirectURI']

                # Copy, don't modify params.
                params = {}
                params.update(client.in_params.fcp_params)
                params['URI'] = client.context.last_uri
                params['Identifier'] = client.context.running_id

                # Send new request.
                self.socket.write_bytes(make_request(client.in_params.
                                                     definition, params))

                #print "MAPPED(1) [%s]->[%s]" % (client.context.running_id,
                #                                str(client))
                self.running_clients[client.context.running_id] = client

                # REDFLAG: change callback to include identifier?
                # Hmmm...fixup identifier in msg?
                if client.message_callback:
                    client.message_callback(client, msg)
                return True

        return False


    def handle_unexpected_msgs(self, msg):
        """ INTERNAL: Process unexpected messages. """

        if not self.node_hello:
            if msg[0] == 'NodeHello':
                self.node_hello = msg
                self.state_callback(self, CONNECTED)
                return True

            raise Exception("Unexpected message before NodeHello: %s"
                            % msg[0])

        if not 'Identifier' in msg[1]:
            print "Saw message without 'Identifier': %s" % msg[0]
            print msg
            return True

        if not msg[1]['Identifier'] in self.running_clients:
            #print "No client for identifier: %s" % msg[1]['Identifier']
            # BITCH: You get a PersistentRequestRemoved msg even for non
            #        peristent requests AND you get it after the GetFailed.
            #print msg[0]
            return True

        return False

    def get_context(self, request_id):
        """ INTERNAL: Lookup RequestContexts for the FCPParser delegate.
        """

        client = self.running_clients.get(request_id)
        if not client:
            raise Exception("No client for identifier: %s" % request_id)
        assert client.context
        return client.context

    def msg_handler(self, msg):
        """INTERNAL: Process incoming FCP messages from the FCPParser delegate.
        """

        if self.handle_unexpected_msgs(msg):
            return

        client = self.running_clients[msg[1]['Identifier']]
        assert client.is_running()

        if msg_is_terminal(msg, client.in_params.fcp_params):
            if self.handled_redirect(msg, client):
                return

            # Remove running context entries
            assert msg[1]['Identifier'] == client.context.running_id
            #print "DELETED: ", client.context.running_id
            del self.running_clients[client.context.running_id]
            if client.context.running_id != client.context.initiating_id:
                #print "DELETED: ", client.context.initiating_id
                del self.running_clients[client.context.initiating_id]

            if msg[0] == 'DataFound' or msg[0] == 'AllData':
                # REDFLAG: Always do this? and fix FCPError.last_uri?
                # Copy URI into final message. i.e. so client
                # sees the final redirect not the inital URI.
                msg[1]['URI'] = client.context.last_uri
            if msg[0] == 'AllData':
                # Copy metadata into final message
                msg[1]['Metadata.ContentType'] = client.context.metadata

                # Add a third entry to the msg tuple containing the raw data,
                # or a comment saying where it was written.
                assert len(msg) == 2
                msg = list(msg)
                if client.context.data_sink.file_name:
                    msg.append("Wrote raw data to: %s" \
                               % client.context.file_name)
                else:
                    msg.append(client.context.data_sink.raw_data)
                msg = tuple(msg)


            # So that MinimalClient.request_id() returns the
            # initiating id correctly even after following
            # redirects.
            msg[1]['Identifier'] = client.context.initiating_id

            # Reset the context
            client.context.release()
            client.context = None

            client.response = msg
            assert not client.is_running()
        else:
            if 'Metadata.ContentType' in msg[1]:
                # Keep track of metadata as we follow redirects
                client.context.metadata = msg[1]['Metadata.ContentType']

        # Notify client.
        if client.message_callback:
            client.message_callback(client, msg)

    def closed_handler(self):
        """ INTERNAL: Callback called by the IAsyncSocket delegate when the
            socket closes. """
        def dropping(data): # REDFLAG: Harmless but remove eventually.
            """ INTERNAL: Print warning when data is dropped after close. """
            print "DROPPING %i BYTES OF DATA AFTER CLOSE!" % len(data)

        self.node_hello = None
        if not self.socket is None:
            self.socket.recv_callback = lambda x:None
            self.socket.recv_callback = dropping # Ignore any subsequent data.

        # Hmmmm... other info, ok to share this?
        fake_msg = ('ProtocolError', {'CodeDescription':'Socket closed'})
        #print "NOTIFIED: CLOSED"

        # Hmmmm... iterate over values instead of keys?
        for identifier in self.running_clients:
            client = self.running_clients[identifier]
            # Remove client from list of running clients.
            #print "CLIENT:", client
            #print "CLIENT.CONTEXT:", client.context
            assert client.context
            assert client.context.running_id
            # Notify client that it has stopped.
            if (client.context.initiating_id == client.context.running_id
                and client.message_callback):
                client.message_callback(client, fake_msg)

        self.running_clients.clear()
        self.state_callback(self, CLOSED)

    def writable_handler(self):
        """ INTERNAL: Callback called by the IAsyncSocket delegate when
            it needs more data to write.
        """

        if not self.data_source:
            return
        data = self.data_source.read()
        if not data:
            self.data_source.release()
            self.data_source = None
            self.socket.writable_callback = None
            if self.is_connected():
                self.state_callback(self, CONNECTED)
            return
        self.socket.write_bytes(data)

# Writes to file if file_name is set, raw_data otherwise
class DataSink:
    """ INTERNAL: Helper class used to save trailing data for FCP
        messages.
    """

    def __init__(self):
        self.file_name = None
        self.file = None
        self.raw_data = ''
        self.data_bytes = 0

    def initialize(self, data_length, file_name):
        """ Initialize the instance.
            If file_name is not None the data is written into
            the file, otherwise, it is saved in the raw_data member.
        """
        # This should only be called once. You can't reuse the datasink.
        assert not self.file and not self.raw_data and not self.data_bytes
        self.data_bytes = data_length
        self.file_name = file_name

    def write_bytes(self, bytes):
        """ Write bytes into the instance.

            Multiple calls can be made. The final amount of
            data written into the instance MUST be equal to
            the data_length value passed into the initialize()
            call.
        """

        #print "WRITE_BYTES called."
        if self.file_name and not self.file:
            self.file = open(self.file_name, 'wb')

        if self.file:
            #print "WRITE_BYTES writing to file"
            if self.file.closed:
                print "FileOrStringDataSink -- refusing to write" \
                      + " to closed file!"
                return
            self.file.write(bytes)
            self.data_bytes -= len(bytes)
            assert self.data_bytes >= 0
            if self.data_bytes == 0:
                self.file.close()
            return

        self.raw_data += bytes
        self.data_bytes -= len(bytes)
        assert self.data_bytes >= 0

    def release(self):
        """ Release all resources associated with the instance. """

        if self.data_bytes != 0:
            print "DataSink.release -- DIDN'T FINISH PREVIOUS READ!", \
                  self.data_bytes
        if self.file:
            self.file.close()
        self.file_name = None
        self.file = None
        self.raw_data = ''
        self.data_bytes = 0

class RequestContext:
    """ INTERNAL: 'Live' context information which an FCPConnection needs
        to keep about a single FCP request.
    """
    def __init__(self, allowed_redirects, identifier, uri):
        self.initiating_id = identifier
        self.running_id = identifier

        # Redirect handling
        self.allowed_redirects = allowed_redirects
        self.last_uri = uri
        self.metadata = "" # Hmmm...

        # Incoming data handling
        self.data_sink = DataSink()

    def writable(self):
        """ Returns the number of additional bytes which can be written
            into the data_sink member.
        """

        return self.data_sink.data_bytes

    def release(self):
        """ Release all resources associated with the instance. """

        self.data_sink.release()


#-----------------------------------------------------------#
# Client code
#-----------------------------------------------------------#

# Hmmmm... created separate class because pylint was complaining
# about too many attributes in MinimalClient and FCPClient
class ClientParams:
    """ A helper class to aggregate request parameters. """

    def __init__(self):
        self.definition = None
        # These are default values which can be modified by the client code.
        # THE IMPLEMENTATION CODE i.e. fcp(connection/client/message)
        # MUST NOT MODIFY THEM.
        self.default_fcp_params = {}
        # These are per request values. They can be modified / reset.
        self.fcp_params = {}
        self.async = False
        self.file_name = None
        self.send_data = None
        self.allowed_redirects = 0

    def reset(self):
        """ Reset all members EXCEPT async, allowed_redirects and
            default_fcp_params to their default values.
        """

        self.definition = None
        self.fcp_params = {}
        self.file_name = None
        self.send_data = None

    # HACK: Not really required, but supresses pylint R0903
    def pretty(self):
        """Returns a human readable rep of the params. """

        return "%s: %s %s %s %s %s %s" % \
               ( self.definition[0],
                 str(self.send_data),
                 str(self.async),
                 self.file_name,
                 self.allowed_redirects,
                 self.fcp_params,
                 self.default_fcp_params )

class MinimalClient:
    """ A single FCP request which can be executed via the
        FCPConnection.start_request() method.

        If in_params.async is True the request runs asynchronously,
        otherwise it causes FCPConnection.start_request() to block.

        The message_callback notifier function is called for
        each incoming FCP message during the request. The first
        argument is the client instance.  Its is_finished()
        method will return True for the final message. message_callback
        implementations MUST NOT modify the state of the client
        instance while is_finished() is False.
    """

    def __init__(self):
        # IN parameters.
        self.in_params = ClientParams()

        # OUT parameter
        self.response = None

        # Variables used while client request is running.
        self.context = None

        # Notification
        self.message_callback = lambda client, msg:None

    def reset(self, reset_params = True):
        """ Reset all members EXCEPT self.in_params.allowed_redirects,
            self.in_params.default_fcp_params and
            self.in_params.async to their default values.
        """
        assert not self.is_running()
        if reset_params:
            self.in_params.reset()
        self.response = None
        self.context = None

    def is_running(self):
        """ Returns True if a request is running, False otherwise. """

        return self.context

    def is_finished(self):
        """ Returns True if the request is finished, False otherwise. """

        return not self.response is None

    def request_id(self):
        """ Returns the request id. """
        if self.response and not self.context:
            return self.response[1]['Identifier']
        elif self.context:
            return self.context.initiating_id
        return None