#include <windows.h>
#include <stdio.h>
#include "KbGzFile.h"

//#ifdef  TGZ_SUPPORT

//#define USE_GZIO 
//gzio.c が提供する方の gzread/gzseek を使う(FILE* 経由)場合は USE_GZIO を定義する。
//gzio.c を使用するとシークが非常に遅い。

#ifndef USE_GZIO
//gzio.c を参考
//余計なコードを除去して見通しを良くする。
//gzopen/gzread/gzseek を IKpiFile 経由で使用出来るようにする。
//gzseek でシークバック（戻す）が大幅に高速化。

#include "zutil.h"

#define Z_BUFSIZE 16384

#define ALLOC(size) malloc(size)
#define TRYFREE(p) {if (p) free(p);}

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

/* gzip flag byte */
#define ASCII_FLAG   0x01 /* bit 0 set: file probably ascii text */
#define HEAD_CRC     0x02 /* bit 1 set: header CRC present */
#define EXTRA_FIELD  0x04 /* bit 2 set: extra field present */
#define ORIG_NAME    0x08 /* bit 3 set: original file name present */
#define COMMENT      0x10 /* bit 4 set: file comment present */
#define RESERVED     0xE0 /* bits 5..7: reserved */

class KbGzSeekTable;
typedef struct gz_stream {
    z_stream stream;
    int       z_err;   /* error code for last stream operation */
    int       z_eof;   /* set if end of input file */
    IKpiFile  *file;
    Byte      *inbuf;  /* input buffer */
    Byte      *outbuf; /* output buffer */
    uLong     crc;     /* crc32 of uncompressed data */
    int       transparent; /* 1 if input file is not a .gz file */
    z_off64_t start;   /* start of compressed data in file (header skipped) */
    z_off64_t in;      /* bytes into deflate or inflate */
    z_off64_t out;     /* bytes out of deflate or inflate */
    int       back;    /* one character push-back */
    int       last;    /* true if push-back is last character */
    KbGzSeekTable *pSeekTable;
} gz_stream;

class KbGzSeekTable
{
/*
inflateCopy の内部でinflateStateCheckが呼ばれる

local int inflateStateCheck(strm)
z_streamp strm;
{
    struct inflate_state FAR *state;
    if (strm == Z_NULL ||
        strm->zalloc == (alloc_func)0 || strm->zfree == (free_func)0)
        return 1;
    state = (struct inflate_state FAR *)strm->state;
    if (state == Z_NULL || state->strm != strm || //GzContext の z_stream をポインタにしておかないとここで引っかかる
        state->mode < HEAD || state->mode > SYNC)
        return 1;
    return 0;
}*/
    struct GzContext{
        z_stream stream; //これのアドレスが変わらないように注意が必要
        int       z_err;  
        int       z_eof;   
        uLong     crc;     
        int       transparent;
        z_off64_t start;
        z_off64_t in;
        z_off64_t out;
        int       back;
        int       last;
        UINT64    qwFileOffset;
    };
    enum{
        SEEK_BLOCK = 256 * 1024 //256KB ごとにテーブルを作成
    };
    GzContext **m_ppContexts;//ポインタの配列にすること
    DWORD       m_dwCapacity;//GzContextの配列にするとinflateStateCheck()に引っかかる
    DWORD       m_dwCount;   //(realloc 時に stream のアドレスが変わってしまう)
    DWORD       m_dwOffset;
public:
    KbGzSeekTable(void);
    ~KbGzSeekTable(void);
    void __fastcall SaveContext(gz_stream *pGzStream);
    void __fastcall RestoreContext(UINT64 qwOffset,
                                   gz_stream *pGzStream);
};
KbGzSeekTable::KbGzSeekTable(void)
{
    m_ppContexts = NULL;
    m_dwCapacity = 0;
    m_dwCount = 0;
    m_dwOffset = SEEK_BLOCK;
}
KbGzSeekTable::~KbGzSeekTable(void)
{
    if(m_ppContexts){
        int i;
        for(i = m_dwCount - 1; i >= 0; i--){
            inflateEnd(&m_ppContexts[i]->stream);
            free(m_ppContexts[i]);
        } 
        free(m_ppContexts);
    }
}
void __fastcall KbGzSeekTable::SaveContext(gz_stream *pGzStream)
{
    if(pGzStream->out < m_dwOffset){
        return;
    }
    if(m_dwCount >= m_dwCapacity){
        m_dwCapacity += 32;
        m_ppContexts = (GzContext**)realloc(m_ppContexts, sizeof(GzContext*)*m_dwCapacity);
        ZeroMemory(&m_ppContexts[m_dwCount], sizeof(GzContext*)*(m_dwCapacity-m_dwCount));
    }
    GzContext *pContext = (GzContext*)malloc(sizeof(GzContext));
    ZeroMemory(pContext, sizeof(GzContext));
    inflateInit2(&pContext->stream, -MAX_WBITS);
    inflateCopy(&pContext->stream, &pGzStream->stream);
    m_ppContexts[m_dwCount++] = pContext;

    pContext->z_err = pGzStream->z_err;
    pContext->z_eof = pGzStream->z_eof;
    pContext->crc   = pGzStream->crc;
    pContext->transparent = pGzStream->transparent;
    pContext->start = pGzStream->start;
    pContext->in = pGzStream->in;
    pContext->out = pGzStream->out;
    pContext->back = pGzStream->back;
    pContext->last = pGzStream->last;
    pContext->qwFileOffset = pGzStream->file->Seek(0, FILE_CURRENT);
    m_dwOffset += SEEK_BLOCK;
}
void __fastcall KbGzSeekTable::RestoreContext(UINT64 qwOffset,
                                              gz_stream *pGzStream)
{
    if(!m_ppContexts){
        return;
    }
    if(qwOffset >= pGzStream->out){
        if(qwOffset < pGzStream->out + SEEK_BLOCK){
            return;
        }
    }
    GzContext *pContext = NULL;
    int i;
    for(i = m_dwCount - 1; i >= 0; i--){
        if(m_ppContexts[i]->out <= qwOffset){
            pContext = m_ppContexts[i];
            break;
        }
    }
    if(!pContext){
        return;
    }
    inflateCopy(&pGzStream->stream, &pContext->stream);
    pGzStream->z_err = pContext->z_err;
    pGzStream->z_eof = pContext->z_eof;
    pGzStream->crc   = pContext->crc;
    pGzStream->transparent = pContext->transparent;
    pGzStream->start = pContext->start;
    pGzStream->in    = pContext->in;
    pGzStream->out   = pContext->out;
    pGzStream->back  = pContext->back;
    pGzStream->last  = pContext->last;
    pGzStream->file->Seek(pContext->qwFileOffset, FILE_BEGIN);
}

local int    get_byte     OF((gz_stream *s));
local void   check_header OF((gz_stream *s));
local int    destroy      OF((gz_stream *s));
local uLong  getLong      OF((gz_stream *s));

/* ===========================================================================
     Opens a gzip (.gz) file for reading or writing. The mode parameter
   is as in fopen ("rb" or "wb"). The file is given either by file descriptor
   or path name (if fd == -1).
     gz_open returns NULL if the file could not be opened or if there was
   insufficient memory to allocate the (de)compression state; errno
   can be checked to distinguish the two cases (if errno is zero, the
   zlib error is Z_MEM_ERROR).
*/
local gzFile gzopen(IKpiFile *pFile)
{
    int err;
    int level = Z_DEFAULT_COMPRESSION; /* compression level */
    int strategy = Z_DEFAULT_STRATEGY; /* compression strategy */
    gz_stream *s;

    s = (gz_stream *)ALLOC(sizeof(gz_stream));
    if (!s) return Z_NULL;
    ZeroMemory(s, sizeof(gz_stream));
/*    
    s->stream.zalloc = (alloc_func)0;
    s->stream.zfree = (free_func)0;
    s->stream.opaque = (voidpf)0;
    s->stream.next_in = s->inbuf = Z_NULL;
    s->stream.next_out = s->outbuf = Z_NULL;
    s->stream.avail_in = s->stream.avail_out = 0;
    s->file = NULL;
    s->z_err = Z_OK;
    s->z_eof = 0;
    s->in = 0;
    s->out = 0;
    s->crc = 0;
    s->transparent = 0;*/
    s->back = EOF;//これだけ 0 でない

    {
        s->stream.next_in  = s->inbuf = (Byte*)ALLOC(Z_BUFSIZE);

        err = inflateInit2(&(s->stream), -MAX_WBITS);
        /* windowBits is passed < 0 to tell that there is no zlib header.
         * Note that in this case inflate *requires* an extra "dummy" byte
         * after the compressed stream in order to complete decompression and
         * return Z_STREAM_END. Here the gzip CRC32 ensures that 4 bytes are
         * present after the compressed stream.
         */
        if (err != Z_OK || s->inbuf == Z_NULL) {
            return destroy(s), (gzFile)Z_NULL;
        }
    }
    s->stream.avail_out = Z_BUFSIZE;
    s->file = pFile;
    s->pSeekTable = new KbGzSeekTable;
    pFile->AddRef();
    pFile->Seek(0, FILE_BEGIN);
 
    check_header(s); /* skip the .gz header */
    s->start = s->file->Seek(0, FILE_CURRENT) - s->stream.avail_in;

    return (gzFile)s;
}
/* ===========================================================================
     Read a byte from a gz_stream; update next_in and avail_in. Return EOF
   for end of file.
   IN assertion: the stream s has been sucessfully opened for reading.
*/
local int get_byte(gz_stream *s)
{
    if (s->z_eof) return EOF;
    if (s->stream.avail_in == 0) {
        s->stream.avail_in = s->file->Read(s->inbuf, Z_BUFSIZE);
        if (s->stream.avail_in == 0) {
            s->z_eof = 1;
            return EOF;
        }
        s->stream.next_in = s->inbuf;
    }
    s->stream.avail_in--;
    return *(s->stream.next_in)++;
}
/* ===========================================================================
      Check the gzip header of a gz_stream opened for reading. Set the stream
    mode to transparent if the gzip magic header is not present; set s->err
    to Z_DATA_ERROR if the magic header is present but the rest of the header
    is incorrect.
    IN assertion: the stream s has already been created sucessfully;
       s->stream.avail_in is zero for the first time, but may be non-zero
       for concatenated .gz files.
*/
local void check_header(gz_stream *s)
{
    int method; /* method byte */
    int flags;  /* flags byte */
    uInt len;
    int c;
    /* Assure two bytes in the buffer so we can peek ahead -- handle case
       where first byte of header is at the end of the buffer after the last
       gzip segment */
    len = s->stream.avail_in;
    if (len < 2) {
        if (len) s->inbuf[0] = s->stream.next_in[0];
        len = s->file->Read(s->inbuf + len, Z_BUFSIZE >> len);
        s->stream.avail_in += len;
        s->stream.next_in = s->inbuf;
        if (s->stream.avail_in < 2) {
            s->transparent = s->stream.avail_in;
            return;
        }
    }

    /* Peek ahead to check the gzip magic header */
    if (s->stream.next_in[0] != gz_magic[0] ||
        s->stream.next_in[1] != gz_magic[1]) {
        s->transparent = 1;
        return;
    }
    s->stream.avail_in -= 2;
    s->stream.next_in += 2;

    /* Check the rest of the gzip header */
    method = get_byte(s);
    flags = get_byte(s);
    if (method != Z_DEFLATED || (flags & RESERVED) != 0) {
        s->z_err = Z_DATA_ERROR;
        return;
    }

    /* Discard time, xflags and OS code: */
    for (len = 0; len < 6; len++) (void)get_byte(s);

    if ((flags & EXTRA_FIELD) != 0) { /* skip the extra field */
        len  =  (uInt)get_byte(s);
        len += ((uInt)get_byte(s))<<8;
        /* len is garbage if EOF but the loop below will quit anyway */
        while (len-- != 0 && get_byte(s) != EOF) ;
    }
    if ((flags & ORIG_NAME) != 0) { /* skip the original file name */
        while ((c = get_byte(s)) != 0 && c != EOF) ;
    }
    if ((flags & COMMENT) != 0) {   /* skip the .gz file comment */
        while ((c = get_byte(s)) != 0 && c != EOF) ;
    }
    if ((flags & HEAD_CRC) != 0) {  /* skip the header crc */
        for (len = 0; len < 2; len++) (void)get_byte(s);
    }
    s->z_err = s->z_eof ? Z_DATA_ERROR : Z_OK;
}

 /* ===========================================================================
 * Cleanup then free the given gz_stream. Return a zlib error code.
   Try freeing in the reverse order of allocations.
 */
local int destroy (gz_stream *s)
{
    int err = Z_OK;

    if (s->stream.state != NULL) {
        err = inflateEnd(&(s->stream));
    }
    if(s->file){
        s->file->Release();
    }
    if (s->z_err < 0) err = s->z_err;
    delete s->pSeekTable;
    TRYFREE(s->inbuf);
    TRYFREE(s->outbuf);
    TRYFREE(s);
    return err;
}
/* ===========================================================================
     Reads the given number of uncompressed bytes from the compressed file.
   gzread returns the number of bytes actually read (0 for end of file).
*/
local int ZEXPORT gzread (gzFile file, voidp buf, unsigned len)
{
    gz_stream *s = (gz_stream*)file;
    Bytef *start = (Bytef*)buf; /* starting point for crc computation */
    Byte  *next_out; /* == stream.next_out but not forced far (for MSDOS) */

    if (s->z_err == Z_DATA_ERROR || s->z_err == Z_ERRNO) return -1;
    if (s->z_err == Z_STREAM_END) return 0;  /* EOF */

    next_out = (Byte*)buf;
    s->stream.next_out = (Bytef*)buf;
    s->stream.avail_out = len;

    if (s->stream.avail_out && s->back != EOF) {
        *next_out++ = s->back;
        s->stream.next_out++;
        s->stream.avail_out--;
        s->back = EOF;
        s->out++;
        start++;
        if (s->last) {
            s->z_err = Z_STREAM_END;
            return 1;
        }
    }

    while (s->stream.avail_out != 0) {
        if (s->transparent) {
            /* Copy first the lookahead bytes: */
            uInt n = s->stream.avail_in;
            if (n > s->stream.avail_out) n = s->stream.avail_out;
            if (n > 0) {
                zmemcpy(s->stream.next_out, s->stream.next_in, n);
                next_out += n;
                s->stream.next_out = next_out;
                s->stream.next_in   += n;
                s->stream.avail_out -= n;
                s->stream.avail_in  -= n;
            }
            if (s->stream.avail_out > 0) {
                s->stream.avail_out -=
                    s->file->Read(next_out, s->stream.avail_out);
            }
            len -= s->stream.avail_out;
            s->in  += len;
            s->out += len;
            if (len == 0) s->z_eof = 1;
            return (int)len;
        }
        if (s->stream.avail_in == 0 && !s->z_eof) {
            s->pSeekTable->SaveContext(s);
            s->stream.avail_in = s->file->Read(s->inbuf, Z_BUFSIZE);
            if (s->stream.avail_in == 0) {
                s->z_eof = 1;
            }
            s->stream.next_in = s->inbuf;
        }
        s->in += s->stream.avail_in;
        s->out += s->stream.avail_out;
        s->z_err = inflate(&(s->stream), Z_NO_FLUSH);
        s->in -= s->stream.avail_in;
        s->out -= s->stream.avail_out;

        if (s->z_err == Z_STREAM_END) {
            /* Check CRC and original size */
            s->crc = crc32(s->crc, start, (uInt)(s->stream.next_out - start));
            start = s->stream.next_out;

            if (getLong(s) != s->crc) {
                s->z_err = Z_DATA_ERROR;
            } else {
                (void)getLong(s);
                /* The uncompressed length returned by above getlong() may be
                 * different from s->out in case of concatenated .gz files.
                 * Check for such files:
                 */
                check_header(s);
                if (s->z_err == Z_OK) {
                    inflateReset(&(s->stream));
                    s->crc = crc32(0L, Z_NULL, 0);
                }
            }
        }
        if (s->z_err != Z_OK || s->z_eof) break;
    }
    s->crc = crc32(s->crc, start, (uInt)(s->stream.next_out - start));

    if (len == s->stream.avail_out &&
        (s->z_err == Z_DATA_ERROR || s->z_err == Z_ERRNO))
        return -1;
    return (int)(len - s->stream.avail_out);
}
local z_off64_t ZEXPORT gzseek64(gzFile file, z_off64_t offset, int whence)
{
    gz_stream *s = (gz_stream*)file;

    if(s->z_err == Z_ERRNO || s->z_err == Z_DATA_ERROR) {
        return -1i64;
    }

    /* Rest of function is for reading only */
    /* compute absolute position */
    if (whence == SEEK_CUR) {
        offset += s->out;
    }
    if (offset < 0) return -1L;

    if (s->transparent) {
        /* map to fseek */
        s->back = EOF;
        s->stream.avail_in = 0;
        s->stream.next_in = s->inbuf;
        //if(fseek(s->file, offset, SEEK_SET) < 0) return -1L;
        if(s->file->Seek(offset, SEEK_SET) < 0) return -1L;

        s->in = s->out = offset;
        return offset;
    }
    s->pSeekTable->RestoreContext(offset, s);

    /* For a negative seek, rewind and use positive seek */
    if (offset >= s->out) {
        offset -= s->out;
    } else if (gzrewind(file) < 0) {
        return -1L;
    }
    /* offset is now the number of bytes to skip. */

    if (offset != 0 && s->outbuf == Z_NULL) {
        s->outbuf = (Byte*)ALLOC(Z_BUFSIZE);
        if (s->outbuf == Z_NULL) return -1L;
    }
    if (offset && s->back != EOF) {
        s->back = EOF;
        s->out++;
        offset--;
        if (s->last) s->z_err = Z_STREAM_END;
    }
    while (offset > 0)  {
        int size = Z_BUFSIZE;
        if (offset < Z_BUFSIZE) size = (int)offset;

        size = gzread(file, s->outbuf, (uInt)size);
        if (size <= 0) return -1L;
        offset -= size;
    }
    return s->out;
}
/* ===========================================================================
     Rewinds input file.
*/
local int ZEXPORT gzrewind (gzFile file)
{
    gz_stream *s = (gz_stream*)file;

    s->z_err = Z_OK;
    s->z_eof = 0;
    s->back = EOF;
    s->stream.avail_in = 0;
    s->stream.next_in = s->inbuf;
    s->crc = crc32(0L, Z_NULL, 0);
    if (!s->transparent) (void)inflateReset(&s->stream);
    s->in = 0;
    s->out = 0;
    //return fseek(s->file, s->start, SEEK_SET);
    s->file->Seek(s->start, SEEK_SET);
    return 0;
}
/* ===========================================================================
   Reads a long in LSB order from the given gz_stream. Sets z_err in case
   of error.
*/
local uLong getLong (gz_stream *s)
{
    uLong x = (uLong)get_byte(s);
    int c;

    x += ((uLong)get_byte(s))<<8;
    x += ((uLong)get_byte(s))<<16;
    c = get_byte(s);
    if (c == EOF) s->z_err = Z_DATA_ERROR;
    x += ((uLong)c)<<24;
    return x;
}
/* ===========================================================================
     Flushes all pending output if necessary, closes the compressed file
   and deallocates all the (de)compression state.
*/
local int ZEXPORT gzclose (gzFile file)
{
    gz_stream *s = (gz_stream*)file;

    if (s == NULL) return Z_STREAM_ERROR;

    return destroy((gz_stream*)file);
}

#endif // USE_GZIO

//static const wchar_t g_cszKbGzFile[] = L"::KbGzFile::";//IKpiFile のインスタンスが
//                                                   //KbGzFile かどうかの判別用 
KbGzFile::KbGzFile(void)
    : KbKpiFileImpl()
{
    //m_pShared = NULL;
    m_pFile = NULL;
    m_gzFile = NULL;
    m_dwSize = 0;
    m_qwPos = 0;
}
/*
KbGzFile::KbGzFile(Shared *pShared)
    : KbKpiFileImpl()
{
    m_pShared = pShared;
    m_qwPos = 0;
    InterlockedIncrement(&pShared->cRef);
}
*/
KbGzFile::~KbGzFile(void)
{
    if(m_gzFile){
        gzclose((gzFile)m_gzFile);
    }
    if(m_pFile){
        m_pFile->Release();
    }
    /*
    Shared *pShared = m_pShared;
    if(pShared && InterlockedDecrement(&pShared->cRef) == 0){
        gzclose((gzFile)pShared->gzFile);
        pShared->pFile->Release();
        DeleteCriticalSection(&pShared->cs);
        delete pShared;
    }*/
}
BOOL __fastcall KbGzFile::Open(IKpiFile *pFile)
{
    BYTE header[10];
    pFile->Seek(0, FILE_BEGIN);
    if(pFile->Read(header, 10) != 10){
        return FALSE;
    }
    if(header[0] != 0x1f || header[1] != 0x8b){//gz ではない
        return FALSE;
    }
    DWORD dwSize = 0;
    pFile->Seek(-4, FILE_END);
    if(pFile->Read(&dwSize, 4) != 4){
        return FALSE;
    }
#ifndef USE_GZIO
    //IKpiFile* 経由で開く
    gzFile gzfile = gzopen(pFile);
#else
    //FILE* 経由で開く
    const char *cszFileName;
    if(!pFile->GetFile(&cszFileName)){
        return FALSE;
    }
    gzFile gzfile = gzopen(cszFileName, "rb");
#endif
    if(!gzfile){
        return FALSE;
    }
    /*Shared *pShared = new Shared;
    InitializeCriticalSection(&pShared->cs);
    pShared->cRef = 1;
    pShared->pFile = pFile;
    pShared->gzFile = gzfile;
    pShared->dwSize = dwSize;
    pFile->AddRef();
    m_pShared = pShared;
    */
    m_gzFile = gzfile;
    m_dwSize = dwSize;
    m_pFile = pFile;
    pFile->AddRef();
    return TRUE;
}
DWORD  WINAPI KbGzFile::Read(void *pBuffer, DWORD dwSize)
{
    //Lock();
    gzFile pgzFile = (gzFile)m_gzFile;//m_pShared->gzFile;
    gzseek64(pgzFile, m_qwPos, FILE_BEGIN); 
    int ret = gzread(pgzFile, pBuffer, dwSize);
    if(ret < 0){
        ret = 0;
    }
    m_qwPos += ret;
    //Unlock();
    return ret;
}
UINT64 WINAPI KbGzFile::Seek(__int64 i64Pos, DWORD dwOrigin)
{
    //Lock();
    gzFile pgzFile = (gzFile)m_gzFile;//m_pShared->gzFile;
    switch(dwOrigin){
        case FILE_BEGIN:{
            break;
        }
        case FILE_CURRENT:{
            i64Pos += m_qwPos;
            break;
        }
        case FILE_END:{
            i64Pos += m_dwSize;//m_pShared->dwSize;
            break;
        }
    }
    if(i64Pos > m_dwSize/*m_pShared->dwSize*/){
        i64Pos = m_dwSize/*m_pShared->dwSize*/;
    }
    else if(i64Pos < 0){
        i64Pos = 0;
    }
    m_qwPos = gzseek64(pgzFile, i64Pos, FILE_BEGIN);
    if((_int64)m_qwPos < 0){
//        Unlock();
//        return KPI_FILE_EOF;
        m_qwPos = m_dwSize;//m_pShared->dwSize;
    }
    //Unlock();
    return m_qwPos;
}
UINT64 WINAPI KbGzFile::GetSize(void)
{
    return m_dwSize;//m_pShared->dwSize;
}
/*
BOOL WINAPI KbGzFile::CreateClone(IKpiFile **ppFile)
{
    *ppFile = new KbGzFile(m_pShared);
    return TRUE;
}*/
//#endif //TGZ_SUPPORT
