/***************************************************************************
*   Copyright (C) 2004-2009 by Thomas Fischer                             *
*   fischer@unix-ag.uni-kl.de                                             *
*                                                                         *
*   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.,                                       *
*   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
***************************************************************************/
#include <file.h>
#include <element.h>
#include <entry.h>
#include <macro.h>
#include <preamble.h>
#include <value.h>
#include <comment.h>
#include <encoderlatex.h>

#include "fileexporterbibtex.h"

namespace BibTeX
{

    FileExporterBibTeX::FileExporterBibTeX() : FileExporter(),
            m_iconvBufferSize( 16384 ), m_stringOpenDelimiter( '"' ), m_stringCloseDelimiter( '"' ), m_keywordCasing( kcCamelCase ), m_encoding( "latex" ), m_protectCasing( FALSE ), cancelFlag( FALSE )
    {
        m_iconvBuffer = new char[m_iconvBufferSize];
    }

    FileExporterBibTeX::~FileExporterBibTeX()
    {
        delete[] m_iconvBuffer;
    }

    bool FileExporterBibTeX::save( QIODevice* iodevice, const File* bibtexfile, QStringList * /*errorLog*/ )
    {
        m_mutex.lock();
        bool result = TRUE;

        /**
          * Categorize elements from the bib file into four groups,
          * to ensure that BibTeX finds all connected elements
          * in the correct order.
          */

        QValueList<Comment*> parameterCommentsList;
        QValueList<Preamble*> preambleList;
        QValueList<Macro*> macroList;
        QValueList<Entry*> crossRefingEntryList;
        QValueList<Element*> remainingList;

        for ( File::ElementList::const_iterator it = bibtexfile->elements.begin(); it != bibtexfile->elements.end() && result && !cancelFlag; it++ )
        {
            Preamble *preamble = dynamic_cast<Preamble*>( *it );
            if ( preamble != NULL )
                preambleList.append( preamble );
            else
            {
                Macro *macro = dynamic_cast<Macro*>( *it );
                if ( macro != NULL )
                    macroList.append( macro );
                else
                {
                    Entry *entry = dynamic_cast<Entry*>( *it );
                    if (( entry != NULL ) && ( entry->getField( EntryField::ftCrossRef ) != NULL ) )
                        crossRefingEntryList.append( entry );
                    else
                    {
                        Comment *comment = dynamic_cast<Comment*>( *it );
                        QString commentText = QString::null;
                        /** check if this file requests a special encoding */
                        if ( comment != NULL && comment->useCommand() && (( commentText = comment->text().lower() ) ).startsWith( "x-kbibtex-encoding=" ) )
                        {
                            m_encoding = commentText.mid( 19 );
                            qDebug( "Switching encoding to <%s>", m_encoding.latin1() );
                            parameterCommentsList.append( comment );
                        }
                        else
                            remainingList.append( *it );
                    }
                }
            }
        }

        int totalElements = ( int ) bibtexfile->count();
        int currentPos = 0;

        const char *encodingTo = m_encoding == "latex" ? "utf-8\0" : m_encoding.append( "\0" ).ascii();
        m_iconvHandle = iconv_open( encodingTo, "utf-8" );

        /** before anything else, write parameter comments */
        for ( QValueList<Comment*>::iterator it = parameterCommentsList.begin(); it != parameterCommentsList.end() && result && !cancelFlag; it++ )
        {
            result &= writeComment( *iodevice, *it );
            emit progress( ++currentPos, totalElements );
        }

        /** first, write preambles and strings (macros) at the beginning */
        for ( QValueList<Preamble*>::iterator it = preambleList.begin(); it != preambleList.end() && result && !cancelFlag; it++ )
        {
            result &= writePreamble( *iodevice, *it );
            emit progress( ++currentPos, totalElements );
        }

        for ( QValueList<Macro*>::iterator it = macroList.begin(); it != macroList.end() && result && !cancelFlag; it++ )
        {
            result &= writeMacro( *iodevice, *it );
            emit progress( ++currentPos, totalElements );
        }

        /** second, write cross-referencing elements */
        for ( QValueList<Entry*>::iterator it = crossRefingEntryList.begin(); it != crossRefingEntryList.end() && result && !cancelFlag; it++ )
        {
            result &= writeEntry( *iodevice, *it );
            emit progress( ++currentPos, totalElements );
        }

        /** third, write remaining elements */
        for ( QValueList<Element*>::iterator it = remainingList.begin(); it != remainingList.end() && result && !cancelFlag; it++ )
        {
            Entry *entry = dynamic_cast<Entry*>( *it );
            if ( entry != NULL )
                result &= writeEntry( *iodevice, entry );
            else
            {
                Comment *comment = dynamic_cast<Comment*>( *it );
                if ( comment != NULL )
                    result &= writeComment( *iodevice, comment );
            }
            emit progress( ++currentPos, totalElements );
        }

        iconv_close( m_iconvHandle );
        m_mutex.unlock();
        return result && !cancelFlag;
    }

    bool FileExporterBibTeX::save( QIODevice* iodevice, const Element* element, QStringList * /*errorLog*/ )
    {
        m_mutex.lock();
        bool result = FALSE;

        const char *encodingTo = m_encoding == "latex" ? "utf-8\0" : m_encoding.append( "\0" ).ascii();
        m_iconvHandle = iconv_open( encodingTo, "utf-8" );

        const Entry *entry = dynamic_cast<const Entry*>( element );
        if ( entry != NULL )
            result |= writeEntry( *iodevice, entry );
        else
        {
            const Macro * macro = dynamic_cast<const Macro*>( element );
            if ( macro != NULL )
                result |= writeMacro( *iodevice, macro );
            else
            {
                const Comment * comment = dynamic_cast<const Comment*>( element );
                if ( comment != NULL )
                    result |= writeComment( *iodevice, comment );
                else
                {
                    const Preamble * preamble = dynamic_cast<const Preamble*>( element );
                    if ( preamble != NULL )
                        result |= writePreamble( *iodevice, preamble );
                }
            }
        }

        iconv_close( m_iconvHandle );
        m_mutex.unlock();
        return result && !cancelFlag;
    }

    void FileExporterBibTeX::cancel()
    {
        cancelFlag = TRUE;
    }

    bool FileExporterBibTeX::writeEntry( QIODevice &device, const Entry* entry )
    {
        writeString( device, QString( "@%1{ %2" ).arg( applyKeywordCasing( entry->entryTypeString() ) ).arg( entry->id() ) );

        for ( Entry::EntryFields::ConstIterator it = entry->begin(); it != entry->end(); ++it )
        {
            EntryField *field = *it;
            QString text = valueToString( field->value(), field->fieldType(), field->fieldTypeName() );
            if ( m_protectCasing && dynamic_cast<BibTeX::PlainText*>( field->value()->items.first() ) != NULL && ( field->fieldType() == EntryField::ftTitle || field->fieldType() == EntryField::ftBookTitle || field->fieldType() == EntryField::ftSeries ) )
                addProtectiveCasing( text );
            writeString( device, QString( ",\n\t%1 = %2" ).arg( field->fieldTypeName() ).arg( text ) );
        }
        writeString( device, "\n}\n\n" );
        return TRUE;
    }

    bool FileExporterBibTeX::writeMacro( QIODevice &device, const Macro *macro )
    {
        QString text = valueToString( macro->value() );
        if ( m_protectCasing )
            addProtectiveCasing( text );

        writeString( device, QString( "@%1{ %2 = %3 }\n\n" ).arg( applyKeywordCasing( "String" ) ).arg( macro->key() ).arg( text ) );

        return TRUE;
    }

    bool FileExporterBibTeX::writeComment( QIODevice &device, const Comment *comment )
    {
        if ( !comment->useCommand() )
        {
            QString text = comment->text() ;

            if ( m_encoding == "latex" )
                text = EncoderLaTeX::currentEncoderLaTeX() ->encode( text );

            QStringList commentLines = QStringList::split( '\n', text );
            for ( QStringList::Iterator it = commentLines.begin(); it != commentLines.end(); it++ )
            {
                writeString( device, ( *it ).append( "\n" ) );
            }
            writeString( device, "\n" );
        }
        else
        {
            QString text = comment->text() ;

            if ( m_encoding == "latex" )
                text = EncoderLaTeX::currentEncoderLaTeX() ->encode( text );

            writeString( device, QString( "@%1{%2}\n\n" ).arg( applyKeywordCasing( "Comment" ) ).arg( text ) );
        }
        return TRUE;
    }

    bool FileExporterBibTeX::writePreamble( QIODevice &device, const Preamble* preamble )
    {
        writeString( device, QString( "@%1{%2}\n\n" ).arg( applyKeywordCasing( "Preamble" ) ).arg( valueToString( preamble->value() ) ) );

        return TRUE;
    }

    bool FileExporterBibTeX::writeString( QIODevice &device, const QString& text )
    {
        size_t utf8datasize = 1;
        QCString utf8 = text.utf8();
        char *utf8data = utf8.data();
        utf8datasize = utf8.length();
        char *outputdata = m_iconvBuffer;
        size_t outputdatasize = m_iconvBufferSize;

        size_t result = iconv( m_iconvHandle, &utf8data, &utf8datasize, &outputdata, &outputdatasize );
        if ( result != 0 )
        {
            qWarning( "Cannot convert string using iconv" );
            return false;
        }

        if ( device.writeBlock( m_iconvBuffer, m_iconvBufferSize - outputdatasize ) != ( int )( m_iconvBufferSize - outputdatasize ) )
        {
            qWarning( "Cannot write string to device" );
            return false;
        }

        return true;
    }

    void FileExporterBibTeX::setStringDelimiter( const QChar& stringOpenDelimiter, const QChar& stringCloseDelimiter )
    {
        m_stringOpenDelimiter = stringOpenDelimiter;
        m_stringCloseDelimiter = stringCloseDelimiter;
    }

    void FileExporterBibTeX::setKeywordCasing( const KeywordCasing keywordCasing )
    {
        m_keywordCasing = keywordCasing;
    }

    void FileExporterBibTeX::setEncoding( const QString& encoding )
    {
        m_encoding = encoding;
    }

    void FileExporterBibTeX::setEnclosingCurlyBrackets( bool protectCasing )
    {
        m_protectCasing = protectCasing;
    }

    QString FileExporterBibTeX::valueToString( const Value *value, const EntryField::FieldType fieldType, const QString &fieldTypeName )
    {
        if ( value == NULL )
            return "";

        QString result;
        bool isFirst = TRUE;
        EncoderLaTeX *encoder = EncoderLaTeX::currentEncoderLaTeX();

        for ( QValueList<ValueItem*>::ConstIterator it = value->items.begin(); it != value->items.end(); ++it )
        {
            if ( !isFirst )
                result.append( " # " );
            else
                isFirst = FALSE;

            MacroKey *macroKey = dynamic_cast<MacroKey*>( *it );
            if ( macroKey != NULL )
                result.append( macroKey->text() );
            else
            {
                QString text;
                BibTeX::PersonContainer *personContainer = dynamic_cast<BibTeX::PersonContainer*>( *it );
                BibTeX::PlainText *plainText = dynamic_cast<BibTeX::PlainText*>( *it );
                BibTeX::KeywordContainer *keywordContainer = dynamic_cast<BibTeX::KeywordContainer*>( *it );

                if ( plainText != NULL )
                    text = plainText->text();
                else if ( keywordContainer != NULL )
                {
                    bool first = TRUE;
                    for ( QValueList<Keyword*>::Iterator it = keywordContainer->keywords.begin(); it != keywordContainer->keywords.end(); ++it )
                    {
                        if ( !first )
                            text.append( ", " );
                        else
                            first = FALSE;
                        text.append(( *it )->text() );
                    }
                }
                else if ( personContainer != NULL )
                {
                    bool first = TRUE;
                    for ( QValueList<Person*>::Iterator it = personContainer->persons.begin(); it != personContainer->persons.end(); ++it )
                    {
                        if ( !first )
                            text.append( " and " );
                        else
                            first = FALSE;

                        QString v = ( *it )->firstName();
                        if ( !v.isEmpty() )
                        {
                            bool requiresQuoting = requiresPersonQuoting( v, FALSE );
                            if ( requiresQuoting ) text.append( "{" );
                            text.append( v );
                            if ( requiresQuoting ) text.append( "}" );
                            text.append( " " );
                        }

                        v = ( *it )->lastName();
                        if ( !v.isEmpty() )
                        {
                            /** Multi-part surnames (such as "Garcia Marquez") have to be enquoted.
                              * However, "von"-Parts (as in "von Hofmannsthal") must _not_ be enquoted.
                              * Examples:
                              * -- Robson de Souza
                              * -- Hartmann von der Tann
                              * -- Ronaldo de {Assis Moreira} ("Ronaldo de Assis Moreira" works as well)
                              * -- Ailton {Goncalves da Silva}
                              * -- Gloria von {Thurn und Taxis}
                              * Thus we split the von-Parts from the surname (= everything after the first upcase char).
                              * FIXME: Make the personContainer aware of von-Parts and jr-Parts, instead.
                              */
                            QStringList list = QStringList::split( " ", v );
                            QString von;
                            for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it )
                            {
                                QString str = *it;
                                if ( str != "others" && str[0].category() == QChar::Letter_Lowercase )
                                {
                                    von += *it;
                                    von += " ";
                                }
                                else
                                    break;
                            }
                            if ( !von.isEmpty() )
                            {
                                text.append( von );
                                v = v.right( v.length() - von.length() );
                            }
                            bool requiresQuoting = requiresPersonQuoting( v, TRUE );
                            if ( requiresQuoting ) text.append( "{" );
                            text.append( v );
                            if ( requiresQuoting ) text.append( "}" );
                        }
                    }
                }

                if ( m_encoding == "latex" )
                    text = encoder->encodeSpecialized( text, fieldType );

                if ( fieldType == EntryField::ftURL || fieldType == EntryField::ftDoi || ( fieldType == EntryField::ftUnknown && fieldTypeName.lower() == "slaccitation" ) )
                    removeBackslashQuoting( text );

                /** if the text to save contains a quote char ("),
                  * force string delimiters to be curly brackets,
                  * as quote chars as string delimiters would result
                  * in parser failures
                  */
                QChar stringOpenDelimiter = m_stringOpenDelimiter;
                QChar stringCloseDelimiter = m_stringCloseDelimiter;
                if ( text.contains( '"' ) && ( m_stringOpenDelimiter == '"' || m_stringCloseDelimiter == '"' ) )
                {
                    stringOpenDelimiter = '{';
                    stringCloseDelimiter = '}';
                }

                result.append( stringOpenDelimiter ).append( text ).append( stringCloseDelimiter );
            }
        }

        return result;
    }

    void FileExporterBibTeX::removeBackslashQuoting( QString &text )
    {
        text.replace( "\\&", "&" ).replace( "\\#", "#" ).replace( "\\_", "_" ).replace( "\\%", "%" );
    }

    QString FileExporterBibTeX::applyKeywordCasing( const QString &keyword )
    {
        switch ( m_keywordCasing )
        {
        case kcLowerCase: return keyword.lower();
        case kcInitialCapital: return keyword.at( 0 ) + keyword.lower().mid( 1 );
        case kcCapital: return keyword.upper();
        default: return keyword;
        }
    }

    bool FileExporterBibTeX::requiresPersonQuoting( const QString &text, bool isLastName )
    {
        if ( isLastName && !text.contains( " " ) )
            /** Last name contains NO spaces, no quoting necessary */
            return FALSE;
        else if ( isLastName && text[0].category() == QChar::Letter_Lowercase )
            /** Last name starts with lower case character (e.g. as in "van der Linden") */
            return FALSE;
        else if ( !isLastName && !text.contains( " and " ) )
            /** First name contains no " and " no quoting necessary */
            return FALSE;
        else if ( text[0] != '{' || text[text.length()-1] != '}' )
            /** as either last name contains spaces or first name contains " and " and there is no protective quoting yet, there must be a protective quoting added */
            return TRUE;

        /** check for cases like "{..}..{..}", which must be surrounded with a protective quoting, too */
        int bracketCounter = 0;
        for ( int i = text.length() - 1; i >= 0; --i )
        {
            if ( text[i] == '{' )
                ++bracketCounter;
            else if ( text[i] == '}' )
                --bracketCounter;
            if ( bracketCounter == 0 && i > 0 )
                return TRUE;
        }
        return FALSE;
    }

    void FileExporterBibTeX::addProtectiveCasing( QString &text )
    {
        if (( text[0] != '"' || text[text.length()-1] != '"' ) && ( text[0] != '{' || text[text.length()-1] != '}' ) )
        {
            /** nothing to protect, as this is no text string */
            return;
        }

        bool addBrackets = TRUE;

        if ( text[1] == '{' && text[text.length() - 2] == '}' )
        {
            addBrackets = FALSE;
            int count = 0;
            for ( int i = text.length() - 2; !addBrackets && i >= 1; --i )
                if ( text[i] == '{' )++count;
                else if ( text[i] == '}' )--count;
                else if ( count == 0 ) addBrackets = TRUE;
        }

        if ( addBrackets )
            text.insert( 1, '{' ).insert( text.length(), '}' );
    }

}
