/***************************************************************************
 *   crengine-ng                                                           *
 *   Copyright (C) 2007,2009,2012 Vadim Lopatin <coolreader.org@gmail.com> *
 *   Copyright (C) 2010 Kirill Erofeev <erofeev.info@gmail.com>            *
 *                                                                         *
 *   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 2        *
 *   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, write to the Free Software           *
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,            *
 *   MA 02110-1301, USA.                                                   *
 ***************************************************************************/

/**
 * \file wolutil.cpp
 * \brief WOL file format support
 * 
 * Based on code by SeNS
 */

#include <lvstreamutils.h>
#include <lvbyteorder.h>

#include "wolutil.h"
#include "private/dumpfile.h"

#include <string.h>
#include <stdio.h>

#define N         4096
#define F         18
#define THRESHOLD 2
#define NIL       N

//#define _DEBUG_LOG

#ifdef _DEBUG_LOG
DumpFile log("wol.log");
#endif

static lvByteOrderConv cnv;

/*
 * Based on LZSS.C by Haruhiko Okumura 4/6/1989
 */
class LZSSUtil
{
    lUInt16 textsize;                               /* text size counter */
    lUInt16 codesize;                               /* code size counter */
    lUInt16 printcount;                             /* counter for reporting progress every 1K bytes */
    lUInt8 text_buf[N + F - 1];                     /* ring buffer of size N,
              with extra F-1 bytes to facilitate string comparison */
    lUInt16 match_position, match_length;           /* of longest match.  These are
              set by the InsertNode() procedure. */
    lUInt16 lson[N + 1], rson[N + 257], dad[N + 1]; /* left & right children &
              parents -- These constitute binary search trees. */

    // private methods
    void InsertNode(int r);
    void DeleteNode(int p);
public:
    /// init tables
    LZSSUtil();
    /// encode buffer
    bool Encode(
            const lUInt8* in_buf,
            int in_length,
            lUInt8* out_buf,
            int& out_length);
    /// decode buffer
    bool Decode(
            const lUInt8* in_buf,
            int in_length,
            lUInt8* out_buf,
            int& out_length);
};

class InBuf
{
    const lUInt8* _buf;
    int _size;
    int _pos;
public:
    InBuf(const lUInt8* buf, int size)
            : _buf(buf)
            , _size(size)
            , _pos(0) { }
    bool get(lUInt32& ch) {
        if (_pos >= _size)
            return false;
        ch = _buf[_pos++];
        return true;
    }
};

class OutBuf
{
    lUInt8* _buf;
    int _size;
    int _pos;
    bool _overflow;
public:
    OutBuf(lUInt8* buf, int size)
            : _buf(buf)
            , _size(size)
            , _pos(0)
            , _overflow(false) { }
    bool getOverflow() {
        return _overflow;
    }
    bool put(lUInt32 ch) {
        if (_pos >= _size) {
            _overflow = true;
            return false;
        }
        _buf[_pos++] = (lUInt8)ch;
        return true;
    }
    int getPos() {
        return _pos;
    }
};

LZSSUtil::LZSSUtil() {
    /* initialize trees */
    int i;
    /* For i = 0 to N - 1, rson[i] and lson[i] will be the right and
       left children of node i.  These nodes need not be initialized.
       Also, dad[i] is the parent of node i.  These are initialized to
       NIL (= N), which stands for 'not used.'
       For i = 0 to 255, rson[N + i + 1] is the root of the tree
       for strings that begin with character i.  These are initialized
       to NIL.  Note there are 256 trees. */
    for (i = N + 1; i <= N + 256; i++)
        rson[i] = NIL;
    for (i = 0; i < N; i++)
        dad[i] = NIL;
}

void LZSSUtil::InsertNode(int r)
/* Inserts string of length F, text_buf[r..r+F-1], into one of the
       trees (text_buf[r]'th tree) and returns the longest-match position
       and length via the global variables match_position and match_length.
       If match_length = F, then removes the old node in favor of the new
       one, because the old one will be deleted sooner.
       Note r plays double role, as tree node and position in buffer. */
{
    int i, p, cmp;
    unsigned char* key;

    cmp = 1;
    key = &text_buf[r];
    p = N + 1 + key[0];
    rson[r] = lson[r] = NIL;
    match_length = 0;
    for (;;) {
        if (cmp >= 0) {
            if (rson[p] != NIL)
                p = rson[p];
            else {
                rson[p] = r;
                dad[r] = p;
                return;
            }
        } else {
            if (lson[p] != NIL)
                p = lson[p];
            else {
                lson[p] = r;
                dad[r] = p;
                return;
            }
        }
        for (i = 1; i < F; i++)
            if ((cmp = key[i] - text_buf[p + i]) != 0)
                break;
        if (i > match_length) {
            match_position = p;
            if ((match_length = i) >= F)
                break;
        }
    }
    dad[r] = dad[p];
    lson[r] = lson[p];
    rson[r] = rson[p];
    dad[lson[p]] = r;
    dad[rson[p]] = r;
    if (rson[dad[p]] == p)
        rson[dad[p]] = r;
    else
        lson[dad[p]] = r;
    dad[p] = NIL; /* remove p */
}

void LZSSUtil::DeleteNode(int p) /* deletes node p from tree */
{
    int q;

    if (dad[p] == NIL)
        return; /* not in tree */
    if (rson[p] == NIL)
        q = lson[p];
    else if (lson[p] == NIL)
        q = rson[p];
    else {
        q = lson[p];
        if (rson[q] != NIL) {
            do {
                q = rson[q];
            } while (rson[q] != NIL);
            rson[dad[q]] = lson[q];
            dad[lson[q]] = dad[q];
            lson[q] = lson[p];
            dad[lson[p]] = q;
        }
        rson[q] = rson[p];
        dad[rson[p]] = q;
    }
    dad[q] = dad[p];
    if (rson[dad[p]] == p)
        rson[dad[p]] = q;
    else
        lson[dad[p]] = q;
    dad[p] = NIL;
}

bool LZSSUtil::Encode(
        const lUInt8* in_buf,
        int in_length,
        lUInt8* out_buf,
        int& out_length) {
    InBuf in(in_buf, in_length);
    OutBuf out(out_buf, out_length);
    int i, len, r, s, last_match_length, code_buf_ptr;
    lUInt32 c;
    lUInt8 code_buf[17], mask;
    code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and
         code_buf[0] works as eight flags, "1" representing that the unit
         is an unencoded letter (1 byte), "0" a position-and-length pair
         (2 bytes).  Thus, eight units require at most 16 bytes of code. */
    code_buf_ptr = mask = 1;
    s = 0;
    r = N - F;
    for (i = s; i < r; i++)
        text_buf[i] = ' '; /* Clear the buffer with
         any character that will appear often. */
    for (len = 0; len < F && in.get(c); len++)
        text_buf[r + len] = (lUInt8)c; /* Read F bytes into the last F bytes of
              the buffer */
    if ((textsize = len) == 0)
        return false; /* text of size zero */
    //for (i = 1; i <= F; i++) InsertNode(r - i);
    /* Insert the F strings,
         each of which begins with one or more 'space' characters.  Note
         the order in which these strings are inserted.  This way,
         degenerate trees will be less likely to occur. */
    InsertNode(r); /* Finally, insert the whole string just read.  The
         global variables match_length and match_position are set. */
    do {
        if (match_length > len)
            match_length = len; /* match_length
              may be spuriously long near the end of text. */
        if (match_length <= THRESHOLD) {
            match_length = 1;                       /* Not long enough match.  Send one byte. */
            code_buf[0] |= mask;                    /* 'send one byte' flag */
            code_buf[code_buf_ptr++] = text_buf[r]; /* Send uncoded. */
        } else {
            code_buf[code_buf_ptr++] = (lUInt8)match_position;
            code_buf[code_buf_ptr++] = (lUInt8)(((match_position >> 4) & 0xf0) | (match_length - (THRESHOLD + 1))); /* Send position and
                        length pair. Note match_length > THRESHOLD. */
        }
        if ((mask <<= 1) == 0) {               /* Shift mask left one bit. */
            for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */
                out.put(code_buf[i]);          /* code together */
            codesize += code_buf_ptr;
            code_buf[0] = 0;
            code_buf_ptr = mask = 1;
        }
        last_match_length = match_length;
        for (i = 0; i < last_match_length && in.get(c); i++) {
            DeleteNode(s);           /* Delete old strings and */
            text_buf[s] = (lUInt8)c; /* read new bytes */
            if (s < F - 1)
                text_buf[s + N] = (lUInt8)c; /* If the position is
                   near the end of buffer, extend the buffer to make
                   string comparison easier. */
            s = (s + 1) & (N - 1);
            r = (r + 1) & (N - 1);
            /* Since this is a ring buffer, increment the position
                      modulo N. */
            InsertNode(r); /* Register the string in text_buf[r..r+F-1] */
        }
        while (i++ < last_match_length) { /* After the end of text, */
            DeleteNode(s);                /* no need to read, but */
            s = (s + 1) & (N - 1);
            r = (r + 1) & (N - 1);
            if (--len)
                InsertNode(r); /* buffer may not be empty. */
        }
    } while (len > 0);      /* until length of string to be processed is zero */
    if (code_buf_ptr > 1) { /* Send remaining code. */
        for (i = 0; i < code_buf_ptr; i++)
            out.put(code_buf[i]);
        codesize += code_buf_ptr;
    }
    out_length = out.getPos();
    return true;
}

/* Just the reverse of Encode(). */

#if 0

bool LZSSUtil::Decode(
  const lUInt8 * in_buf,
  int in_length,
  lUInt8 * out_buf,
  int & out_length
)
{
    InBitStream in( in_buf, in_length );
    OutBuf out( out_buf, out_length );
    lUInt32 i, k, r;
    lUInt32 c, ci, cj;
    unsigned int  flags;

    for (i = 0; i < N - F; i++)
        text_buf[i] = 0; //' '
    r = N - F;  flags = 0;
    for ( ; ; ) {
        if (!in.read(c, 1))
            break;
        if (c)
        {
            if (!in.read(c, 8))
                break;
            if (!out.put(c))
                break;
            text_buf[r++] = c;
            r &= (N - 1);
        }
        else
        {
            if (!in.read(cj, 4))
                break;
            cj += THRESHOLD;
            if (!in.read(ci, 12))
                break;
            for (k = 0; k <= cj; k++) {
                c = text_buf[(ci + k) & (N - 1)];
                if (!out.put(c))
                     break;
                text_buf[r++] = c;
                r &= (N - 1);
            }
        }
    }
    out_length=out.getPos();
    return !out.getOverflow();
}
#endif

bool LZSSUtil::Decode(
        const lUInt8* in_buf,
        int in_length,
        lUInt8* out_buf,
        int& out_length) {
    InBuf in(in_buf, in_length);
    OutBuf out(out_buf, out_length);
    lUInt32 i, k, r;
    lUInt32 c, ci, cj;
    unsigned int flags;

    for (i = 0; i < N - F; i++)
        text_buf[i] = 'a';
    r = N - F;
    flags = 0;
    for (;;) {
        if (((flags >>= 1) & 256) == 0) {
            if (!in.get(c))
                break;
            flags = c | 0xff00; /* uses higher byte cleverly */
        }                       /* to count eight */
        if (flags & 1) {
            if (!in.get(c))
                break;
            if (!out.put(c))
                break;
            text_buf[r++] = (lUInt8)c;
            r &= (N - 1);
        } else {
            if (!in.get(ci))
                break;
            if (!in.get(cj))
                break;
            ci |= ((cj & 0xf0) << 4);
            cj = (cj & 0x0f) + THRESHOLD;
            for (k = 0; k <= cj; k++) {
                c = text_buf[(ci + k) & (N - 1)];
                if (!out.put(c))
                    break;
                text_buf[r++] = (lUInt8)c;
                r &= (N - 1);
            }
        }
    }
    out_length = out.getPos();
    return !out.getOverflow();
}

///////////////////////////////////////////////////////////////////////////
//
// WOLBase class
//
///////////////////////////////////////////////////////////////////////////

WOLBase::WOLBase(LVStream* stream)
        : _stream(stream)
        , _book_title_size(0)
        , _cover_image_size(0)
        , _page_data_size(0)
        , _catalog_size(0)
        , _wolf_start_pos(0)
        , _subcatalog_level23_items(0)
        , _subcatalog_offset(0)
        , _catalog_level1_items(0)
        , _catalog_subcatalog_size(0) {
}

WOLReader::WOLReader(LVStream* stream)
        : WOLBase(stream) {
}

static void readMem(const lUInt8* buf, int offset, lUInt16& dest) {
    dest = ((lUInt16)buf[offset + 1] << 8) | buf[offset];
}

static void readMem(const lUInt8* buf, int offset, lUInt32& dest) {
    dest = ((lUInt32)buf[offset + 3] << 24) | ((lUInt32)buf[offset + 2] << 16) | ((lUInt32)buf[offset + 1] << 8) | buf[offset];
}

bool WOLReader::readHeader() {
    lUInt8 header[0x80];
    if (_stream->Read(header, 0x80, NULL) != LVERR_OK)
        return false;
    ;
    if (memcmp(header, "WolfEbook1.11", 13))
        return false;
    readMem(header, 0x17, _book_title_size);
    readMem(header, 0x19, _cover_image_size);         // 0x19
    readMem(header, 0x5F, _subcatalog_level23_items); // 0x5F
    readMem(header, 0x61, _subcatalog_offset);        // 0x61
    readMem(header, 0x22, _catalog_level1_items);     // 0x1E
    readMem(header, 0x1E, _catalog_subcatalog_size);  // 0x1E
    readMem(header, 0x26, _page_data_size);           // 0x26
    readMem(header, 0x3C, _catalog_size);             // 0x3C
    _book_title = readString(0x80, _book_title_size);
    _stream->SetPos(0x80 + _cover_image_size + _book_title_size);
    lString8 str = readTag();
    if (str != "wolf")
        return false;
    str = readTag();
    if (str != "catalog")
        return false;
    for (;;) {
        str = readTag();
        if (str.empty())
            return false;
        if (str == "/catalog")
            break;
        wolf_img_params params;
        //img bitcount=1 compact=1 width=794 height=1123 length=34020
        if (sscanf(str.c_str(), "img bitcount=%d compact=%d width=%d height=%d length=%d",
                   &params.bitcount, &params.compact,
                   &params.width, &params.height,
                   &params.length) != 5)
            return false;
        params.offset = (lUInt32)_stream->GetPos();
        _stream->SetPos(params.offset + params.length);
        str = readTag();
        const char* s = str.c_str();
        lString8 tst(s);
        tst += s;
        if (str != "/img")
            return false;
        _images.add(params);
    }
    //_wolf_start_pos;
    return true;
}

LVGrayDrawBuf* WOLReader::getImage(int index) {
    if (index < 0 || index >= _images.length())
        return NULL;
    const wolf_img_params* img = &_images[index];
    LVArray<lUInt8> buf(img->length, 0);
    _stream->SetPos(img->offset);
    _stream->Read(buf.ptr(), img->length, NULL);
    int img_size = img->height * ((img->width * img->bitcount + 7) / 8);
    int uncomp_len = img_size + 18;
    LVArray<lUInt8> uncomp(uncomp_len, 0);

    LZSSUtil unpacker;
    if (unpacker.Decode(buf.ptr(), buf.length(), uncomp.ptr(), uncomp_len)) {
        LVStreamRef dump = LVOpenFileStream("test.dat", LVOM_WRITE);
        if (!dump.isNull()) {
            dump->Write(uncomp.ptr(), uncomp_len, NULL);
        }

        // inverse 1-bit images
        if (img->bitcount == 1) {
            for (int i = 0; i < img_size; i++) {
                uncomp[i] = ~uncomp[i];
            }
        }

        LVGrayDrawBuf* image = new LVGrayDrawBuf(img->width, img->height, img->bitcount);
        memcpy(image->GetScanLine(0), uncomp.ptr(), img_size);
        return image;
    }

    //delete image;
    return NULL;
}

lString8 WOLReader::readString(int offset, int size) {
    _stream->SetPos(offset);
    lString8 buf;
    buf.append(size, ' ');
    _stream->Read(buf.modify(), size, NULL);
    return buf;
}

lString8 WOLReader::readTag() {
    lString8 buf;
    lChar8 ch = 0;
    for (;;) {
        if (_stream->Read(&ch, 1, NULL) != LVERR_OK)
            return lString8::empty_str;
        if (ch == ' ' || ch == '\r' || ch == '\n')
            continue;
        if (ch != '<')
            return lString8::empty_str;
        break;
    }
    for (;;) {
        if (_stream->Read(&ch, 1, NULL) != LVERR_OK)
            return lString8::empty_str;
        if (ch == 0 || buf.length() > 100)
            return lString8::empty_str;
        if (ch == '>')
            return buf;
        buf.append(1, ch);
    }
}

LVArray<lUInt8>* WOLReader::getBookCover() {
    LVArray<lUInt8>* cover = new LVArray<lUInt8>(_cover_image_size, 0);
    _stream->SetPos(0x80 + _book_title_size);
    _stream->Read(cover->ptr(), _cover_image_size, NULL);
    return cover;
}

WOLWriter::WOLWriter(LVStream* stream)
        : WOLBase(stream)
        , _catalog_opened(false) {
    lUInt8 header[0x80] = { 0 };
    memcpy(header, "WolfEbook1.11", 13);
    header[0x11] = 1;
    header[0x12] = 2;
    header[0x22] = 1;
    header[0x1D] = 1;
    header[0x40] = 1;
    stream->Write(header, 0x80, NULL);
}

void WOLWriter::startCatalog() {
    if (!_catalog_opened) {
        _catalog_start = (lUInt32)_stream->GetPos();
        *_stream << "<catalog>";
        _catalog_opened = true;
    }
}

void WOLWriter::endCatalog() {
    if (_catalog_opened) {
        *_stream << "</catalog>";
        _catalog_opened = false;
    }
}

typedef struct
{
    lUInt32 PageOffs;     //[0x00] PageOffs: offset from beginning of section "WolfPages" to description of page (<img...>).
    lUInt32 NameOffs;     //[0x04] NameOffs: offset from beginning of file to beginnig of name (in "Names" area).
    lUInt16 NameSize;     //[0x08] NameSize: length (in chars) of name (in "Names" area).
    lUInt16 ChildsCount;  //[0x0A] ChildsCount: count of subitems for this element from TOC.
    lUInt32 PrevPeerOffs; //[0x0C] PrevPeerOffs: offset from beginning of file to the description of previous peer element in SubCatalog table. 0, if current item is the first child of parent.
    lUInt32 NextPeerOffs; //[0x10] NextPeerOffs: offset from beginning of file to the description of next peer element in SubCatalog table. 0, if current item is the last child of parent.
    lUInt32 ChildOffs;    //[0x14] ChildOffs: offset from beginning of file to the description of first child in SubCatalog table. 0, if there is no subitems (in TOC) for current item.
    lUInt32 ParentOffs;   //[0x18] ParentOffs: offset from beginning of file to the description of parent element in SubCatalog table. 0, if element is on level 1.
    lUInt8 Level3Idx;     //[0x1C] Level3Idx: level 3 index of element; 0 if element is on level 1 or level 2 in TOC.
    lUInt8 Level2Idx;     //[0x1D] Level2Idx: level 2 index of element; 0 if element is on level 1 in TOC.
    lUInt8 Level1Idx;     //[0x1E] Level1Idx: level 1 index of element.
    lUInt8 AlignByte;     //[0x1F] AlignByte: constant = 0x00.
    lChar8 ItemName[48];  //[0x20] ItemName: name of item in TOC.
} wol_toc_subcatalog_item;

void WOLWriter::writeToc() {
    // enumerate ordered by level
    int n = 0;
    _subcatalog_level23_items = 0;
    _subcatalog_offset = 0;
    _catalog_level1_items = 0;    // 0x22
    _catalog_subcatalog_size = 0; // 0x1E
    lUInt32 cat_start = (lUInt32)_stream->GetPos();
    int len = _tocItems.length();
    if (len == 0) {
        // fake catalog = book name
        *_stream << "<catalog><item>" << _book_name
                 << "</item>";
        *_stream << cnv.lsf(getPageOffset(0)) << "</catalog>";
        _catalog_level1_items = 1;
    } else {
        //============================================================
        // catalog
        *_stream << "<catalog>";
        for (int ti = 0; ti < len; ti++) {
            TocItemInfo* src = _tocItems[ti];
            if (src->getLevel() == 1) {
                *_stream << "<item>" << src->name
                         << "</item>" << cnv.lsf(getPageOffset(src->page));
                _catalog_level1_items++;
            }
        }
        *_stream << "</catalog>";

        //============================================================
        // subcatalog

        // allocate TOC
        _subcatalog_offset = (lUInt32)_stream->GetPos();
        wol_toc_subcatalog_item* toc = new wol_toc_subcatalog_item[len]();
        int catsize = sizeof(wol_toc_subcatalog_item) * len;
        lString8 names;
        for (int lvl = 1; lvl <= 3; lvl++) {
            for (int i = 0; i < _tocItems.length(); i++) {
                if (_tocItems[i]->getLevel() == lvl) {
                    _tocItems[i]->catindex = n++;
                    if (lvl > 1)
                        _subcatalog_level23_items++;
                }
            }
        }
        int names_start = _subcatalog_offset + 12 + catsize;
        int i;
        for (i = 0; i < len; i++) {
            TocItemInfo* src = _tocItems[i];
            int n = src->catindex;
            wol_toc_subcatalog_item* item = &toc[n];
            item->Level1Idx = src->l1index;
            item->Level2Idx = src->l2index;
            item->Level3Idx = src->l3index;
            item->ChildOffs = cnv.lsf(src->firstChild ? src->firstChild->catindex * 80 + _subcatalog_offset + 12 : 0);
            item->ParentOffs = cnv.lsf(src->parent ? src->parent->catindex * 80 + _subcatalog_offset + 12 : 0);
            item->NextPeerOffs = cnv.lsf(src->nextSibling ? src->nextSibling->catindex * 80 + _subcatalog_offset + 12 : 0);
            item->PrevPeerOffs = cnv.lsf(src->prevSibling ? src->prevSibling->catindex * 80 + _subcatalog_offset + 12 : 0);
            lString8 name = src->name;
            item->NameOffs = cnv.lsf((lUInt32)(names_start + names.length()));
            name << ' ';
            item->NameSize = cnv.lsf((lUInt16)(name.length()));
            lStr_ncpy(item->ItemName, name.c_str(), 47);
            item->PageOffs = cnv.lsf(getPageOffset(src->page)); // ???
            names += name;
        }
#ifdef _DEBUG_LOG
        for (i = 0; i < len; i++) {
            wol_toc_subcatalog_item* item = &toc[i];
            lUInt32 currOffset = i * 80 + _subcatalog_offset + 12;
            fprintf(log, "%2d   %07d : (%d,%d,%d) \t parent=%07d  child=%07d  prev=%07d  next=%07d\t %s\n",
                    i, currOffset, item->Level1Idx, item->Level2Idx, item->Level3Idx, item->ParentOffs, item->ChildOffs, item->PrevPeerOffs, item->NextPeerOffs, item->ItemName);
        }
#endif
        *_stream << "<subcatalog>";
        _stream->Write(toc, sizeof(wol_toc_subcatalog_item) * len, NULL);
        *_stream << names;
        // SubcatalogSize = 26 + CountOfAllTOCItems * 80 + Length(NamesInAllTOCLevels)
        *_stream << "\x08</subcatalog>";
        delete[](toc);
    }
    // finalize
    lUInt32 cat_end = (lUInt32)_stream->GetPos();
    _catalog_subcatalog_size = cat_end - cat_start;
}

WOLWriter::~WOLWriter() {
    writePageIndex();
    updateHeader();
}

void WOLWriter::updateHeader() {
    _stream->SetPos(0x17);
    *_stream << cnv.lsf(_book_title_size);
    _stream->SetPos(0x19);
    *_stream << cnv.lsf(_cover_image_size);
    _stream->SetPos(0x26);
    *_stream << cnv.lsf(_page_data_size);
    _stream->SetPos(0x1e);
    *_stream << cnv.lsf(_catalog_subcatalog_size);
    _stream->SetPos(0x22);
    *_stream << cnv.lsf(_catalog_level1_items);
    _stream->SetPos(0x3c);
    *_stream << cnv.lsf(_catalog_size);
    _stream->SetPos(0x42);
    *_stream << cnv.lsf((lUInt32)_page_starts.length());
    _stream->SetPos(0x4b);
    *_stream << cnv.lsf((lUInt32)_page_starts.length());
    _stream->SetPos(0x5F);
    *_stream << cnv.lsf(_subcatalog_level23_items); // 0x5F
    _stream->SetPos(0x61);
    *_stream << cnv.lsf(_subcatalog_offset); // 0x61
}

void WOLWriter::addTitle(
        const lString8& title,
        const lString8& subject,
        const lString8& author,
        const lString8& adapter,
        const lString8& translator,
        const lString8& publisher,
        const lString8& time_publish,
        const lString8& introduction,
        const lString8& isbn) {
    //
    _book_name = title;
    lString8 buf;
    buf.reserve(128);
    buf << "<title>" << title << "\r\n"
        << "<subject>" << subject << "\r\n"
        << "<author>" << author << "\r\n"
        << "<adpter>" << adapter << "\r\n"
        << "<translator>" << translator << "\r\n"
        << "<publisher>" << publisher << "\r\n"
        << "<time_publish>" << time_publish << "\r\n"
        << "<introduction>" << introduction << "\r\n"
        << "<ISBN>" << isbn << "\r\n";
    _book_title_size = buf.length();
    *_stream << buf;
}

void WOLWriter::addCoverImage(const lUInt8* buf, int size) {
    static const lUInt8 cover_hdr[10] = {
        0xFF, 0xFF, 0x58, 0x02, 0x01, 0x00, 0x4B, 0x00, 0x20, 0x03
    };
    _stream->Write(cover_hdr, 10, NULL);
    _cover_image_size = size + 10;
    _stream->Write(buf, size, NULL);
    _wolf_start_pos = (lUInt32)_stream->GetPos();
    *_stream << "<wolf>\r\n";
}

void WOLWriter::addImage(
        int width,
        int height,
        const lUInt8* bitmap, // [width*height/4]
        int num_bits) {
    int bmp_sz = (width * height * num_bits) >> 3;
    startCatalog();
#if 0
    lUInt8 * inversed = NULL;
    if (num_bits==1)
    {
        inversed = new lUInt8 [bmp_sz];
        for (int i=0; i<bmp_sz; i++)
        {
           inversed[i]= ~bitmap[i];
        }
        bitmap = inversed;
    }
#endif
    /*
#if 0
  compressed.reserve(bitmap.size()*9/8);
  for(int i=0; i<bitmap.size(); ) {
    compressed.push_back((unsigned char)0xff);
    for(int j=0; j<8 && i<bitmap.size(); j++) {
      compressed.push_back(bitmap[i++]);
    }
  }
  compressed.push_back((unsigned char)0x0); // extra last dummy char
#else
  compressed.resize(bitmap.size()*2);
  int compressed_len;
#if 1
  Encode((const unsigned char*)&(bitmap[0]), bitmap.size(),
    (unsigned char*)&(compressed[0]), &compressed_len);
#else
  EncodeLZSS(&(bitmap[0]), bitmap.size(), &(compressed[0]),
    compressed_len);
#endif
#endif
*/

    int compressed_len = bmp_sz * 9 / 8 + 18;
    lUInt8* compressed = new lUInt8[compressed_len];

    LZSSUtil packer;
    packer.Encode(bitmap, bmp_sz, compressed, compressed_len);

    compressed[compressed_len++] = 0; // extra last dummy char

#if 0 //def _DEBUG_LOG
    LZSSUtil unpacker;
    lUInt8 * decomp = new lUInt8 [bmp_sz*2];
    int decomp_len = 0;
    unpacker.Decode(compressed, compressed_len-1, decomp, decomp_len);
    assert(compressed_len==decomp_len);
    for(int i=0; i<decomp_len; i++) {
        assert(bitmap[i]==decomp[i]);
    }
    delete[] decomp;
#endif

    _page_starts.add((lUInt32)_stream->GetPos());

    lString8 buf;
    buf.reserve(128);
    buf << "<img bitcount="
        << fmt::decimal(num_bits)
        << " compact=1 width="
        << fmt::decimal(width)
        << " height="
        << fmt::decimal(height)
        << " length="
        << fmt::decimal((int)compressed_len)
        << ">";
    *_stream << buf;

    //_page_starts.add( (lUInt32)_stream->GetPos() );

    _stream->Write(compressed, compressed_len, NULL);
    endPage();
    *_stream << cs8("</img>");

    // cleanup
    delete[] compressed;
    //if (inversed)
    //     delete inversed;
}

void WOLWriter::endPage() {
    //m_page_starts.push_back(ftell(m_fp));
}

#define USE_001_FORMAT 0

void WOLWriter::addTocItem(int level1index, int level2index, int level3index, int pageNumber, lString8 title) {
#ifdef _DEBUG_LOG
    fprintf(log, "addTocItem(lvl=%d,%d,%d, page=%d, text=%s\n", level1index, level2index, level3index, pageNumber, title.c_str());
#endif
    TocItemInfo* item = new TocItemInfo(_tocItems.length(), level1index, level2index, level3index, pageNumber, title);
    _tocItems.add(item);
    for (int k = _tocItems.length() - 2; k >= 0; k--) {
        TocItemInfo* last = item;
        TocItemInfo* ki = _tocItems[k];
        if (last->isPrevSibling(*ki)) {
            last->prevSibling = ki;
            ki->nextSibling = last;
        } else if (last->isParent(*ki)) {
            last->parent = ki;
            if (ki->firstChild == NULL)
                ki->firstChild = last;
            break;
        }
    }
}

void WOLWriter::writePageIndex() {
    endCatalog();
    *_stream << "</wolf>";
    int pos0 = (int)_stream->GetPos();
    _page_data_size = pos0 - _wolf_start_pos;

    writeToc();

    //*_stream << "<catalog><item>" << _book_name
    //    << "</item>";
    //*_stream << (lUInt32)0x11 << "</catalog>";

    int pos1 = (int)_stream->GetPos();

    //_page_index_size = pos1-pos0; //0x1E

#if (USE_001_FORMAT == 1)
    *_stream << "<pagetable ver=\"001\">";
#else
    *_stream << "<pagetable ver=\"021211 \">";
#endif

    int start_of_catalog_table = (int)_stream->GetPos();

    LVArray<lUInt32> pagegroup_1;
    LVArray<lUInt32> pagegroup_2;
    LVArray<lUInt32> pagegroup_delim;

    pagegroup_delim.add(0xFFFFFFFF);
    pagegroup_1.add(cnv.lsf(_catalog_start));
#if (USE_001_FORMAT != 1)
    pagegroup_1.add(cnv.lsf(_page_starts[0]));
#endif
    pagegroup_2.add(cnv.lsf(_catalog_start));
    for (int i = 1; i < _page_starts.length(); i++) {
        pagegroup_1.add(cnv.lsf(_page_starts[i]));
#if (USE_001_FORMAT != 1)
        pagegroup_1.add(cnv.lsf(_page_starts[i]));
#endif
        pagegroup_2.add(cnv.lsf(_page_starts[i]));
    }
    int pagegroups_start = start_of_catalog_table + 13 * 4 + 12;
    lUInt32 p = pagegroups_start;
    LVArray<lUInt32> catalog;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_delim.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_delim.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_2.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_2.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_delim.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_1.length() * 4;
    catalog.add(cnv.lsf(p));
    p += pagegroup_delim.length() * 4;
    catalog.add(cnv.lsf(p));
    *_stream << catalog << "</pagetable>";

    *_stream << pagegroup_1;
    *_stream << pagegroup_1;
    *_stream << pagegroup_delim;
    *_stream << pagegroup_1;
    *_stream << pagegroup_1;
    *_stream << pagegroup_delim;
    *_stream << pagegroup_2;
    *_stream << pagegroup_2;
    *_stream << pagegroup_delim;
    *_stream << pagegroup_1;
    *_stream << pagegroup_1;
    *_stream << pagegroup_delim;

    int pos2 = (lUInt32)_stream->GetPos();
    _catalog_size = pos2 - pos1;
}

void WOLWriter::addImage(LVGrayDrawBuf& image) {
    addImage(image.GetWidth(), image.GetHeight(),
             image.GetScanLine(0), image.GetBitsPerPixel());
}

typedef struct
{
    lUInt16 compression;  // Compression: 0xFFFF for raw data (no compression); 0x0001 for LZSS compression.
    lUInt16 width;        // ImageWidth: width of image (in pixels)
    lUInt16 bpp;          // BitsPerPixel: 1 for monochrome images; 2 for 4-level gray images.
    lUInt16 bytesPerLine; // BytesPerLine: count of bytes in one row of image. (= ImageWidth*BitsPerPixel/8)
    lUInt16 height;       // ImageHeight: height of image (in pixels)
} CoverPageHeader_t;

void WOLWriter::addCoverImage(LVGrayDrawBuf& image) {
    // convert 2bpp to 1bpp
#if 1

    CoverPageHeader_t hdr;
    hdr.compression = cnv.lsf((lUInt16)1);
    lUInt16 width = image.GetWidth();
    lUInt16 height = image.GetHeight();
    lUInt16 bpp = image.GetBitsPerPixel();
    lUInt16 bpl = (lUInt16)((width * bpp + 7) >> 3);
    hdr.width = cnv.lsf(width);
    hdr.height = cnv.lsf(height);
    hdr.bpp = cnv.lsf(bpp);
    hdr.bytesPerLine = cnv.lsf(bpl);

    lUInt32 cover_start_pos = (lUInt32)_stream->GetPos();
    _stream->Write(&hdr, sizeof(hdr), NULL);

    int bmp_sz = bpl * height;

    lUInt8* data = new lUInt8[bmp_sz];
    memcpy(data, image.GetScanLine(0), bmp_sz);
    if (hdr.bpp == 2) {
        for (int i = 0; i < bmp_sz; i++)
            data[i] = ~data[i];
    }

    int compressed_len = bmp_sz * 9 / 8 + 18;
    lUInt8* compressed = new lUInt8[compressed_len];

    LZSSUtil packer;
    packer.Encode(data, bmp_sz, compressed, compressed_len);

    compressed[compressed_len++] = 0; // extra last dummy char

    delete[] data;

    _stream->Write(compressed, compressed_len, NULL);

    _wolf_start_pos = (lUInt32)_stream->GetPos();
    _cover_image_size = _wolf_start_pos - cover_start_pos;
    *_stream << "<wolf>\r\n";

#else
    image.ConvertToBitmap(true);
    int sz = (image.GetWidth() + 7) / 8 * image.GetHeight();
    addCoverImage(image.GetScanLine(0), sz);
#endif
}
