/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */
/*
 * Stub protocol for turning socket i/o into a stream interface.
 */

#include "xp.h"
#include "netutils.h"
#include "mktcp.h"
#include "mkgeturl.h"
#include "mkselect.h"
#include "netstream.h"
#include "plstr.h"
#include "prerror.h"
#include "prmem.h"
#include "prmon.h"

#include "nsHashtable.h"
#include "sockstub.h"

#define DEF_SOCKSTUB_PORT 80

extern "C" {
extern int MK_UNABLE_TO_LOCATE_HOST;
extern int MK_ZERO_LENGTH_FILE;
extern int MK_TCP_READ_ERROR;
extern int MK_OUT_OF_MEMORY;

}

/* definitions of state for the state machine design
 */
typedef enum {
    SOCKSTUB_START_CONNECT,
    SOCKSTUB_FINISH_CONNECT,
    SOCKSTUB_SETUP_STREAM,
    SOCKSTUB_FLUSH_DATA,
    SOCKSTUB_DONE,
    SOCKSTUB_ERROR_DONE,
    SOCKSTUB_FREE
} SockStubStatesEnum;

typedef struct _SockStubConData {
    SockStubStatesEnum  next_state;       /* the next state or action to be taken */
    char*               line_buffer;      /* temporary string storage */
    int32               line_buffer_size; /* current size of the line buffer */
    NET_StreamClass*    stream; /* The output stream */
    Bool                pause_for_read;   /* Pause now for next read? */
    TCP_ConData*        tcp_con_data;  /* Data pointer for tcp connect state machine */
    Bool                reuse_stream;
    Bool                connection_is_valid;
    char*               hostname;       /* hostname string (may contain :port) */
    PRFileDesc*         sock;       /* socket */
    int32               original_content_length;
} SockStubConData;

PRIVATE int32
net_InterruptSockStub (ActiveEntry * ce);

PRIVATE int32
net_ProcessSockStub (ActiveEntry * ce);

PRIVATE
int ReturnErrorStatus (int status)
{
  if (status < 0)
    status |= status; /* set a breakpoint HERE to find errors */
  return status;
}


#define STATUS(Status)      ReturnErrorStatus (Status)

#define PUTBLOCK(b, l)  (*cd->stream->put_block) \
                                    (cd->stream, b, l)
#define PUTSTRING(s)    (*cd->stream->put_block) \
                                    (cd->stream, s, PL_strlen(s))
#define COMPLETE_STREAM (*cd->stream->complete) \
                                    (cd->stream)
#define ABORT_STREAM(s) (*cd->stream->abort) \
                                    (cd->stream, s)


// forward declaration
PRIVATE int32 net_ProcessSockStub (ActiveEntry * ce);

static nsHashtable *theUrlToSocketHashtable = new nsHashtable();


class net_IntegerKey: public nsHashKey {
private:
  PRUint32 itsHash;
  
public:
  net_IntegerKey(PRUint32 hash) {
    itsHash = hash;
  }
  
  PRUint32 HashValue(void) const {
    return itsHash;
  }

  PRBool Equals(const nsHashKey *aKey) const {
    return (itsHash == (((const net_IntegerKey *) aKey)->itsHash)) ? PR_TRUE : PR_FALSE;
  }

  nsHashKey *Clone(void) const {
    return new net_IntegerKey(itsHash);
  }
};


static PRMonitor *sockStub_lock = NULL;

PRBool
nsSockStub_lock(void)
{
    if(sockStub_lock == NULL)
    {
    sockStub_lock = PR_NewMonitor();
    if(sockStub_lock == NULL)
        return PR_FALSE;
    } 
    PR_EnterMonitor(sockStub_lock);
    return PR_TRUE;
}

void
nsSockStub_unlock(void)
{
    PR_ASSERT(sockStub_lock != NULL);
  
    if(sockStub_lock != NULL)
    {
        PR_ExitMonitor(sockStub_lock);
    }
}



PRIVATE void
net_AddSocketToHashTable(URL_Struct* URL_s, ActiveEntry * ce) 
{
  nsSockStub_lock();
  net_IntegerKey ikey((PRUint32)URL_s);
  theUrlToSocketHashtable->Put(&ikey, ce); 
  nsSockStub_unlock();
}

PRIVATE void
net_RemoveSocketToHashTable(URL_Struct* URL_s) 
{
  nsSockStub_lock();
  net_IntegerKey ikey((PRUint32)URL_s);
  theUrlToSocketHashtable->Remove(&ikey); 
  nsSockStub_unlock();
}

extern "C" {
PUBLIC PRFileDesc *
NET_GetSocketToHashTable(URL_Struct_* URL_s) 
{
  nsSockStub_lock();
  net_IntegerKey ikey((PRUint32)URL_s);
  ActiveEntry * ce = (ActiveEntry *)theUrlToSocketHashtable->Get(&ikey); 
  nsSockStub_unlock();

  SockStubConData * cd = (SockStubConData *)ce->con_data;
  return cd->sock;
}
}

extern "C" {
PUBLIC int32
NET_FreeSocket(URL_Struct_* URL_s) 
{
  nsSockStub_lock();
  net_IntegerKey ikey((PRUint32)URL_s);
  ActiveEntry * ce = (ActiveEntry *)theUrlToSocketHashtable->Get(&ikey); 
  nsSockStub_unlock();

  /* XXX: The interrupt will set the error state and will do the 
   * cleanup of the socket 
   */
  SockStubConData * cd = (SockStubConData *)ce->con_data;

  ce->status = MK_DATA_LOADED;
  cd->next_state = SOCKSTUB_DONE;
 
  return STATUS (net_ProcessSockStub(ce));
}
}

/* begins the connect process
 *
 * returns the TCP status code
 */
PRIVATE int
net_start_sockstub_connect(ActiveEntry * ce) {
    SockStubConData * cd = (SockStubConData *)ce->con_data;

   int def_port = DEF_SOCKSTUB_PORT;
   char * host_string = NET_ParseURL(ce->URL_s->address, GET_HOST_PART);

	// now try to extract the port number from the url
	char * startOfPort = PL_strchr(host_string, ':');
	if (startOfPort && *startOfPort && *(startOfPort+1))
		def_port = atoi(startOfPort+1);

    ce->status = NET_BeginConnect (ce->URL_s->address,
                    ce->URL_s->IPAddressString,
                    "SOCKSTUB", 
                    def_port, 
                    &cd->sock, 
                    HG02873  
                    &cd->tcp_con_data, 
                    ce->window_id, 
                    &ce->URL_s->error_msg,
                    ce->socks_host,
                    ce->socks_port,
                    ce->URL_s->localIP);


    /* set this so mkgeturl can select on it */
    ce->socket = cd->sock;

    if(cd->sock != NULL)
        NET_TotalNumberOfOpenConnections++;

    if (ce->status < 0) {
        if(ce->status == MK_UNABLE_TO_LOCATE_HOST)
        {
            /* no need to set the state since this
             * is the state we want again
             */
            if(cd->sock != NULL) {
                PR_Close(cd->sock);
                cd->sock = NULL;
                ce->socket = NULL;
            }
            return(0);
        }

        TRACEMSG(("SockStub: Unable to connect to host for `%s' (errno = %d).", 
                          ce->URL_s->address, PR_GetOSError()));
        /*
         * Proxy failover
         */

        cd->next_state = SOCKSTUB_ERROR_DONE;
        return STATUS(ce->status);
    } /* end status < 0 */

    if(ce->status == MK_CONNECTED)
    {
        cd->next_state = SOCKSTUB_SETUP_STREAM;
        TRACEMSG(("Connected through SockStub gateway on sock #%d",cd->sock));
        NET_SetReadSelect(ce->window_id, cd->sock);
    } else {
        cd->next_state = SOCKSTUB_FINISH_CONNECT;
        cd->pause_for_read = TRUE;
        ce->con_sock = cd->sock;  /* set the socket for select */
        NET_SetConnectSelect(ce->window_id, ce->con_sock);
    }

    net_AddSocketToHashTable(ce->URL_s, ce);
    return STATUS(ce->status);
}


/*  begins the connect process
 *
 *  returns the TCP status code
 */
PRIVATE int
net_finish_sockstub_connect(ActiveEntry * ce)
{
    SockStubConData * cd = (SockStubConData *)ce->con_data;
    int def_port;

    def_port = DEF_SOCKSTUB_PORT;

    ce->status = NET_FinishConnect(ce->URL_s->address,
                    "SOCKSTUB",
                    def_port,
                    &cd->sock,  
                    &cd->tcp_con_data, 
                    ce->window_id,
                    &ce->URL_s->error_msg,
                    ce->URL_s->localIP);

    if (ce->status < 0) {
        NET_ClearConnectSelect(ce->window_id, ce->con_sock);
        TRACEMSG(("SockStub: Unable to connect to host for `%s' (errno = %d).",
                                              ce->URL_s->address, PR_GetOSError()));

        cd->next_state = SOCKSTUB_ERROR_DONE;
        return STATUS(ce->status);
    } /* end status < 0 */

    if(ce->status == MK_CONNECTED) {
        NET_ClearConnectSelect(ce->window_id, ce->con_sock);
        ce->con_sock = NULL;  /* reset the socket so we don't select on it  */
        /* set this so mkgeturl can select on it */
        ce->socket = cd->sock;
        cd->next_state = SOCKSTUB_SETUP_STREAM;
        NET_SetReadSelect(ce->window_id, cd->sock);
        TRACEMSG(("Connected through SockStub on sock #%d",cd->sock));
    } else {
        /* unregister the old ce->socket from the select list
         * and register the new value in the case that it changes
         */
        if(ce->con_sock != cd->sock) {
            NET_ClearConnectSelect(ce->window_id, ce->con_sock);
            ce->con_sock = cd->sock; 
            NET_SetConnectSelect(ce->window_id, ce->con_sock);
        }
  
        cd->pause_for_read = TRUE;
    } 

    net_AddSocketToHashTable(ce->URL_s, ce);

    return STATUS(ce->status);
}


/* sets up the stream
 *
 * returns the tcp status code
 */
PRIVATE int
net_setup_sockstub_stream(ActiveEntry * ce) {
    SockStubConData * cd = (SockStubConData *)ce->con_data;
    XP_Bool need_to_do_again = FALSE;
    MWContext * stream_context;

    TRACEMSG(("NET_ProcessSockStub: setting up stream"));

    /* set a default content type if one wasn't given 
     */
    if(!ce->URL_s->content_type || !*ce->URL_s->content_type)
        StrAllocCopy(ce->URL_s->content_type, TEXT_HTML);

    /* If a stream previously exists from a partial cache
     * situation, reuse it
     */
    if(!cd->stream) {
        /* clear to prevent tight loop */
        NET_ClearReadSelect(ce->window_id, cd->sock);
        stream_context = ce->window_id;

        /* Set up the stream stack to handle the body of the message */
        cd->stream = NET_StreamBuilder(ce->format_out, 
                        ce->URL_s, 
                        stream_context);

        if (!cd->stream) {
            ce->status = MK_UNABLE_TO_CONVERT;
            ce->URL_s->error_msg = NET_ExplainErrorDetails(MK_UNABLE_TO_CONVERT);
            return STATUS(ce->status);
        }

        NET_SetReadSelect(ce->window_id, cd->sock);

    }

    cd->next_state = SOCKSTUB_FLUSH_DATA;

    return STATUS(ce->status);
}


/* flushes data from/to socket to/from stream
 * (For now, it only reads from the socket to the stream.)
 * returns the tcp status code
 */
PRIVATE int
net_flush_sockstub_data(ActiveEntry * ce)
{
    SockStubConData * cd = (SockStubConData *)ce->con_data;
    unsigned int write_ready, read_size;

    TRACEMSG(("NET_ProcessSockStub: flushing data"));

    ///////// SOCKET->STREAM (reading from the net) ////////////////


    /* check to see if the stream is ready for writing
   */
    write_ready = (*cd->stream->is_write_ready)(cd->stream);

    TRACEMSG(("NET_ProcessSockStub: Flush: write ready returned %d", write_ready));

  if(!write_ready)
    {
    cd->pause_for_read = TRUE;
    return(0);  /* wait until we are ready to write */
    }
  else if(write_ready < (unsigned int) NET_Socket_Buffer_Size)
    {
    read_size = write_ready;
    }
  else
    {
    read_size = NET_Socket_Buffer_Size;
    }

    ce->status = PR_Read(cd->sock, NET_Socket_Buffer, read_size);

    TRACEMSG(("NET_ProcessSockStub: Flush: just read %d bytes.", ce->status));
    cd->pause_for_read = TRUE;  /* pause for the next read */

    if(ce->status > 0)
    {
        ce->bytes_received += ce->status;
        ce->status = PUTBLOCK(NET_Socket_Buffer, ce->status);

        // For now, just so we exit when we have at least gotten something,
        // to test that stream data is crossing the thread.
        /* cd->next_state = SOCKSTUB_DONE; */


        if(cd->original_content_length 
          && ce->bytes_received >= cd->original_content_length)
        {
            // normal end of transfer
            /* XXX: We should go back and wait for data
             ce->status = MK_DATA_LOADED;
             cd->next_state = SOCKSTUB_DONE;
             */
            cd->pause_for_read = FALSE;
        }
    }
    else if(ce->status == 0)
    {
        // temporary mscott testing hack:
        cd->next_state = SOCKSTUB_FLUSH_DATA;
        ce->status = 1;
        cd->pause_for_read = TRUE;
        return STATUS(ce->status);

        // transfer finished 
        TRACEMSG(("SOCKSTUB: Caught TCP EOF ending stream"));

        ce->URL_s->error_msg = NET_ExplainErrorDetails(MK_ZERO_LENGTH_FILE);
        cd->next_state = SOCKSTUB_ERROR_DONE;
        cd->pause_for_read = FALSE;
        return(MK_ZERO_LENGTH_FILE);
    }
    else /* error */
    {
        // temporary hack by mscott
        // otherwise data isn't ready yet:
        cd->pause_for_read = TRUE;
        ce->status = 1;
        return STATUS(ce->status);

        int err = PR_GetError();

        TRACEMSG(("TCP Error: %d", err));

        if (err == PR_WOULD_BLOCK_ERROR)
        {
            cd->pause_for_read = TRUE;
            return (0);
        }

        ce->URL_s->error_msg = NET_ExplainErrorDetails(MK_TCP_READ_ERROR, err);

        /* return TCP error
         */
        return MK_TCP_READ_ERROR;
    }

     return STATUS(ce->status);
}


PRIVATE int32
net_LoadSockStub (ActiveEntry * ce)
{
    SockStubConData * cd = PR_NEW(SockStubConData);
    ce->con_data = cd;
    if(!ce->con_data)
    {
        ce->status = MK_OUT_OF_MEMORY;
        ce->URL_s->error_msg = NET_ExplainErrorDetails(MK_OUT_OF_MEMORY);
        return STATUS(ce->status);
    }

    char *use_host = NET_ParseURL(ce->URL_s->address, GET_HOST_PART);

    /* Init connection data */
    memset(cd, 0, sizeof(SockStubConData));
    cd->sock = NULL;
    StrAllocCopy(cd->hostname, use_host);

    PR_Free(use_host);

    return STATUS (net_ProcessSockStub(ce));
}

PRIVATE int32
net_ProcessSockStub (ActiveEntry * ce)
{
    SockStubConData * cd = (SockStubConData *)ce->con_data;
    TRACEMSG(("Entering NET_ProcessSockStub"));

    cd->pause_for_read = FALSE; /* already paused; reset */

    while(!cd->pause_for_read)
    {
    
        switch(cd->next_state)
        {

        case SOCKSTUB_START_CONNECT:
            ce->status = net_start_sockstub_connect(ce);
            break;
        
        case SOCKSTUB_FINISH_CONNECT:
            ce->status = net_finish_sockstub_connect(ce);
            break;
        
        case SOCKSTUB_SETUP_STREAM:
            ce->status = net_setup_sockstub_stream(ce);
            break;

        case SOCKSTUB_FLUSH_DATA:
            ce->status = net_flush_sockstub_data(ce);
            break;
        
        case SOCKSTUB_DONE:
            net_RemoveSocketToHashTable(ce->URL_s);
            NET_ClearReadSelect(ce->window_id, cd->sock);
            NET_TotalNumberOfOpenConnections--;
            PR_Close(cd->sock);

            if(cd->stream) {
                COMPLETE_STREAM;
                PR_Free(cd->stream);
                cd->stream = 0;
            }

            cd->next_state = SOCKSTUB_FREE;
            break;
        
        case SOCKSTUB_ERROR_DONE:
            if(cd->sock != NULL) {
                net_RemoveSocketToHashTable(ce->URL_s);
                NET_ClearDNSSelect(ce->window_id, cd->sock);
                NET_ClearReadSelect(ce->window_id, cd->sock);
                NET_ClearConnectSelect(ce->window_id, cd->sock);
                PR_Close(cd->sock);
                NET_TotalNumberOfOpenConnections--;
                cd->sock = NULL;
            }

            cd->next_state = SOCKSTUB_FREE;

            if(cd->stream) {
                ABORT_STREAM(ce->status);
                PR_Free(cd->stream);
                cd->stream = 0;
            }

            break; /* SOCKSTUB_ERROR_DONE */
        
        case SOCKSTUB_FREE:

            PR_FREEIF(cd->line_buffer);
            PR_Free(cd->stream); /* don't forget the stream */
            PR_Free(cd->hostname);
            if(cd->tcp_con_data)
                NET_FreeTCPConData(cd->tcp_con_data);
            PR_FREEIF(cd);
            return STATUS (-1); /* final end SOCKSTUB_FREE */
        
        default: /* should never happen !!! */
            TRACEMSG(("SOCKSTUB: BAD STATE!"));
            cd->next_state = SOCKSTUB_ERROR_DONE;
            break;
        } /* end switch */

        /* check for errors during load and call error state if found */
        if(ce->status < 0 && cd->next_state != SOCKSTUB_FREE)
        {
            cd->next_state = SOCKSTUB_ERROR_DONE;
        }
        
    } /* while(!cd->pause_for_read) */
    
    return STATUS(ce->status);
}

PRIVATE int32
net_InterruptSockStub (ActiveEntry * ce)
{
    SockStubConData * cd = (SockStubConData *)ce->con_data;

    cd->next_state = SOCKSTUB_ERROR_DONE;
    ce->status = MK_INTERRUPTED;
 
    return STATUS (net_ProcessSockStub(ce));
}

PRIVATE void
net_CleanupSockStub(void)
{
}


#define SOCKSTUB_SCHEME "sockstub:"

extern "C" {

MODULE_PRIVATE void
NET_InitSockStubProtocol(void)
{
  static NET_ProtoImpl sockstub_proto_impl;

  sockstub_proto_impl.init = net_LoadSockStub;
  sockstub_proto_impl.process = net_ProcessSockStub;
  sockstub_proto_impl.interrupt = net_InterruptSockStub;
  sockstub_proto_impl.cleanup = net_CleanupSockStub;
  StrAllocCopy(sockstub_proto_impl.scheme, SOCKSTUB_SCHEME); 


  NET_RegisterProtocolImplementation(&sockstub_proto_impl, SOCKSTUB_TYPE_URL);
}

}
