package edu.columbia.sip;

import java.net.Socket;
import java.net.UnknownHostException;
import java.io.IOException;
import java.util.Enumeration;

import mjs.util.Debug;

/** SIPConnection maintains and manages an individual connection for its entire
 * lifetime.  This class contains all the functionality required to implement
 * the server behavior defined in the draft.  When a standalone server uses
 * connections for incoming calls, the connection is run as a seperate thread and
 * it utilizes its built in knowledge to handle requests.  When user agents open
 * connections for queries, they simply dont "run" the connection, so they use 
 * the same class but interact with it differently.  <p>
 * SIPConnection holds a SIPParser which translates information to and from the
 * SIP encoding for it.
 *
 * @author Moshe Sambol
 */
public class SIPConnection extends Thread {
    /** a parser which the SIPConnection uses to translate information to and from
     * the SIP encoding.
     */
    private SIPParser _parser;

    /** Constructor.  The SIPConnection is established based on a socket opened
     * by the SIPServer.
     *
     * @param sock the socket through which the connection is maintained
     *
     * @throws IOException (passes it on from SIPParser CTOR).
     */
    public SIPConnection(Socket sock) throws IOException {
        System.out.println("SIPConnection(sock): connection created to port " + sock.getPort() + " from port " + sock.getLocalPort());
        _parser = new SIPParser(sock);
    } // end CTOR
    
    
    /** Alternative constructor.  User passes in host and port, as strings,
     * and the connection creates the socket.
     *
     * @param host name of the host to contact
     * @param port the port at which to connect to that host
     *
     * @throws IOException (as above).
     * @throws UnknownHostException (passed on from socket construction).
     *
     */
    public SIPConnection(String host, int port) throws IOException, UnknownHostException {
 //       System.out.println("SIPConnection.CTOR(" + host + ", " + port + ")...");
        if (port == -1) {
            port = SIPServer.DEFAULT_PORT;
        }
        Socket s = new Socket(host, port);
  //      System.out.println("SIPConnection() created to host ");
  //      System.out.println(s.getInetAddress() + ", port " + s.getPort() + " from port " + s.getLocalPort());
        _parser = new SIPParser(s);
    } // end CTOR2

    /** Alternative constructor.  User passes in a SIPURL which encapsulates host and 
     * port information.  This function then forwards that info on to the host,port CTOR.
     */
    public SIPConnection(SIPURL s) throws IOException, UnknownHostException {
        this(s.getHost(), s.getPort());
    }
        
        
    /** for use by user agent when connection isn't running on auto. 
     */
    public void putMessage(SIPMessage mesg) {
        _parser.putMessage(mesg);
    }
    
    /** for use by user agent when connection isn't running on auto. 
     */
    public SIPMessage getMessage() throws SIPParseException, SIPException {
        return _parser.getMessage();
    }
    
    /** close the connection.
     */
    public void close() throws SIPException {
        Debug.println("SIPConnection.close()");
        try {
            _parser.close();
        } catch (IOException ioe) {
            throw new SIPException("SIPConnection.close(): error trying to close socket: " +
            ioe.getMessage());
        }
    }


    /** The run() function is called to start this as a seperate thread which will
     * handle the call.  When this function exits this thread ends and this
     * connection is considered dead.  (User agents call close() instead.)
     *
     * @throws SIPParseException for errors parsing the SIP message
     */
    public void run() throws RuntimeException {
        SIPMessage msg;
        SIPRequest request;
        SIPToken method = null;
        RegistrationManager rm = SIPServer.getRegistrationManager();
//        while (true) {
            try {
                msg = _parser.getMessage();

                if (msg instanceof SIPRequest) {
                    request = (SIPRequest) msg;
                    method = request.getMethod();

                    // handle the message
                    if (method == SIPToken.REGISTER) {
                        Debug.println("SIPConnection.run() handling REGISTER request.");
                        boolean unregistering = request.getHeader(SIPToken.EXPIRES).equals("0");
                        if (unregistering) {
                            rm.unregister(request.getURL(), request.getHeader("Location"));
                            rm.notifyGone(request.getURL());
                        } else {
                            rm.register(request.getURL(), request.getHeader("Location"));
                            rm.notifyHere(request.getURL());
                        }
                    } else if (method == SIPToken.HERE) {
                        System.out.println("SIPConnection.run() handling a HERE request.");
                        /// send a 100 Trying response

                        /// if the subject has already registered, respond to the request
                        if (rm.isRegistered(request.getURL())) {
                            System.out.println("SIPConnection.run(): registration manager says that user "
                            + request.getURL().toString() + " *is* registered...");
                            SIPResponse resp = new SIPResponse(request, 200, "User is available.");
                            /// must send response to a different socket than the one we're currently
                            /// communicating over - we want to send it to the main port the other server
                            /// is listening on.
                            try {
                                _parser.reconnectTo(new Socket(request.getResponseAddress(), 
                                                               request.getResponsePort()));
                                putMessage(resp);
                            } catch (Exception e) {
                                System.out.println("SIPConnection.run(): Error sending response: " + e);
                            }                            
                        } else {
                            Debug.println("SIPConnection.run(): registration manager says that user " +
                            request.getURL().toString() + " is *not* registered.");
                            rm.addAvailabilityListener(new RemoteAvailabilityListener(request));                        
                        } // end if (rm.isRegistered())
                    } else if (method == SIPToken.GONE) {
                        System.out.println("SIPConnection.run() handling a GONE request.");

                        /// if the subject has already registered, respond to the request
                        if (! rm.isRegistered(request.getURL())) {
                            System.out.println("SIPConnection.run(): registration manager says that user "
                            + request.getURL().toString() + " is *not* registered...");
                            SIPResponse resp = new SIPResponse(request, 200, "User is not available.");
                            /// must send response to a different socket than the one we're currently
                            /// communicating over - we want to send it to the main port the other server
                            /// is listening on.
                            try {
                                _parser.reconnectTo(new Socket(request.getResponseAddress(), 
                                                               request.getResponsePort()));
                                putMessage(resp);
                            } catch (Exception e) {
                                System.out.println("SIPConnection.run(): Error sending response: " + e);
                            }                            
                        } else {
                            Debug.println("SIPConnection.run(): registration manager says that user " +
                            request.getURL().toString() + " is registered.");
                            rm.addAvailabilityListener(new RemoteAvailabilityListener(request));
                        } // end if (rm.isRegistered())
                    } // end if (method == SIPToken.GONE)                        
                } else { 

                    // message is *not* a SIPRequest, so we assume it is a SIPResponse
                    
                    if ( ! (msg instanceof SIPResponse)) {
                        System.out.println("ERROR: message is neither a SIPRequest or a SIPResponse.  Ignoring it.");
                        return;
                    }
                    Debug.println("SIPConnection.run() handling a SIPResponse.");
                    SIPResponse response = (SIPResponse) msg;
                    String callid = response.getHeader(SIPToken.CALL_ID);
                    System.out.println("this response is regarding " + callid);
                    SIPRequest req = SIPServer.getCallIDManager().getAssociatedRequest(callid);
                    // tell the CallID Manager that this call is over and it no longer
                    // needs to track this call-id.
                    SIPServer.getCallIDManager().remove(callid);
                    if (req == null) {
                        System.out.println("SIPConnection.run(): received a response to a request " +
                        "for which there is no information:\n" + response.toString());
                       // continue; // do nothing with this response
                       close();
                       return;
                    }
                    
                    // we have gotten a response to a request we sent.  Handle it here.
                    if (req.getMethod().equals(SIPToken.HERE)) {
                        // tell the registration manager that the subject of the HERE request
                        // has been found to be available.
                        Debug.println("SIPConnection.run() sending notification of availability for "
                        + req.getURL());
                        
                        rm.notifyHere(req.getURL());
                        
                        // immediately send a GONE to the server which sent the here notification
                        try {
                            SIPRequest goneReq = new SIPRequest(SIPToken.GONE, req.getURL(), 
                                                                req.getDestinationURL());
                                                                
                            goneReq.setHeader(SIPToken.TO, req.getHeader(SIPToken.TO));
                            goneReq.setHeader(SIPToken.FROM, req.getHeader(SIPToken.FROM));
                            goneReq.setHeader(SIPToken.LOCATION, req.getHeader(SIPToken.LOCATION));
                            _parser.reconnectTo(new Socket(req.getDestinationAddress(), 
                                                           req.getDestinationPort()));
                            putMessage(goneReq);
                        } catch (Exception e) {
                            System.out.println("SIPConnection.run(): Error sending request: " + e);
                        }                            

                    } else if (req.getMethod().equals(SIPToken.GONE)) {
                        Debug.println("SIPConnection.run() got notification of unavailability for " 
                        + req.getURL());
                        
                        rm.notifyGone(req.getURL());
                        
                        // immediately send a HERE to the server which sent the gone notification
                        try {
                            SIPRequest hereReq = new SIPRequest(SIPToken.HERE, req.getURL(), 
                                                                req.getDestinationURL());
                            hereReq.setHeader(SIPToken.TO, req.getHeader(SIPToken.TO));
                            hereReq.setHeader(SIPToken.FROM, req.getHeader(SIPToken.FROM));
                            hereReq.setHeader(SIPToken.LOCATION, req.getHeader(SIPToken.LOCATION));
                            
                            _parser.reconnectTo(new Socket(req.getDestinationAddress(), 
                                                           req.getDestinationPort()));
                            putMessage(hereReq);
                        } catch (Exception e) {
                            System.out.println("SIPConnection.run(): Error sending request: " + e);
                        }                            
                        
                        
                    } // end checking various request methods
                    
                } // end if (message instanceof SIPRequest)
            } catch (SIPException se) {
                throw new RuntimeException(se.getMessage());
            } // end try
 //           Debug.println("SIPServer waiting for another request...");
 //       } // end while (true)
    } // end run()
} // end class SIPConnection