/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * 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 Zip Writer Component.
 *
 * The Initial Developer of the Original Code is
 * Dave Townsend <dtownsend@oxymoronical.com>.
 *
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *      Mook <mook.moz+random.code@gmail.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 MPL, 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 MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK *****
 */

#include "StreamFunctions.h"
#include "nsZipWriter.h"
#include "nsZipDataStream.h"
#include "nsISeekableStream.h"
#include "nsIAsyncStreamCopier.h"
#include "nsIStreamListener.h"
#include "nsIInputStreamPump.h"
#include "nsComponentManagerUtils.h"
#include "nsMemory.h"
#include "nsNetError.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"
#include "nsNetUtil.h"
#include "prio.h"

#define ZIP_EOCDR_HEADER_SIZE 22
#define ZIP_EOCDR_HEADER_SIGNATURE 0x06054b50

/**
 * nsZipWriter is used to create and add to zip files.
 * It is based on the spec available at
 * http://www.pkware.com/documents/casestudies/APPNOTE.TXT.
 * 
 * The basic structure of a zip file created is slightly simpler than that
 * illustrated in the spec because certain features of the zip format are
 * unsupported:
 * 
 * [local file header 1]
 * [file data 1]
 * . 
 * .
 * .
 * [local file header n]
 * [file data n]
 * [central directory]
 * [end of central directory record]
 */
NS_IMPL_ISUPPORTS2(nsZipWriter, nsIZipWriter,
                                nsIRequestObserver)

nsZipWriter::nsZipWriter()
{
    mEntryHash.Init();
    mInQueue = PR_FALSE;
}

nsZipWriter::~nsZipWriter()
{
    if (mStream && !mInQueue)
        Close();
}

/* attribute AString comment; */
NS_IMETHODIMP nsZipWriter::GetComment(nsACString & aComment)
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    aComment = mComment;
    return NS_OK;
}

NS_IMETHODIMP nsZipWriter::SetComment(const nsACString & aComment)
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    mComment = aComment;
    mCDSDirty = PR_TRUE;
    return NS_OK;
}

/* readonly attribute boolean inQueue; */
NS_IMETHODIMP nsZipWriter::GetInQueue(PRBool *aInQueue)
{
    *aInQueue = mInQueue;
    return NS_OK;
}

/* readonly attribute nsIFile file; */
NS_IMETHODIMP nsZipWriter::GetFile(nsIFile **aFile)
{
    if (!mFile)
        return NS_ERROR_NOT_INITIALIZED;

    nsCOMPtr<nsIFile> file;
    nsresult rv = mFile->Clone(getter_AddRefs(file));
    NS_ENSURE_SUCCESS(rv, rv);

    NS_ADDREF(*aFile = file);
    return NS_OK;
}

/*
 * Reads file entries out of an existing zip file.
 */
nsresult nsZipWriter::ReadFile(nsIFile *aFile)
{
    PRInt64 size;
    nsresult rv = aFile->GetFileSize(&size);
    NS_ENSURE_SUCCESS(rv, rv);

    // If the file is too short, it cannot be a valid archive, thus we fail
    // without even attempting to open it
    NS_ENSURE_TRUE(size > ZIP_EOCDR_HEADER_SIZE, NS_ERROR_FILE_CORRUPTED);

    nsCOMPtr<nsIInputStream> inputStream;
    rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
    NS_ENSURE_SUCCESS(rv, rv);

    char buf[1024];
    PRInt64 seek = size - 1024;
    PRUint32 length = 1024;

    nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(inputStream);

    while (true) {
        if (seek < 0) {
            length += (PRInt32)seek;
            seek = 0;
        }

        rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, seek);
        if (NS_FAILED(rv)) {
            inputStream->Close();
            return rv;
        }
        rv = ZW_ReadData(inputStream, buf, length);
        if (NS_FAILED(rv)) {
            inputStream->Close();
            return rv;
        }

        /*
         * We have to backtrack from the end of the file until we find the
         * CDS signature
         */
        // We know it's at least this far from the end
        for (PRUint32 pos = length - ZIP_EOCDR_HEADER_SIZE;
             (PRInt32)pos >= 0; pos--) {
            PRUint32 sig = PEEK32((unsigned char *)buf + pos);
            if (sig == ZIP_EOCDR_HEADER_SIGNATURE) {
                // Skip down to entry count
                pos += 10;
                PRUint32 entries = READ16(buf, &pos);
                // Skip past CDS size
                pos += 4;
                mCDSOffset = READ32(buf, &pos);
                PRUint32 commentlen = READ16(buf, &pos);

                if (commentlen == 0)
                    mComment.Truncate();
                else if (pos + commentlen <= length)
                    mComment.Assign(buf + pos, commentlen);
                else {
                    if ((seek + pos + commentlen) > size) {
                        inputStream->Close();
                        return NS_ERROR_FILE_CORRUPTED;
                    }
                    nsAutoArrayPtr<char> field(new char[commentlen]);
                    NS_ENSURE_TRUE(field, NS_ERROR_OUT_OF_MEMORY);
                    rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
                                        seek + pos);
                    if (NS_FAILED(rv)) {
                        inputStream->Close();
                        return rv;
                    }
                    rv = ZW_ReadData(inputStream, field.get(), length);
                    if (NS_FAILED(rv)) {
                        inputStream->Close();
                        return rv;
                    }
                    mComment.Assign(field.get(), commentlen);
                }

                rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
                                    mCDSOffset);
                if (NS_FAILED(rv)) {
                    inputStream->Close();
                    return rv;
                }

                for (PRUint32 entry = 0; entry < entries; entry++) {
                    nsZipHeader* header = new nsZipHeader();
                    if (!header) {
                        inputStream->Close();
                        mEntryHash.Clear();
                        mHeaders.Clear();
                        return NS_ERROR_OUT_OF_MEMORY;
                    }
                    rv = header->ReadCDSHeader(inputStream);
                    if (NS_FAILED(rv)) {
                        inputStream->Close();
                        mEntryHash.Clear();
                        mHeaders.Clear();
                        return rv;
                    }
                    if (!mEntryHash.Put(header->mName, mHeaders.Count()))
                        return NS_ERROR_OUT_OF_MEMORY;
                    if (!mHeaders.AppendObject(header))
                        return NS_ERROR_OUT_OF_MEMORY;
                }

                return inputStream->Close();
            }
        }

        if (seek == 0) {
            // We've reached the start with no signature found. Corrupt.
            inputStream->Close();
            return NS_ERROR_FILE_CORRUPTED;
        }

        // Overlap by the size of the end of cdr
        seek -= (1024 - ZIP_EOCDR_HEADER_SIZE);
    }
    // Will never reach here in reality
    NS_NOTREACHED("Loop should never complete");
    return NS_ERROR_UNEXPECTED;
}

/* void open (in nsIFile aFile, in PRInt32 aIoFlags); */
NS_IMETHODIMP nsZipWriter::Open(nsIFile *aFile, PRInt32 aIoFlags)
{
    if (mStream)
        return NS_ERROR_ALREADY_INITIALIZED;

    NS_ENSURE_ARG_POINTER(aFile);

    // Need to be able to write to the file
    if (aIoFlags & PR_RDONLY)
        return NS_ERROR_FAILURE;
    
    nsresult rv = aFile->Clone(getter_AddRefs(mFile));
    NS_ENSURE_SUCCESS(rv, rv);

    PRBool exists;
    rv = mFile->Exists(&exists);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!exists && !(aIoFlags & PR_CREATE_FILE))
        return NS_ERROR_FILE_NOT_FOUND;

    if (exists && !(aIoFlags & (PR_TRUNCATE | PR_WRONLY))) {
        rv = ReadFile(mFile);
        NS_ENSURE_SUCCESS(rv, rv);
        mCDSDirty = PR_FALSE;
    }
    else {
        mCDSOffset = 0;
        mCDSDirty = PR_TRUE;
        mComment.Truncate();
    }

    // Silently drop PR_APPEND
    aIoFlags &= 0xef;

    nsCOMPtr<nsIOutputStream> stream;
    rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), mFile, aIoFlags);
    if (NS_FAILED(rv)) {
        mHeaders.Clear();
        mEntryHash.Clear();
        return rv;
    }

    rv = NS_NewBufferedOutputStream(getter_AddRefs(mStream), stream, 0x800);
    if (NS_FAILED(rv)) {
        stream->Close();
        mHeaders.Clear();
        mEntryHash.Clear();
        return rv;
    }

    if (mCDSOffset > 0) {
        rv = SeekCDS();
        NS_ENSURE_SUCCESS(rv, rv);
    }

    return NS_OK;
}

/* nsIZipEntry getEntry (in AString aZipEntry); */
NS_IMETHODIMP nsZipWriter::GetEntry(const nsACString & aZipEntry,
                                    nsIZipEntry **_retval)
{
    PRInt32 pos;
    if (mEntryHash.Get(aZipEntry, &pos))
        NS_ADDREF(*_retval = mHeaders[pos]);
    else
        *_retval = nsnull;

    return NS_OK;
}

/* boolean hasEntry (in AString aZipEntry); */
NS_IMETHODIMP nsZipWriter::HasEntry(const nsACString & aZipEntry,
                                    PRBool *_retval)
{
    *_retval = mEntryHash.Get(aZipEntry, nsnull);

    return NS_OK;
}

/* void addEntryDirectory (in AUTF8String aZipEntry, in PRTime aModTime,
 *                         in boolean aQueue); */
NS_IMETHODIMP nsZipWriter::AddEntryDirectory(const nsACString & aZipEntry,
                                             PRTime aModTime, PRBool aQueue)
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    if (aQueue) {
        nsZipQueueItem item;
        item.mOperation = OPERATION_ADD;
        item.mZipEntry = aZipEntry;
        item.mModTime = aModTime;
        item.mPermissions = PERMISSIONS_DIR;
        if (!mQueue.AppendElement(item))
            return NS_ERROR_OUT_OF_MEMORY;
        return NS_OK;
    }

    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;
    return InternalAddEntryDirectory(aZipEntry, aModTime, PERMISSIONS_DIR);
}

/* void addEntryFile (in AUTF8String aZipEntry, in PRInt32 aCompression,
 *                    in nsIFile aFile, in boolean aQueue); */
NS_IMETHODIMP nsZipWriter::AddEntryFile(const nsACString & aZipEntry,
                                        PRInt32 aCompression, nsIFile *aFile,
                                        PRBool aQueue)
{
    NS_ENSURE_ARG_POINTER(aFile);
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    nsresult rv;
    if (aQueue) {
        nsZipQueueItem item;
        item.mOperation = OPERATION_ADD;
        item.mZipEntry = aZipEntry;
        item.mCompression = aCompression;
        rv = aFile->Clone(getter_AddRefs(item.mFile));
        NS_ENSURE_SUCCESS(rv, rv);
        if (!mQueue.AppendElement(item))
            return NS_ERROR_OUT_OF_MEMORY;
        return NS_OK;
    }

    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;

    PRBool exists;
    rv = aFile->Exists(&exists);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!exists)
        return NS_ERROR_FILE_NOT_FOUND;

    PRBool isdir;
    rv = aFile->IsDirectory(&isdir);
    NS_ENSURE_SUCCESS(rv, rv);

    PRInt64 modtime;
    rv = aFile->GetLastModifiedTime(&modtime);
    NS_ENSURE_SUCCESS(rv, rv);
    modtime *= PR_USEC_PER_MSEC;

    PRUint32 permissions;
    rv = aFile->GetPermissions(&permissions);
    NS_ENSURE_SUCCESS(rv, rv);

    if (isdir)
        return InternalAddEntryDirectory(aZipEntry, modtime, permissions);

    if (mEntryHash.Get(aZipEntry, nsnull))
        return NS_ERROR_FILE_ALREADY_EXISTS;

    nsCOMPtr<nsIInputStream> inputStream;
    rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
                                    aFile);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = AddEntryStream(aZipEntry, modtime, aCompression, inputStream,
                        PR_FALSE, permissions);
    NS_ENSURE_SUCCESS(rv, rv);

    return inputStream->Close();
}

/* void addEntryChannel (in AUTF8String aZipEntry, in PRTime aModTime,
 *                       in PRInt32 aCompression, in nsIChannel aChannel,
 *                       in boolean aQueue); */
NS_IMETHODIMP nsZipWriter::AddEntryChannel(const nsACString & aZipEntry,
                                           PRTime aModTime,
                                           PRInt32 aCompression,
                                           nsIChannel *aChannel,
                                           PRBool aQueue)
{
    NS_ENSURE_ARG_POINTER(aChannel);
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    if (aQueue) {
        nsZipQueueItem item;
        item.mOperation = OPERATION_ADD;
        item.mZipEntry = aZipEntry;
        item.mModTime = aModTime;
        item.mCompression = aCompression;
        item.mPermissions = PERMISSIONS_FILE;
        item.mChannel = aChannel;
        if (!mQueue.AppendElement(item))
            return NS_ERROR_OUT_OF_MEMORY;
        return NS_OK;
    }

    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;
    if (mEntryHash.Get(aZipEntry, nsnull))
        return NS_ERROR_FILE_ALREADY_EXISTS;

    nsCOMPtr<nsIInputStream> inputStream;
    nsresult rv = aChannel->Open(getter_AddRefs(inputStream));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = AddEntryStream(aZipEntry, aModTime, aCompression, inputStream,
                        PR_FALSE, PERMISSIONS_FILE);
    NS_ENSURE_SUCCESS(rv, rv);

    return inputStream->Close();
}

/* void addEntryStream (in AUTF8String aZipEntry, in PRTime aModTime,
 *                      in PRInt32 aCompression, in nsIInputStream aStream,
 *                      in boolean aQueue); */
NS_IMETHODIMP nsZipWriter::AddEntryStream(const nsACString & aZipEntry,
                                          PRTime aModTime,
                                          PRInt32 aCompression,
                                          nsIInputStream *aStream,
                                          PRBool aQueue)
{
    return AddEntryStream(aZipEntry, aModTime, aCompression, aStream, aQueue,
                          PERMISSIONS_FILE);
}

/* void addEntryStream (in AUTF8String aZipEntry, in PRTime aModTime,
 *                      in PRInt32 aCompression, in nsIInputStream aStream,
 *                      in boolean aQueue, in unsigned long aPermissions); */
nsresult nsZipWriter::AddEntryStream(const nsACString & aZipEntry,
                                     PRTime aModTime,
                                     PRInt32 aCompression,
                                     nsIInputStream *aStream,
                                     PRBool aQueue,
                                     PRUint32 aPermissions)
{
    NS_ENSURE_ARG_POINTER(aStream);
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    if (aQueue) {
        nsZipQueueItem item;
        item.mOperation = OPERATION_ADD;
        item.mZipEntry = aZipEntry;
        item.mModTime = aModTime;
        item.mCompression = aCompression;
        item.mPermissions = aPermissions;
        item.mStream = aStream;
        if (!mQueue.AppendElement(item))
            return NS_ERROR_OUT_OF_MEMORY;
        return NS_OK;
    }

    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;
    if (mEntryHash.Get(aZipEntry, nsnull))
        return NS_ERROR_FILE_ALREADY_EXISTS;

    nsRefPtr<nsZipHeader> header = new nsZipHeader();
    NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY);
    header->Init(aZipEntry, aModTime, ZIP_ATTRS(aPermissions, ZIP_ATTRS_FILE),
                 mCDSOffset);
    nsresult rv = header->WriteFileHeader(mStream);
    if (NS_FAILED(rv)) {
        SeekCDS();
        return rv;
    }

    nsRefPtr<nsZipDataStream> stream = new nsZipDataStream();
    if (!stream) {
        SeekCDS();
        return NS_ERROR_OUT_OF_MEMORY;
    }
    rv = stream->Init(this, mStream, header, aCompression);
    if (NS_FAILED(rv)) {
        SeekCDS();
        return rv;
    }

    rv = stream->ReadStream(aStream);
    if (NS_FAILED(rv))
        SeekCDS();
    return rv;
}

/* void removeEntry (in AUTF8String aZipEntry, in boolean aQueue); */
NS_IMETHODIMP nsZipWriter::RemoveEntry(const nsACString & aZipEntry,
                                       PRBool aQueue)
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;

    if (aQueue) {
        nsZipQueueItem item;
        item.mOperation = OPERATION_REMOVE;
        item.mZipEntry = aZipEntry;
        if (!mQueue.AppendElement(item))
            return NS_ERROR_OUT_OF_MEMORY;
        return NS_OK;
    }

    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;

    PRInt32 pos;
    if (mEntryHash.Get(aZipEntry, &pos)) {
        // Flush any remaining data before we seek.
        nsresult rv = mStream->Flush();
        NS_ENSURE_SUCCESS(rv, rv);
        if (pos < mHeaders.Count() - 1) {
            // This is not the last entry, pull back the data.
            nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream);
            rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
                                mHeaders[pos]->mOffset);
            NS_ENSURE_SUCCESS(rv, rv);

            nsCOMPtr<nsIInputStream> inputStream;
            rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
                                            mFile);
            NS_ENSURE_SUCCESS(rv, rv);
            seekable = do_QueryInterface(inputStream);
            rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
                                mHeaders[pos + 1]->mOffset);
            if (NS_FAILED(rv)) {
                inputStream->Close();
                return rv;
            }

            PRUint32 count = mCDSOffset - mHeaders[pos + 1]->mOffset;
            PRUint32 read = 0;
            char buf[4096];
            while (count > 0) {
                if (count < sizeof(buf))
                    read = count;
                else
                    read = sizeof(buf);

                rv = inputStream->Read(buf, read, &read);
                if (NS_FAILED(rv)) {
                    inputStream->Close();
                    Cleanup();
                    return rv;
                }

                rv = ZW_WriteData(mStream, buf, read);
                if (NS_FAILED(rv)) {
                    inputStream->Close();
                    Cleanup();
                    return rv;
                }

                count -= read;
            }
            inputStream->Close();

            // Rewrite header offsets and update hash
            PRUint32 shift = (mHeaders[pos + 1]->mOffset -
                              mHeaders[pos]->mOffset);
            mCDSOffset -= shift;
            PRInt32 pos2 = pos + 1;
            while (pos2 < mHeaders.Count()) {
                if (!mEntryHash.Put(mHeaders[pos2]->mName, pos2-1)) {
                    Cleanup();
                    return NS_ERROR_OUT_OF_MEMORY;
                }
                mHeaders[pos2]->mOffset -= shift;
                pos2++;
            }
        }
        else {
            // Remove the last entry is just a case of moving the CDS
            mCDSOffset = mHeaders[pos]->mOffset;
            rv = SeekCDS();
            NS_ENSURE_SUCCESS(rv, rv);
        }

        mEntryHash.Remove(mHeaders[pos]->mName);
        mHeaders.RemoveObjectAt(pos);
        mCDSDirty = PR_TRUE;

        return NS_OK;
    }

    return NS_ERROR_FILE_NOT_FOUND;
}

/* void processQueue (in nsIRequestObserver aObserver,
 *                    in nsISupports aContext); */
NS_IMETHODIMP nsZipWriter::ProcessQueue(nsIRequestObserver *aObserver,
                                        nsISupports *aContext)
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;
    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;

    mProcessObserver = aObserver;
    mProcessContext = aContext;
    mInQueue = PR_TRUE;

    if (mProcessObserver)
        mProcessObserver->OnStartRequest(nsnull, mProcessContext);

    BeginProcessingNextItem();

    return NS_OK;
}

/* void close (); */
NS_IMETHODIMP nsZipWriter::Close()
{
    if (!mStream)
        return NS_ERROR_NOT_INITIALIZED;
    if (mInQueue)
        return NS_ERROR_IN_PROGRESS;

    if (mCDSDirty) {
        PRUint32 size = 0;
        for (PRInt32 i = 0; i < mHeaders.Count(); i++) {
            nsresult rv = mHeaders[i]->WriteCDSHeader(mStream);
            if (NS_FAILED(rv)) {
                Cleanup();
                return rv;
            }
            size += mHeaders[i]->GetCDSHeaderLength();
        }

        char buf[ZIP_EOCDR_HEADER_SIZE];
        PRUint32 pos = 0;
        WRITE32(buf, &pos, ZIP_EOCDR_HEADER_SIGNATURE);
        WRITE16(buf, &pos, 0);
        WRITE16(buf, &pos, 0);
        WRITE16(buf, &pos, mHeaders.Count());
        WRITE16(buf, &pos, mHeaders.Count());
        WRITE32(buf, &pos, size);
        WRITE32(buf, &pos, mCDSOffset);
        WRITE16(buf, &pos, mComment.Length());

        nsresult rv = ZW_WriteData(mStream, buf, pos);
        if (NS_FAILED(rv)) {
            Cleanup();
            return rv;
        }

        rv = ZW_WriteData(mStream, mComment.get(), mComment.Length());
        if (NS_FAILED(rv)) {
            Cleanup();
            return rv;
        }

        nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream);
        rv = seekable->SetEOF();
        if (NS_FAILED(rv)) {
            Cleanup();
            return rv;
        }
    }

    nsresult rv = mStream->Close();
    mStream = nsnull;
    mHeaders.Clear();
    mEntryHash.Clear();
    mQueue.Clear();

    return rv;
}

// Our nsIRequestObserver monitors removal operations performed on the queue
/* void onStartRequest (in nsIRequest aRequest, in nsISupports aContext); */
NS_IMETHODIMP nsZipWriter::OnStartRequest(nsIRequest *aRequest,
                                          nsISupports *aContext)
{
    return NS_OK;
}

/* void onStopRequest (in nsIRequest aRequest, in nsISupports aContext,
 *                                             in nsresult aStatusCode); */
NS_IMETHODIMP nsZipWriter::OnStopRequest(nsIRequest *aRequest,
                                         nsISupports *aContext,
                                         nsresult aStatusCode)
{
    if (NS_FAILED(aStatusCode)) {
        FinishQueue(aStatusCode);
        Cleanup();
    }

    nsresult rv = mStream->Flush();
    if (NS_FAILED(rv)) {
        FinishQueue(rv);
        Cleanup();
        return rv;
    }
    rv = SeekCDS();
    if (NS_FAILED(rv)) {
        FinishQueue(rv);
        return rv;
    }

    BeginProcessingNextItem();

    return NS_OK;
}

nsresult nsZipWriter::InternalAddEntryDirectory(const nsACString & aZipEntry,
                                                PRTime aModTime,
                                                PRUint32 aPermissions)
{
    nsRefPtr<nsZipHeader> header = new nsZipHeader();
    NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY);

    PRUint32 zipAttributes = ZIP_ATTRS(aPermissions, ZIP_ATTRS_DIRECTORY);

    if (aZipEntry.Last() != '/') {
        nsCString dirPath;
        dirPath.Assign(aZipEntry + NS_LITERAL_CSTRING("/"));
        header->Init(dirPath, aModTime, zipAttributes, mCDSOffset);
    }
    else
        header->Init(aZipEntry, aModTime, zipAttributes, mCDSOffset);

    if (mEntryHash.Get(header->mName, nsnull))
        return NS_ERROR_FILE_ALREADY_EXISTS;

    nsresult rv = header->WriteFileHeader(mStream);
    if (NS_FAILED(rv)) {
        Cleanup();
        return rv;
    }

    mCDSDirty = PR_TRUE;
    mCDSOffset += header->GetFileHeaderLength();
    if (!mEntryHash.Put(header->mName, mHeaders.Count())) {
        Cleanup();
        return NS_ERROR_OUT_OF_MEMORY;
    }
    if (!mHeaders.AppendObject(header)) {
        Cleanup();
        return NS_ERROR_OUT_OF_MEMORY;
    }

    return NS_OK;
}

/*
 * Recovering from an error while adding a new entry is simply a case of
 * seeking back to the CDS. If we fail trying to do that though then cleanup
 * and bail out.
 */
nsresult nsZipWriter::SeekCDS()
{
    nsresult rv;
    nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream, &rv);
    if (NS_FAILED(rv)) {
        Cleanup();
        return rv;
    }
    rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, mCDSOffset);
    if (NS_FAILED(rv))
        Cleanup();
    return rv;
}

/*
 * In a bad error condition this essentially closes down the component as best
 * it can.
 */
void nsZipWriter::Cleanup()
{
    mHeaders.Clear();
    mEntryHash.Clear();
    if (mStream)
        mStream->Close();
    mStream = nsnull;
    mFile = nsnull;
}

/*
 * Called when writing a file to the zip is complete.
 */
nsresult nsZipWriter::EntryCompleteCallback(nsZipHeader* aHeader,
                                            nsresult aStatus)
{
    if (NS_SUCCEEDED(aStatus)) {
        if (!mEntryHash.Put(aHeader->mName, mHeaders.Count())) {
            SeekCDS();
            return NS_ERROR_OUT_OF_MEMORY;
        }
        if (!mHeaders.AppendObject(aHeader)) {
            mEntryHash.Remove(aHeader->mName);
            SeekCDS();
            return NS_ERROR_OUT_OF_MEMORY;
        }
        mCDSDirty = PR_TRUE;
        mCDSOffset += aHeader->mCSize + aHeader->GetFileHeaderLength();

        if (mInQueue)
            BeginProcessingNextItem();

        return NS_OK;
    }

    nsresult rv = SeekCDS();
    if (mInQueue)
        FinishQueue(aStatus);
    return rv;
}

inline nsresult nsZipWriter::BeginProcessingAddition(nsZipQueueItem* aItem,
                                                     PRBool* complete)
{
    if (aItem->mFile) {
        PRBool exists;
        nsresult rv = aItem->mFile->Exists(&exists);
        NS_ENSURE_SUCCESS(rv, rv);

        if (!exists) return NS_ERROR_FILE_NOT_FOUND;

        PRBool isdir;
        rv = aItem->mFile->IsDirectory(&isdir);
        NS_ENSURE_SUCCESS(rv, rv);

        rv = aItem->mFile->GetLastModifiedTime(&aItem->mModTime);
        NS_ENSURE_SUCCESS(rv, rv);
        aItem->mModTime *= PR_USEC_PER_MSEC;

        rv = aItem->mFile->GetPermissions(&aItem->mPermissions);
        NS_ENSURE_SUCCESS(rv, rv);

        if (!isdir) {
            // Set up for fall through to stream reader
            rv = NS_NewLocalFileInputStream(getter_AddRefs(aItem->mStream),
                                            aItem->mFile);
            NS_ENSURE_SUCCESS(rv, rv);
        }
        // If a dir then this will fall through to the plain dir addition
    }

    PRUint32 zipAttributes = ZIP_ATTRS(aItem->mPermissions, ZIP_ATTRS_FILE);

    if (aItem->mStream) {
        nsRefPtr<nsZipHeader> header = new nsZipHeader();
        NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY);

        header->Init(aItem->mZipEntry, aItem->mModTime, zipAttributes,
                     mCDSOffset);
        nsresult rv = header->WriteFileHeader(mStream);
        NS_ENSURE_SUCCESS(rv, rv);

        nsRefPtr<nsZipDataStream> stream = new nsZipDataStream();
        rv = stream->Init(this, mStream, header, aItem->mCompression);
        NS_ENSURE_SUCCESS(rv, rv);

        nsCOMPtr<nsIInputStreamPump> pump;
        rv = NS_NewInputStreamPump(getter_AddRefs(pump), aItem->mStream, -1,
                                   -1, 0, 0, PR_TRUE);
        NS_ENSURE_SUCCESS(rv, rv);

        rv = pump->AsyncRead(stream, nsnull);
        NS_ENSURE_SUCCESS(rv, rv);

        return NS_OK;
    }

    if (aItem->mChannel) {
        nsRefPtr<nsZipHeader> header = new nsZipHeader();
        NS_ENSURE_TRUE(header, NS_ERROR_OUT_OF_MEMORY);

        header->Init(aItem->mZipEntry, aItem->mModTime, zipAttributes,
                     mCDSOffset);

        nsRefPtr<nsZipDataStream> stream = new nsZipDataStream();
        NS_ENSURE_TRUE(stream, NS_ERROR_OUT_OF_MEMORY);
        nsresult rv = stream->Init(this, mStream, header, aItem->mCompression);
        NS_ENSURE_SUCCESS(rv, rv);
        rv = aItem->mChannel->AsyncOpen(stream, nsnull);
        NS_ENSURE_SUCCESS(rv, rv);

        return NS_OK;
    }

    // Must be plain directory addition
    *complete = PR_TRUE;
    return InternalAddEntryDirectory(aItem->mZipEntry, aItem->mModTime,
                                     aItem->mPermissions);
}

inline nsresult nsZipWriter::BeginProcessingRemoval(PRInt32 aPos)
{
    // Open the zip file for reading
    nsCOMPtr<nsIInputStream> inputStream;
    nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
                                             mFile);
    NS_ENSURE_SUCCESS(rv, rv);
    nsCOMPtr<nsIInputStreamPump> pump;
    rv = NS_NewInputStreamPump(getter_AddRefs(pump), inputStream, -1, -1, 0,
                               0, PR_TRUE);
    if (NS_FAILED(rv)) {
        inputStream->Close();
        return rv;
    }
    nsCOMPtr<nsIStreamListener> listener;
    rv = NS_NewSimpleStreamListener(getter_AddRefs(listener), mStream, this);
    if (NS_FAILED(rv)) {
        inputStream->Close();
        return rv;
    }

    nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream);
    rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET,
                        mHeaders[aPos]->mOffset);
    if (NS_FAILED(rv)) {
        inputStream->Close();
        return rv;
    }

    PRUint32 shift = (mHeaders[aPos + 1]->mOffset -
                      mHeaders[aPos]->mOffset);
    mCDSOffset -= shift;
    PRInt32 pos2 = aPos + 1;
    while (pos2 < mHeaders.Count()) {
        mEntryHash.Put(mHeaders[pos2]->mName, pos2 - 1);
        mHeaders[pos2]->mOffset -= shift;
        pos2++;
    }

    mEntryHash.Remove(mHeaders[aPos]->mName);
    mHeaders.RemoveObjectAt(aPos);
    mCDSDirty = PR_TRUE;

    rv = pump->AsyncRead(listener, nsnull);
    if (NS_FAILED(rv)) {
        inputStream->Close();
        Cleanup();
        return rv;
    }
    return NS_OK;
}

/*
 * Starts processing on the next item in the queue.
 */
void nsZipWriter::BeginProcessingNextItem()
{
    while (!mQueue.IsEmpty()) {

        nsZipQueueItem next = mQueue[0];
        mQueue.RemoveElementAt(0);

        if (next.mOperation == OPERATION_REMOVE) {
            PRInt32 pos = -1;
            if (mEntryHash.Get(next.mZipEntry, &pos)) {
                if (pos < mHeaders.Count() - 1) {
                    nsresult rv = BeginProcessingRemoval(pos);
                    if (NS_FAILED(rv)) FinishQueue(rv);
                    return;
                }

                mCDSOffset = mHeaders[pos]->mOffset;
                nsresult rv = SeekCDS();
                if (NS_FAILED(rv)) {
                    FinishQueue(rv);
                    return;
                }
                mEntryHash.Remove(mHeaders[pos]->mName);
                mHeaders.RemoveObjectAt(pos);
            }
            else {
                FinishQueue(NS_ERROR_FILE_NOT_FOUND);
                return;
            }
        }
        else if (next.mOperation == OPERATION_ADD) {
            if (mEntryHash.Get(next.mZipEntry, nsnull)) {
                FinishQueue(NS_ERROR_FILE_ALREADY_EXISTS);
                return;
            }

            PRBool complete = PR_FALSE;
            nsresult rv = BeginProcessingAddition(&next, &complete);
            if (NS_FAILED(rv)) {
                SeekCDS();
                FinishQueue(rv);
                return;
            }
            if (!complete)
                return;
        }
    }

    FinishQueue(NS_OK);
}

/*
 * Ends processing with the given status.
 */
void nsZipWriter::FinishQueue(nsresult aStatus)
{
    nsCOMPtr<nsIRequestObserver> observer = mProcessObserver;
    nsCOMPtr<nsISupports> context = mProcessContext;
    // Clean up everything first in case the observer decides to queue more
    // things
    mProcessObserver = nsnull;
    mProcessContext = nsnull;
    mInQueue = PR_FALSE;

    if (observer)
        observer->OnStopRequest(nsnull, context, aStatus);
}
