/***************************************************************************
 *   Copyright (C) 2006 by Pino Toscano <toscano.pino@tiscali.it>          *
 *                                                                         *
 *   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.                                   *
 ***************************************************************************/

#include "kdjvu.h"

#include <QByteArray>
#include <QDomDocument>
#include <QFile>
#include <QHash>
#include <QList>
#include <QPainter>
#include <QQueue>
#include <QString>

#include <QDebug>
#include <KLocalizedString>

#include <libdjvu/ddjvuapi.h>
#include <libdjvu/miniexp.h>

#include <stdio.h>

QDebug &operator<<( QDebug & s, const ddjvu_rect_t r )
{
    s.nospace() << "[" << r.x << "," << r.y << " - " << r.w << "x" << r.h << "]";
    return s.space();
}

static void which_ddjvu_message( const ddjvu_message_t *msg )
{
#ifdef KDJVU_DEBUG
    qDebug() << "which_djvu_message(...):" << msg->m_any.tag;
    switch( msg->m_any.tag )
    {
        case DDJVU_ERROR:
            qDebug().nospace() << "ERROR: file " << msg->m_error.filename << ", line " << msg->m_error.lineno;
            qDebug().nospace() << "ERROR: function '" << msg->m_error.function << "'";
            qDebug().nospace() << "ERROR: '" << msg->m_error.message << "'";
            break;
        case DDJVU_INFO:
            qDebug().nospace() << "INFO: '" << msg->m_info.message << "'";
            break;
        case DDJVU_CHUNK:
            qDebug().nospace() << "CHUNK: '" << QByteArray( msg->m_chunk.chunkid ) << "'";
            break;
        case DDJVU_PROGRESS:
            qDebug().nospace() << "PROGRESS: '" << msg->m_progress.percent << "'";
            break;
        default: ;
    }
#else
    Q_UNUSED( msg );
#endif
}

/**
 * Explore the message queue until there are message left in it.
 */
static void handle_ddjvu_messages( ddjvu_context_t *ctx, int wait )
{
    const ddjvu_message_t *msg;
    if ( wait )
        ddjvu_message_wait( ctx );
    while ( ( msg = ddjvu_message_peek( ctx ) ) )
    {
        which_ddjvu_message( msg );
        ddjvu_message_pop( ctx );
    }
}

/**
 * Explore the message queue until the message \p mid is found.
 */
static void wait_for_ddjvu_message( ddjvu_context_t *ctx, ddjvu_message_tag_t mid )
{
    ddjvu_message_wait( ctx );
    const ddjvu_message_t *msg;
    while ( ( msg = ddjvu_message_peek( ctx ) ) && msg && ( msg->m_any.tag != mid ) )
    {
        which_ddjvu_message( msg );
        ddjvu_message_pop( ctx );
    }
}

/**
 * Convert a clockwise coefficient \p r for a rotation to a counter-clockwise
 * and vice versa.
 */
static int flipRotation( int r )
{
    return ( 4 - r ) % 4;
}

static miniexp_t find_second_in_pair( miniexp_t theexp, const char* which )
{
    miniexp_t exp = theexp;
    while ( exp )
    {
        miniexp_t cur = miniexp_car( exp );
        if ( !miniexp_consp( cur ) || !miniexp_symbolp( miniexp_car( cur ) ) )
        {
            exp = miniexp_cdr( exp );
            continue;
        }

        const QString id = QString::fromUtf8( miniexp_to_name( miniexp_car( cur ) ) );
        if ( id == QLatin1String( which ) )
            return miniexp_cadr( cur );
        exp = miniexp_cdr( exp );
    }
    return miniexp_nil;
}

static bool find_replace_or_add_second_in_pair( miniexp_t theexp, const char* which, miniexp_t replacement )
{
    miniexp_t exp = miniexp_cdddr( theexp );
    while ( exp )
    {
        miniexp_t cur = miniexp_car( exp );
        if ( !miniexp_consp( cur ) || !miniexp_symbolp( miniexp_car( cur ) ) )
        {
            exp = miniexp_cdr( exp );
            continue;
        }

        const QString id = QString::fromUtf8( miniexp_to_name( miniexp_car( cur ) ) );
        if ( id == QLatin1String( which ) )
        {
            miniexp_t reversed = miniexp_reverse( cur );
            miniexp_rplaca( reversed, replacement );
            cur = miniexp_reverse( reversed );
            return true;
        }
        exp = miniexp_cdr( exp );
    }
    // TODO add the new replacement ad the end of the list
    return false;
}

// ImageCacheItem

class ImageCacheItem
{
    public:
        ImageCacheItem( int p, int w, int h, const QImage& i )
          : page( p ), width( w ), height( h ), img( i ) { }

        int page;
        int width;
        int height;
        QImage img;
};


// KdjVu::Page

KDjVu::Page::Page()
{
}

KDjVu::Page::~Page()
{
}

int KDjVu::Page::width() const
{
    return m_width;
}

int KDjVu::Page::height() const
{
    return m_height;
}

int KDjVu::Page::dpi() const
{
    return m_dpi;
}

int KDjVu::Page::orientation() const
{
    return m_orientation;
}

// KDjVu::Link

KDjVu::Link::~Link()
{
}

KDjVu::Link::LinkArea KDjVu::Link::areaType() const
{
    return m_area;
}

QPoint KDjVu::Link::point() const
{
    return m_point;
}

QSize KDjVu::Link::size() const
{
    return m_size;
}

QPolygon KDjVu::Link::polygon() const
{
    return m_poly;
}

// KDjVu::PageLink

KDjVu::PageLink::PageLink()
{
}

int KDjVu::PageLink::type() const
{
    return KDjVu::Link::PageLink;
}

QString KDjVu::PageLink::page() const
{
    return m_page;
}

// KDjVu::UrlLink

KDjVu::UrlLink::UrlLink()
{
}

int KDjVu::UrlLink::type() const
{
    return KDjVu::Link::UrlLink;
}

QString KDjVu::UrlLink::url() const
{
    return m_url;
}

// KDjVu::Annotation

KDjVu::Annotation::Annotation( miniexp_t anno )
    : m_anno( anno )
{
}

KDjVu::Annotation::~Annotation()
{
}

QPoint KDjVu::Annotation::point() const
{
    miniexp_t area = miniexp_nth( 3, m_anno );
    int a = miniexp_to_int( miniexp_nth( 1, area ) );
    int b = miniexp_to_int( miniexp_nth( 2, area ) );
    return QPoint( a, b );
}

QString KDjVu::Annotation::comment() const
{
    return QString::fromUtf8( miniexp_to_str( miniexp_nth( 2, m_anno ) ) );
}

void KDjVu::Annotation::setComment( const QString &comment )
{
    miniexp_t exp = m_anno;
    exp = miniexp_cdr( exp );
    exp = miniexp_cdr( exp );
    miniexp_rplaca( exp, miniexp_string( comment.toUtf8().constData() ) );
}

QColor KDjVu::Annotation::color() const
{
    return QColor();
}

void KDjVu::Annotation::setColor( const QColor & )
{
}

// KDjVu::TextAnnotation

KDjVu::TextAnnotation::TextAnnotation( miniexp_t anno )
  : Annotation( anno ), m_inlineText( true )
{
    const int num = miniexp_length( m_anno );
    for ( int j = 4; j < num; ++j )
    {
        miniexp_t curelem = miniexp_nth( j, m_anno );
        if ( !miniexp_listp( curelem ) )
            continue;

        QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, curelem ) ) );
        if ( id == QLatin1String( "pushpin" ) )
            m_inlineText = false;
    }
}

QSize KDjVu::TextAnnotation::size() const
{
    miniexp_t area = miniexp_nth( 3, m_anno );
    int c = miniexp_to_int( miniexp_nth( 3, area ) );
    int d = miniexp_to_int( miniexp_nth( 4, area ) );
    return QSize( c, d );
}

int KDjVu::TextAnnotation::type() const
{
    return KDjVu::Annotation::TextAnnotation;
}

QColor KDjVu::TextAnnotation::color() const
{
    miniexp_t col = find_second_in_pair( m_anno, "backclr" );
    if ( !miniexp_symbolp( col ) )
        return Qt::transparent;

    return QColor( QString::fromUtf8( miniexp_to_name( col ) ) );
}

void KDjVu::TextAnnotation::setColor( const QColor &color )
{
    const QByteArray col = color.name().toLatin1();
    find_replace_or_add_second_in_pair( m_anno, "backclr", miniexp_symbol( col.constData() ) );
}

bool KDjVu::TextAnnotation::inlineText() const
{
    return m_inlineText;
}

// KDjVu::LineAnnotation

KDjVu::LineAnnotation::LineAnnotation( miniexp_t anno )
  : Annotation( anno ), m_isArrow( false ), m_width( miniexp_nil )
{
    const int num = miniexp_length( m_anno );
    for ( int j = 4; j < num; ++j )
    {
        miniexp_t curelem = miniexp_nth( j, m_anno );
        if ( !miniexp_listp( curelem ) )
            continue;

        QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, curelem ) ) );
        if ( id == QLatin1String( "arrow" ) )
            m_isArrow = true;
        else if ( id == QLatin1String( "width" ) )
            m_width = curelem;
    }
}

int KDjVu::LineAnnotation::type() const
{
    return KDjVu::Annotation::LineAnnotation;
}

QColor KDjVu::LineAnnotation::color() const
{
    miniexp_t col = find_second_in_pair( m_anno, "lineclr" );
    if ( !miniexp_symbolp( col ) )
        return Qt::black;

    return QColor( QString::fromUtf8( miniexp_to_name( col ) ) );
}

void KDjVu::LineAnnotation::setColor( const QColor &color )
{
    const QByteArray col = color.name().toLatin1();
    find_replace_or_add_second_in_pair( m_anno, "lineclr", miniexp_symbol( col.constData() ) );
}

QPoint KDjVu::LineAnnotation::point2() const
{
    miniexp_t area = miniexp_nth( 3, m_anno );
    int c = miniexp_to_int( miniexp_nth( 3, area ) );
    int d = miniexp_to_int( miniexp_nth( 4, area ) );
    return QPoint( c, d );
}

bool KDjVu::LineAnnotation::isArrow() const
{
    return m_isArrow;
}

int KDjVu::LineAnnotation::width() const
{
    if ( m_width == miniexp_nil )
        return 1;

    return miniexp_to_int( miniexp_cadr( m_width ) );
}

void KDjVu::LineAnnotation::setWidth( int width )
{
    find_replace_or_add_second_in_pair( m_anno, "width", miniexp_number( width ) );
}

// KDjVu::TextEntity

KDjVu::TextEntity::TextEntity()
{
}

KDjVu::TextEntity::~TextEntity()
{
}

QString KDjVu::TextEntity::text() const
{
    return m_text;
}

QRect KDjVu::TextEntity::rect() const
{
    return m_rect;
}


class KDjVu::Private
{
    public:
        Private()
          : m_djvu_cxt( nullptr ), m_djvu_document( nullptr ), m_format( nullptr ), m_docBookmarks( nullptr ),
            m_cacheEnabled( true )
        {
        }

        QImage generateImageTile( ddjvu_page_t *djvupage, int& res,
            int width, int row, int xdelta, int height, int col, int ydelta );

        void readBookmarks();
        void fillBookmarksRecurse( QDomDocument& maindoc, QDomNode& curnode,
            miniexp_t exp, int offset = -1 );

        void readMetaData( int page );

        int pageWithName( const QString & name );

        ddjvu_context_t *m_djvu_cxt;
        ddjvu_document_t *m_djvu_document;
        ddjvu_format_t *m_format;

        QVector<KDjVu::Page*> m_pages;
        QVector<ddjvu_page_t *> m_pages_cache;

        QList<ImageCacheItem*> mImgCache;

        QHash<QString, QVariant> m_metaData;
        QDomDocument * m_docBookmarks;

        QHash<QString, int> m_pageNamesCache;

        bool m_cacheEnabled;

        static unsigned int s_formatmask[4];
};

unsigned int KDjVu::Private::s_formatmask[4] = { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 };

QImage KDjVu::Private::generateImageTile( ddjvu_page_t *djvupage, int& res,
    int width, int row, int xdelta, int height, int col, int ydelta )
{
    ddjvu_rect_t renderrect;
    renderrect.x = row * xdelta;
    renderrect.y = col * ydelta;
    int realwidth = qMin( width - renderrect.x, xdelta );
    int realheight = qMin( height - renderrect.y, ydelta );
    renderrect.w = realwidth;
    renderrect.h = realheight;
#ifdef KDJVU_DEBUG
    qDebug() << "renderrect:" << renderrect;
#endif
    ddjvu_rect_t pagerect;
    pagerect.x = 0;
    pagerect.y = 0;
    pagerect.w = width;
    pagerect.h = height;
#ifdef KDJVU_DEBUG
    qDebug() << "pagerect:" << pagerect;
#endif
    handle_ddjvu_messages( m_djvu_cxt, false );
    QImage res_img( realwidth, realheight, QImage::Format_RGB32 );
    // the following line workarounds a rare crash in djvulibre;
    // it should be fixed with >= 3.5.21
    ddjvu_page_get_width( djvupage );
    res = ddjvu_page_render( djvupage, DDJVU_RENDER_COLOR,
                  &pagerect, &renderrect, m_format, res_img.bytesPerLine(), (char *)res_img.bits() );
    if (!res)
    {
        res_img.fill(Qt::white);
    }
#ifdef KDJVU_DEBUG
    qDebug() << "rendering result:" << res;
#endif
    handle_ddjvu_messages( m_djvu_cxt, false );

    return res_img;
}

void KDjVu::Private::readBookmarks()
{
    if ( !m_djvu_document )
        return;

    miniexp_t outline;
    while ( ( outline = ddjvu_document_get_outline( m_djvu_document ) ) == miniexp_dummy )
        handle_ddjvu_messages( m_djvu_cxt, true );

    if ( miniexp_listp( outline ) &&
         ( miniexp_length( outline ) > 0 ) &&
         miniexp_symbolp( miniexp_nth( 0, outline ) ) &&
         ( QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, outline ) ) ) == QLatin1String( "bookmarks" ) ) )
    {
        m_docBookmarks = new QDomDocument( QStringLiteral("KDjVuBookmarks") );
        fillBookmarksRecurse( *m_docBookmarks, *m_docBookmarks, outline, 1 );
        ddjvu_miniexp_release( m_djvu_document, outline );
    }
}

void KDjVu::Private::fillBookmarksRecurse( QDomDocument& maindoc, QDomNode& curnode,
    miniexp_t exp, int offset )
{
    if ( !miniexp_listp( exp ) )
        return;

    int l = miniexp_length( exp );
    for ( int i = qMax( offset, 0 ); i < l; ++i )
    {
        miniexp_t cur = miniexp_nth( i, exp );

        if ( miniexp_consp( cur ) && ( miniexp_length( cur ) > 0 ) &&
             miniexp_stringp( miniexp_nth( 0, cur ) ) && miniexp_stringp( miniexp_nth( 1, cur ) ) )
        {
            QString title = QString::fromUtf8( miniexp_to_str( miniexp_nth( 0, cur ) ) );
            QString dest = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) );
            QDomElement el = maindoc.createElement( QStringLiteral("item") );
            el.setAttribute( QStringLiteral("title"), title );
            if ( !dest.isEmpty() )
            {
                if ( dest.at( 0 ) == QLatin1Char( '#' ) )
                {
                    dest.remove( 0, 1 );
                    bool isNumber = false;
                    dest.toInt( &isNumber );
                    if ( isNumber )
                    {
                        // it might be an actual page number, but could also be a page label
                        // so resolve the number, and get the real page number
                        int pageNo = pageWithName( dest );
                        if ( pageNo != -1 )
                        {
                            el.setAttribute( QStringLiteral("PageNumber"), QString::number( pageNo + 1 ) );
                        }
                        else
                        {
                            el.setAttribute( QStringLiteral("PageNumber"), dest );
                        }
                    }
                    else
                    {
                       el.setAttribute( QStringLiteral("PageName"), dest );
                    }
                }
                else
                {
                    el.setAttribute( QStringLiteral("URL"), dest );
                }
            }
            curnode.appendChild( el );
            if ( !el.isNull() && ( miniexp_length( cur ) > 2 ) )
            {
                fillBookmarksRecurse( maindoc, el, cur, 2 );
            }
        }
    }
}

void KDjVu::Private::readMetaData( int page )
{
    if ( !m_djvu_document )
        return;

    miniexp_t annots;
    while ( ( annots = ddjvu_document_get_pageanno( m_djvu_document, page ) ) == miniexp_dummy )
        handle_ddjvu_messages( m_djvu_cxt, true );

    if ( !miniexp_listp( annots ) || miniexp_length( annots ) == 0 )
        return;

    miniexp_t exp = miniexp_nth( 0, annots );
    int size = miniexp_length( exp );
    if ( size <= 1 ||
         qstrncmp( miniexp_to_name( miniexp_nth( 0, exp ) ), "metadata", 8 ) )
        return;

    for ( int i = 1; i < size; ++i )
    {
        miniexp_t cur = miniexp_nth( i, exp );
        if ( miniexp_length( cur ) != 2 )
            continue;

        QString id = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, cur ) ) );
        QString value = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) );
        m_metaData[ id.toLower() ] = value;
    }
}

int KDjVu::Private::pageWithName( const QString & name )
{
    const int pageNo = m_pageNamesCache.value( name, -1 );
    if ( pageNo != -1 )
        return pageNo;

    const QByteArray utfName = name.toUtf8();
    const int fileNum = ddjvu_document_get_filenum( m_djvu_document );
    ddjvu_fileinfo_t info;
    for ( int i = 0; i < fileNum; ++i )
    {
        if ( DDJVU_JOB_OK != ddjvu_document_get_fileinfo( m_djvu_document, i, &info ) )
            continue;
        if ( info.type != 'P' )
            continue;
        if ( ( utfName == info.id ) || ( utfName == info.name ) || ( utfName == info.title ) )
        {
            m_pageNamesCache.insert( name, info.pageno );
            return info.pageno;
        }
    }
    return -1;
}


KDjVu::KDjVu() : d( new Private )
{
    // creating the djvu context
    d->m_djvu_cxt = ddjvu_context_create( "KDjVu" );
    // creating the rendering format
#if DDJVUAPI_VERSION >= 18
    d->m_format = ddjvu_format_create( DDJVU_FORMAT_RGBMASK32, 4, Private::s_formatmask );
#else
    d->m_format = ddjvu_format_create( DDJVU_FORMAT_RGBMASK32, 3, Private::s_formatmask );
#endif
    ddjvu_format_set_row_order( d->m_format, 1 );
    ddjvu_format_set_y_direction( d->m_format, 1 );
}


KDjVu::~KDjVu()
{
    closeFile();

    ddjvu_format_release( d->m_format );
    ddjvu_context_release( d->m_djvu_cxt );

    delete d;
}

bool KDjVu::openFile( const QString & fileName )
{
    // first, close the old file
    if ( d->m_djvu_document )
        closeFile();

    // load the document..., use UTF-8 variant to work on Windows, too, see bug 422500
    d->m_djvu_document = ddjvu_document_create_by_filename_utf8( d->m_djvu_cxt, fileName.toUtf8().constData(), true );
    if ( !d->m_djvu_document ) return false;
    // ...and wait for its loading
    wait_for_ddjvu_message( d->m_djvu_cxt, DDJVU_DOCINFO );
    if ( ddjvu_document_decoding_error( d->m_djvu_document ) )
    {
        ddjvu_document_release( d->m_djvu_document );
        d->m_djvu_document = nullptr;
        return false;
    }

    qDebug() << "# of pages:" << ddjvu_document_get_pagenum( d->m_djvu_document );
    int numofpages = ddjvu_document_get_pagenum( d->m_djvu_document );
    d->m_pages.clear();
    d->m_pages.resize( numofpages );
    d->m_pages_cache.clear();
    d->m_pages_cache.resize( numofpages );

    // get the document type
    QString doctype;
    switch ( ddjvu_document_get_type( d->m_djvu_document ) )
    {
        case DDJVU_DOCTYPE_UNKNOWN:
            doctype = i18nc( "Type of DjVu document", "Unknown" );
            break;
        case DDJVU_DOCTYPE_SINGLEPAGE:
            doctype = i18nc( "Type of DjVu document", "Single Page" );
            break;
        case DDJVU_DOCTYPE_BUNDLED:
            doctype = i18nc( "Type of DjVu document", "Bundled" );
            break;
        case DDJVU_DOCTYPE_INDIRECT:
            doctype = i18nc( "Type of DjVu document", "Indirect" );
            break;
        case DDJVU_DOCTYPE_OLD_BUNDLED:
            doctype = i18nc( "Type of DjVu document", "Bundled (old)" );
            break;
        case DDJVU_DOCTYPE_OLD_INDEXED:
            doctype = i18nc( "Type of DjVu document", "Indexed (old)" );
            break;
    }
    if ( !doctype.isEmpty() )
        d->m_metaData[ QStringLiteral("documentType") ] = doctype;
    // get the number of components
    d->m_metaData[ QStringLiteral("componentFile") ] = ddjvu_document_get_filenum( d->m_djvu_document );

    // read the pages
    for ( int i = 0; i < numofpages; ++i )
    {
        ddjvu_status_t sts;
        ddjvu_pageinfo_t info;
        while ( ( sts = ddjvu_document_get_pageinfo( d->m_djvu_document, i, &info ) ) < DDJVU_JOB_OK )
            handle_ddjvu_messages( d->m_djvu_cxt, true );
        if ( sts >= DDJVU_JOB_FAILED )
        {
            qDebug().nospace() << "\t>>> page " << i << " failed: " << sts;
            return false;
        }

        KDjVu::Page *p = new KDjVu::Page();
        p->m_width = info.width;
        p->m_height = info.height;
        p->m_dpi = info.dpi;
#if DDJVUAPI_VERSION >= 18
        p->m_orientation = flipRotation( info.rotation );
#else
        p->m_orientation = 0;
#endif
        d->m_pages[i] = p;
    }

    // reading the metadata from the first page only should be enough
    if ( numofpages > 0 )
        d->readMetaData( 0 );

    return true;
}

void KDjVu::closeFile()
{
    // deleting the old TOC
    delete d->m_docBookmarks;
    d->m_docBookmarks = nullptr;
    // deleting the pages
    qDeleteAll( d->m_pages );
    d->m_pages.clear();
    // releasing the djvu pages
    QVector<ddjvu_page_t *>::Iterator it = d->m_pages_cache.begin(), itEnd = d->m_pages_cache.end();
    for ( ; it != itEnd; ++it )
        ddjvu_page_release( *it );
    d->m_pages_cache.clear();
    // clearing the image cache
    qDeleteAll( d->mImgCache );
    d->mImgCache.clear();
    // clearing the old metadata
    d->m_metaData.clear();
    // cleaning the page names mapping
    d->m_pageNamesCache.clear();
    // releasing the old document
    if ( d->m_djvu_document )
        ddjvu_document_release( d->m_djvu_document );
    d->m_djvu_document = nullptr;
}

QVariant KDjVu::metaData( const QString & key ) const
{
    QHash<QString, QVariant>::ConstIterator it = d->m_metaData.constFind( key );
    return it != d->m_metaData.constEnd() ? it.value() : QVariant();
}

const QDomDocument * KDjVu::documentBookmarks() const
{
    if ( !d->m_docBookmarks )
        d->readBookmarks();
    return d->m_docBookmarks;
}

void KDjVu::linksAndAnnotationsForPage( int pageNum, QList<KDjVu::Link*> *links, QList<KDjVu::Annotation*> *annotations ) const
{
    if ( ( pageNum < 0 ) || ( pageNum >= d->m_pages.count() ) || ( !links && !annotations ) )
        return;

    miniexp_t annots;
    while ( ( annots = ddjvu_document_get_pageanno( d->m_djvu_document, pageNum ) ) == miniexp_dummy )
        handle_ddjvu_messages( d->m_djvu_cxt, true );

    if ( !miniexp_listp( annots ) )
        return;

    if ( links )
        links->clear();
    if ( annotations )
        annotations->clear();

    int l = miniexp_length( annots );
    for ( int i = 0; i < l; ++i )
    {
        miniexp_t cur = miniexp_nth( i, annots );
        int num = miniexp_length( cur );
        if ( ( num < 4 ) || !miniexp_symbolp( miniexp_nth( 0, cur ) ) ||
             ( qstrncmp( miniexp_to_name( miniexp_nth( 0, cur ) ), "maparea", 7 ) != 0 ) )
            continue;

        QString target;
        QString type;
        if ( miniexp_symbolp( miniexp_nth( 0, miniexp_nth( 3, cur ) ) ) )
            type = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, miniexp_nth( 3, cur ) ) ) );
        KDjVu::Link* link = nullptr;
        KDjVu::Annotation* ann = nullptr;
        miniexp_t urlexp = miniexp_nth( 1, cur );
        if ( links &&
             ( type == QLatin1String( "rect" ) ||
               type == QLatin1String( "oval" ) ||
               type == QLatin1String( "poly" ) ) )
        {
            if ( miniexp_stringp( urlexp ) )
            {
                target = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, cur ) ) );
            }
            else if ( miniexp_listp( urlexp ) && ( miniexp_length( urlexp ) == 3 ) &&
                      miniexp_symbolp( miniexp_nth( 0, urlexp ) ) &&
                      ( qstrncmp( miniexp_to_name( miniexp_nth( 0, urlexp ) ), "url", 3 ) == 0 ) )
            {
                target = QString::fromUtf8( miniexp_to_str( miniexp_nth( 1, urlexp ) ) );
            }
            if ( target.isEmpty() || ( ( target.length() > 0 ) && target.at(0) == QLatin1Char( '#' ) ) )
            {
                KDjVu::PageLink* plink = new KDjVu::PageLink();
                plink->m_page = target;
                link = plink;
            }
            else
            {
                KDjVu::UrlLink* ulink = new KDjVu::UrlLink();
                ulink->m_url = target;
                link = ulink;
            }
        }
        else if ( annotations &&
                  ( type == QLatin1String( "text" ) ||
                    type == QLatin1String( "line" ) ) )
        {
            if ( type == QLatin1String( "text" ) )
            {
                KDjVu::TextAnnotation * textann = new KDjVu::TextAnnotation( cur );
                ann = textann;
            }
            else if ( type == QLatin1String( "line" ) )
            {
                KDjVu::LineAnnotation * lineann = new KDjVu::LineAnnotation( cur );
                ann = lineann;
            }
        }
        if ( link /* safety check */ && links )
        {
            link->m_area = KDjVu::Link::UnknownArea;
            miniexp_t area = miniexp_nth( 3, cur );
            int arealength = miniexp_length( area );
            if ( ( arealength == 5 ) && ( type == QLatin1String( "rect" ) || type == QLatin1String( "oval" ) ) )
            {
                link->m_point = QPoint( miniexp_to_int( miniexp_nth( 1, area ) ), miniexp_to_int( miniexp_nth( 2, area ) ) );
                link->m_size = QSize( miniexp_to_int( miniexp_nth( 3, area ) ), miniexp_to_int( miniexp_nth( 4, area ) ) );
                if ( type == QLatin1String( "rect" ) )
                {
                    link->m_area = KDjVu::Link::RectArea;
                }
                else
                {
                    link->m_area = KDjVu::Link::EllipseArea;
                }
            }
            else if ( ( arealength > 0 ) && ( arealength % 2 == 1 ) &&
                      type == QLatin1String( "poly" ) )
            {
                link->m_area = KDjVu::Link::PolygonArea;
                QPolygon poly;
                for ( int j = 1; j < arealength; j += 2 )
                {
                    poly << QPoint( miniexp_to_int( miniexp_nth( j, area ) ), miniexp_to_int( miniexp_nth( j + 1, area ) ) );
                }
                link->m_poly = poly;
            }

            if ( link->m_area != KDjVu::Link::UnknownArea )
                links->append( link );
        }
        else if ( ann /* safety check */ && annotations )
        {
            annotations->append( ann );
        }
    }
}

const QVector<KDjVu::Page*> &KDjVu::pages() const
{
    return d->m_pages;
}

QImage KDjVu::image( int page, int width, int height, int rotation )
{
    if ( d->m_cacheEnabled )
    {
        bool found = false;
        QList<ImageCacheItem*>::Iterator it = d->mImgCache.begin(), itEnd = d->mImgCache.end();
        for ( ; ( it != itEnd ) && !found; ++it )
        {
            ImageCacheItem* cur = *it;
            if ( ( cur->page == page ) &&
                ( rotation % 2 == 0
                ? cur->width == width && cur->height == height
                : cur->width == height && cur->height == width ) )
                found = true;
        }
        if ( found )
        {
            // taking the element and pushing to the top of the list
            --it;
            ImageCacheItem* cur2 = *it;
            d->mImgCache.erase( it );
            d->mImgCache.push_front( cur2 );

            return cur2->img;
        }
    }

    if ( !d->m_pages_cache.at( page ) )
    {
        ddjvu_page_t *newpage = ddjvu_page_create_by_pageno( d->m_djvu_document, page );
        // wait for the new page to be loaded
        ddjvu_status_t sts;
        while ( ( sts = ddjvu_page_decoding_status( newpage ) ) < DDJVU_JOB_OK )
            handle_ddjvu_messages( d->m_djvu_cxt, true );
        d->m_pages_cache[page] = newpage;
    }
    ddjvu_page_t *djvupage = d->m_pages_cache[page];

/*
    if ( ddjvu_page_get_rotation( djvupage ) != flipRotation( rotation ) )
    {
// TODO: test documents with initial rotation != 0
//        ddjvu_page_set_rotation( djvupage, m_pages.at( page )->orientation() );
        ddjvu_page_set_rotation( djvupage, (ddjvu_page_rotation_t)flipRotation( rotation ) );
    }
*/

    static const int xdelta = 1500;
    static const int ydelta = 1500;

    int xparts = width / xdelta + 1;
    int yparts = height / ydelta + 1;

    QImage newimg;

    int res = 10000;
    if ( ( xparts == 1 ) && ( yparts == 1 ) )
    {
         // only one part -- render at once with no need to auxiliary image
         newimg = d->generateImageTile( djvupage, res,
                 width, 0, xdelta, height, 0, ydelta );
    }
    else
    {
        // more than one part -- need to render piece-by-piece and to compose
        // the results
        newimg = QImage( width, height, QImage::Format_RGB32 );
        QPainter p;
        p.begin( &newimg );
        int parts = xparts * yparts;
        for ( int i = 0; i < parts; ++i )
        {
            const int row = i % xparts;
            const int col = i / xparts;
            int tmpres = 0;
            const QImage tempp = d->generateImageTile( djvupage, tmpres,
                    width, row, xdelta, height, col, ydelta );
            p.drawImage( row * xdelta, col * ydelta, tempp );
            res = qMin( tmpres, res );
        }
        p.end();
    }

    if ( res && d->m_cacheEnabled )
    {
        // delete all the cached pixmaps for the current page with a size that
        // differs no more than 35% of the new pixmap size
        int imgsize = newimg.width() * newimg.height();
        if ( imgsize > 0 )
        {
            for( int i = 0; i < d->mImgCache.count(); )
            {
                ImageCacheItem* cur = d->mImgCache.at(i);
                if ( ( cur->page == page ) &&
                     ( abs( cur->img.width() * cur->img.height() - imgsize ) < imgsize * 0.35 ) )
                {
                    d->mImgCache.removeAt( i );
                    delete cur;
                }
                else
                    ++i;
            }
        }

        // the image cache has too many elements, remove the last
        if ( d->mImgCache.size() >= 10 )
        {
            delete d->mImgCache.last();
            d->mImgCache.removeLast();
        }
        ImageCacheItem* ich = new ImageCacheItem( page, width, height, newimg );
        d->mImgCache.push_front( ich );
    }

    return newimg;
}

bool KDjVu::exportAsPostScript( const QString & fileName, const QList<int>& pageList ) const
{
    if ( !d->m_djvu_document || fileName.trimmed().isEmpty() || pageList.isEmpty() )
        return false;

    QFile f( fileName );
    f.open( QIODevice::ReadWrite );
    bool ret = exportAsPostScript( &f, pageList );
    if ( ret )
    {
        f.close();
    }
    return ret;
}

bool KDjVu::exportAsPostScript( QFile* file, const QList<int>& pageList ) const
{
    if ( !d->m_djvu_document || !file || pageList.isEmpty() )
        return false;

    FILE* f = fdopen( file->handle(), "w+" );
    if ( !f )
    {
        qDebug() << "error while getting the FILE*";
        return false;
    }

    QString pl;
    for ( const int p : pageList )
    {
        if ( !pl.isEmpty() )
            pl += QLatin1String( "," );
        pl += QString::number( p );
    }
    pl.prepend( QStringLiteral ( "-page=" ) );

    // setting the options
    static const int optc = 1;
    const char ** optv = (const char**)malloc( 1 * sizeof( char* ) );
    QByteArray plb = pl.toLatin1();
    optv[0] = plb.constData();

    ddjvu_job_t *printjob = ddjvu_document_print( d->m_djvu_document, f, optc, optv );
    while ( !ddjvu_job_done( printjob ) )
       handle_ddjvu_messages( d->m_djvu_cxt, true );

    free( optv );

    return fclose( f ) == 0;
}

QList<KDjVu::TextEntity> KDjVu::textEntities( int page, const QString & granularity ) const
{
    if ( ( page < 0 ) || ( page >= d->m_pages.count() ) )
        return QList<KDjVu::TextEntity>();

    miniexp_t r;
    while ( ( r = ddjvu_document_get_pagetext( d->m_djvu_document, page, nullptr ) ) == miniexp_dummy )
        handle_ddjvu_messages( d->m_djvu_cxt, true );

    if ( r == miniexp_nil )
        return QList<KDjVu::TextEntity>();

    QList<KDjVu::TextEntity> ret;

    int height = d->m_pages.at( page )->height();

    QQueue<miniexp_t> queue;
    queue.enqueue( r );

    while ( !queue.isEmpty() )
    {
        miniexp_t cur = queue.dequeue();

        if ( miniexp_listp( cur )
             && ( miniexp_length( cur ) > 0 )
             && miniexp_symbolp( miniexp_nth( 0, cur ) ) )
        {
            int size = miniexp_length( cur );
            QString sym = QString::fromUtf8( miniexp_to_name( miniexp_nth( 0, cur ) ) );
            if ( sym == granularity )
            {
                if ( size >= 6 )
                {
                    int xmin = miniexp_to_int( miniexp_nth( 1, cur ) );
                    int ymin = miniexp_to_int( miniexp_nth( 2, cur ) );
                    int xmax = miniexp_to_int( miniexp_nth( 3, cur ) );
                    int ymax = miniexp_to_int( miniexp_nth( 4, cur ) );
                    QRect rect( xmin, height - ymax, xmax - xmin, ymax - ymin );
                    KDjVu::TextEntity entity;
                    entity.m_rect = rect;
                    entity.m_text = QString::fromUtf8( miniexp_to_str( miniexp_nth( 5, cur ) ) );
                    ret.append( entity );
                }
            }
            else
            {
                for ( int i = 5; i < size; ++i )
                    queue.enqueue( miniexp_nth( i, cur ) );
            }
        }
    }

    return ret;
}

void KDjVu::setCacheEnabled( bool enable )
{
    if ( enable == d->m_cacheEnabled )
        return;

    d->m_cacheEnabled = enable;
    if ( !d->m_cacheEnabled )
    {
        qDeleteAll( d->mImgCache );
        d->mImgCache.clear();
    }
}

bool KDjVu::isCacheEnabled() const
{
    return d->m_cacheEnabled;
}

int KDjVu::pageNumber( const QString & name ) const
{
    if ( !d->m_djvu_document )
        return -1;

    return d->pageWithName( name );
}
