/* ScummVM Tools
 *
 * ScummVM Tools is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * Additionally this file is based on the ScummVM source code.
 * Copyright information for the ScummVM source code is
 * available in the COPYRIGHT file of the ScummVM source
 * distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <iostream>
#include <fstream>

#include "common/scummsys.h"
#include "common/endian.h"
#include "common/zlib.h"

#define MIN(x,y) (((x)<(y)) ? (x) : (y))

#if defined(USE_ZLIB)

GZipReadStream::GZipReadStream(std::ifstream *w, uint32 start, uint32 size_p) : _wrapped(w), _stream(), _start(start), _size(size_p) {
	char buf[4];
	assert(w != 0);

	// Verify file header is correct
	w->seekg(_start, std::ios::beg);
	w->read(buf, 2);
	uint16 header = READ_BE_UINT16(buf);
	assert(header == 0x1F8B ||
			((header & 0x0F00) == 0x0800 && header % 31 == 0));

	if (header == 0x1F8B && _size > 0) {
		// Retrieve the original file size
		w->seekg(_start + _size - 4, std::ios::beg);
		w->read(buf, 4);
		_origSize = READ_LE_UINT32(buf);
	} else {
		// Original size not available in zlib format
		_origSize = 0;
	}
	_pos = 0;
	w->seekg(_start, std::ios::beg);
	_eos = false;

	// Adding 32 to windowBits indicates to zlib that it is supposed to
	// automatically detect whether gzip or zlib headers are used for
	// the compressed file. This feature was added in zlib 1.2.0.4,
	// released 10 August 2003.
	// Note: This is *crucial* for savegame compatibility, do *not* remove!
	_zlibErr = inflateInit2(&_stream, MAX_WBITS + 32);
	if (_zlibErr != Z_OK)
		return;

	// Setup input buffer
	_stream.next_in = _buf;
	_stream.avail_in = 0;
}

GZipReadStream::~GZipReadStream() {
	inflateEnd(&_stream);
}

bool GZipReadStream::err() const { return (_zlibErr != Z_OK) && (_zlibErr != Z_STREAM_END); }
void GZipReadStream::clearErr() {
	// only reset _eos; I/O errors are not recoverable
	_eos = false;
}

uint32 GZipReadStream::read(void *dataPtr, uint32 dataSize) {
	_stream.next_out = (byte *)dataPtr;
	_stream.avail_out = dataSize;

	// Keep going while we get no error
	while (_zlibErr == Z_OK && _stream.avail_out) {
		if (_stream.avail_in == 0 && !_wrapped->eof()) {
			// If we are out of input data: Read more data, if available.
			_wrapped->read((char*)_buf, BUFSIZE);
			_stream.next_in = _buf;
			_stream.avail_in = _wrapped->gcount();
		}
		_zlibErr = inflate(&_stream, Z_NO_FLUSH);
	}

	// Update the position counter
	_pos += dataSize - _stream.avail_out;

	if (_zlibErr == Z_STREAM_END && _stream.avail_out > 0)
		_eos = true;

	return dataSize - _stream.avail_out;
}

bool GZipReadStream::eos() const {
	return _eos;
}

int32 GZipReadStream::pos() const {
	return _pos;
}

int32 GZipReadStream::size() const {
	return _origSize;
}

bool GZipReadStream::seek(int32 offset, std::ios::seekdir whence) {
	int32 newPos = 0;
	assert(whence != std::ios::end);	// std::ios::end not supported
	switch (whence) {
	case std::ios::beg:
		newPos = offset;
		break;
	case std::ios::cur:
		newPos = _pos + offset;
		break;
	default:
		assert(false);
	}

	assert(newPos >= 0);

	if ((uint32)newPos < _pos) {
		// To search backward, we have to restart the whole decompression
		// from the start of the file. A rather wasteful operation, best
		// to avoid it. :/
#if DEBUG
		warning("Backward seeking in GZipReadStream detected");
#endif
		_pos = 0;
		_wrapped->seekg(_start, std::ios_base::beg);
		_zlibErr = inflateReset(&_stream);
		if (_zlibErr != Z_OK)
			return false;	// FIXME: STREAM REWRITE
		_stream.next_in = _buf;
		_stream.avail_in = 0;
	}

	offset = newPos - _pos;

	// Skip the given amount of data (very inefficient if one tries to skip
	// huge amounts of data, but usually client code will only skip a few
	// bytes, so this should be fine.
	byte tmpBuf[1024];
	while (!err() && offset > 0) {
		offset -= read(tmpBuf, MIN((int32)sizeof(tmpBuf), offset));
	}

	_eos = false;
	return true;	// FIXME: STREAM REWRITE
}

void GZipWriteStream::processData(int flushType) {
	// This function is called by both write() and finalize().
	while (_zlibErr == Z_OK && (_stream.avail_in || flushType == Z_FINISH)) {
		if (_stream.avail_out == 0) {
			_wrapped->write((char*)_buf, BUFSIZE);
			if (_wrapped->bad()) {
				_zlibErr = Z_ERRNO;
				break;
			}

			_stream.next_out = _buf;
			_stream.avail_out = BUFSIZE;
		}
		_zlibErr = deflate(&_stream, flushType);
	}
}

GZipWriteStream::GZipWriteStream(std::ofstream *w) : _wrapped(w), _stream() {
	assert(w != 0);

	// Adding 16 to windowBits indicates to zlib that it is supposed to
	// write gzip headers. This feature was added in zlib 1.2.0.4,
	// released 10 August 2003.
	// Note: This is *crucial* for savegame compatibility, do *not* remove!
	_zlibErr = deflateInit2(&_stream,
						Z_DEFAULT_COMPRESSION,
						Z_DEFLATED,
						MAX_WBITS + 16,
						8,
				Z_DEFAULT_STRATEGY);
	assert(_zlibErr == Z_OK);

	_stream.next_out = _buf;
	_stream.avail_out = BUFSIZE;
	_stream.avail_in = 0;
	_stream.next_in = 0;
}

GZipWriteStream::~GZipWriteStream() {
	finalize();
	deflateEnd(&_stream);
}

bool GZipWriteStream::err() const {
	// CHECKME: does Z_STREAM_END make sense here?
	return (_zlibErr != Z_OK && _zlibErr != Z_STREAM_END) || _wrapped->bad();
}

void GZipWriteStream::finalize() {
	if (_zlibErr != Z_OK)
		return;

	// Process whatever remaining data there is.
	processData(Z_FINISH);

	// Since processData only writes out blocks of size BUFSIZE,
	// we may have to flush some stragglers.
	uint remainder = BUFSIZE - _stream.avail_out;
	if (remainder > 0) {
		_wrapped->write((char*)_buf, remainder);
		if (_wrapped->bad())
			_zlibErr = Z_ERRNO;
	}

	// Finalize the wrapped savefile, too
	_wrapped->flush();
}

uint32 GZipWriteStream::write(const void *dataPtr, uint32 dataSize) {
	if (err())
		return 0;

	// Hook in the new data ...
	// Note: We need to make a const_cast here, as zlib is not aware
	// of the const keyword.
	_stream.next_in = const_cast<byte *>((const byte *)dataPtr);
	_stream.avail_in = dataSize;

	// ... and flush it to disk
	processData(Z_NO_FLUSH);

	return dataSize - _stream.avail_in;
}

#endif
