/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * The Original Code is the mozilla.org LDAP XPCOM SDK.
 * 
 * The Initial Developer of the Original Code is Netscape
 * Communications Corporation.  Portions created by Netscape are 
 * Copyright (C) 2000 Netscape Communications Corporation.  All
 * Rights Reserved.
 * 
 * Contributor(s): Dan Mosedale <dmose@mozilla.org> (original author)
 *                 Leif Hedstrom <leif@netscape.com>
 *                 Kipp Hickman <kipp@netscape.com>
 *                 Warren Harris <warren@netscape.com>
 *                 Dan Matejka <danm@netscape.com>
 *  
 * 
 * Alternatively, the contents of this file may be used under the
 * terms of the GNU General Public License Version 2 or later (the
 * "GPL"), in which case the provisions of the GPL are applicable 
 * instead of those above.  If you wish to allow use of your 
 * version of this file only under the terms of the GPL and not to
 * allow others to use your version of this file under the MPL,
 * indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by
 * the GPL.  If you do not delete the provisions above, a recipient
 * may use your version of this file under either the MPL or the
 * GPL.
 */

#include "nsLDAPInternal.h"
#include "nsIServiceManager.h"
#include "nsString.h"
#include "nsReadableUtils.h"
#include "nsIComponentManager.h"
#include "nsLDAPConnection.h"
#include "nsLDAPMessage.h"
#include "nsIEventQueueService.h"
#include "nsIConsoleService.h"
#include "nsIDNSService.h"
#include "nsIRequestObserver.h"
#include "nsIProxyObjectManager.h"
#include "netCore.h"

const char kConsoleServiceContractId[] = "@mozilla.org/consoleservice;1";
const char kDNSServiceContractId[] = "@mozilla.org/network/dns-service;1";

// constructor
//
nsLDAPConnection::nsLDAPConnection()
    : mConnectionHandle(0),
      mBindName(0),
      mPendingOperations(0),
      mRunnable(0),
      mSSL(PR_FALSE),
      mDNSRequest(0),
      mDNSFinished(PR_FALSE)
{
}

// destructor
//
nsLDAPConnection::~nsLDAPConnection()
{
  int rc;

  PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, ("unbinding\n"));

  if (mConnectionHandle) {
      // note that the ldap_unbind() call in the 5.0 version of the LDAP C SDK
      // appears to be exactly identical to ldap_unbind_s(), so it may in fact
      // still be synchronous
      //
      rc = ldap_unbind(mConnectionHandle);
#ifdef PR_LOGGING
      if (rc != LDAP_SUCCESS) {
          PR_LOG(gLDAPLogModule, PR_LOG_WARNING, 
                 ("nsLDAPConnection::~nsLDAPConnection: %s\n", 
                  ldap_err2string(rc)));
      }
#endif
  }

  PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, ("unbound\n"));

  if (mPendingOperations) {
      delete mPendingOperations;
  }

  // Cancel the DNS lookup if needed, and also drop the reference to the
  // Init listener (if still there).
  //
  if (mDNSRequest) {
      mDNSRequest->Cancel(NS_BINDING_ABORTED);
      mDNSRequest = 0;
  }
  mInitListener = 0;

  // Release the reference to the runnable object.
  //
  NS_IF_RELEASE(mRunnable);
}

// We need our own Release() here, so that we can lock around the delete.
// This is needed to avoid a race condition with the weak reference to us,
// which is used in nsLDAPConnectionLoop. A problem could occur if the
// nsLDAPConnection gets destroyed while do_QueryReferent() is called,
// since converting to the strong reference isn't MT safe.
//
NS_IMPL_THREADSAFE_ADDREF(nsLDAPConnection);
NS_IMPL_THREADSAFE_QUERY_INTERFACE3(nsLDAPConnection,
                                    nsILDAPConnection,
                                    nsISupportsWeakReference,
                                    nsIDNSListener);

nsrefcnt
nsLDAPConnection::Release(void)
{
    nsrefcnt count;

    NS_PRECONDITION(0 != mRefCnt, "dup release");
    count = PR_AtomicDecrement((PRInt32 *)&mRefCnt);
    NS_LOG_RELEASE(this, count, "nsLDAPConnection");
    if (0 == count) {
        // As commented by danm: In the object's destructor, if by some
        // convoluted, indirect means it happens to run into some code
        // that temporarily references it (addref/release), then if the
        // refcount had been left at 0 the unexpected release would
        // attempt to reenter the object's destructor.
        //
        mRefCnt = 1; /* stabilize */

        // If we have a mRunnable object, we need to make sure to lock it's
        // mLock before we try to DELETE. This is to avoid a race condition.
        // We also make sure to keep a strong reference to the runnable
        // object, to make sure it doesn't get GCed from underneath us,
        // while we are still holding a lock for instance.
        //
        if (mRunnable && mRunnable->mLock) {
            nsLDAPConnectionLoop *runnable  = mRunnable;

            NS_ADDREF(runnable);
            PR_Lock(runnable->mLock);
            NS_DELETEXPCOM(this);
            PR_Unlock(runnable->mLock);
            NS_RELEASE(runnable);
        } else {
            NS_DELETEXPCOM(this);
        }

        return 0;
    }
    return count;
}


NS_IMETHODIMP
nsLDAPConnection::Init(const char *aHost, PRInt16 aPort, PRBool aSSL,
                       const nsACString& aBindName, 
                       nsILDAPMessageListener *aMessageListener,
                       nsISupports *aClosure)
{
    nsCOMPtr<nsIDNSListener> selfProxy;
    nsresult rv;

    if ( !aHost || !aMessageListener) {
        return NS_ERROR_ILLEGAL_VALUE;
    }

    // Make sure we haven't called Init earlier, i.e. there's a DNS
    // request pending.
    //
    NS_ASSERTION(!mDNSRequest, "nsLDAPConnection::Init() "
                 "Connection was already initialized\n");

    mBindName.Assign(aBindName);

    mClosure = aClosure;

    // Save the port number for later use, once the DNS server(s) has
    // resolved the host part.
    //
    mPort = aPort;

    // Save the SSL flag for later use
    mSSL = aSSL;

    // Save the Init listener reference, we need it when the async
    // DNS resolver has finished.
    //
    mInitListener = aMessageListener;

    // allocate a hashtable to keep track of pending operations.
    // 10 buckets seems like a reasonable size, and we do want it to 
    // be threadsafe
    //
    mPendingOperations = new nsSupportsHashtable(10, PR_TRUE);
    if ( !mPendingOperations) {
        NS_ERROR("failure initializing mPendingOperations hashtable");
        return NS_ERROR_FAILURE;
    }

    // Get a proxy object so the callback happens on the current thread.
    // This is now a Synchronous proxy, due to the fact that the DNS
    // service hands out data which it later deallocates, and the async
    // proxy makes this unreliable. See bug 102227 for more details.
    //
    rv = NS_GetProxyForObject(NS_CURRENT_EVENTQ,
                              NS_GET_IID(nsIDNSListener), 
                              NS_STATIC_CAST(nsIDNSListener*, this), 
                              PROXY_SYNC | PROXY_ALWAYS, 
                              getter_AddRefs(selfProxy));

    if (NS_FAILED(rv)) {
        NS_ERROR("nsLDAPConnection::Init(): couldn't "
                 "create proxy to this object for callback");
        return NS_ERROR_FAILURE;
    }

    // Do the pre-resolve of the hostname, using the DNS service. This
    // will also initialize the LDAP connection properly, once we have
    // the IPs resolved for the hostname. This includes creating the
    // new thread for this connection.
    //
    // XXX - What return codes can we expect from the DNS service?
    //
    nsCOMPtr<nsIDNSService>
        pDNSService(do_GetService(kDNSServiceContractId, &rv));

    if (NS_FAILED(rv)) {
        NS_ERROR("nsLDAPConnection::Init(): couldn't "
                 "create the DNS Service object");

        return NS_ERROR_FAILURE;
    }
    rv = pDNSService->Lookup(aHost, 
                             selfProxy,
                             nsnull, 
                             getter_AddRefs(mDNSRequest));

    if (NS_FAILED(rv)) {
        switch (rv) {
        case NS_ERROR_OUT_OF_MEMORY:
        case NS_ERROR_UNKNOWN_HOST:
        case NS_ERROR_FAILURE:
        case NS_ERROR_OFFLINE:
            return rv;

        default:
            return NS_ERROR_UNEXPECTED;
        }
    }

    // The DNS service can actually call the listeners even before the
    // Lookup() function has returned. If that happens, we can still hold
    // a reference to the DNS request, even after the DNS lookup is done.
    // If this happens, lets just get rid of the DNS request, since we won't
    // need it any more.
    //
    if (mDNSFinished && mDNSRequest) {
        mDNSRequest = 0;
    }

    return NS_OK;
}

NS_IMETHODIMP
nsLDAPConnection::GetClosure(nsISupports **_retval)
{
    if (!_retval) {
        return NS_ERROR_ILLEGAL_VALUE;
    }
    NS_IF_ADDREF(*_retval = mClosure);
    return NS_OK;
}

NS_IMETHODIMP
nsLDAPConnection::SetClosure(nsISupports *aClosure)
{
    mClosure = aClosure;
    return NS_OK;
}

// who we're binding as
//
// readonly attribute AUTF8String bindName
//
NS_IMETHODIMP
nsLDAPConnection::GetBindName(nsACString& _retval)
{
    _retval.Assign(mBindName);
    return NS_OK;
}

// wrapper for ldap_get_lderrno
// XXX should copy before returning
//
NS_IMETHODIMP
nsLDAPConnection::GetLdErrno(nsACString& matched, nsACString& errString, 
                             PRInt32 *_retval)
{
    char *match, *err;

    NS_ENSURE_ARG_POINTER(_retval);

    *_retval = ldap_get_lderrno(mConnectionHandle, &match, &err);
    matched.Assign(match);
    errString.Assign(err);
    return NS_OK;
}

// return the error string corresponding to GetLdErrno.
//
// XXX - deal with optional params
// XXX - how does ldap_perror know to look at the global errno?
//
NS_IMETHODIMP
nsLDAPConnection::GetErrorString(PRUnichar **_retval)
{
    NS_ENSURE_ARG_POINTER(_retval);

    // get the error string
    //
    char *rv = ldap_err2string(ldap_get_lderrno(mConnectionHandle, 0, 0));
    if (!rv) {
        return NS_ERROR_OUT_OF_MEMORY;
    }

    // make a copy using the XPCOM shared allocator
    //
    *_retval = ToNewUnicode(NS_ConvertUTF8toUCS2(rv));
    if (!*_retval) {
        return NS_ERROR_OUT_OF_MEMORY;
    }
    return NS_OK;
}

/** 
 * Add an nsILDAPOperation to the list of operations pending on
 * this connection.  This is also mainly intended for use by the
 * nsLDAPOperation code.
 */
nsresult
nsLDAPConnection::AddPendingOperation(nsILDAPOperation *aOperation)
{
    PRInt32 msgID;

    if (!aOperation) {
        return NS_ERROR_ILLEGAL_VALUE;
    }

    // find the message id
    //
    aOperation->GetMessageID(&msgID);

    // turn it into an nsVoidKey.  note that this is another spot that
    // assumes that sizeof(void*) >= sizeof(PRInt32).  
    //
    // XXXdmose  should really create an nsPRInt32Key.
    //
    nsVoidKey *key = new nsVoidKey(NS_REINTERPRET_CAST(void *, msgID));
    if (!key) {
        return NS_ERROR_OUT_OF_MEMORY;
    }

    // actually add it to the queue.  if Put indicates that an item in 
    // the hashtable was actually overwritten, something is really wrong.
    //
    if (mPendingOperations->Put(key, aOperation)) {
        NS_ERROR("nsLDAPConnection::AddPendingOperation() "
                 "mPendingOperations->Put() overwrote an item.  msgId "
                 "is supposed to be unique\n");
        delete key;
        return NS_ERROR_UNEXPECTED;
    }

    PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
           ("pending operation added; total pending operations now = %d\n", 
            mPendingOperations->Count()));

    delete key;
    return NS_OK;
}

/**
 * Remove an nsILDAPOperation from the list of operations pending on this
 * connection.  Mainly intended for use by the nsLDAPOperation code.
 *
 * @param aOperation    operation to add
 * @exception NS_ERROR_INVALID_POINTER  aOperation was NULL
 * @exception NS_ERROR_OUT_OF_MEMORY    out of memory
 * @exception NS_ERROR_FAILURE      could not delete the operation 
 *
 * void removePendingOperation(in nsILDAPOperation aOperation);
 */
nsresult
nsLDAPConnection::RemovePendingOperation(nsILDAPOperation *aOperation)
{
    nsresult rv;
    PRInt32 msgID;

    NS_ENSURE_ARG_POINTER(aOperation);

    // find the message id
    //
    rv = aOperation->GetMessageID(&msgID);
    NS_ENSURE_SUCCESS(rv, rv);

    // turn it into an nsVoidKey.  note that this is another spot that
    // assumes that sizeof(void*) >= sizeof(PRInt32).  
    //
    // XXXdmose  should really create an nsPRInt32Key.
    //
    nsVoidKey *key = new nsVoidKey(NS_REINTERPRET_CAST(void *, msgID));
    if (!key) {
        return NS_ERROR_OUT_OF_MEMORY;
    }

    // remove the operation if it's still there.  
    //
    if (!mPendingOperations->Remove(key)) {

        PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
               ("nsLDAPConnection::RemovePendingOperation(): could not remove "
                "operation; perhaps it already completed."));
    } else {

        PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
               ("nsLDAPConnection::RemovePendingOperation(): operation "
                "removed; total pending operations now = %d\n", 
                mPendingOperations->Count()));
    }

    delete key;
    return NS_OK;
}

nsresult
nsLDAPConnection::InvokeMessageCallback(LDAPMessage *aMsgHandle, 
                                        nsILDAPMessage *aMsg,
                                        PRBool aRemoveOpFromConnQ)
{
    PRInt32 msgId;
    nsresult rv;
    nsCOMPtr<nsILDAPOperation> operation;
    nsCOMPtr<nsILDAPMessageListener> listener;

    PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, ("InvokeMessageCallback entered\n"));

    // get the message id corresponding to this operation
    //
    msgId = ldap_msgid(aMsgHandle);
    if (msgId == -1) {
        NS_ERROR("nsLDAPConnection::GetCallbackByMessage(): "
                 "ldap_msgid() failed\n");
        return NS_ERROR_FAILURE;
    }

    // get this in key form.  note that using nsVoidKey in this way assumes
    // that sizeof(void *) >= sizeof PRInt32
    //
    nsVoidKey *key = new nsVoidKey(NS_REINTERPRET_CAST(void *, msgId));
    if (!key)
        return NS_ERROR_OUT_OF_MEMORY;

    // find the operation in question
    //
    nsISupports *data = mPendingOperations->Get(key);
    if (!data) {

        PR_LOG(gLDAPLogModule, PR_LOG_WARNING, 
               ("Warning: InvokeMessageCallback(): couldn't find "
                "nsILDAPOperation corresponding to this message id\n"));
        delete key;

        // this may well be ok, since it could just mean that the operation
        // was aborted while some number of messages were already in transit.
        //
        return NS_OK;
    }

    operation = getter_AddRefs(NS_STATIC_CAST(nsILDAPOperation *, data));

    // Make sure the mOperation member is set to this operation before
    // we call the callback.
    //
    NS_STATIC_CAST(nsLDAPMessage *, aMsg)->mOperation = operation;

    // get the message listener object (this may be a proxy for a
    // callback which should happen on another thread)
    //
    rv = operation->GetMessageListener(getter_AddRefs(listener));
    if (NS_FAILED(rv)) {
        NS_ERROR("nsLDAPConnection::InvokeMessageCallback(): probable "
                 "memory corruption: GetMessageListener() returned error");
        delete key;
        return NS_ERROR_UNEXPECTED;
    }

    // invoke the callback 
    //
    listener->OnLDAPMessage(aMsg);

    // if requested (ie the operation is done), remove the operation
    // from the connection queue.
    //
    if (aRemoveOpFromConnQ) {
        rv = mPendingOperations->Remove(key);
        if (NS_FAILED(rv)) {
            NS_ERROR("nsLDAPConnection::InvokeMessageCallback: unable to "
                     "remove operation from the connection queue\n");
            delete key;
            return NS_ERROR_UNEXPECTED;
        }

        PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
               ("pending operation removed; total pending operations now ="
                " %d\n", mPendingOperations->Count()));
    }

    delete key;
    return NS_OK;
}

// constructor
//
nsLDAPConnectionLoop::nsLDAPConnectionLoop()
    : mWeakConn(0),
      mLock(0)
{
}

// destructor
//
nsLDAPConnectionLoop::~nsLDAPConnectionLoop()
{
    // Delete the lock object
    if (mLock)
        PR_DestroyLock(mLock);
}

NS_IMPL_THREADSAFE_ISUPPORTS1(nsLDAPConnectionLoop, nsIRunnable);

NS_IMETHODIMP
nsLDAPConnectionLoop::Init()
{
    if (!mLock) {
        mLock = PR_NewLock();
        if (!mLock) {
            NS_ERROR("nsLDAPConnectionLoop::Init: out of memory ");
            return NS_ERROR_OUT_OF_MEMORY;
        }
    }

    return NS_OK;
}

// typedef PRBool
// (*PR_CALLBACK nsHashtableEnumFunc)
//      (nsHashKey *aKey, void *aData, void* aClosure);
PRBool PR_CALLBACK
CheckLDAPOperationResult(nsHashKey *aKey, void *aData, void* aClosure)
{
    int lderrno;
    nsresult rv;
    PRInt32 returnCode;
    LDAPMessage *msgHandle;
    nsCOMPtr<nsILDAPMessage> msg;
    PRBool operationFinished = PR_TRUE;
    struct timeval timeout = { 1, 0 }; 
    PRIntervalTime sleepTime = PR_MillisecondsToInterval(40);

    // we need to access some of the connection loop's objects
    //
    nsLDAPConnectionLoop *loop = 
        NS_STATIC_CAST(nsLDAPConnectionLoop *, aClosure);

    // get the console service so we can log messages
    //
    nsCOMPtr<nsIConsoleService> consoleSvc = 
        do_GetService(kConsoleServiceContractId, &rv);
    if (NS_FAILED(rv)) {
        NS_ERROR("CheckLDAPOperationResult() couldn't get console service");
        return NS_ERROR_FAILURE;
    }

    returnCode = ldap_result(loop->mRawConn->mConnectionHandle,
                             aKey->HashCode(), LDAP_MSG_ONE,
                                 &timeout, &msgHandle);

        // if we didn't error or timeout, create an nsILDAPMessage
        //      
        switch (returnCode) {

        case 0: // timeout

            // the connection may not exist yet.  sleep for a while
            // and try again
            //
        PR_LOG(gLDAPLogModule, PR_LOG_WARNING, ("ldap_result() timed out.\n"));

            // The sleep here is to avoid a problem where the LDAP
            // Connection/thread isn't ready quite yet, and we want to
            // avoid a very busy loop.
            //
            PR_Sleep(sleepTime);
        return PR_TRUE;

        case -1: // something went wrong 

        lderrno = ldap_get_lderrno(loop->mRawConn->mConnectionHandle, 0, 0);

            // Sleep briefly, to avoid a very busy loop again.
            //
            PR_Sleep(sleepTime);

            switch (lderrno) {

            case LDAP_SERVER_DOWN:
                // We might want to shutdown the thread here, but it has
                // implications to the user of the nsLDAPConnection, so
                // for now we just ignore it. It's up to the owner of
                // the nsLDAPConnection to detect the error, and then
                // create a new connection.
                //
                PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
                       ("CheckLDAPOperationResult(): ldap_result returned" 
                        " LDAP_SERVER_DOWN"));
                break;

            case LDAP_DECODING_ERROR:
                consoleSvc->LogStringMessage(
                    NS_LITERAL_STRING("LDAP: WARNING: decoding error; possible corrupt data received").get());
                NS_WARNING("CheckLDAPOperationResult(): ldaperrno = "
                           "LDAP_DECODING_ERROR after ldap_result()");
                break;

            case LDAP_NO_MEMORY:
                NS_ERROR("CheckLDAPOperationResult(): Couldn't allocate memory"
                         " while getting async operation result");
                // punt and hope things work out better next time around
                break;

            case LDAP_PARAM_ERROR:
                // I think it's possible to hit a race condition where we're
                // continuing to poll for a result after the C SDK connection
                // has removed the operation because the connection has gone
                // dead.  In theory we should fix this.  Practically, it's
                // unclear to me whether it matters.
                //
                NS_WARNING("CheckLDAPOperationResult(): ldap_result returned"
                           " LDAP_PARAM_ERROR");
                break;

            default:
                NS_ERROR("CheckLDAPOperationResult(): lderrno set to "
                           "unexpected value after ldap_result() "
                           "call in nsLDAPConnection::Run()");
                PR_LOG(gLDAPLogModule, PR_LOG_ERROR, 
                       ("lderrno = 0x%x", lderrno));
                break;
            }
            break;

        case LDAP_RES_SEARCH_ENTRY:
        case LDAP_RES_SEARCH_REFERENCE:
            // XXX what should we do with LDAP_RES_SEARCH_EXTENDED?

            // not done yet, so we shouldn't remove the op from the conn q
            operationFinished = PR_FALSE;

            // fall through to default case

        default: // initialize the message and call the callback

            // we want nsLDAPMessage specifically, not a compatible, since
            // we're sharing native objects used by the LDAP C SDK
            //
            nsLDAPMessage *rawMsg;
            NS_NEWXPCOM(rawMsg, nsLDAPMessage);
            if (!rawMsg) {
            NS_ERROR("CheckLDAPOperationResult(): couldn't allocate memory"
                     " for new LDAP message; search entry dropped");
                // punt and hope things work out better next time around
                break;
            }

            // initialize the message, using a protected method not available
            // through nsILDAPMessage (which is why we need the raw pointer)
            //
        rv = rawMsg->Init(loop->mRawConn, msgHandle);

            switch (rv) {

            case NS_OK: 
                break;

            case NS_ERROR_LDAP_DECODING_ERROR:
                consoleSvc->LogStringMessage(
                    NS_LITERAL_STRING("LDAP: WARNING: decoding error; possible corrupt data received").get());
            NS_WARNING("CheckLDAPOperationResult(): ldaperrno = "
                           "LDAP_DECODING_ERROR after ldap_result()");
            return PR_TRUE;

            case NS_ERROR_OUT_OF_MEMORY:
                // punt and hope things work out better next time around
            return PR_TRUE;

            case NS_ERROR_ILLEGAL_VALUE:
            case NS_ERROR_UNEXPECTED:
            default:
                // shouldn't happen; internal error
                //
            NS_ERROR("CheckLDAPOperationResult(): nsLDAPMessage::Init() "
                           "returned unexpected value.");

                // punt and hope things work out better next time around
            return PR_TRUE;
            }

            // now let the scoping mechanisms provided by nsCOMPtr manage
            // the reference for us.
            //
            msg = rawMsg;

            // invoke the callback on the nsILDAPOperation corresponding to 
            // this message
            //
        rv = loop->mRawConn->InvokeMessageCallback(msgHandle, msg, 
                                                    operationFinished);
            if (NS_FAILED(rv)) {
            NS_ERROR("CheckLDAPOperationResult(): error invoking message"
                     " callback");
                // punt and hope things work out better next time around
            return PR_TRUE;
            }

            break;
        }       

    return PR_TRUE;
}

// for nsIRunnable.  this thread spins in ldap_result() awaiting the next
// message.  once one arrives, it dispatches it to the nsILDAPMessageListener 
// on the main thread.
//
// XXX do all returns from this function need to do thread cleanup?
//
NS_IMETHODIMP
nsLDAPConnectionLoop::Run(void)
{
    PR_LOG(gLDAPLogModule, PR_LOG_DEBUG, 
           ("nsLDAPConnection::Run() entered\n"));

    // wait for results
    //
    while(1) {

        // Exit this thread if we no longer have an nsLDAPConnection
        // associated with it. We also aquire a lock here, to make sure
        // to avoid a possible race condition when the nsLDAPConnection
        // is destructed during the call to do_QueryReferent() (since that
        // function isn't MT safe).
        //
        nsresult rv;

        PR_Lock(mLock);
        nsCOMPtr<nsILDAPConnection> strongConn = 
            do_QueryReferent(mWeakConn, &rv);
        PR_Unlock(mLock);

        if (NS_FAILED(rv)) {
            mWeakConn = 0;
            return NS_OK;
        }
        // we use a raw connection because we need to call non-interface
        // methods
        mRawConn = NS_STATIC_CAST(nsLDAPConnection *, 
                                  NS_STATIC_CAST(nsILDAPConnection *, 
                                                 strongConn.get()));

        // XXX deal with timeouts better
        //
        NS_ASSERTION(mRawConn->mConnectionHandle, "nsLDAPConnection::Run(): "
                     "no connection created.\n");

        // We can't enumerate over mPendingOperations itself, because the
        // callback needs to modify mPendingOperations.  So we clone it first,
        // and enumerate over the clone.  It kinda sucks that we need to do
        // this everytime we poll, but the hashtable will pretty much always
        // be small.
        //
        // only clone if the number of pending operations is non-zero
        // otherwise, put the LDAP connection thread to sleep (briefly)
        // until there is pending operations..
        if (mRawConn->mPendingOperations->Count()) {
          nsHashtable *hashtableCopy = mRawConn->mPendingOperations->Clone();
          if (hashtableCopy) {
            hashtableCopy->Enumerate(CheckLDAPOperationResult, this);
            delete hashtableCopy;
          } else {
            // punt and hope it works next time around
            NS_ERROR("nsLDAPConnectionLoop::Run() error cloning hashtable");
          }
        }
        else {
          PR_Sleep(PR_MillisecondsToInterval(40));
        }
    }

    // This will never happen, but here just in case.
    //
    return NS_OK;
}

//
// nsIDNSListener implementation, for asynchronous DNS. Once the lookup
// has finished, we will initialize the LDAP connection properly.
//
NS_IMETHODIMP
nsLDAPConnection::OnStartLookup(nsISupports *aContext, const char *aHostName)
{
    // Initialize some members which will be used in the other callbacks.
    //
    mDNSStatus = NS_OK;
    mResolvedIP = "";

    return NS_OK;
}

NS_IMETHODIMP
nsLDAPConnection::OnFound(nsISupports *aContext, 
                          const char* aHostName,
                          nsHostEnt *aHostEnt) 
{
    PRUint32 index = 0;
    PRNetAddr netAddress;
    char addrbuf[64];

    // Do we have a proper host entry? If not, set the internal DNS
    // status to indicate that host lookup failed.
    //
    if (!aHostEnt->hostEnt.h_addr_list || !aHostEnt->hostEnt.h_addr_list[0]) {
        mDNSStatus = NS_ERROR_UNKNOWN_HOST;

        return NS_ERROR_UNKNOWN_HOST;
    }
    
    // Make sure our address structure is initialized properly
    //
    memset(&netAddress, 0, sizeof(netAddress));
    PR_SetNetAddr(PR_IpAddrAny, PR_AF_INET6, 0, &netAddress);

    // Loop through the addresses, and add them to our IP string.
    //
    while (aHostEnt->hostEnt.h_addr_list[index]) {
        if (aHostEnt->hostEnt.h_addrtype == PR_AF_INET6) {
            memcpy(&netAddress.ipv6.ip, aHostEnt->hostEnt.h_addr_list[index],
                   sizeof(netAddress.ipv6.ip));
        } else {
            // Can this ever happen? Not sure, cause everything seems to be
            // IPv6 internally, even in the DNS service.
            //
            PR_ConvertIPv4AddrToIPv6(*(PRUint32*)aHostEnt->hostEnt.h_addr_list[0],
                                     &netAddress.ipv6.ip);
        }
        if (PR_IsNetAddrType(&netAddress, PR_IpAddrV4Mapped)) {
            // If there are more IPs in the list, we separate them with
            // a space, as supported/used by the LDAP C-SDK.
            //
            if (index)
                mResolvedIP.Append(' ');

            // Convert the IPv4 address to a string, and append it to our
            // list of IPs.
            //
            PR_NetAddrToString(&netAddress, addrbuf, sizeof(addrbuf));
            if ((addrbuf[0] == ':') && (strlen(addrbuf) > 7))
                mResolvedIP.Append(addrbuf+7);
            else
                mResolvedIP.Append(addrbuf);
        }
        index++;
    }

    return NS_OK;
}

NS_IMETHODIMP
nsLDAPConnection::OnStopLookup(nsISupports *aContext,
                               const char *aHostName,
                               nsresult aStatus)
{
    nsCOMPtr<nsILDAPMessageListener> selfProxy;
    nsresult rv = NS_OK;

    if (NS_FAILED(mDNSStatus)) {
        // We failed previously in the OnFound() callback
        //
        switch (mDNSStatus) {
        case NS_ERROR_UNKNOWN_HOST:
        case NS_ERROR_FAILURE:
            rv = mDNSStatus;
            break;

        default:
            rv = NS_ERROR_UNEXPECTED;
            break;
        }
    } else if (NS_FAILED(aStatus)) {
        // The DNS service failed , lets pass something reasonable
        // back to the listener.
        //
        switch (aStatus) {
        case NS_ERROR_OUT_OF_MEMORY:
        case NS_ERROR_UNKNOWN_HOST:
        case NS_ERROR_FAILURE:
        case NS_ERROR_OFFLINE:
            rv = aStatus;
            break;

        default:
            rv = NS_ERROR_UNEXPECTED;
            break;
        }
    } else if (!mResolvedIP.Length()) {
        // We have no host resolved, that is very bad, and should most
        // likely have been caught earlier.
        //
        NS_ERROR("nsLDAPConnection::OnStopLookup(): the resolved IP "
                 "string is empty.\n");
        
        rv = NS_ERROR_UNKNOWN_HOST;
    } else {
        // We've got the IP(s) for the hostname, now lets setup the
        // LDAP connection using this information. Note that if the
        // LDAP server returns a referral, the C-SDK will perform a
        // new, synchronous DNS lookup, which might hang (but hopefully
        // if we've come this far, DNS is working properly).
        //
        mConnectionHandle = ldap_init(mResolvedIP.get(),
                                      mPort == -1 ? LDAP_PORT : mPort);
        // Check that we got a proper connection, and if so, setup the
        // threading functions for this connection.
        //
        if ( !mConnectionHandle ) {
            rv = NS_ERROR_FAILURE;  // LDAP C SDK API gives no useful error
        } else {
#ifdef DEBUG_dmose
            const int lDebug = 0;
            ldap_set_option(mConnectionHandle, LDAP_OPT_DEBUG_LEVEL, &lDebug);
#endif
        }

#ifdef MOZ_PSM
        // This code sets up the current connection to use PSM for SSL
        // functionality.  Making this use libssldap instead for
        // non-browser user shouldn't be hard.

        extern nsresult nsLDAPInstallSSL(LDAP *ld, const char *aHostName);

        if (mSSL) {
            if (ldap_set_option(mConnectionHandle, LDAP_OPT_SSL, LDAP_OPT_ON)
                != LDAP_SUCCESS ) {
                NS_ERROR("nsLDAPConnection::OnStopLookup(): Error configuring"
                         " connection to use SSL");
                rv = NS_ERROR_UNEXPECTED;
            }

            rv = nsLDAPInstallSSL(mConnectionHandle, aHostName);
            if (NS_FAILED(rv)) {
                NS_ERROR("nsLDAPConnection::OnStopLookup(): Error installing"
                         " secure LDAP routines for connection");
            }
        }
#endif
        // Create a new runnable object, and increment the refcnt. The
        // thread will also hold a strong ref to the runnable, but we need
        // to make sure it doesn't get destructed until we are done with
        // all locking etc. in nsLDAPConnection::Release().
        //
        mRunnable = new nsLDAPConnectionLoop();
        NS_ADDREF(mRunnable);
        rv = mRunnable->Init();
        if (NS_FAILED(rv)) {
            rv = NS_ERROR_OUT_OF_MEMORY;
        } else {
            // Here we keep a weak reference in the runnable object to the
            // nsLDAPConnection ("this"). This avoids the problem where a
            // connection can't get destructed because of the new thread
            // keeping a strong reference to it. It also helps us know when
            // we need to exit the new thread: when we can't convert the weak
            // reference to a strong ref, we know that the nsLDAPConnection
            // object is gone, and we need to stop the thread running.
            //
            nsCOMPtr<nsILDAPConnection> conn =
                NS_STATIC_CAST(nsILDAPConnection *, this);

            mRunnable->mWeakConn = do_GetWeakReference(conn);

            // kick off a thread for result listening and marshalling
            // XXXdmose - should this be JOINABLE?
            //
            rv = NS_NewThread(getter_AddRefs(mThread), mRunnable, 0,
                          PR_UNJOINABLE_THREAD);
            if (NS_FAILED(rv)) {
                rv = NS_ERROR_NOT_AVAILABLE;
            }
        }
    }

    // Drop the DNS request object, we no longer need it, and set the flag
    // indicating that DNS has finished.
    //
    mDNSRequest = 0;
    mDNSFinished = PR_TRUE;

    // Call the listener, and then we can release our reference to it.
    //
    mInitListener->OnLDAPInit(this, rv);
    mInitListener = 0;

    return rv;
}
