/***************************************************************************
 *   Copyright (C) 2007 by Tobias Koenig <tokoe@kde.org>                   *
 *                                                                         *
 *   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 "textdocumentgenerator.h"
#include "textdocumentgenerator_p.h"

#include <QFile>
#include <QMutex>
#include <QStack>
#include <QTextStream>
#include <QVector>
#include <QFontDatabase>
#include <QImage>
#include <QPainter>
#include <QPrinter>
#include <QTextDocumentWriter>

#include "action.h"
#include "annotations.h"
#include "document_p.h"
#include "page.h"
#include "textpage.h"

#include "document.h"

#include <cmath>

using namespace Okular;

/**
 * Generic Converter Implementation
 */
TextDocumentConverter::TextDocumentConverter()
    : QObject( nullptr ), d_ptr( new TextDocumentConverterPrivate )
{
}

TextDocumentConverter::~TextDocumentConverter()
{
    delete d_ptr;
}

QTextDocument *TextDocumentConverter::convert( const QString & )
{
    return nullptr;
}

Document::OpenResult TextDocumentConverter::convertWithPassword( const QString &fileName, const QString & )
{
    QTextDocument *doc = convert( fileName );
    setDocument( doc );
    return doc != nullptr ? Document::OpenSuccess : Document::OpenError;
}

QTextDocument *TextDocumentConverter::document()
{
    return d_ptr->mDocument;
}

void TextDocumentConverter::setDocument( QTextDocument *document )
{
    d_ptr->mDocument = document;
}

DocumentViewport TextDocumentConverter::calculateViewport( QTextDocument *document, const QTextBlock &block )
{
    return TextDocumentUtils::calculateViewport( document, block );
}

TextDocumentGenerator* TextDocumentConverter::generator() const
{
    return d_ptr->mParent ? d_ptr->mParent->q_func() : nullptr;
}

/**
 * Generic Generator Implementation
 */
Okular::TextPage* TextDocumentGeneratorPrivate::createTextPage( int pageNumber ) const
{
#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    Q_Q( const TextDocumentGenerator );
#endif

    Okular::TextPage *textPage = new Okular::TextPage;

    int start, end;

#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    q->userMutex()->lock();
#endif
    TextDocumentUtils::calculatePositions( mDocument, pageNumber, start, end );

    {
    QTextCursor cursor( mDocument );
    for ( int i = start; i < end - 1; ++i ) {
        cursor.setPosition( i );
        cursor.setPosition( i + 1, QTextCursor::KeepAnchor );

        QString text = cursor.selectedText();
        if ( text.length() == 1 ) {
            QRectF rect;
            TextDocumentUtils::calculateBoundingRect( mDocument, i, i + 1, rect, pageNumber );
            if ( pageNumber == -1 )
                text = QStringLiteral("\n");

            textPage->append( text, new Okular::NormalizedRect( rect.left(), rect.top(), rect.right(), rect.bottom() ) );
        }
    }
    }
#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    q->userMutex()->unlock();
#endif

    return textPage;
}

void TextDocumentGeneratorPrivate::addAction( Action *action, int cursorBegin, int cursorEnd )
{
    if ( !action )
        return;

    LinkPosition position;
    position.link = action;
    position.startPosition = cursorBegin;
    position.endPosition = cursorEnd;

    mLinkPositions.append( position );
}

void TextDocumentGeneratorPrivate::addAnnotation( Annotation *annotation, int cursorBegin, int cursorEnd )
{
    if ( !annotation )
        return;

    annotation->setFlags( annotation->flags() | Okular::Annotation::External );

    AnnotationPosition position;
    position.annotation = annotation;
    position.startPosition = cursorBegin;
    position.endPosition = cursorEnd;

    mAnnotationPositions.append( position );
}

void TextDocumentGeneratorPrivate::addTitle( int level, const QString &title, const QTextBlock &block )
{
    TitlePosition position;
    position.level = level;
    position.title = title;
    position.block = block;

    mTitlePositions.append( position );
}

void TextDocumentGeneratorPrivate::addMetaData( const QString &key, const QString &value, const QString &title )
{
    mDocumentInfo.set( key, value, title );
}

void TextDocumentGeneratorPrivate::addMetaData( DocumentInfo::Key key, const QString &value )
{
    mDocumentInfo.set( key, value );
}

QList<TextDocumentGeneratorPrivate::LinkInfo> TextDocumentGeneratorPrivate::generateLinkInfos() const
{
    QList<LinkInfo> result;

    for ( int i = 0; i < mLinkPositions.count(); ++i ) {
        const LinkPosition &linkPosition = mLinkPositions[ i ];

        const QVector<QRectF> rects = TextDocumentUtils::calculateBoundingRects( mDocument, linkPosition.startPosition, linkPosition.endPosition );

        for ( int i = 0; i < rects.count(); ++i) {
            const QRectF &rect = rects[ i ];

            LinkInfo info;
            info.link = linkPosition.link;
            info.ownsLink = i == 0;
            info.page = std::floor( rect.y() );
            info.boundingRect = QRectF( rect.x(), rect.y() - info.page, rect.width(), rect.height() );
            result.append( info );
        }
    }

    return result;
}

QList<TextDocumentGeneratorPrivate::AnnotationInfo> TextDocumentGeneratorPrivate::generateAnnotationInfos() const
{
    QList<AnnotationInfo> result;

    for ( int i = 0; i < mAnnotationPositions.count(); ++i ) {
        const AnnotationPosition &annotationPosition = mAnnotationPositions[ i ];

        AnnotationInfo info;
        info.annotation = annotationPosition.annotation;

        TextDocumentUtils::calculateBoundingRect( mDocument, annotationPosition.startPosition, annotationPosition.endPosition,
                                                  info.boundingRect, info.page );

        if ( info.page >= 0 )
            result.append( info );
    }

    return result;
}

void TextDocumentGeneratorPrivate::generateTitleInfos()
{
    QStack< QPair<int,QDomNode> > parentNodeStack;

    QDomNode parentNode = mDocumentSynopsis;

    parentNodeStack.push( qMakePair( 0, parentNode ) );

    for ( int i = 0; i < mTitlePositions.count(); ++i ) {
        const TitlePosition &position = mTitlePositions[ i ];

        Okular::DocumentViewport viewport = TextDocumentUtils::calculateViewport( mDocument, position.block );

        QDomElement item = mDocumentSynopsis.createElement( position.title );
        item.setAttribute( QStringLiteral("Viewport"), viewport.toString() );

        int headingLevel = position.level;

        // we need a parent, which has to be at a higher heading level than this heading level
        // so we just work through the stack
        while ( ! parentNodeStack.isEmpty() ) {
            int parentLevel = parentNodeStack.top().first;
            if ( parentLevel < headingLevel ) {
                // this is OK as a parent
                parentNode = parentNodeStack.top().second;
                break;
            } else {
                // we'll need to be further into the stack
                parentNodeStack.pop();
            }
        }
        parentNode.appendChild( item );
        parentNodeStack.push( qMakePair( headingLevel, QDomNode(item) ) );
    }
}

void TextDocumentGeneratorPrivate::initializeGenerator()
{
    Q_Q( TextDocumentGenerator );

    mConverter->d_ptr->mParent = q->d_func();

    if ( mGeneralSettings ) {
        mFont = mGeneralSettings->font();
    }

    q->setFeature( Generator::TextExtraction );
    q->setFeature( Generator::PrintNative );
    q->setFeature( Generator::PrintToFile );
#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    q->setFeature( Generator::Threaded );
#endif

    QObject::connect( mConverter, &TextDocumentConverter::addAction,
                      q, [this](Action *a, int cb, int ce) { addAction(a, cb, ce); } );
    QObject::connect( mConverter, &TextDocumentConverter::addAnnotation,
                      q, [this](Annotation *a, int cb, int ce) { addAnnotation(a, cb, ce); } );
    QObject::connect( mConverter, &TextDocumentConverter::addTitle,
                      q, [this](int l, const QString &t, const QTextBlock &b) { addTitle(l, t, b); } );
    QObject::connect( mConverter, QOverload<const QString &, const QString &, const QString &>::of(&TextDocumentConverter::addMetaData),
                      q, [this](const QString &k, const QString &v, const QString &t) { addMetaData(k, v, t); } );
    QObject::connect( mConverter, QOverload<DocumentInfo::Key,const QString &>::of(&TextDocumentConverter::addMetaData),
                      q, [this](DocumentInfo::Key k, const QString &v) { addMetaData(k, v); } );

    QObject::connect( mConverter, &TextDocumentConverter::error,
                      q, &Generator::error );
    QObject::connect( mConverter, &TextDocumentConverter::warning,
                      q, &Generator::warning );
    QObject::connect( mConverter, &TextDocumentConverter::notice,
                      q, &Generator::notice );
}

TextDocumentGenerator::TextDocumentGenerator(TextDocumentConverter *converter, const QString& configName , QObject *parent, const QVariantList &args)
    : Okular::Generator( *new TextDocumentGeneratorPrivate( converter ), parent, args )
{
    Q_D( TextDocumentGenerator );
    d->mGeneralSettings = new TextDocumentSettings( configName, this );

    d->initializeGenerator();
}

TextDocumentGenerator::~TextDocumentGenerator()
{
}

Document::OpenResult TextDocumentGenerator::loadDocumentWithPassword( const QString & fileName, QVector<Okular::Page*> & pagesVector, const QString &password )
{
    Q_D( TextDocumentGenerator );
    const Document::OpenResult openResult = d->mConverter->convertWithPassword( fileName, password );

    if ( openResult != Document::OpenSuccess )
    {
        d->mDocument = nullptr;

        // loading failed, cleanup all the stuff eventually gathered from the converter
        d->mTitlePositions.clear();
        for ( const TextDocumentGeneratorPrivate::LinkPosition &linkPos : qAsConst(d->mLinkPositions) )
        {
            delete linkPos.link;
        }
        d->mLinkPositions.clear();
        for ( const TextDocumentGeneratorPrivate::AnnotationPosition &annPos : qAsConst(d->mAnnotationPositions) )
        {
            delete annPos.annotation;
        }
        d->mAnnotationPositions.clear();

        return openResult;
    }
    d->mDocument = d->mConverter->document();

    d->generateTitleInfos();
    const QList<TextDocumentGeneratorPrivate::LinkInfo> linkInfos = d->generateLinkInfos();
    const QList<TextDocumentGeneratorPrivate::AnnotationInfo> annotationInfos = d->generateAnnotationInfos();

    pagesVector.resize( d->mDocument->pageCount() );

    const QSize size = d->mDocument->pageSize().toSize();

    QVector< QLinkedList<Okular::ObjectRect*> > objects( d->mDocument->pageCount() );
    for ( const TextDocumentGeneratorPrivate::LinkInfo &info : linkInfos ) {
        // in case that the converter report bogus link info data, do not assert here
        if ( info.page < 0 || info.page >= objects.count() )
          continue;

        const QRectF rect = info.boundingRect;
        if ( info.ownsLink ) {
            objects[ info.page ].append( new Okular::ObjectRect( rect.left(), rect.top(), rect.right(), rect.bottom(), false,
                                                                 Okular::ObjectRect::Action, info.link ) );
        } else {
            objects[ info.page ].append( new Okular::NonOwningObjectRect( rect.left(), rect.top(), rect.right(), rect.bottom(), false,
                                                                          Okular::ObjectRect::Action, info.link ) );
        }
    }

    QVector< QLinkedList<Okular::Annotation*> > annots( d->mDocument->pageCount() );
    for ( const TextDocumentGeneratorPrivate::AnnotationInfo &info : annotationInfos ) {
        annots[ info.page ].append( info.annotation );
    }

    for ( int i = 0; i < d->mDocument->pageCount(); ++i ) {
        Okular::Page * page = new Okular::Page( i, size.width(), size.height(), Okular::Rotation0 );
        pagesVector[ i ] = page;

        if ( !objects.at( i ).isEmpty() ) {
            page->setObjectRects( objects.at( i ) );
        }
        QLinkedList<Okular::Annotation*>::ConstIterator annIt = annots.at( i ).begin(), annEnd = annots.at( i ).end();
        for ( ; annIt != annEnd; ++annIt ) {
            page->addAnnotation( *annIt );
        }
    }

    return openResult;
}

bool TextDocumentGenerator::doCloseDocument()
{
    Q_D( TextDocumentGenerator );
    delete d->mDocument;
    d->mDocument = nullptr;

    d->mTitlePositions.clear();
    d->mLinkPositions.clear();
    d->mAnnotationPositions.clear();
    // do not use clear() for the following two, otherwise they change type
    d->mDocumentInfo = Okular::DocumentInfo();
    d->mDocumentSynopsis = Okular::DocumentSynopsis();

    return true;
}

bool TextDocumentGenerator::canGeneratePixmap() const
{
    return Generator::canGeneratePixmap();
}

void TextDocumentGenerator::generatePixmap( Okular::PixmapRequest * request )
{
    Generator::generatePixmap( request );
}

QImage TextDocumentGeneratorPrivate::image( PixmapRequest * request )
{
    if ( !mDocument )
        return QImage();

#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    Q_Q( TextDocumentGenerator );
#endif

    QImage image( request->width(), request->height(), QImage::Format_ARGB32 );
    image.fill( Qt::white );

    QPainter p;
    p.begin( &image );

    qreal width = request->width();
    qreal height = request->height();

    const QSize size = mDocument->pageSize().toSize();

    p.scale( width / (qreal)size.width(), height / (qreal)size.height() );

    QRect rect;
    rect = QRect( 0, request->pageNumber() * size.height(), size.width(), size.height() );
    p.translate( QPoint( 0, request->pageNumber() * size.height() * -1 ) );
    p.setClipRect( rect );
#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    q->userMutex()->lock();
#endif
    QAbstractTextDocumentLayout::PaintContext context;
    context.palette.setColor( QPalette::Text, Qt::black );
//  FIXME Fix Qt, this doesn't work, we have horrible hacks
//        in the generators that return html, remove that code
//        if Qt ever gets fixed
//     context.palette.setColor( QPalette::Link, Qt::blue );
    context.clip = rect;
    mDocument->setDefaultFont( mFont );
    mDocument->documentLayout()->draw( &p, context );
#ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING
    q->userMutex()->unlock();
#endif
    p.end();

    return image;
}

Okular::TextPage* TextDocumentGenerator::textPage( Okular::TextRequest * request )
{
    Q_D( TextDocumentGenerator );
    return d->createTextPage( request->page()->number() );
}

bool TextDocumentGenerator::print( QPrinter& printer )
{
    Q_D( TextDocumentGenerator );
    if ( !d->mDocument )
        return false;

    d->mDocument->print( &printer );

    return true;
}

Okular::DocumentInfo TextDocumentGenerator::generateDocumentInfo( const QSet<DocumentInfo::Key> & /*keys*/ ) const
{
    Q_D( const TextDocumentGenerator );
    return d->mDocumentInfo;
}

const Okular::DocumentSynopsis* TextDocumentGenerator::generateDocumentSynopsis()
{
    Q_D( TextDocumentGenerator );
    if ( !d->mDocumentSynopsis.hasChildNodes() )
        return nullptr;
    else
        return &d->mDocumentSynopsis;
}

QVariant TextDocumentGeneratorPrivate::metaData( const QString &key, const QVariant &option ) const
{
    Q_UNUSED( option )
    if ( key == QLatin1String("DocumentTitle") )
    {
        return mDocumentInfo.get( DocumentInfo::Title );
    }
    return QVariant();
}

Okular::ExportFormat::List TextDocumentGenerator::exportFormats(   ) const
{
    static Okular::ExportFormat::List formats;
    if ( formats.isEmpty() ) {
        formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) );
        formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PDF ) );
        if ( QTextDocumentWriter::supportedDocumentFormats().contains( "ODF" ) ) {
            formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::OpenDocumentText ) );
        }
        if ( QTextDocumentWriter::supportedDocumentFormats().contains( "HTML" ) ) {
            formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::HTML ) );
        }
    }

    return formats;
}

bool TextDocumentGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format )
{
    Q_D( TextDocumentGenerator );
    if ( !d->mDocument )
        return false;

    if ( format.mimeType().name() == QLatin1String( "application/pdf" ) ) {
        QFile file( fileName );
        if ( !file.open( QIODevice::WriteOnly ) )
            return false;

        QPrinter printer( QPrinter::HighResolution );
        printer.setOutputFormat( QPrinter::PdfFormat );
        printer.setOutputFileName( fileName );
        d->mDocument->print( &printer );

        return true;
    } else if ( format.mimeType().name() == QLatin1String( "text/plain" ) ) {
        QFile file( fileName );
        if ( !file.open( QIODevice::WriteOnly ) )
            return false;

        QTextStream out( &file );
        out << d->mDocument->toPlainText();

        return true;
    } else if ( format.mimeType().name() == QLatin1String( "application/vnd.oasis.opendocument.text" ) ) {
        QTextDocumentWriter odfWriter( fileName, "odf" );

        return odfWriter.write( d->mDocument );
    } else if ( format.mimeType().name() == QLatin1String( "text/html" ) ) {
        QTextDocumentWriter odfWriter( fileName, "html" );

        return odfWriter.write( d->mDocument );
    }
    return false;
}

bool TextDocumentGenerator::reparseConfig()
{
    Q_D( TextDocumentGenerator );
    const QFont newFont = d->mGeneralSettings->font();

    if ( newFont != d->mFont ) {
        d->mFont = newFont;
        return true;
    }

    return false;
}

void TextDocumentGenerator::addPages( KConfigDialog* /*dlg*/ )
{
    qCWarning(OkularCoreDebug) << "You forgot to reimplement addPages in your TextDocumentGenerator";
    return;
}

TextDocumentSettings* TextDocumentGenerator::generalSettings()
{
    Q_D( TextDocumentGenerator );

    return d->mGeneralSettings;
}

TextDocumentConverter* TextDocumentGenerator::converter()
{
    Q_D( TextDocumentGenerator );

    return d->mConverter;
}

void TextDocumentGenerator::setTextDocument( QTextDocument *textDocument )
{
    Q_D( TextDocumentGenerator );

    d->mDocument = textDocument;

    for (Page *p : qAsConst(d->m_document->m_pagesVector))
    {
        p->setTextPage( nullptr );
    }
}
