/* 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 "StreamFunctions.h"
#include "nsZipDataStream.h"
#include "nsStringStream.h"
#include "nsISeekableStream.h"
#include "nsDeflateConverter.h"
#include "nsNetUtil.h"
#include "nsComponentManagerUtils.h"

#define ZIP_METHOD_STORE 0
#define ZIP_METHOD_DEFLATE 8

using namespace mozilla;

/**
 * nsZipDataStream handles the writing an entry's into the zip file.
 * It is set up to wither write the data as is, or in the event that compression
 * has been requested to pass it through a stream converter.
 * Currently only the deflate compression method is supported.
 * The CRC checksum for the entry's data is also generated here.
 */
NS_IMPL_ISUPPORTS(nsZipDataStream, nsIStreamListener, nsIRequestObserver)

nsresult nsZipDataStream::Init(nsZipWriter* aWriter, nsIOutputStream* aStream,
                               nsZipHeader* aHeader, int32_t aCompression) {
  mWriter = aWriter;
  mHeader = aHeader;
  mStream = aStream;
  mHeader->mCRC = crc32(0L, Z_NULL, 0);

  nsresult rv =
      NS_NewSimpleStreamListener(getter_AddRefs(mOutput), aStream, nullptr);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aCompression > 0) {
    mHeader->mMethod = ZIP_METHOD_DEFLATE;
    nsCOMPtr<nsIStreamConverter> converter =
        new nsDeflateConverter(aCompression);
    NS_ENSURE_TRUE(converter, NS_ERROR_OUT_OF_MEMORY);

    rv = converter->AsyncConvertData("uncompressed", "rawdeflate", mOutput,
                                     nullptr);
    NS_ENSURE_SUCCESS(rv, rv);

    mOutput = converter;
  } else {
    mHeader->mMethod = ZIP_METHOD_STORE;
  }

  return NS_OK;
}

NS_IMETHODIMP nsZipDataStream::OnDataAvailable(nsIRequest* aRequest,
                                               nsIInputStream* aInputStream,
                                               uint64_t aOffset,
                                               uint32_t aCount) {
  if (!mOutput) return NS_ERROR_NOT_INITIALIZED;

  auto buffer = MakeUnique<char[]>(aCount);
  NS_ENSURE_TRUE(buffer, NS_ERROR_OUT_OF_MEMORY);

  nsresult rv = ZW_ReadData(aInputStream, buffer.get(), aCount);
  NS_ENSURE_SUCCESS(rv, rv);

  return ProcessData(aRequest, nullptr, buffer.get(), aOffset, aCount);
}

NS_IMETHODIMP nsZipDataStream::OnStartRequest(nsIRequest* aRequest) {
  if (!mOutput) return NS_ERROR_NOT_INITIALIZED;

  return mOutput->OnStartRequest(aRequest);
}

NS_IMETHODIMP nsZipDataStream::OnStopRequest(nsIRequest* aRequest,
                                             nsresult aStatusCode) {
  if (!mOutput) return NS_ERROR_NOT_INITIALIZED;

  nsresult rv = mOutput->OnStopRequest(aRequest, aStatusCode);
  mOutput = nullptr;
  if (NS_FAILED(rv)) {
    mWriter->EntryCompleteCallback(mHeader, rv);
  } else {
    rv = CompleteEntry();
    rv = mWriter->EntryCompleteCallback(mHeader, rv);
  }

  mStream = nullptr;
  mWriter = nullptr;
  mHeader = nullptr;

  return rv;
}

inline nsresult nsZipDataStream::CompleteEntry() {
  nsresult rv;
  nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mStream, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  int64_t pos;
  rv = seekable->Tell(&pos);
  NS_ENSURE_SUCCESS(rv, rv);

  mHeader->mCSize = pos - mHeader->mOffset - mHeader->GetFileHeaderLength();
  mHeader->mWriteOnClose = true;
  return NS_OK;
}

nsresult nsZipDataStream::ProcessData(nsIRequest* aRequest,
                                      nsISupports* aContext, char* aBuffer,
                                      uint64_t aOffset, uint32_t aCount) {
  mHeader->mCRC = crc32(
      mHeader->mCRC, reinterpret_cast<const unsigned char*>(aBuffer), aCount);

  MOZ_ASSERT(aCount <= INT32_MAX);
  nsCOMPtr<nsIInputStream> stream;
  nsresult rv = NS_NewByteInputStream(
      getter_AddRefs(stream), Span(aBuffer, aCount), NS_ASSIGNMENT_DEPEND);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mOutput->OnDataAvailable(aRequest, stream, aOffset, aCount);
  mHeader->mUSize += aCount;

  return rv;
}

nsresult nsZipDataStream::ReadStream(nsIInputStream* aStream) {
  if (!mOutput) return NS_ERROR_NOT_INITIALIZED;

  nsresult rv = OnStartRequest(nullptr);
  NS_ENSURE_SUCCESS(rv, rv);

  auto buffer = MakeUnique<char[]>(4096);
  NS_ENSURE_TRUE(buffer, NS_ERROR_OUT_OF_MEMORY);

  uint32_t read = 0;
  uint32_t offset = 0;
  do {
    rv = aStream->Read(buffer.get(), 4096, &read);
    if (NS_FAILED(rv)) {
      OnStopRequest(nullptr, rv);
      return rv;
    }

    if (read > 0) {
      rv = ProcessData(nullptr, nullptr, buffer.get(), offset, read);
      if (NS_FAILED(rv)) {
        OnStopRequest(nullptr, rv);
        return rv;
      }
      offset += read;
    }
  } while (read > 0);

  return OnStopRequest(nullptr, NS_OK);
}
