/*<<<***************************************************************************
The MIT License (MIT)

Copyright(C) 2013 - 2015 Marc Mirti Mancinelli, Christophe Lardau and Philippe Pamart (xdcube.org)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

!! IMPORTANT !!
This plugin/software uses and integrates the following sources which are 
obviously not covered/excluded from this license: 
- Independent JPEG Group's software library 6b used for jpeg support 
	(folder jpeg-6b, see README.txt for license)
- Glenn Randers- Pehrson's libpng-1.2.8
	(folder libpng-128, see LICENSE.txt)
- Jean-loup Gailly/Mark Adler's zlib-1.2.3 for png support and gzip compression
	(folder zlib-123, see README.txt for license)

For this source, please refer to their own license.
////////////////////////////////////////////////////////////////////////////////
#Title: xdcCompGzip
#Project: xdcPicStd
////////////////////////////////////////////////////////////////////////////////
#Author: Marc MIRTI MANCINELLI
#History:
<15/09/2013: creation.
12/02/2014: this notice inserted.
***************************************************************************>>>*/
#include "stdafx.h"
#include "xdcPicStd.h"
#include "xdcCompGzip.h"
#include <xbConfig.h>

//http://www.zlib.net/
//http://www.ietf.org/rfc/rfc1952.txt (format gzip)

static uchar const gz_magic[2] = {0x1f, 0x8b}; /* gzip magic header */

/* gzip flag byte */
#define gz_ASCII_FLAG   0x01 /* bit 0 set: file probably ascii text */
#define gz_HEAD_CRC     0x02 /* bit 1 set: header CRC present */
#define gz_EXTRA_FIELD  0x04 /* bit 2 set: extra field present */
#define gz_ORIG_NAME    0x08 /* bit 3 set: original file name present */
#define gz_COMMENT      0x10 /* bit 4 set: file comment present */
#define gz_RESERVED     0xE0 /* bits 5..7: reserved */


//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIP : base for compression and decompression
//////////////////////////////////////////////////////////////////////////////////////////
ulong eproGZIP::getCodePage() const
{
	if (m_cp == CP_ACP)
		m_cp = g_xbCfgEngine.GetRealCP(m_cp);
	return m_cp;
}


void eproGZIP::Close(bool bFreeMem)
{
	m_bStarted = false;

	if (bFreeMem)
		resetMemory();
}

ulong eproGZIP::GetCaps() const //cf xbCOMPRESSCAPS_
{
	ulong caps = 0;
/*	if (m_bAcceptHeaderParam)
		caps |= (xbCOMPRESSCAPS_HEADERPARAM|xbCOMPRESSCAPS_HEADERPARAM_ATSTART);
	if (m_bAcceptFileList)
		caps |= xbCOMPRESSCAPS_FILELIST;
	if (m_bAcceptFileParam)
		caps |= (xbCOMPRESSCAPS_FILEPARAM|xbCOMPRESSCAPS_FILEPARAM_ATSTART);
*/
	return caps;
}

bool eproGZIP::EnumFile(int no, xbString *pPath, ulong *pLength,ulong cp) const
{
	return false;
}

int eproGZIP::GetFileCount() const
{
	return 0;
}

bool eproGZIP::EnumFileParam(int no, xbString *pName, xbString *pVal,ulong cp) const
{
	return true;
}

bool eproGZIP::EnumHeaderParam(int no, xbString *pName, xbString *pVal,ulong cp) const
{
	return false;
/*	if (no > m_arrParam.Length)
		return false;

	if (pName)
		xbCPconvert(getCodePage(),m_arrParam[no].name,cp,pName);
	if (pVal)
		xbCPconvert(getCodePage(),m_arrParam[no].value,cp,pVal);

	return true;*/
}

bool eproGZIP::GetFileParam(const xbString &name, xbString *pVal,ulong cp,xbOpMode opm) const
{
	if (opm == xbOp_Required)
		xbThrow xbEcompress("GZIP compression: can't get file parameter '"+name+"' because there is no current file","eproGZIP::GetFileParam()"); 
	return false;
}

bool eproGZIP::GetHeaderParam(const xbString &name, xbString *pVal,ulong cp,xbOpMode opm) const
{
	if (opm == xbOp_Required)
		xbThrow xbEcompress("GZIP compression: header parameter '"+name+"' doesn't exist","eproGZIP::GetHeaderParam()"); 
	return false;
}


//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIPcompress : compression base
//////////////////////////////////////////////////////////////////////////////////////////
void eproGZIPcompress::SetProgressCb(IxbCompressProgress *pProgressCb)
{
	if (pProgressCb)
	{
		m_prProgressCb = pProgressCb;
		m_bProgressCb = true;
		return;
	}
	m_prProgressCb.Empty();
	m_bProgressCb = false;
}

void eproGZIPcompress::Close(bool bFreeMem)
{
	m_prProgressCb.Empty();
	m_bProgressCb = false;
	eproGZIP::Close(bFreeMem);
}

xbString eproGZIPcompress::GetCompressLabel(bool bShort) const
{
	xbString ret("GZIP compression");
	if (!bShort)
		ret += " (level: "+xbString(m_compressLevel)+")";
	return ret;
}

bool eproGZIPcompress::GetCompressLevelRange(int *pMin, int *pMax, int *pDflt) const
{
	if (pMin) 
		*pMin = 1;
	if (pMax) 
		*pMax = 9;
	if (pDflt)
		*pDflt = 6;
	return true;
}

bool eproGZIPcompress::GetCompressBlockRange(int *pMin, int *pMax, int *pDflt) const
{
	if (pMin) 
		*pMin = eproGZIP_BLOCKSIZE_MIN;
	if (pMax) 
		*pMax = eproGZIP_BLOCKSIZE_MAX;
	if (pDflt)
		*pDflt = eproGZIP_BLOCKSIZE;
	return true;
}

bool eproGZIPcompress::EnumCompressSetting(int no, xbString *pName, xbString *pLabel, int *pMin, int *pMax, int *pDflt) const
{
	if (no == 1)
	{
		if (pName)
			*pName = "memoryLevel";
		if (pLabel)
			*pLabel = "Memory usage level";
		if (pMin)
			*pMin = 1;
		if (pMax)
			*pMax = 9;
		if (pDflt)
			*pDflt = 8;
		return true;
	}
	if (no == 2)
	{
		if (pName)
			*pName = "windowBits";
		if (pLabel)
			*pLabel = "Window size in bits";
		if (pMin)
			*pMin = 8;
		if (pMax)
			*pMax = 15;
		if (pDflt)
			*pDflt = 15;
		return true;
	}

	return false;
}

void eproGZIPcompress::SetCompressLevel(int compressLevel)
{
	if (m_bStarted)
		xbThrow xbEcompress("GZIP compression: can't change compression level after compression is started","eproGZIPcompressPush::SetCompressLevel("+xbString(compressLevel)+")"); 
	switch (compressLevel)
	{
		case xbCompressLow:
		case 1:
			compressLevel = 1;
			break;
		
		case 2:							
			break;
		
		case 3:
			break;

		case xbCompressMid:
		case 4:
			compressLevel = 4;
			break;
		
		case 5:
			break;

		case xbCompressDefault:
		case 6:
			compressLevel = 6;
			break;

		case 7:
			break;

		case 8:
			break;

		case xbCompressHigh:
		case 9:
			compressLevel = 9;
			break;

		default:
			xbThrow xbEcompress("GZIP compression: Bad compression level","eproGZIPcompressPush::SetCompressionLevel("+xbString(compressLevel)+")"); 
	}
	m_compressLevel = compressLevel;
}

int eproGZIPcompress::GetCompressLevel() const
{
	return m_compressLevel;
}

void eproGZIPcompress::SetCompressBlock(int compressBlock)
{
	if (m_bStarted)
		xbThrow xbEcompress("GZIP compression: can't change compression block size after compression is started","eproGZIPcompressPush::SetCompressBlock("+xbString(compressBlock)+")"); 
	switch (compressBlock)
	{
		case xbCompressLow:
			m_cByteBlock = eproGZIP_BLOCKSIZE_MIN;
			break;
		case xbCompressDefault:
			m_cByteBlock = eproGZIP_BLOCKSIZE;
			break;
		case xbCompressMid:
			m_cByteBlock = eproGZIP_BLOCKSIZE_MID;
			break;
		case xbCompressHigh:
			m_cByteBlock = eproGZIP_BLOCKSIZE_HIGH;
			break;
		default:
			compressBlock = (compressBlock/4)*4;
			if (compressBlock < eproGZIP_BLOCKSIZE_MIN
				|| compressBlock > eproGZIP_BLOCKSIZE_MAX)
				xbThrow xbEcompress("GZIP compression: Bad compression block size","eproGZIPcompressPush::SetCompressionLevel("+xbString(compressBlock)+")"); 
			m_cByteBlock = compressBlock;
			break;
	}
}

int eproGZIPcompress::GetCompressBlock() const
{
	return m_cByteBlock;
}

bool eproGZIPcompress::SetCompressSetting(LPCSTR pszName,int val,xbOpMode opm)
{
	if (m_bStarted)
		xbThrow xbEcompress("GZIP compression: can't change settings after compression is started","eproGZIPcompressPush::SetCompressSetting()"); 
	if (stricmp(pszName,"memoryLevel")==0)
	{
		switch(val)
		{
		case xbCompressLow:
			val = 1;
			break;
		case xbCompressDefault:
			val = 8;
			break;
		case xbCompressMid:
			val = 6;
			break;
		case xbCompressHigh:
			val = 9;
			break;
		default:
			break;
		}
		if (val < 1 || val > 9)
			xbThrow xbEcompress("GZIP compression: Bad memory level","GZIP.memoryLevel="+xbString(val)); 
		m_compressMemLevel = val;
		return true;
	}
	if (stricmp(pszName,"windowBits")==0)
	{
		switch(val)
		{
		case xbCompressLow:
			val = 8;
			break;
		case xbCompressDefault:
			val = 15;
			break;
		case xbCompressMid:
			val = 12;
			break;
		case xbCompressHigh:
			val = 15;
			break;
		default:
			break;
		}
		if (val < 8 || val > 15)
			xbThrow xbEcompress("GZIP compression: Bad window bits","GZIP.windowBits="+xbString(val)); 
		m_compressWindowBits = val;
		return true;
	}
	if (opm == xbOp_Required)
		xbThrow xbEcompress(E_NOTIMPL,"GZIP compression: setting "+xbString(pszName)+" unknown","eproGZIPcompressPush::SetCompressSetting()"); 
	return false;
}

int eproGZIPcompress::GetCompressSetting(LPCSTR pszName,bool *pbExist) const
{
	if (stricmp(pszName,"memoryLevel")==0)
	{
		if (pbExist)
			*pbExist = true;
		return m_compressMemLevel;
	}
	if (stricmp(pszName,"windowBits")==0)
	{
		if (pbExist)
			*pbExist = true;
		return m_compressWindowBits;
	}
	if (!pbExist)
		xbThrow xbEcompress(E_NOTIMPL,"GZIP compression: setting "+xbString(pszName)+" unknown","eproGZIPcompressPush::GetCompressSetting()"); 
	*pbExist = false;
	return -1;
}

void eproGZIPcompress::ResetCompressSettings(int compressLevel)
{
	if (m_bStarted)
		xbThrow xbEcompress("GZIP compression: can't reset settings after compression is started","eproGZIPcompressPush::ResetCompressSettings("+xbString(compressLevel)+")"); 
	m_compressMemLevel = 8;
	m_compressWindowBits = MAX_WBITS;
	m_cByteBlock = eproGZIP_BLOCKSIZE;
	SetCompressLevel(compressLevel);
}

void eproGZIPcompress::SetCodePage(ulong cp)
{
	if (m_bStarted)
		xbThrow xbEcompress("GZIP compression: can't change code page after compression is started","eproGZIPcompressPush::SetCodePage("+xbString(cp)+")"); 
	m_cp = g_xbCfgEngine.GetRealCP(cp);
}

//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIPcompressPush : compression push mode
//////////////////////////////////////////////////////////////////////////////////////////
bool eproGZIPcompressPush::NewFile(const xbString &path,ulong length,ulong cp,xbOpMode addopm)
{
	return false;
}

bool eproGZIPcompressPush::AddFileParam(const xbString &name, const xbString &val,ulong cp,xbOpMode addopm)
{
	return false;
}

bool eproGZIPcompressPush::AddHeaderParam(const xbString &name, const xbString &val,ulong cp,xbOpMode addopm)
{
	return false;
}

void eproGZIPcompressPush::New(IxbOutputStream *pStm)
{
	Close(false);
	m_prStm = pStm;
}

void eproGZIPcompressPush::SetOutputStream(IxbOutputStream *pStm)
{
	m_prStm = pStm;
}

void eproGZIPcompressPush::SetNotifCb(IxbCompressNotif *pNotifCb)
{
	if (pNotifCb)
	{
		m_prNotifCb = pNotifCb;
		m_bNotifCb = true;
		return;
	}
	m_prNotifCb.Empty();
	m_bNotifCb = false;
}

bool eproGZIPcompressPush::GetOutputStream(xbRefPtr<IxbOutputStream> *pprStm,xbOpMode opm)
{
	if (m_prStm.IsEmpty())
	{
		if (opm == xbOp_Required)
			xbThrow xbEcompress("GZIP compression: no output stream defined","eproGZIPcompressPush::GetOutputStream()"); 
		return false;
	}
	if (pprStm)
		*pprStm = m_prStm;
	return true;
}

void eproGZIPcompressPush::CompressFlush()
{
	flush();
}

void eproGZIPcompressPush::Close(bool bFreeMem)
{
	if (m_prStm.IsValid() && m_bStarted)
	{
		flush(true,true);
		
		if (initNotifCb())
			m_prNotifCb->onCompressClose(this);
	}
	m_prStm.Empty();
	m_prNotifCb.Empty();
	m_bNotifCb = false;
	eproGZIPcompress::Close(bFreeMem);
}

bool eproGZIPcompressPush::CanWrite() const
{
	return true;
}

ulong eproGZIPcompressPush::Write(const void *pData,ulong DataLg,bool DataLg_Required)
{
	dataCompressWrite(pData,DataLg);
	return DataLg;
}

ulong eproGZIPcompressPush::WriteStream(IxbInputStream *pstm)
{
	if (!pstm->CanRead()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP compression: WriteStream failed, permission denied reading stream");
	if (!CanWrite()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP compression: WriteStream failed, permission denied writing stream");

	ulong	data_wr = 0;
	char	buf[xbSTREAM_BUFSIZE];

	for(;;)
	{
		ulong	data_rd;
		if ((data_rd = pstm->Read(buf,xbSTREAM_BUFSIZE,false)) <= 0) break;
		dataCompressWrite(buf,data_rd);
		data_wr += data_rd;
	}

	return data_wr;
}

ulong eproGZIPcompressPush::WriteStream(IxbInputStream *pstm,ulong DataLg,bool DataLg_Required)
{
	if (!pstm->CanRead()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP compression: WriteStream failed, permission denied reading stream");
	if (!CanWrite()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP compression: WriteStream failed, permission denied writing stream");

	ulong data_towr = DataLg;
	char	buf[xbSTREAM_BUFSIZE];

	while(data_towr>0)
	{
		ulong	data_wr = data_towr>xbSTREAM_BUFSIZE?xbSTREAM_BUFSIZE:data_towr;
		if ((data_wr = pstm->Read(buf,data_wr,false)) <= 0) break;
		dataCompressWrite(buf,data_wr);
		data_towr -= data_wr;
	}
	if (DataLg_Required && data_towr != 0)
		xbThrow xbEcompress(STG_E_WRITEFAULT,"GZIP compression: WriteStream failed");

	return DataLg - data_towr;
}

ulong eproGZIPcompressPush::WriteString(const char *pStr,ulong lgStr,bool bAddLf,IxbStreamMode *pMode)
{
	if (!bAddLf && (!pMode || !pMode->CRLFasLF))
		return Write(pStr,lgStr);

	xbString str(pStr,lgStr);
	if (bAddLf) str += '\n';
	if (pMode && pMode->CRLFasLF)
	{
		int pos = -1;
		while((pos = str.IndexOf('\n',pos+1)) >= 0)
		{
			if (pos > 0 && str.unsafeGetAt(pos-1) == '\r') 
				continue;
			str.Insert(pos,'\r');
		}
	}

	dataCompressWrite((LPCSTR)str,str.Length);
	return str.Length;
}

ulong eproGZIPcompressPush::WriteStringW(const wchar *pStr,ulong lgStr,bool bAddLf,IxbStreamMode *pMode)
{
	xbThrow xbEcompress(E_NOTIMPL,"GZIP compression: WriteStringW not supported","eproGZIPcompressPush::WriteStringW()"); 
}

bool eproGZIPcompressPush::initNotifCb()
{
	if (m_bNotifCb)
		return m_prNotifCb.IsValid();
	m_bNotifCb = true;
	m_prNotifCb = xbRefQIPtr<IxbCompressNotif>(m_prStm,xbOp_Optional);
	return m_prNotifCb.IsValid();
}

bool eproGZIPcompressPush::initProgressCb()
{
	if (m_bProgressCb)
		return m_prProgressCb.IsValid();
	m_bProgressCb = true;
	m_prProgressCb = xbRefQIPtr<IxbCompressProgress>(m_prStm,xbOp_Optional);
	return m_prProgressCb.IsValid();
}

void eproGZIPcompressPush::flush(bool bEnd,bool bClose)
{
	dataCompressFlush(bEnd,bClose);

	if (initNotifCb())
		m_prNotifCb->onCompressFlush(this,bEnd,bClose);
}

#define zlibCheck(err) _zlibCheck(#err,err)

void zlibThrow(LPCSTR pCtx, int err)
{
	switch(err)
	{
	case Z_STREAM_ERROR:
		xbThrow xbEcompressCorrupt("GZIP compression failed", xbString(pCtx)+", stream error"); 
	case Z_DATA_ERROR:
		xbThrow xbEcompressCorrupt("GZIP compression failed", xbString(pCtx)+", data error"); 
	case Z_STREAM_END:
		xbThrow xbEcompressCorrupt("GZIP compression failed", xbString(pCtx)+", stream end"); 
	case Z_MEM_ERROR:
		xbThrow xbEcompress(E_OUTOFMEMORY,"GZIP compression failed, out of memory", xbString(pCtx)); 
	default:
		xbThrow xbEcompress("GZIP compression failed", xbString(pCtx)+", error code: "+xbString(int(err))); 
	}
}
inline void _zlibCheck(LPCSTR pCtx, int err)
{
	if (err == Z_OK)
		return;
	zlibThrow(pCtx,err);
}

voidpf eprobaseAllocFunc(voidpf opaque, uInt items, uInt size)
{
	return g_xbEngineCenter.m_heap.xbAlloc(items * size);
}

void eprobaseFreeFunc(voidpf opaque, voidpf address)
{
	return g_xbEngineCenter.m_heap.Free(address);
}

void eproGZIPcompressPush::onCompressStreamStart()
{
	if (m_prStm.IsEmpty())
		xbThrow xbEcompress("GZIP compression failed", "No output stream"); 
	zStreamEnd();

	memset(&m_zStream,0,sizeof(m_zStream));
	m_zStream.zalloc = eprobaseAllocFunc;
	m_zStream.zfree = eprobaseFreeFunc;

	m_bZstreamInit = true;

	zlibCheck(deflateInit2(&m_zStream, m_compressLevel, Z_DEFLATED, -m_compressWindowBits, m_compressMemLevel, Z_DEFAULT_STRATEGY));

	m_crc = 0;
	m_cByteComp = 0;
	m_cByteReal = 0;
	m_zStream.avail_out = m_cByteBlock;

	m_zStream.next_out = (Bytef*)(LPCVOID)m_compData.GetWritePtr(m_cByteBlock);

	{
		gz_Header hdr;
		memset(&hdr,0,sizeof(hdr));
		hdr.magic[0] = gz_magic[0];
		hdr.magic[1] = gz_magic[1];
		hdr.compressionMethod = Z_DEFLATED;
		hdr.os = 255;

		m_prStm->Write(&hdr,sizeof(hdr));
	}
}

void eproGZIPcompressPush::dataCompressWrite(const void *_pData,ulong DataLg)
{
	if (!m_bStarted)
	{
		onCompressStreamStart();
		m_bStarted = true;
	}

	m_zStream.avail_in = DataLg;
	m_zStream.next_in = (Bytef*)_pData;

    while (m_zStream.avail_in != 0) 
	{
        if (m_zStream.avail_out == 0) 
		{
            m_zStream.next_out = (Bytef*)(LPCVOID)m_compData;

			m_prStm->Write(m_compData,m_cByteBlock);

			m_zStream.avail_out = m_cByteBlock;
        }
        m_cByteReal += m_zStream.avail_in;
        m_cByteComp += m_zStream.avail_out;
        zlibCheck(deflate(&m_zStream, Z_NO_FLUSH));
        m_cByteReal -= m_zStream.avail_in;
        m_cByteComp -= m_zStream.avail_out;
    }
	if (DataLg>0)
	{
		m_crc = crc32(m_crc, (const Bytef *)_pData, DataLg);
		xbRdbgAssert(m_crc!=0);
	}
}

void eproGZIPcompressPush::dataCompressFlush(bool bEnd,bool bClose)
{
    m_zStream.avail_in = 0; /* should be zero already anyway */

	ulong len;
	bool bDone = false;
	int z_err;

	for (;;) 
	{
        len = m_cByteBlock - m_zStream.avail_out;

        if (len != 0)
		{
			m_prStm->Write(m_compData, len);

			m_zStream.next_out = (Bytef*)(LPCVOID)m_compData;
            m_zStream.avail_out = m_cByteBlock;
        }
        if (bDone) 
			break;
        m_cByteComp += m_zStream.avail_out;
		z_err = deflate(&m_zStream, bClose?Z_FINISH:Z_SYNC_FLUSH /*Z_NO_FLUSH*/);
        m_cByteComp -= m_zStream.avail_out;

        /* Ignore the second of two consecutive flushes: */
        if (len == 0 && z_err == Z_BUF_ERROR) z_err = Z_OK;

        /* deflate has finished flushing only when it hasn't used up
         * all the available space in the output buffer:
         */
        bDone = (m_zStream.avail_out != 0 ||z_err == Z_STREAM_END);

        if (z_err != Z_OK && z_err != Z_STREAM_END)
			zlibThrow("flush",z_err);
	}
	if (bClose)
	{
//		xbRdbgAssert(m_crc!=0);
		m_prStm->WriteUlong(m_crc);
		m_prStm->WriteUlong(m_cByteReal);
	}
}



///* allocate deflate state */
//    strm.zalloc = Z_NULL;
//    strm.zfree = Z_NULL;
//    strm.opaque = Z_NULL;
//    ret = deflateInit(&strm, level);
//    if (ret != Z_OK)
//        return ret;
//
//    /* compress until end of file */
//    do {
//        strm.avail_in = fread(in, 1, CHUNK, source);
//        if (ferror(source)) {
//            (void)deflateEnd(&strm);
//            return Z_ERRNO;
//        }
//        flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
//        strm.next_in = in;
//        /* run deflate() on input until output buffer not full, finish
//           compression if all of source has been read in */
//        do {
//            strm.avail_out = CHUNK;
//            strm.next_out = out;
//            ret = deflate(&strm, flush);    /* no bad return value */
//            assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
//
//            have = CHUNK - strm.avail_out;
//            if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
//                (void)deflateEnd(&strm);
//                return Z_ERRNO;
//            }
//        } while (strm.avail_out == 0);
//        assert(strm.avail_in == 0);     /* all input will be used */
//        /* done when last data in file processed */
//    } while (flush != Z_FINISH);
//    assert(ret == Z_STREAM_END);        /* stream will be complete */
//
//    /* clean up and return */
//    (void)deflateEnd(&strm);
//    return Z_OK;
//}

//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIPdecompress : decompression base
//////////////////////////////////////////////////////////////////////////////////////////
void eproGZIPdecompress::SetProgressCb(IxbDecompressProgress *pProgressCb)
{
	if (pProgressCb)
	{
		m_prProgressCb = pProgressCb;
		m_bProgressCb = true;
		return;
	}
	m_prProgressCb.Empty();
	m_bProgressCb = false;
}

void eproGZIPdecompress::Close(bool bFreeMem)
{
	m_prProgressCb.Empty();
	m_bProgressCb = false;
	eproGZIP::Close(bFreeMem);
}

xbString eproGZIPdecompress::GetCompressLabel(bool bShort) const
{
	xbString ret("GZIP decompression");
	return ret;
}

//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIPdecompressPush : decompression
//////////////////////////////////////////////////////////////////////////////////////////
void eproGZIPdecompressPush::New()
{
	Close(false);
}

void eproGZIPdecompressPush::SetOutputStream(IxbOutputStream *pStm)
{
	m_prStm = pStm;
}


void eproGZIPdecompressPush::SetNotifCb(IxbDecompressNotif *pNotifCb)
{
	if (pNotifCb)
	{
		m_prNotifCb = pNotifCb;
		m_bNotifCb = true;
		return;
	}
	m_prNotifCb.Empty();
	m_bNotifCb = false;
}

bool eproGZIPdecompressPush::GetOutputStream(xbRefPtr<IxbOutputStream> *pprStm,xbOpMode opm)
{
	if (m_prStm.IsEmpty())
	{
		if (opm == xbOp_Required)
			xbThrow xbEcompress("GZIP decompression: no output stream defined","eproGZIPcompressPush::GetOutputStream()"); 
		return false;
	}
	if (pprStm)
		*pprStm = m_prStm;
	return true;
}

void eproGZIPdecompressPush::DecompressFlush()
{
	flush();
}

void eproGZIPdecompressPush::Close(bool bFreeMem)
{
	if (m_prStm.IsValid() && m_bStarted)
	{
		flush(true,true);
		
		if (initNotifCb())
			m_prNotifCb->onDecompressClose(this);
	}
	m_prStm.Empty();
	m_prNotifCb.Empty();
	m_bNotifCb = false;

	eproGZIPdecompress::Close(bFreeMem);
}

bool eproGZIPdecompressPush::CanWrite() const
{
	return true;
}

ulong eproGZIPdecompressPush::Write(const void *pData,ulong DataLg,bool DataLg_Required)
{
	dataDecompressWrite(pData,DataLg);
	return DataLg;
}

ulong eproGZIPdecompressPush::WriteStream(IxbInputStream *pstm)
{
	if (!pstm->CanRead()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP decompression: WriteStream failed, permission denied reading stream");
	if (!CanWrite()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP decompression: WriteStream failed, permission denied writing stream");

	ulong	data_wr = 0;
	char	buf[xbSTREAM_BUFSIZE];

	for(;;)
	{
		ulong	data_rd;
		if ((data_rd = pstm->Read(buf,xbSTREAM_BUFSIZE,false)) <= 0) break;
		dataDecompressWrite(buf,data_rd);
		data_wr += data_rd;
	}

	return data_wr;
}

ulong eproGZIPdecompressPush::WriteStream(IxbInputStream *pstm,ulong DataLg,bool DataLg_Required)
{
	if (!pstm->CanRead()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP decompression: WriteStream failed, permission denied reading stream");
	if (!CanWrite()) xbThrow xbEcompress(STG_E_ACCESSDENIED,"GZIP decompression: WriteStream failed, permission denied writing stream");

	if (DataLg<=0) return 0;

	ulong data_towr = DataLg;
	char	buf[xbSTREAM_BUFSIZE];

	while(data_towr>0)
	{
		ulong	data_wr = data_towr>xbSTREAM_BUFSIZE?xbSTREAM_BUFSIZE:data_towr;
		if ((data_wr = pstm->Read(buf,data_wr,false)) <= 0) break;
		dataDecompressWrite(buf,data_wr);
		data_towr -= data_wr;
	}
	if (DataLg_Required && data_towr != 0)
		xbThrow xbEcompress(STG_E_WRITEFAULT,"GZIP decompression: WriteStream failed");

	return DataLg - data_towr;
}

ulong eproGZIPdecompressPush::WriteString(const char *pStr,ulong lgStr,bool bAddLf,IxbStreamMode *pMode)
{
	if (!bAddLf && (!pMode || !pMode->CRLFasLF))
		return Write(pStr,lgStr);

	xbString str(pStr,lgStr);
	if (bAddLf) str += '\n';
	if (pMode && pMode->CRLFasLF)
	{
		int pos = -1;
		while((pos = str.IndexOf('\n',pos+1)) >= 0)
		{
			if (pos > 1 && str.unsafeGetAt(pos-1) == '\r') 
				continue;
			str.Insert(pos,'\r');
		}
	}

	dataDecompressWrite((LPCSTR)str,str.Length);
	return str.Length;
}

ulong eproGZIPdecompressPush::WriteStringW(const wchar *pStr,ulong lgStr,bool bAddLf,IxbStreamMode *pMode)
{
	xbThrow xbEcompress(E_NOTIMPL,"GZIP decompression: WriteStringW not supported","eproGZIPdecompressPush::WriteStringW()"); 
}

bool eproGZIPdecompressPush::initNotifCb()
{
	if (m_bNotifCb)
		return m_prNotifCb.IsValid();
	m_bNotifCb = true;
	m_prNotifCb = xbRefQIPtr<IxbDecompressNotif>(m_prStm,xbOp_Optional);
	return m_prNotifCb.IsValid();
}

bool eproGZIPdecompressPush::initProgressCb()
{
	if (m_bProgressCb)
		return m_prProgressCb.IsValid();
	m_bProgressCb = true;
	m_prProgressCb = xbRefQIPtr<IxbDecompressProgress>(m_prStm,xbOp_Optional);
	return m_prProgressCb.IsValid();
}

void eproGZIPdecompressPush::flush(bool bEnd,bool bClose)
{
//	tmpDataFlush(bEnd,bClose);
//	outpDataFlush(bEnd,bClose);
}

void eproGZIPdecompressPush::onDecompressStreamStart()
{
	if (m_prStm.IsEmpty())
		xbThrow xbEcompress("GZIP compression failed", "No output stream"); 
	zStreamEnd();

	memset(&m_zStream,0,sizeof(m_zStream));
	m_zStream.zalloc = eprobaseAllocFunc;
	m_zStream.zfree = eprobaseFreeFunc;

	m_bZstreamInit = true;
	zlibCheck(inflateInit2(&m_zStream, -MAX_WBITS));

	m_cByteBlock = 8192;

	m_crc = 0;
	m_cByteComp = 0;
	m_cByteReal = 0;
	m_zStream.avail_out = m_cByteBlock;

	m_zStream.next_out = (Bytef*)(LPCVOID)m_decompData.GetWritePtr(m_cByteBlock);

	m_readStep = readHeader;
	m_tmpData.Empty();

	if (initProgressCb())
		m_prProgressCb->onDecompressProgress(this,NULL,"GZIP decompression",m_cByteReal,m_cByteComp,-1);
}

void eproGZIPdecompressPush::dataDecompressWrite(const void *_pData,ulong DataLg)
{
	if (!m_bStarted)
	{
		onDecompressStreamStart();
		m_bStarted = true;
	}
	const BYTE	*pData;
	ulong		cByte;
	if(m_tmpData.IsValid())
	{
		if (DataLg)
			m_tmpData.Add(_pData,DataLg);
		pData = (const BYTE*)(LPCVOID)m_tmpData;
		cByte = m_tmpData.Length;
		_pData = NULL;
		DataLg = 0;
	}
	else
	{
		pData = (const BYTE*)_pData;
		cByte = DataLg;
	}

continueRead:
	while(cByte>0)
	{
		switch(m_readStep)
		{
		case readHeader:
			if (cByte < sizeof(gz_Header))
				goto dataNotComplete;
			memcpy(&m_gzHeader,pData,sizeof(gz_Header));
			pData+=sizeof(gz_Header);
			cByte-=sizeof(gz_Header);
			if (m_gzHeader.magic[0] != gz_magic[0]
				|| m_gzHeader.magic[1] != gz_magic[1])
				xbThrow xbEcompressCorrupt("GZIP decompression: invalid header, maybe not a gzip stream","eproGZIPdecompressPush::dataDecompressWrite()"); 
			if (m_gzHeader.compressionMethod != Z_DEFLATED || (m_gzHeader.flags & gz_RESERVED) != 0)
				xbThrow xbEcompress("GZIP decompression: gzip stream not supported","eproGZIPdecompressPush::dataDecompressWrite()"); 
			goto label_headerEnd;
		case readExtraFieldLength:
			if (cByte < sizeof(m_dataToSkip))
				goto dataNotComplete;
			memcpy(&m_dataToSkip,pData,sizeof(m_dataToSkip));
			pData+=sizeof(m_dataToSkip);
			cByte-=sizeof(m_dataToSkip);
			m_readStep = readExtraFieldData;
			continue;
		case readExtraFieldData:
			if (cByte < m_dataToSkip)
				goto dataNotComplete;
			pData+=m_dataToSkip;
			cByte-=m_dataToSkip;
			goto label_extraFieldEnd;
		case readOrigName:
			{
				int len = stopCharPos(pData,cByte);
				if (len < 0)
					goto dataNotComplete;
				pData+=len;
				cByte-=len;
				goto label_origNameEnd;
			}
		case readComment:
			{
				int len = stopCharPos(pData,cByte);
				if (len < 0)
					goto dataNotComplete;
				pData+=len;
				cByte-=len;
				goto label_commentEnd;
			}
		case readHeaderCrc:
			if (cByte < sizeof(ushort))
				goto dataNotComplete;
			pData+=sizeof(ushort);
			cByte-=sizeof(ushort);
			goto label_headCrcEnd;

label_headerEnd:
			if (m_gzHeader.flags & gz_EXTRA_FIELD)
			{
				m_readStep = readExtraFieldLength;
				continue;
			}

label_extraFieldEnd:
			if (m_gzHeader.flags & gz_ORIG_NAME)
			{
				m_readStep = readOrigName;
				continue;
			}

label_origNameEnd:
			if (m_gzHeader.flags & gz_COMMENT)
			{
				m_readStep = readComment;
				continue;
			}

label_commentEnd:
			if (m_gzHeader.flags & gz_HEAD_CRC)
			{
				m_readStep = readHeaderCrc;
				continue;
			}

label_headCrcEnd:
			m_readStep = readStream;
			continue;
		case readStream:
			m_zStream.avail_in = cByte;
			m_zStream.next_in = (Bytef*)pData;

			while (m_zStream.avail_in != 0) 
			{
				m_cByteComp += m_zStream.avail_in;
				m_cByteReal += m_zStream.avail_out;
				int z_err = inflate(&m_zStream, Z_NO_FLUSH);
				m_cByteComp -= m_zStream.avail_in;
				m_cByteReal -= m_zStream.avail_out;
				if (m_zStream.avail_out < m_cByteBlock) 
				{
					m_zStream.next_out = (Bytef*)(LPCVOID)m_decompData;

					m_prStm->Write(m_decompData,m_cByteBlock - m_zStream.avail_out);

					m_crc = crc32(m_crc, (const Bytef *)(LPCVOID)m_decompData, m_cByteBlock - m_zStream.avail_out);
					xbRdbgAssert(m_crc!=0);

					m_zStream.avail_out = m_cByteBlock;
				}
				if (z_err == Z_STREAM_END)
				{
					int len = cByte - m_zStream.avail_in;

					pData+=len;
					cByte-=len;
					m_readStep = readStreamCrc;
					goto continueRead;
				}
				if (z_err != Z_OK)
					zlibThrow("inflate",z_err);
			}

			m_tmpData.Empty();
			return;
		case readStreamCrc:
			{
				ulong crc[2];
				if (cByte < sizeof(crc))
					goto dataNotComplete;
				memcpy(&crc,pData,sizeof(crc));
				pData+=sizeof(crc);
				cByte-=sizeof(crc);
				m_readStep = readEnd;
				//xbRdbgAssert(crc[0] == m_crc);
				if (crc[0] != m_crc || crc[1] != m_cByteReal)
					xbThrow xbEcompressCorrupt("GZIP decompression: stream corrupted",xbFormat("eproGZIPdecompressPush::dataDecompressWrite(), crc=%08lX/%08lX, size=%08lX/%08lX",m_crc,crc[0],m_cByteReal,crc[1])); 
			}
			continue;
		default:
			xbThrow xbEcompressCorrupt("GZIP decompression: invalid read step","eproGZIPdecompressPush::dataDecompressWrite()"); 
		}
	}

	m_tmpData.Empty();
	return;
dataNotComplete:
	if (m_tmpData.IsEmpty())
	{
		if (DataLg)
			m_tmpData.Add(pData,cByte);
	}
	else
	{
		int len = int(xbNptr(pData) - xbNptr((LPCVOID)m_tmpData));
		if (len>0)
			m_tmpData.Delete(0,len);
	}
	return;
}

//////////////////////////////////////////////////////////////////////////////////////////
// eproGZIPplug : plugin for compression and decompression
//////////////////////////////////////////////////////////////////////////////////////////
bool eproGZIPplug::CanWorkWithType(LPCSTR pszType, IxbInputStream *pStm) const
{
	if ((!pszType || !*pszType) && !pStm)
		xbThrow xbEcompress("GZIP compression : invalid argument for type detection","eproGZIPplug::CanWorkWithType()");
	if (m_bDestruction)
		throwEnd("type detection aborted","CanWorkWithType()");
	if (pStm)
	{
		xbRefQIPtr<IxbSeekStream>	prSkStm(pStm,xbOp_Optional);

		if (prSkStm.IsEmpty())
		{
			if (!pszType || !*pszType)
				return false;
		}
		else
		{
			ulong pos = prSkStm->GetPosition();
			bool bRet = false;
			for(;;)
			{
				gz_Header hdr;
				if (pStm->Read(&hdr,sizeof(hdr),false) != sizeof(hdr))
					break;

				if (memcmp(hdr.magic,gz_magic,2) != 0)
					break;
				bRet = true;
			}
			prSkStm->Seek(pos);
			return bRet;
		}
	}

	if (xbStricmp(pszType,"gzip") == 0)
		return true;
	return false;
}

bool eproGZIPplug::Decompress(IxbInputStream *pStm, ulong caps, xbRefPtr<IxbDecompressPull> *pprUncompStm,xbOpMode opm)
{
	if (m_bDestruction)
		throwEnd("decompression aborted","Decompress(pull)");
	if (opm == xbOp_Required)
		xbThrow xbEcompress(E_NOTIMPL,"GZIP compression : decompression in pull mode not supported","eproGZIPplug::Decompress(pull)");
	return false;
}

bool eproGZIPplug::Decompress(IxbOutputStream *pStm, ulong caps, xbRefPtr<IxbDecompressPush> *pprUncompStm,xbOpMode opm)
{
	if (m_bDestruction)
		throwEnd("decompression aborted","Decompress(push)");
	if (caps != 0)
	{
		if (opm == xbOp_Required)
			xbThrow xbEcompress(E_NOTIMPL,"GZIP compression : invalid mode for decompression","eproGZIPplug::Decompress(push)");
		return false;
	}
	eproGZIPdecompressPush *plg = xbDnew eproGZIPdecompressPush();
	try
	{
		plg->setFactory(m_pFact);
		plg->Create();

		if (pStm)
		{
			plg->New();
			plg->SetOutputStream(pStm);
		}

		(*pprUncompStm) = plg;

		plg->Release();
	}
	catch(...)
	{
		plg->Release();
		throw;
	}
	return true;
}

bool eproGZIPplug::Compress(IxbOutputStream *pStm, ulong caps, xbRefPtr<IxbCompressPush> *pprCompStm,xbOpMode opm)
{
	if (m_bDestruction)
		throwEnd("compression aborted","Compress(push)");
	if (caps != 0)
	{
		if (opm == xbOp_Required)
			xbThrow xbEcompress(E_NOTIMPL,"GZIP compression : invalid mode for compression","eproGZIPplug::Compress(push)");
		return false;
	}
	eproGZIPcompressPush *plg = xbDnew eproGZIPcompressPush();
	try
	{
		plg->setFactory(m_pFact);
		plg->Create();

		if (pStm)
		{
			plg->New(pStm);
		}

		(*pprCompStm) = plg;

		plg->Release();
	}
	catch(...)
	{
		plg->Release();
		throw;
	}
	return true;
}

bool eproGZIPplug::Compress(IxbInputStream *pStm, ulong caps, xbRefPtr<IxbCompressPull> *pprCompStm,xbOpMode opm)
{
	if (m_bDestruction)
		throwEnd("compression aborted","Compress(pull)");
	if (opm == xbOp_Required)
		xbThrow xbEcompress(E_NOTIMPL,"GZIP compression : compression in pull mode not supported","eproGZIPplug::Compress(pull)");
	return false;
}

void eproGZIPplug::askClose(LPCSTR pReason)
{
	{
		xbSectLock lck(&m_sectPtr);
		if (pReason && m_closeReason.IsEmpty())
			m_closeReason = pReason;
	}
	Destroy();
}

xbString eproGZIPplug::getCloseReason() const
{
	xbSectLock lck(&m_sectPtr);
	xbString ret(m_closeReason);
	ret.OwnData();
	return ret;
}

xbString eproGZIPplug::GetCompressLabel(bool bShort) const
{
	xbString ret("GZIP compression");
	return ret;
}

bool eproGZIPplug::GetCompressLevelRange(int *pMin, int *pMax, int *pDflt) const
{
	if (pMin) 
		*pMin = 1;
	if (pMax) 
		*pMax = 9;
	if (pDflt)
		*pDflt = 6;
	return true;
}

bool eproGZIPplug::GetCompressBlockRange(int *pMin, int *pMax, int *pDflt) const
{
	if (pMin) 
		*pMin = eproGZIP_BLOCKSIZE_MIN;
	if (pMax) 
		*pMax = eproGZIP_BLOCKSIZE_MAX;
	if (pDflt)
		*pDflt = eproGZIP_BLOCKSIZE;
	return true;
}


bool eproGZIPplug::EnumCompressSetting(int no, xbString *pName, xbString *pLabel, int *pMin, int *pMax, int *pDflt) const
{
	if (no == 1)
	{
		if (pName)
			*pName = "memoryLevel";
		if (pLabel)
			*pLabel = "Memory usage level";
		if (pMin)
			*pMin = 1;
		if (pMax)
			*pMax = 9;
		if (pDflt)
			*pDflt = 8;
		return true;
	}
	if (no == 2)
	{
		if (pName)
			*pName = "windowBits";
		if (pLabel)
			*pLabel = "Window size in bits";
		if (pMin)
			*pMin = 8;
		if (pMax)
			*pMax = 15;
		if (pDflt)
			*pDflt = 15;
		return true;
	}
	return false;
}

ulong eproGZIPplug::GetCaps() const
{
	return 0;

}

void eproGZIPplug::throwEnd(const xbString &strEnd,const xbString &strCtx) const
{
	if (!g_xbCompressPlugType.isNowStarted())
		xbThrow xbEhotPlug(E_ABORT,"GZIP compression : "+strEnd+", plugin in destruction",closeReasonLabel()+xbEdebug("eproGZIPplug::"+strCtx+""));
	xbThrow xbEcompressUser(E_ABORT,"GZIP compression : "+strEnd+", operation cancelled by application",closeReasonLabel()+xbEdebug("eproGZIPplug::"+strCtx+""));
}
