/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "TestCommon.h"
#include "nsIComponentRegistrar.h"
#include "nsIStreamTransportService.h"
#include "nsIAsyncInputStream.h"
#include "nsIProgressEventSink.h"
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIRequest.h"
#include "nsIServiceManager.h"
#include "nsIComponentManager.h"
#include "nsCOMPtr.h"
#include "nsMemory.h"
#include "nsStringAPI.h"
#include "nsIFileStreams.h"
#include "nsIStreamListener.h"
#include "nsIFile.h"
#include "nsNetUtil.h"
#include "nsAutoLock.h"
#include "mozilla/Logging.h"
#include "prenv.h"

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

//
// set NSPR_LOG_MODULES=Test:5
//
static PRLogModuleInfo *gTestLog = nullptr;
#define LOG(args) MOZ_LOG(gTestLog, mozilla::LogLevel::Debug, args)

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

static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID);

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

#define CHUNK_SIZE 500

class MyCopier : public nsIInputStreamCallback
               , public nsIOutputStreamCallback
{
public:
    NS_DECL_THREADSAFE_ISUPPORTS

    MyCopier()
        : mLock(nullptr)
        , mInputCondition(NS_OK)
    {
    }

    virtual ~MyCopier()
    {
        if (mLock)
            nsAutoLock::DestroyLock(mLock);
        if (mInput)
            mInput->Close();
        if (mOutput)
            mOutput->Close();
    }

    // called on any thread
    NS_IMETHOD OnInputStreamReady(nsIAsyncInputStream *inStr)
    {
        LOG(("OnInputStreamReady\n"));
        nsAutoLock lock(mLock);
        NS_ASSERTION(inStr == mInput, "unexpected stream");
        Process_Locked();
        return NS_OK;
    }

    // called on any thread
    NS_IMETHOD OnOutputStreamReady(nsIAsyncOutputStream *outStr)
    {
        LOG(("OnOutputStreamReady\n"));
        nsAutoLock lock(mLock);
        NS_ASSERTION(outStr == mOutput, "unexpected stream");
        Process_Locked();
        return NS_OK;
    }

    void Close_Locked()
    {
        LOG(("Close_Locked\n"));

        mOutput->Close();
        mOutput = 0;
        mInput->Close();
        mInput = 0;

        // post done copying event
        QuitPumpingEvents();
    }

    void Process_Locked()
    {
        while (1) {
            mInputCondition = NS_OK; // reset

            uint32_t n;
            nsresult rv = mOutput->WriteSegments(FillOutputBuffer, this, CHUNK_SIZE, &n);
            if (NS_FAILED(rv) || (n == 0)) {
                if (rv == NS_BASE_STREAM_WOULD_BLOCK)
                    mOutput->AsyncWait(this, 0, 0, nullptr);
                else if (mInputCondition == NS_BASE_STREAM_WOULD_BLOCK)
                    mInput->AsyncWait(this, 0, 0, nullptr);
                else
                    Close_Locked();
                break;
            }
        }
    }

    nsresult AsyncCopy(nsITransport *srcTrans, nsITransport *destTrans)
    {
        mLock = nsAutoLock::NewLock("MyCopier::mLock");
        if (!mLock)
            return NS_ERROR_OUT_OF_MEMORY;

        nsresult rv;

        nsCOMPtr<nsIInputStream> inStr;
        rv = srcTrans->OpenInputStream(0, 0, 0, getter_AddRefs(inStr));
        if (NS_FAILED(rv)) return rv;

        nsCOMPtr<nsIOutputStream> outStr;
        rv = destTrans->OpenOutputStream(0, 0, 0, getter_AddRefs(outStr));
        if (NS_FAILED(rv)) return rv;

        mInput = do_QueryInterface(inStr);
        mOutput = do_QueryInterface(outStr);

        return mInput->AsyncWait(this, 0, 0, nullptr);
    }

    static nsresult FillOutputBuffer(nsIOutputStream *outStr,
                                     void *closure,
                                     char *buffer,
                                     uint32_t offset,
                                     uint32_t count,
                                     uint32_t *countRead)
    {
        MyCopier *self = (MyCopier *) closure;

        nsresult rv = self->mInput->Read(buffer, count, countRead);
        if (NS_FAILED(rv))
            self->mInputCondition = rv;
        else if (*countRead == 0)
            self->mInputCondition = NS_BASE_STREAM_CLOSED;

        return self->mInputCondition;
    }

protected:
    PRLock                        *mLock;
    nsCOMPtr<nsIAsyncInputStream>  mInput;
    nsCOMPtr<nsIAsyncOutputStream> mOutput;
    nsresult                       mInputCondition;
};

NS_IMPL_ISUPPORTS(MyCopier,
                  nsIInputStreamCallback,
                  nsIOutputStreamCallback)

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

/**
 * asynchronously copy file.
 */
static nsresult
RunTest(nsIFile *srcFile, nsIFile *destFile)
{
    nsresult rv;

    LOG(("RunTest\n"));

    nsCOMPtr<nsIStreamTransportService> sts =
        do_GetService(kStreamTransportServiceCID, &rv);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIInputStream> srcStr;
    rv = NS_NewLocalFileInputStream(getter_AddRefs(srcStr), srcFile);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIOutputStream> destStr;
    rv = NS_NewLocalFileOutputStream(getter_AddRefs(destStr), destFile);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsITransport> srcTransport;
    rv = sts->CreateInputTransport(srcStr, int64_t(-1), int64_t(-1), true,
                                   getter_AddRefs(srcTransport));
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsITransport> destTransport;
    rv = sts->CreateOutputTransport(destStr, int64_t(-1), int64_t(-1), true,
                                    getter_AddRefs(destTransport));
    if (NS_FAILED(rv)) return rv;

    MyCopier *copier = new MyCopier();
    if (copier == nullptr)
        return NS_ERROR_OUT_OF_MEMORY;
    NS_ADDREF(copier);

    rv = copier->AsyncCopy(srcTransport, destTransport);
    if (NS_FAILED(rv)) return rv;

    PumpEvents();

    NS_RELEASE(copier);
    return NS_OK;
}

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

static nsresult
RunBlockingTest(nsIFile *srcFile, nsIFile *destFile)
{
    nsresult rv;

    LOG(("RunBlockingTest\n"));

    nsCOMPtr<nsIStreamTransportService> sts =
        do_GetService(kStreamTransportServiceCID, &rv);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIInputStream> srcIn;
    rv = NS_NewLocalFileInputStream(getter_AddRefs(srcIn), srcFile);
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIOutputStream> fileOut;
    rv = NS_NewLocalFileOutputStream(getter_AddRefs(fileOut), destFile);
    if (NS_FAILED(rv)) return rv;
    
    nsCOMPtr<nsITransport> destTransport;
    rv = sts->CreateOutputTransport(fileOut, int64_t(-1), int64_t(-1),
                                    true, getter_AddRefs(destTransport));
    if (NS_FAILED(rv)) return rv;

    nsCOMPtr<nsIOutputStream> destOut;
    rv = destTransport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 100, 10, getter_AddRefs(destOut));
    if (NS_FAILED(rv)) return rv;

    char buf[120];
    uint32_t n;
    for (;;) {
        rv = srcIn->Read(buf, sizeof(buf), &n);
        if (NS_FAILED(rv) || (n == 0)) return rv;

        rv = destOut->Write(buf, n, &n);
        if (NS_FAILED(rv)) return rv;
    }

    return NS_OK;
}

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

int
main(int argc, char* argv[])
{
    if (test_common_init(&argc, &argv) != 0)
        return -1;

    nsresult rv;

    if (argc < 2) {
        printf("usage: %s <file-to-read>\n", argv[0]);
        return -1;
    }
    char* fileName = argv[1];
    {
        nsCOMPtr<nsIServiceManager> servMan;
        NS_InitXPCOM2(getter_AddRefs(servMan), nullptr, nullptr);
        nsCOMPtr<nsIComponentRegistrar> registrar = do_QueryInterface(servMan);
        NS_ASSERTION(registrar, "Null nsIComponentRegistrar");
        if (registrar)
            registrar->AutoRegister(nullptr);

        gTestLog = PR_NewLogModule("Test");

        nsCOMPtr<nsIFile> srcFile;
        rv = NS_NewNativeLocalFile(nsDependentCString(fileName), false, getter_AddRefs(srcFile));
        if (NS_FAILED(rv)) return rv;

        nsCOMPtr<nsIFile> destFile;
        rv = srcFile->Clone(getter_AddRefs(destFile));
        if (NS_FAILED(rv)) return rv;

        nsAutoCString leafName;
        rv = destFile->GetNativeLeafName(leafName);
        if (NS_FAILED(rv)) return rv;

        nsAutoCString newName(leafName);
        newName.AppendLiteral(".1");
        rv = destFile->SetNativeLeafName(newName);
        if (NS_FAILED(rv)) return rv;

        rv = RunTest(srcFile, destFile);
        NS_ASSERTION(NS_SUCCEEDED(rv), "RunTest failed");

        newName = leafName;
        newName.AppendLiteral(".2");
        rv = destFile->SetNativeLeafName(newName);
        if (NS_FAILED(rv)) return rv;

        rv = RunBlockingTest(srcFile, destFile);
        NS_ASSERTION(NS_SUCCEEDED(rv), "RunBlockingTest failed");

        // give background threads a chance to finish whatever work they may
        // be doing.
        PR_Sleep(PR_SecondsToInterval(1));
    } // this scopes the nsCOMPtrs
    // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM
    rv = NS_ShutdownXPCOM(nullptr);
    NS_ASSERTION(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed");
    return NS_OK;
}