/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: NPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Netscape 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/NPL/
 *
 * 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 mozilla.org code.
 *
 * The Initial Developer of the Original Code is 
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Darin Fisher <darin@netscape.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or 
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the NPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the NPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include <limits.h>

#include "nsFileTransport.h"
#include "nsFileTransportService.h"
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsAutoLock.h"
#include "netCore.h"
#include "nsIFileStreams.h"
#include "nsCOMPtr.h"
#include "nsReadableUtils.h"
#include "nsIProxyObjectManager.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"

static NS_DEFINE_CID(kProxyObjectManagerCID, NS_PROXYEVENT_MANAGER_CID);

#define NS_OUTPUT_STREAM_BUFFER_SIZE    (64 * 1024)

//////////////////////////////////////////////////////////////////////////////////

#if defined(PR_LOGGING)
static PRLogModuleInfo *gFileTransportLog = nsnull;
#endif

#define LOG(args) PR_LOG(gFileTransportLog, PR_LOG_DEBUG, args)

//////////////////////////////////////////////////////////////////////////////////

//
// An nsFileTransportSourceWrapper captures the number of bytes read from an
// input stream.
//
class nsFileTransportSourceWrapper : public nsIInputStream
{
public:
    NS_DECL_ISUPPORTS

    nsFileTransportSourceWrapper()
        : mBytesRead(0), mLastError(NS_OK) { }
    virtual ~nsFileTransportSourceWrapper() {}

    //
    // nsIInputStream implementation...
    //
    NS_IMETHOD Close() {
        return mSource->Close();
    }
    NS_IMETHOD Available(PRUint32 *aCount) {
        return mSource->Available(aCount);
    }
    NS_IMETHOD Read(char *aBuf, PRUint32 aCount, PRUint32 *aBytesRead) {
        nsresult rv = mSource->Read(aBuf, aCount, aBytesRead);
        if (NS_SUCCEEDED(rv))
            mBytesRead += *aBytesRead;
        mLastError = rv;
        return rv;
    }
    NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void *aClosure,
                            PRUint32 aCount, PRUint32 *aBytesRead) {
        nsresult rv = mSource->ReadSegments(aWriter, aClosure, aCount, aBytesRead);
        if (NS_SUCCEEDED(rv))
            mBytesRead += *aBytesRead;
        mLastError = rv;
        return rv;
    }
    NS_IMETHOD IsNonBlocking(PRBool *aValue) {
        return mSource->IsNonBlocking(aValue);
    }

    //
    // Helper functions
    //
    void SetSource(nsIInputStream *aSource) {
        mSource = aSource;
    }
    PRUint32 GetBytesRead() {
        return mBytesRead;
    }
    nsresult GetLastError() {
        return mLastError;
    }

protected:
    // 
    // State variables
    //
    PRUint32                 mBytesRead;
    nsCOMPtr<nsIInputStream> mSource;
    nsresult                 mLastError;
};

// This must be threadsafe since different threads can run the same transport
NS_IMPL_THREADSAFE_ISUPPORTS1(nsFileTransportSourceWrapper, nsIInputStream)

//////////////////////////////////////////////////////////////////////////////////

//
// An nsFileTransportSinkWrapper captures the number of bytes written to an
// output stream.
//
class nsFileTransportSinkWrapper : public nsIOutputStream
{
public:
    NS_DECL_ISUPPORTS

    nsFileTransportSinkWrapper()
        : mBytesWritten(0), mLastError(NS_OK) { }
    virtual ~nsFileTransportSinkWrapper() {}

    //
    // nsIInputStream implementation...
    //
    NS_IMETHOD Close() {
        return mSink->Close();
    }
    NS_IMETHOD Flush() {
        return mSink->Flush();
    }
    NS_IMETHOD Write(const char *aBuf, PRUint32 aCount, PRUint32 *aBytesWritten) {
        nsresult rv = mSink->Write(aBuf, aCount, aBytesWritten);
        if (NS_SUCCEEDED(rv))
            mBytesWritten += *aBytesWritten;
        mLastError = rv;
        return rv;
    }
    NS_IMETHOD WriteFrom(nsIInputStream *aSource, PRUint32 aCount, PRUint32 *aBytesWritten) {
        nsresult rv = mSink->WriteFrom(aSource, aCount, aBytesWritten);
        if (NS_SUCCEEDED(rv))
            mBytesWritten += *aBytesWritten;
        mLastError = rv;
        return rv;
    }
    NS_IMETHOD WriteSegments(nsReadSegmentFun aReader, void *aClosure,
                            PRUint32 aCount, PRUint32 *aBytesWritten) {
        nsresult rv = mSink->WriteSegments(aReader, aClosure, aCount, aBytesWritten);
        if (NS_SUCCEEDED(rv))
            mBytesWritten += *aBytesWritten;
        mLastError = rv;
        return rv;
    }
    NS_IMETHOD IsNonBlocking(PRBool *aValue) {
        return mSink->IsNonBlocking(aValue);
    }

    //
    // Helper functions
    //
    void SetSink(nsIOutputStream *aSink) {
        mSink = aSink;
    }
    PRUint32 GetBytesWritten() {
        return mBytesWritten;
    }
    nsresult GetLastError() {
        return mLastError;
    }

protected:
    // 
    // State variables
    //
    PRUint32                  mBytesWritten;
    nsCOMPtr<nsIOutputStream> mSink;
    nsresult                  mLastError;
};

// This must be threadsafe since different threads can run the same transport
NS_IMPL_THREADSAFE_ISUPPORTS1(nsFileTransportSinkWrapper, nsIOutputStream)

//////////////////////////////////////////////////////////////////////////////////

nsFileTransport::nsFileTransport()
    : mBufferSegmentSize(NS_FILE_TRANSPORT_DEFAULT_SEGMENT_SIZE),
      mBufferMaxSize(NS_FILE_TRANSPORT_DEFAULT_BUFFER_SIZE),
      mXferState(CLOSED),
      mRunState(RUNNING),
      mCancelStatus(NS_OK),
      mSuspendCount(0),
      mLock(nsnull),
      mActive(PR_FALSE),
      mCloseStreamWhenDone(PR_TRUE),
      mStatus(NS_OK),
      mOffset(0),
      mTotalAmount(-1),
      mTransferAmount(-1),
      mSourceWrapper(nsnull),
      mSinkWrapper(nsnull),
      mService(nsnull)
{
#if defined(PR_LOGGING)
    if (!gFileTransportLog)
        gFileTransportLog = PR_NewLogModule("nsFileTransport");
#endif
}

nsresult
nsFileTransport::Init(nsFileTransportService *aService,
                      nsIFile *file,
                      PRInt32 ioFlags,
                      PRInt32 perm,
                      PRBool closeStreamWhenDone)
{
    nsresult rv;
    nsCOMPtr<nsIFileIO> io;
    rv = NS_NewFileIO(getter_AddRefs(io), file, ioFlags, perm);
    if (NS_FAILED(rv)) return rv;

    return Init(aService, io, closeStreamWhenDone);
}

nsresult
nsFileTransport::Init(nsFileTransportService *aService,
                      const nsACString &name,
                      nsIInputStream* inStr,
                      const nsACString &contentType,
                      const nsACString &contentCharset,
                      PRInt32 contentLength,
                      PRBool closeStreamWhenDone)
{
    nsresult rv;
    nsCOMPtr<nsIInputStreamIO> io;
    rv = NS_NewInputStreamIO(getter_AddRefs(io), name, inStr,
                             contentType, contentCharset, contentLength);
    if (NS_FAILED(rv)) return rv;
    return Init(aService, io, closeStreamWhenDone);
}

nsresult
nsFileTransport::Init(nsFileTransportService *aService,
                      nsIStreamIO* io,
                      PRBool closeStreamWhenDone)
{
    nsresult rv = NS_OK;
    if (mLock == nsnull) {
        mLock = PR_NewLock();
        if (mLock == nsnull)
            return NS_ERROR_OUT_OF_MEMORY;
    }
    mStreamIO = io;
    rv = mStreamIO->GetName(mStreamName);
    NS_ASSERTION(NS_SUCCEEDED(rv), "GetName failed");

    mCloseStreamWhenDone = closeStreamWhenDone;

    NS_ADDREF(mService = aService);
    PR_AtomicIncrement(&mService->mTotalTransports);

    return rv;
}

nsFileTransport::~nsFileTransport()
{
    if (mXferState != CLOSED)
        DoClose();

    NS_ASSERTION(mSourceWrapper == nsnull, "transport not closed");
    NS_ASSERTION(mSink == nsnull, "transport not closed");
    NS_ASSERTION(mSinkWrapper == nsnull, "transport not closed");

    if (mLock) {
        PR_DestroyLock(mLock);
        mLock = nsnull;
    }
    NS_IF_RELEASE(mService);
}

NS_IMPL_THREADSAFE_ISUPPORTS4(nsFileTransport, 
                         nsITransport, 
                         nsITransportRequest,
                         nsIRequest, 
                         nsIRunnable)

NS_METHOD
nsFileTransport::Create(nsISupports* aOuter, const nsIID& aIID, void* *aResult)
{
    nsFileTransport* fc = new nsFileTransport();
    if (fc == nsnull)
        return NS_ERROR_OUT_OF_MEMORY;
    NS_ADDREF(fc);
    nsresult rv = fc->QueryInterface(aIID, aResult);
    NS_RELEASE(fc);
    return rv;
}

////////////////////////////////////////////////////////////////////////////////
// From nsIRequest
////////////////////////////////////////////////////////////////////////////////

NS_IMETHODIMP
nsFileTransport::GetName(nsACString &result)
{
    result = mStreamName;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::IsPending(PRBool *result)
{
    *result = mXferState != CLOSED;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::GetStatus(nsresult *status)
{
    *status = mRunState == CANCELED ? mCancelStatus : mStatus;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::Cancel(nsresult status)
{
    nsresult rv = NS_OK;

    nsAutoLock lock(mLock);
    NS_ASSERTION(NS_FAILED(status), "shouldn't cancel with a success code");

    mCancelStatus = status;

    // Only dispatch a new thread, if there isn't one currently active.
    if (!mActive) {
#ifdef TIMING
        mStartTime = PR_IntervalNow();
#endif
        rv = mService->DispatchRequest(this);
    }

    LOG(("nsFileTransport: Cancel [this=%x %s]\n", this, mStreamName.get()));
    return rv;
}

NS_IMETHODIMP
nsFileTransport::Suspend()
{
    nsAutoLock lock(mLock);
    if (mRunState != CANCELED) {
        LOG(("nsFileTransport: Suspend [this=%x %s]\n", this, mStreamName.get()));
        PR_AtomicIncrement(&mSuspendCount);
        mService->AddSuspendedTransport(this);
    }
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::Resume()
{
    nsAutoLock lock(mLock);
    if (mRunState != CANCELED) {
        LOG(("nsFileTransport: Resume [this=%x %s]\n", this, mStreamName.get()));
        // Allow negative suspend count
        PR_AtomicDecrement(&mSuspendCount);
        mService->RemoveSuspendedTransport(this);
        // Only dispatch a new thread, if there isn't one currently active.
        if (!mActive && (mSuspendCount == 0)) {
            mRunState = RUNNING;
#ifdef TIMING
            mStartTime = PR_IntervalNow();
#endif
            mStatus = mService->DispatchRequest(this);
        }
    }
    else
        LOG(("nsFileTransport: Resume ignored [this=%x %s] status=%x cancelstatus=%x\n",
            this, mStreamName.get(), mStatus, mCancelStatus));
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::GetLoadGroup(nsILoadGroup **loadGroup)
{
    *loadGroup = nsnull;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::SetLoadGroup(nsILoadGroup *loadGroup)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
nsFileTransport::GetLoadFlags(nsLoadFlags *loadFlags)
{
    *loadFlags = nsIRequest::LOAD_NORMAL;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::SetLoadFlags(nsLoadFlags loadFlags)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

////////////////////////////////////////////////////////////////////////////////
// From nsITransportRequest
////////////////////////////////////////////////////////////////////////////////

NS_IMETHODIMP
nsFileTransport::GetTransport(nsITransport **result)
{
    NS_ENSURE_ARG_POINTER(result);
    NS_ADDREF(*result = this);
    return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// From nsITransport
////////////////////////////////////////////////////////////////////////////////

NS_IMETHODIMP
nsFileTransport::OpenInputStream(PRUint32 aTransferOffset,
                                 PRUint32 aTransferCount,
                                 PRUint32 aFlags,
                                 nsIInputStream **aResult)
{
    NS_ENSURE_ARG_POINTER(aResult);
    NS_ASSERTION(aTransferCount == PRUint32(-1), "need to wrap input stream in one that truncates");
    
    nsresult rv = mStreamIO->GetInputStream(aResult);
    if (NS_SUCCEEDED(rv) && aTransferOffset) {
        nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(*aResult, &rv));
        if (NS_SUCCEEDED(rv) && seekable) {
            rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, aTransferOffset);
        }
    }
    
    return rv;
}

NS_IMETHODIMP
nsFileTransport::OpenOutputStream(PRUint32 aTransferOffset,
                                  PRUint32 aTransferCount,
                                  PRUint32 aFlags,
                                  nsIOutputStream **aResult)
{
    NS_ENSURE_ARG_POINTER(aResult);
    NS_ASSERTION(aTransferCount == PRUint32(-1), "need to wrap output stream in one that truncates");
    
    nsresult rv = mStreamIO->GetOutputStream(aResult);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(*aResult, &rv));
    if (seekable) {
        if (aTransferOffset)
            rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, aTransferOffset);
        if (NS_SUCCEEDED(rv))
            rv = seekable->SetEOF();
    }
    // a stream need not support nsISeekableStream unless a transfer offset
    // greater than zero was requested.
    else if (aTransferOffset == 0)
        rv = NS_OK;
    
    return rv;
}

NS_IMETHODIMP
nsFileTransport::AsyncRead(nsIStreamListener *aListener,
                           nsISupports *aContext,
                           PRUint32 aTransferOffset,
                           PRUint32 aTransferCount,
                           PRUint32 aFlags,
                           nsIRequest **aResult)
{
    NS_ENSURE_ARG_POINTER(aResult);
    nsresult rv = NS_OK;

    if (mXferState != CLOSED)
        return NS_ERROR_IN_PROGRESS;

    NS_ENSURE_TRUE(aListener, NS_ERROR_INVALID_ARG);

    rv = NS_NewStreamListenerProxy(getter_AddRefs(mListener),
                                   aListener, nsnull,
                                   mBufferSegmentSize,
                                   mBufferMaxSize);
    if (NS_FAILED(rv)) return rv;
    NS_ASSERTION(mContext == nsnull, "context not released");
    mContext = aContext;
    mOffset = aTransferOffset;
    mTransferAmount = aTransferCount;
    mXferState = OPEN_FOR_READ;

    nsIEventQueueService* eqService = mService->GetCachedEventQueueService(); 
    eqService->GetSpecialEventQueue(nsIEventQueueService::CURRENT_THREAD_EVENT_QUEUE, getter_AddRefs(mEventQ));
    NS_ASSERTION(mEventQ, "No Event Queue on calling thread");

    LOG(("nsFileTransport: AsyncRead [this=%x %s] mOffset=%d mTransferAmount=%d\n",
        this, mStreamName.get(), mOffset, mTransferAmount));

#ifdef TIMING
    mStartTime = PR_IntervalNow();
#endif
    rv = mService->DispatchRequest(this);
    if (NS_FAILED(rv)) return rv;

    NS_ADDREF(*aResult = this);
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::AsyncWrite(nsIStreamProvider *aProvider,
                            nsISupports *aContext,
                            PRUint32 aTransferOffset,
                            PRUint32 aTransferCount,
                            PRUint32 aFlags,
                            nsIRequest **aResult)
{
    NS_ENSURE_ARG_POINTER(aResult);
    nsresult rv = NS_OK;

    LOG(("nsFileTransport: AsyncWrite [this=%x, provider=%x]\n",
        this, aProvider));

    if (mXferState != CLOSED)
        return NS_ERROR_IN_PROGRESS;

    NS_ENSURE_TRUE(aProvider, NS_ERROR_INVALID_ARG);

    rv = NS_NewStreamProviderProxy(getter_AddRefs(mProvider),
                                   aProvider, nsnull,
                                   mBufferSegmentSize,
                                   mBufferMaxSize);
    if (NS_FAILED(rv)) return rv;

    NS_ASSERTION(mContext == nsnull, "context not released");
    mContext = aContext;
    mOffset = aTransferOffset;
    mTransferAmount = aTransferCount;
    mXferState = OPEN_FOR_WRITE;

    nsIEventQueueService* eqService = mService->GetCachedEventQueueService(); 
    eqService->GetSpecialEventQueue(nsIEventQueueService::CURRENT_THREAD_EVENT_QUEUE, getter_AddRefs(mEventQ));
    NS_ASSERTION(mEventQ, "No Event Queue on calling thread");

    LOG(("nsFileTransport: AsyncWrite [this=%x %s] mOffset=%d mTransferAmount=%d\n",
        this, mStreamName.get(), mOffset, mTransferAmount));

#ifdef TIMING
    mStartTime = PR_IntervalNow();
#endif
    rv = mService->DispatchRequest(this);
    if (NS_FAILED(rv)) return rv;

    NS_ADDREF(*aResult = this);
    return NS_OK;
}
    
////////////////////////////////////////////////////////////////////////////////
// nsIRunnable methods:
////////////////////////////////////////////////////////////////////////////////

NS_IMETHODIMP
nsFileTransport::Run(void)
{
    PR_Lock(mLock);
    mActive = PR_TRUE;

    LOG(("nsFileTransport: Inside Run\n"));

#ifdef TIMING
    PRIntervalTime now = PR_IntervalNow();
#ifdef DEBUG
    printf("nsFileTransport: latency=%u ticks\n", now - mStartTime);
#endif
#endif

    if (mRunState == SUSPENDED && NS_FAILED(mCancelStatus)) {
        mRunState = CANCELED;
        mService->RemoveSuspendedTransport(this);
    }

    while (mXferState != CLOSED && mRunState != SUSPENDED) {
        //
        // Change transfer state if canceled.
        //
        if (mRunState == CANCELED) {
            if (mXferState == OPEN_FOR_READ ||
                mXferState == START_READ ||
                mXferState == READING ||
                mXferState == END_READ)
                mXferState = END_READ;
            else if (mXferState == OPEN_FOR_WRITE ||
                     mXferState == START_WRITE ||
                     mXferState == WRITING ||
                     mXferState == END_WRITE)
                mXferState = END_WRITE;
            else
                mXferState = CLOSING;
            mStatus = mCancelStatus;
        }
        // Grab a local reference to mProgressSink
        nsCOMPtr<nsIProgressEventSink> progressSink(mProgressSink);
        //
        // While processing, we allow Suspend, Resume, and Cancel.
        //
        PR_Unlock(mLock);
        Process(progressSink);
        PR_Lock(mLock);

        //
        // Were we canceled ?
        //
        if (NS_FAILED(mCancelStatus))
            mRunState = CANCELED;
        //
        // Were we suspended ?
        //
        else if (mSuspendCount > 0) {
            mRunState = SUSPENDED;
            mService->AddSuspendedTransport(this);
        }
    }

    LOG(("nsFileTransport: Leaving Run [xferState=%d runState=%d]\n",
        mXferState, mRunState));

    mActive = PR_FALSE;
    PR_Unlock(mLock);
    return NS_OK;
}

void
nsFileTransport::Process(nsIProgressEventSink *progressSink)
{
    LOG(("nsFileTransport: Inside Process [this=%x state=%x status=%x]\n",
        this, mXferState, mStatus));

    switch (mXferState) {
      case OPEN_FOR_READ: { 
        mStatus = mStreamIO->Open();
        if (NS_SUCCEEDED(mStatus))
            mStreamIO->GetContentLength(&mTotalAmount);
        LOG(("nsFileTransport: OPEN_FOR_READ [this=%x %s] status=%x\n", this, mStreamName.get(), mStatus));
        if (mListener) {
            nsresult rv = mListener->OnStartRequest(this, mContext);  // always send the start notification
            if (NS_SUCCEEDED(mStatus))
                mStatus = rv;
        }

        PR_AtomicIncrement(&mService->mInUseTransports);

        mXferState = NS_FAILED(mStatus) ? END_READ : START_READ;
        break;
      }

      case START_READ: {
        LOG(("nsFileTransport: START_READ [this=%x %s]\n", this, mStreamName.get()));

        nsCOMPtr<nsIInputStream> source;
        mStatus = mStreamIO->GetInputStream(getter_AddRefs(source));
        if (NS_FAILED(mStatus)) {
            LOG(("nsFileTransport: mStreamIO->GetInputStream() failed [this=%x rv=%x]\n",
                this, mStatus));
            mXferState = END_READ;
            return;
        }

        if (mOffset > 0) {
            // if we need to set a starting offset, QI for the nsISeekableStream
            // and set it
            nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(source, &mStatus);
            if (NS_SUCCEEDED(mStatus)) {
                // for now, assume the offset is always relative to the start of the
                // file (position 0) so use PR_SEEK_SET
                mStatus = seekable->Seek(PR_SEEK_SET, mOffset);
            }
            if (NS_FAILED(mStatus)) {
                mXferState = END_READ;
                return;
            }
        }

        if (!mSourceWrapper) {
            //
            // Allocate an input stream wrapper to capture the number of bytes
            // read from source.
            //
            NS_NEWXPCOM(mSourceWrapper, nsFileTransportSourceWrapper);
            if (!mSourceWrapper) {
                mStatus = NS_ERROR_OUT_OF_MEMORY;
                mXferState = END_READ;
                return;
            }
            NS_ADDREF(mSourceWrapper);
            mSourceWrapper->SetSource(source);
        }

        // capture the total amount for progress information
        if (mTransferAmount < 0) {
            mTransferAmount = mTotalAmount;
        } else
            mTotalAmount = mTransferAmount;

        mXferState = READING;
        break;
      }

      case READING: {
        // Read at most mBufferMaxSize.
        PRInt32 transferAmt = mBufferMaxSize;
        if (mTransferAmount >= 0)
            transferAmt = PR_MIN(transferAmt, mTransferAmount);

        LOG(("nsFileTransport: READING [this=%x %s] transferAmt=%u mBufferMaxSize=%u\n",
            this, mStreamName.get(), transferAmt, mBufferMaxSize));

        PRUint32 total = 0, offset = mSourceWrapper->GetBytesRead();

        // Give the listener a chance to read at most transferAmt bytes from
        // the source input stream.
        nsresult status = NS_ERROR_NOT_INITIALIZED;
        if (mListener)
            status = mListener->OnDataAvailable(this, 
                                                mContext,
                                                mSourceWrapper,
                                                mOffset, 
                                                transferAmt);
        
        if (NS_SUCCEEDED(status)) {
            // Find out what was read.
            total = mSourceWrapper->GetBytesRead() - offset;
            LOG(("status = %x total = %d\n", status, total));
            if (total == 0) {
                status = mSourceWrapper->GetLastError();
                if (NS_SUCCEEDED(status)) { // eof
                    LOG(("eof\n"));
                    status = NS_BASE_STREAM_CLOSED;
                }
            }
            else if (mTransferAmount > 0) {
                mTransferAmount -= total;
                if (mTransferAmount == 0) {
                    LOG(("mTransferAmount == 0\n"));
                    status = NS_BASE_STREAM_CLOSED;
                }
            }
        }

        // Handle the various return codes.
        if (status == NS_BASE_STREAM_WOULD_BLOCK) {
            LOG(("nsFileTransport: READING [this=%x %s] listener would block; suspending self.\n",
                this, mStreamName.get()));
            mStatus = NS_OK;
            PR_AtomicIncrement(&mSuspendCount);
        }
        else if (status == NS_BASE_STREAM_CLOSED) {
            LOG(("nsFileTransport: READING [this=%x %s] done reading file.\n",
                this, mStreamName.get()));
            mStatus = NS_OK;
            mXferState = END_READ;
        }
        else if (NS_FAILED(status)) {
            LOG(("nsFileTransport: READING [this=%x %s] error reading file.\n",
                this, mStreamName.get()));
            mStatus = status;
            mXferState = END_READ;
        }
        if (total) {
            // something was read...
            offset += total;
            mOffset += total;

            LOG(("nsFileTransport: READING [this=%x %s] read %u bytes [offset=%u]\n",
                this, mStreamName.get(), total, mOffset));

            if (progressSink)
                progressSink->OnProgress(this, mContext, offset,
                                         PR_MAX(mTotalAmount, 0));
        }
        break;
      }

      case END_READ: {
        
        LOG(("nsFileTransport: END_READ [this=%x %s] status=%x\n",
            this, mStreamName.get(), mStatus));

        PR_AtomicDecrement(&mService->mInUseTransports);

#if DEBUG
        if (NS_FAILED(mStatus))
          printf("Error reading file %s\n", mStreamName.get());
#endif
                               
#if defined (DEBUG_dougt) || defined (DEBUG_warren) || defined (DEBUG_bienvenu)
        NS_ASSERTION(mTransferAmount <= 0 || NS_FAILED(mStatus), "didn't transfer all the data");
#endif 
        if (mTransferAmount > 0 && NS_SUCCEEDED(mStatus)) {
            //
            // This happens when the requested read amount is more than the amount
            // of the data in the stream/file, or if the listener returned 
            // NS_BASE_STREAM_CLOSED.
            //
            mStatus = NS_BASE_STREAM_CLOSED;
        }

        // need to close before calling OnStopRequest, in case the listener
        // is reusing the stream.
        mXferState = CLOSING;
        DoClose();

        // close the data source
        NS_IF_RELEASE(mSourceWrapper);
        mSourceWrapper = nsnull;

        if (progressSink) {
            progressSink->OnStatus(this, 
                                   mContext, 
                                   NS_NET_STATUS_READ_FROM, 
                                   NS_ConvertASCIItoUCS2(mStreamName).get());
        }

        // see http://bugzilla.mozilla.org/show_bug.cgi?id=139556#c64
        // for the reason behind this evil reference counting.
        nsISupports* doomed = mContext.get();
        NS_IF_ADDREF(doomed);
        mContext = 0;
        if (mListener) {
            nsCOMPtr<nsIStreamListener> listener  = do_QueryInterface(mListener);
            mListener = 0;
            listener->OnStopRequest(this, doomed, mStatus);
        }

        // if we have a context, we have to ensure that it is released on the 
        // proper thread.
        if (doomed) {
            if (mEventQ) 
                NS_ProxyRelease(mEventQ, doomed);
            else
                NS_IF_RELEASE(doomed);
        }
        break;
      }

      case OPEN_FOR_WRITE: {
        LOG(("nsFileTransport: OPEN_FOR_WRITE [this=%x %s]\n",
            this, mStreamName.get()));
        mStatus = mStreamIO->Open();
        if (NS_SUCCEEDED(mStatus)) // XXX why do we care about this when writing?
            mStatus = mStreamIO->GetContentLength(&mTotalAmount);
        
        if (mStatus == NS_ERROR_FILE_NOT_FOUND)
            mStatus = NS_OK;

        if (mProvider) {
            // always send the start notification
            nsresult rv = mProvider->OnStartRequest(this, mContext);
            if (NS_SUCCEEDED(mStatus))
                mStatus = rv;
        }

        PR_AtomicIncrement(&mService->mInUseTransports);

        mXferState = NS_FAILED(mStatus) ? END_WRITE : START_WRITE;
        break;
      }

      case START_WRITE: {
        LOG(("nsFileTransport: START_WRITE [this=%x %s]\n",
            this, mStreamName.get()));

        mStatus = mStreamIO->GetOutputStream(getter_AddRefs(mSink));
        if (NS_FAILED(mStatus)) {
            mXferState = END_WRITE;
            return;
        }

        // QI to a seekable stream so we can set EOF and Seek (if required)
        nsCOMPtr<nsISeekableStream> seekable(do_QueryInterface(mSink, &mStatus));
        if (seekable) {
            if (mOffset)
                mStatus = seekable->Seek(nsISeekableStream::NS_SEEK_SET, mOffset);
            if (NS_SUCCEEDED(mStatus))
                mStatus = seekable->SetEOF();
        }
        // a stream need not support nsISeekableStream unless a transfer offset
        // greater than zero was requested.
        else if (mOffset == 0)
            mStatus = NS_OK;

        if (NS_FAILED(mStatus)) {
            mXferState = END_WRITE;
            return;
        }
        mOffset = 0;

        if (!mSinkWrapper) {
            //
            // Allocate an output stream wrapper to capture the number of bytes
            // written to mSink.
            //
            NS_NEWXPCOM(mSinkWrapper, nsFileTransportSinkWrapper);
            if (!mSinkWrapper) {
                mStatus = NS_ERROR_OUT_OF_MEMORY;
                mXferState = END_WRITE;
                return;
            }
            NS_ADDREF(mSinkWrapper);
            mSinkWrapper->SetSink(mSink);
        }

        mXferState = WRITING;
        break;
      }

      case WRITING: {
        // Write at most mBufferMaxSize
        PRUint32 transferAmt = mBufferMaxSize;
        if (mTransferAmount >= 0)
            transferAmt = PR_MIN(transferAmt, (PRUint32)mTransferAmount);

        PRUint32 total = 0, offset = mSinkWrapper->GetBytesWritten();

        // Ask the provider for data
        nsresult status = NS_ERROR_NOT_INITIALIZED;
        if (mProvider)
            status = mProvider->OnDataWritable(this, 
                                               mContext,
                                               mSinkWrapper,
                                               mOffset, 
                                               transferAmt);

        if (NS_SUCCEEDED(status)) {
            // Find out what was written.
            total = mSinkWrapper->GetBytesWritten() - offset;
            if (total == 0) {
                status = mSinkWrapper->GetLastError();
                if (NS_SUCCEEDED(status)) // eof
                    status = NS_BASE_STREAM_CLOSED;
            }
            else if (mTransferAmount > 0) {
                mTransferAmount -= total;
                if (mTransferAmount == 0)
                    status = NS_BASE_STREAM_CLOSED;
            }
        }
        
        // Handle the various return codes.
        if (status == NS_BASE_STREAM_WOULD_BLOCK) {
            LOG(("nsFileTransport: WRITING [this=%x %s] provider would block; suspending self.\n",
                this, mStreamName.get()));
            mStatus = NS_OK;
            PR_AtomicIncrement(&mSuspendCount);
        }
        else if (status == NS_BASE_STREAM_CLOSED) {
            LOG(("nsFileTransport: WRITING [this=%x %s] no more data to be written.\n",
                this, mStreamName.get()));
            mStatus = NS_OK;
            mXferState = END_WRITE;
        }
        else if (NS_FAILED(status)) {
            LOG(("nsFileTransport: WRITING [this=%x %s] provider failed.\n",
                this, mStreamName.get()));
            mStatus = status;
            mXferState = END_WRITE;
        }
        else {
            // something was written...
            offset += total;
            mOffset += total;

            LOG(("nsFileTransport: WRITING [this=%x %s] wrote %u bytes [offset=%u]\n",
                this, mStreamName.get(), total, mOffset));

            if (progressSink)
                progressSink->OnProgress(this, mContext, offset,
                                         PR_MAX(mTotalAmount, 0));
        }
        break;
      }

      case END_WRITE: {
        LOG(("nsFileTransport: END_WRITE [this=%x %s] status=%x\n",
            this, mStreamName.get(), mStatus));

        PR_AtomicDecrement(&mService->mInUseTransports);

#if defined (DEBUG_dougt) || defined (DEBUG_warren)
        NS_ASSERTION(mTransferAmount <= 0 || NS_FAILED(mStatus), "didn't transfer all the data");
#endif 
        if (mTransferAmount > 0 && NS_SUCCEEDED(mStatus)) {
            // This happens when the requested write amount is more than the amount
            // of the data in the stream/file.
            mStatus = NS_BASE_STREAM_CLOSED;
        }

        if (mSink) {
            mSink->Flush();
            mSink = 0;
        }
        NS_IF_RELEASE(mSinkWrapper);
        mSinkWrapper = nsnull;

        if (progressSink)
            progressSink->OnStatus(this, mContext,
                                   NS_NET_STATUS_WROTE_TO, 
                                   NS_ConvertASCIItoUCS2(mStreamName).get());

        // see http://bugzilla.mozilla.org/show_bug.cgi?id=139556#c64
        // for the reason behind this evil reference counting.
        nsISupports* doomed = mContext.get();
        NS_IF_ADDREF(doomed);
        mContext = 0;
        if (mProvider) {
            nsCOMPtr <nsIStreamProvider> provider = do_QueryInterface(mProvider);
            mProvider = 0;
            provider->OnStopRequest(this, doomed, mStatus);
        }

        // if we have a context, we have to ensure that it is released on the
        // proper thread.
        if (doomed) {
            if (mEventQ)
                NS_ProxyRelease(mEventQ, doomed);
            else
                NS_RELEASE(doomed);
        }
        mXferState = CLOSING;
        break;
      }

      case CLOSING: {
        DoClose();
        break;
      }

      case CLOSED: {
        NS_NOTREACHED("trying to continue a quiescent file transfer");
        break;
      }
    }

    LOG(("nsFileTransport: Leaving Process [this=%x state=%x status=%x]\n",
        this, mXferState, mStatus));
}

void
nsFileTransport::DoClose(void)
{
    if (mCloseStreamWhenDone)
    {
        LOG(("nsFileTransport: CLOSING [this=%x %s] status=%x\n",
            this, mStreamName.get(), mStatus));

      // XXX closing the stream io prevents this file transport from being reused
        if (mStreamIO) {
            nsresult rv = NS_OK;
            rv = mStreamIO->Close(mStatus);
            NS_ASSERTION(NS_SUCCEEDED(rv), "unexpected Close failure");
            mStreamIO = 0;
        }
    }
    mXferState = CLOSED;

    PR_AtomicDecrement(&mService->mConnectedTransports);
}

NS_IMETHODIMP 
nsFileTransport::GetSecurityInfo(nsISupports * *aSecurityInfo)
{
    *aSecurityInfo = nsnull;
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::GetNotificationCallbacks(nsIInterfaceRequestor **aCallbacks)
{
    NS_ENSURE_ARG_POINTER(aCallbacks);
    *aCallbacks = mNotificationCallbacks;
    NS_IF_ADDREF(*aCallbacks);
    return NS_OK;
}

NS_IMETHODIMP
nsFileTransport::SetNotificationCallbacks(nsIInterfaceRequestor *aCallbacks,
                                          PRUint32               aFlags)
{
    nsAutoLock lock(mLock);

    mNotificationCallbacks = aCallbacks;
    mProgressSink = 0;

    if (!mNotificationCallbacks || (aFlags & DONT_REPORT_PROGRESS))
        return NS_OK;

    nsCOMPtr<nsIProgressEventSink> sink(do_GetInterface(mNotificationCallbacks));
    if (!sink) return NS_OK;

    if (aFlags & DONT_PROXY_PROGRESS) {
        mProgressSink = sink;
        return NS_OK;
    }

    // Otherwise, generate a proxied event sink
    nsresult rv;
    nsCOMPtr<nsIProxyObjectManager> proxyMgr = 
             do_GetService(kProxyObjectManagerCID, &rv);
    if (NS_FAILED(rv)) return rv;
        
    return proxyMgr->GetProxyForObject(NS_CURRENT_EVENTQ, // calling thread
                                       NS_GET_IID(nsIProgressEventSink),
                                       sink,
                                       PROXY_ASYNC | PROXY_ALWAYS,
                                       getter_AddRefs(mProgressSink));
}

////////////////////////////////////////////////////////////////////////////////
