/* This file is part of the KDE project
   Copyright (C) 2001 Simon Hausmann <hausmann@kde.org>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/

#include "kstextview.h"
#include "stringparserstate.h"

#include <qpainter.h>
#include <qvaluestack.h>
#include <qdragobject.h>
#include <qtimer.h>
#include <qclipboard.h>
#include <qdict.h>
#include <kcharsets.h>
#include <kapplication.h>
#include <kmimesourcefactory.h>
#include <kcursor.h>
#include <assert.h>

using namespace KSirc;

typedef StringParserState<QChar> ParsingState;

static const int PaintBufferExtend = 128;

// temporary(!)
static QDict<QPixmap> *ksTextViewPixmapDict = 0;
static void cleanupKSTextViewPixmapDict()
{
    delete ksTextViewPixmapDict;
    ksTextViewPixmapDict = 0;
}

QPixmap ksTextViewLoadPixmap( const QString &icon )
{
    if ( !ksTextViewPixmapDict )
    {
        ksTextViewPixmapDict = new QDict<QPixmap>;
        ksTextViewPixmapDict->setAutoDelete( true );
        qAddPostRoutine( cleanupKSTextViewPixmapDict );
    }

    QPixmap *pix = ksTextViewPixmapDict->find( icon );
    if ( !pix )
    {
        QImage img;

        const QMimeSource *src = kapp->mimeSourceFactory()->data( icon, QString::null );
        if ( !src || !QImageDrag::decode( src, img ) || img.isNull() )
            return QPixmap();

        pix = new QPixmap( img );
        ksTextViewPixmapDict->insert( icon, pix );
    }
    return *pix;
}

Item::Item( TextParag *parag, const ItemProperties &props )
    : m_extendsDirty( true ), m_minWidth( -1 ), m_width( - 1 ),
      m_height( - 1 ), m_selection( NoSelection ), m_parag( parag ),
      m_props( props )
{
}

Item::~Item()
{
}

int Item::width() const
{
    if ( m_extendsDirty )
    {
        calcExtends();
        m_extendsDirty = false;
    }
    return m_width;
}

int Item::minWidth() const
{
    if ( m_extendsDirty )
    {
        calcExtends();
        m_extendsDirty = false;
    }
    return m_minWidth;
}

int Item::height() const
{
    if ( m_extendsDirty )
    {
        calcExtends();
        m_extendsDirty = false;
    }
    return m_height;
}

Item *Item::breakLine( int )
{
    return 0; // can't break by default...
}

int Item::calcSelectionOffset( int x )
{
    return x; // default ...
}

void Item::selectionOffsets( int &startOffset, int &endOffset )
{
    m_parag->textView()->selectionOffsets( startOffset, endOffset );
}

int Item::maxSelectionOffset() const
{
    return text().len - 1;
}

StringPtr Item::text() const
{
    return StringPtr();
}

void Item::setProps( const ItemProperties &props )
{
    m_props = props;
    m_extendsDirty = true;
}

Item *Item::create( TextParag *parag, const Token &tok, const ItemProperties &props )
{
    assert( tok.id != Token::TagClose );

    if ( tok.id == Token::Text )
        return new TextChunk( parag, tok.value, props );

    if ( tok.value == "img" )
    {
        QString url = CONSTSTRING( tok.attributes[ "src" ] );
        if ( url.isEmpty() )
            return 0;

        QPixmap pixmap = ksTextViewLoadPixmap( url );
        if ( pixmap.isNull() )
            return 0;

        return new ImageItem( parag, pixmap );
    }

    return 0;
}

TextChunk::TextChunk( TextParag *parag, const StringPtr &text, const ItemProperties &props )
    : Item( parag, props ), m_text( text ), m_originalTextLength( text.len ),
      m_metrics( props.font )
{
}

void TextChunk::paint( QPainter *p )
{
    p->save();
    if ( m_props.color.isValid() )
        p->setPen( m_props.color );
    p->setFont( m_props.font );

    if ( m_selection == NoSelection )
        paintText( p, 0, m_text );
    else
        paintSelection( p );

    p->restore();
}

Item *TextChunk::breakLine( int width )
{
    ParsingState state( m_text.ptr, m_text.len );

    int spaceWidth = m_metrics.width( ' ' );

    state.skip( ' ' );

    if ( state.atEnd() ) // eh?
        return 0;

    StringPtr firstWord; firstWord.ptr = state.current();
    firstWord.len = state.advanceTo( ' ' );

    int firstWordWidth = m_metrics.width( CONSTSTRING( firstWord ) );

    if ( !state.atBegin() ) // some leading spaces?
        width -= spaceWidth;

    width -= firstWordWidth;

    if ( width < 0 )
        return 0;

    while ( !state.atEnd() )
    {
        bool spaceSeen = state.skip( ' ' ) > 0;

        if ( state.atEnd() )
            break;

        StringPtr word; word.ptr = state.current();
        word.len = state.advanceTo( ' ' );

        int wordWidth = m_metrics.width( CONSTSTRING( word ) );

        if ( spaceSeen )
        {
            width -= spaceWidth;
            spaceSeen = false;
        }

        width -= wordWidth;
        if ( width > 0 )
            continue;

        StringPtr split( word.ptr, state.end() - word.ptr );
        TextChunk *chunk = new TextChunk( m_parag, split, m_props );
        chunk->m_originalTextLength = 0; // ### hack...

        m_text.len = word.ptr - m_text.ptr;
        m_extendsDirty = true;

        SelectionPoint *selection = 0;
        if ( m_selection == SelectionStart )
            selection = m_parag->textView()->selectionStart();
        if ( m_selection == SelectionEnd )
            selection = m_parag->textView()->selectionEnd();
        else if ( m_selection == SelectionBoth )
        {
            SelectionPoint *selStart = m_parag->textView()->selectionStart();
            SelectionPoint *selEnd = m_parag->textView()->selectionEnd();

            if ( selStart->offset > m_text.len )
            {
                selStart->offset -= m_text.len;
                selEnd->offset -= m_text.len;
                selStart->item = selEnd->item = chunk;
                chunk->setSelectionStatus( m_selection );
                m_selection = NoSelection;
            }
            else if ( selEnd->offset > m_text.len )
            {
                selEnd->offset -= m_text.len;
                selEnd->item = chunk;
                chunk->setSelectionStatus( SelectionEnd );
                m_selection = SelectionStart;
            }
        }

        if ( selection && selection->offset > m_text.len )
        {
            selection->offset -= m_text.len;
            selection->item = chunk;
            chunk->setSelectionStatus( m_selection );
            m_selection = NoSelection;
        }

        return chunk;
    }
    return 0;
}

Item::LayoutResetStatus TextChunk::resetLayout( Item *originalItem )
{
    TextChunk *parent = dynamic_cast<TextChunk *>( originalItem );
//    assert( !originalItem || parent );

    if ( m_originalTextLength == 0 )
    {
        if ( parent )
        {
            if ( m_selection == SelectionStart )
                parent->mergeSelection( this, m_parag->textView()->selectionStart() );
            else if ( m_selection == SelectionEnd )
                parent->mergeSelection( this, m_parag->textView()->selectionEnd() );
            else if ( m_selection == SelectionBoth )
            {
                parent->mergeSelection( this, m_parag->textView()->selectionStart() );
                parent->mergeSelection( this, m_parag->textView()->selectionEnd() );
            }
        }

        return DeleteItem;
    }

    m_extendsDirty |= ( m_text.len != m_originalTextLength );

    m_text.len = m_originalTextLength;
    return KeepItem;
}

int TextChunk::calcSelectionOffset( int x )
{
    // ### how to optimize?

    QConstString tmp( m_text.ptr, m_text.len );
    const QString &s = tmp.string();

    uint i = 0;
    int px = 0;
    for (; i < m_text.len; ++i )
    {
        int partialWidth = m_metrics.width( s, i + 1 );
        if ( px <= x && x <= partialWidth )
            return i;
        px = partialWidth;
    }

    //assert( false );
    return m_text.len-1;
}

StringPtr TextChunk::text() const
{
    return m_text;
}

void TextChunk::setProps( const ItemProperties &props )
{
    Item::setProps( props );
    m_metrics = QFontMetrics( props.font );
}

void TextChunk::calcExtends() const
{
    m_width = m_metrics.width( CONSTSTRING( m_text ) );
    m_height = m_metrics.lineSpacing();

    m_minWidth = 0;

    ParsingState state( m_text.ptr, m_text.len );

    state.skip( ' ' );

    if ( state.atEnd() ) // eh?
        return;

    StringPtr firstWord; firstWord.ptr = state.current();
    firstWord.len = state.advanceTo( ' ' );

    m_minWidth = m_metrics.width( CONSTSTRING( firstWord ) );
}

void TextChunk::paintSelection( QPainter *p )
{
    int selectionStart = 0;
    int selectionEnd = 0;
    selectionOffsets( selectionStart, selectionEnd );

    if ( m_selection == SelectionStart )
    {
        int width = paintText( p, 0, StringPtr( m_text.ptr, selectionStart ) );
        paintSelection( p, width, StringPtr( m_text.ptr + selectionStart,
                                             m_text.len - selectionStart ) );
    }
    else if ( m_selection == InSelection )
        paintSelection( p, 0, m_text );
    else if ( m_selection == SelectionEnd )
    {
        int width = paintSelection( p, 0, StringPtr( m_text.ptr, selectionEnd + 1 ) );
        paintText( p, width, StringPtr( m_text.ptr + selectionEnd + 1,
                                        m_text.len - selectionEnd - 1 ) );
    }
    else if ( m_selection == SelectionBoth )
    {
        int width = paintText( p, 0, StringPtr( m_text.ptr, selectionStart ) );
        width += paintSelection( p, width, StringPtr( m_text.ptr + selectionStart,
                                                      selectionEnd - selectionStart + 1 ) );
        paintText( p, width, StringPtr( m_text.ptr + selectionEnd + 1,
                                        m_text.len - selectionEnd - 1 ) );
    }
}

int TextChunk::paintSelection( QPainter *p, int x, const StringPtr &text )
{
    QConstString constString( text.ptr, text.len );
    const QString &str = constString.string();

    int width = m_metrics.width( str );

    const QColorGroup &cg = m_parag->textView()->colorGroup();

    p->save();
    p->fillRect( x, 0, width, height(), cg.highlight() );
    p->setPen( cg.highlightedText() );
    p->drawText( x, m_metrics.ascent(), str );
    p->restore();

    return width;
}

int TextChunk::paintText( QPainter *p, int x, const StringPtr &text )
{
    QConstString constString( text.ptr, text.len );
    const QString &str = constString.string();

    int width = m_metrics.width( str );

    if ( m_props.bgColor.isValid() )
        p->fillRect( x, 0, width, height(), m_props.bgColor );

    p->drawText( x, m_metrics.ascent(), str );

    return width;
}

void TextChunk::mergeSelection( TextChunk *child, SelectionPoint *selection )
{
    selection->offset += child->m_text.ptr - m_text.ptr;
    selection->item = this;

    if ( ( m_selection == SelectionStart && child->selectionStatus() == SelectionEnd ) ||
         ( m_selection == SelectionEnd && child->selectionStatus() == SelectionStart ) )
        m_selection = SelectionBoth;
    else
        m_selection = child->selectionStatus();
}

ImageItem::ImageItem( TextParag *parag, const QPixmap &pixmap )
    : Item( parag ), m_pixmap( pixmap )
{
}

void ImageItem::paint( QPainter *painter )
{
    painter->drawPixmap( 0, 0, m_pixmap );
}

Item::LayoutResetStatus ImageItem::resetLayout( Item * )
{
    // nothin' to do
    return KeepItem;
}

void ImageItem::calcExtends() const
{
    m_width = m_minWidth = m_pixmap.width();
    m_height = m_pixmap.height();
}

Tokenizer::Tokenizer( PString &text )
    : m_text( text.data ), m_tags( text.tags ),
      m_textBeforeFirstTagProcessed( false ), m_done( false )
{
//    qDebug( "Tokenizer::Tokenizer( %s )", m_text.ascii() );

    m_lastTag = m_tags.begin();

    if ( !m_tags.isEmpty() )
        assert( ( *m_tags.begin() ).type == TagIndex::Open );
}

Tokenizer::PString Tokenizer::preprocess( const QString &richText )
{
    PString result;
    result.data = richText;
    result.tags = scanTagIndices( result.data );
    resolveEntities( result.data, result.tags );
    return result;
}

QString Tokenizer::convertToRichText( const PString &ptext )
{
    if ( ptext.tags.isEmpty() )
        return ptext.data;

    QString result = ptext.data;
    uint i = 0;
    TagIndexList tags = ptext.tags;
    TagIndexList::Iterator it = tags.begin();
    TagIndexList::Iterator end = tags.end();

    for (; i < result.length(); ++i )
    {
        if ( it != end && i == (*it).index )
        {
            ++it;
            continue;
        }

        unsigned short indexAdjustment = 0;
        QChar ch = result[ i ];
        // ### incomplete!
        /* this doesn't work quite right (when resolving back
         * KCharSet's fromEntity breaks)
        if ( ch == '<' || ch == '>' || ch == '&' )
        {
            QString entity = KGlobal::charsets()->toEntity( ch );
            indexAdjustment = entity.length() - 1;
            result.replace( i, 1, entity );
        }
        */
        if ( ch == '<' )
        {
            result.replace( i, 1, "&lt;" );
            indexAdjustment = 3;
        }
        else if ( ch == '>' )
        {
            result.replace( i, 1, "&gt;" );
            indexAdjustment = 3;
        }
        else if ( ch == '&' )
        {
            result.replace( i, 1, "&amp;" );
            indexAdjustment = 4;
        }

        if ( indexAdjustment > 0 )
        {
            TagIndexList::Iterator tmpIt = it;
            for (; tmpIt != end; ++tmpIt )
                (*tmpIt).index += indexAdjustment;
        }
    }

    return result;
}

bool Tokenizer::parseNextToken( Token &tok )
{
    if ( m_done )
    {
        //qDebug( "Tokenizer: premature end" );
        return false;
    }

    if ( m_tags.isEmpty() )
    {
        tok.id = Token::Text;
        tok.attributes.clear();
        tok.value = StringPtr( m_text );
        m_done = true;
        return true;
    }

    TagIndexList::ConstIterator it = m_lastTag;
    ++it;

    if ( it == m_tags.end() )
    {
        m_done = true;

        uint idx = (*m_lastTag).index + 1;
        if ( idx >= m_text.length() )
            return false;

        tok.id = Token::Text;
        tok.value = StringPtr( m_text.unicode() + idx,
                               m_text.length() - idx );
        tok.attributes.clear();
        return true;
    }

    // text before first tag opening?
    if ( m_lastTag == m_tags.begin() &&
         (*m_lastTag).index > 0 &&
         !m_textBeforeFirstTagProcessed )
    {
        tok.id = Token::Text;
        tok.attributes.clear();
        tok.value = StringPtr( m_text.unicode(),
                               (*m_lastTag).index );

        m_textBeforeFirstTagProcessed = true;
        return true;
    }

    uint index = (*it).index;
    int type = (*it).type;
    uint lastIndex = (*m_lastTag).index;
    uint lastType = (*m_lastTag).type;

    assert( lastIndex < index );

    // a tag
    if ( lastType == TagIndex::Open &&
         type == TagIndex::Close )
    {
        const QChar *tagStart = m_text.unicode() + lastIndex + 1;
        uint tagLen = ( index - 1 ) - ( lastIndex + 1 ) + 1;
        // </bleh> ?
        if ( *tagStart == '/' )
        {
            ++tagStart;
            --tagLen;
            tok.id = Token::TagClose;
        }
        else
            tok.id = Token::TagOpen;

        parseTag( StringPtr( tagStart, tagLen ), tok.value, tok.attributes );

        m_lastTag = it;
        return true;
    }
    // text
    else if ( lastType == TagIndex::Close &&
              type == TagIndex::Open )
    {
        tok.id = Token::Text;
        tok.attributes.clear();
        tok.value = StringPtr( m_text.unicode() + lastIndex + 1,
                              ( index - 1 ) - lastIndex );

        m_lastTag = it;
        return true;
    }
    else
        assert( false );

    return false;
}

Tokenizer::TagIndexList Tokenizer::scanTagIndices( const QString &text )
{
    const QChar *start = text.unicode();
    const QChar *p = start;
    const QChar *endP = p + text.length();

    TagIndexList tags;

    for (; p < endP; ++p )
    {
        QChar ch = *p;
        if ( ch == '<' )
        {
            tags.append( TagIndex( p - start, TagIndex::Open ) );
            continue;
        }
        else if ( ch == '>' )
        {
            tags.append( TagIndex( p - start, TagIndex::Close ) );
            continue;
        }
    }

    return tags;
}

void Tokenizer::resolveEntities( QString &text, TagIndexList &tags )
{
    const QChar *p = text.unicode();
    const QChar *endP = p + text.length();
    uint i = 0;
    bool scanForSemicolon = false;
    const QChar *ampersand = 0;
    TagIndexList::Iterator tagInfoIt = tags.begin();
    TagIndexList::Iterator tagsEnd = tags.end();

    for (; p < endP; ++p, ++i )
    {
        if ( tagInfoIt != tagsEnd &&
              i > (*tagInfoIt).index )
            ++tagInfoIt;

        QChar ch = *p;

        if ( ch == '&' ) {
            ampersand = p;
            scanForSemicolon = true;
            continue;
        }

        if ( ch != ';' || !scanForSemicolon )
            continue;

        assert( ampersand );

        scanForSemicolon = false;

        const QChar *entityBegin = ampersand + 1;

        uint entityLength = p - entityBegin;
        if ( entityLength == 0 )
            continue;

        QChar entityValue = KCharsets::fromEntity( QConstString( entityBegin, entityLength ).string() );
        if ( entityValue.isNull() )
            continue;

        uint ampersandPos = ampersand - text.unicode();

        text[ ampersandPos ] = entityValue;
        text.remove( ampersandPos + 1, entityLength + 1 );
        i = ampersandPos;
        p = text.unicode() + i;
        endP = text.unicode() + text.length();
        ampersand = 0;

        uint adjustment = entityLength + 1;
        TagIndexList::Iterator it = tagInfoIt;
        for (; it != tags.end(); ++it )
            (*it).index -= adjustment;
    }
}

void Tokenizer::parseTag( const StringPtr &text,
                          StringPtr &tag,
                          AttributeMap &attributes )
{
    assert( text.len > 0 );

    attributes.clear();
    tag = StringPtr();

    const QChar *p = text.ptr;
    const QChar *endP = p + text.len;
    const QChar *start = p;

    int state = ScanForName;

    StringPtr key;

    while ( p < endP )
    {
        QChar ch = *p;

        if ( ch == ' ' )
        {
            start = ++p;
            continue;
        }

       if ( state == ScanForEqual )
       {
           if ( ch == '=' )
           {
               state = ScanForValue;
               ++p;
               continue;
           }
           state = ScanForName;
       }

       if ( state == ScanForValue )
       {
           if ( ch == '=' ) // eh?
           {
               qDebug( "EH?" );
               ++p;
               continue;
           }

           if ( key.isNull() )
           {
               qDebug( "Tokenizer: Error, attribute value without key." );
               // reset
               state = ScanForName;
               ++p;
               continue;
           }

           start = p;
           while ( p < endP && *p != ' ' )
           {
               if ( *p == '\"' )
               {
                   ++p;
                   while ( p < endP && *p != '\"' )
                       ++p;
                   continue;
               }
               ++p;
           }

           const QChar *valueEnd = p - 1;

           // skip leading and trailing quotes
           if ( *start == '\"' &&
                start != valueEnd &&
                *valueEnd == '\"' )
               ++start;
           else
               ++valueEnd;

           StringPtr value = StringPtr( start, valueEnd - start );
           attributes[ key ] = value;

           state = ScanForName;
           continue;
       }

       if ( state == ScanForName )
       {
           while ( p < endP && *p != ' ' && *p != '=' )
               ++p;

           key = StringPtr( start, p - start );

           if ( tag.isNull() )
               tag = key;
           else
               attributes[ key ] = StringPtr();

           state = ScanForEqual;
           continue;
       }

       assert( false ); // never reached.
    }

    /*
    qDebug( "tagName: %s", tag.constString().string().ascii() );
    qDebug( "attribute:" );
    AttributeMap::ConstIterator it = attributes.begin();
    for (; it != attributes.end(); ++it )
        qDebug( "%s -> %s", it.key().constString().string().ascii(),
                            it.data().constString().string().ascii() );
                            */
}

ItemProperties::ItemProperties()
{
}

ItemProperties::ItemProperties( const QFont &defaultFont )
    : font( defaultFont )
{
}

ItemProperties::ItemProperties( const ItemProperties &other,
                                const Token &token )
    : attributes( token.attributes )
{
    // inherit
    font = other.font;
    color = other.color;
    bgColor = other.bgColor;

    if ( token.value == "b" )
        font.setBold( true );
    else if ( token.value == "i" )
        font.setItalic( true );
    else if ( token.value == "u" )
        font.setUnderline( true );
    else if ( token.value == "font" )
    {
        StringPtr colAttr = attributes[ "color" ];
        if ( !colAttr.isNull() )
        {
            QColor col( CONSTSTRING( colAttr ) );
            if ( col.isValid() )
                color = col;
        }
        colAttr = attributes[ "bgcolor" ];
        if ( !colAttr.isNull() )
        {
            QColor col( CONSTSTRING( colAttr ) );
            if ( col.isValid() )
                bgColor = col;
        }
    }
    else if ( token.value == "a" )
    {
        color = Qt::blue; // ###
        font.setUnderline( true );
    }
}

ItemProperties::ItemProperties( const ItemProperties &rhs )
{
    (*this) = rhs;
}

ItemProperties &ItemProperties::operator=( const ItemProperties &rhs )
{
    font = rhs.font;
    color = rhs.color;
    bgColor = rhs.bgColor;
    attributes = rhs.attributes;
    return *this;
}

void ItemProperties::updateFont( const QFont &newFont )
{
    QFont f = newFont;
    f.setUnderline( font.underline() );
    f.setBold( font.bold() );
    f.setItalic( font.italic() );
    font = f;
}

TextLine::TextLine()
    : m_maxHeight( 0 )
{
    m_items.setAutoDelete( true );
}

TextLine::TextLine( const QPtrList<Item> &items )
    : m_maxHeight( 0 )
{
    m_items.setAutoDelete( true );

    assert( !items.autoDelete() );

    QPtrListIterator<Item> it( items );
    for (; it.current(); ++it )
        appendItem( it.current(), UpdateMaxHeight );
}

QString TextLine::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
    QString selectedText;

    // fixes a crash where because of an empty list i becomes null
    if ( m_items.isEmpty() )
        return QString::null;

    if ( start.line == this )
    {
        int idx = m_items.findRef( start.item );
        assert( idx != -1 );
    }
    else
        m_items.first();

    Item *i = m_items.current();

    Item *lastItem = 0;

    if ( end.line == this )
    {
        int oldCurrent = m_items.at();

        int idx = m_items.findRef( end.item );
        assert( idx != -1 );
        lastItem = m_items.next();

        m_items.at( oldCurrent );
    }

    for (; i != lastItem && i!=0L; i = m_items.next() )
    {
        if ( i == start.item )
        {
            i->setSelectionStatus( Item::SelectionStart );

            StringPtr txt = i->text();
            if ( !txt.isNull() )
                selectedText += QString( txt.ptr + start.offset,
                                         txt.len - start.offset );
        }
        else if ( i == end.item )
        {
            i->setSelectionStatus( Item::SelectionEnd );

            StringPtr txt = i->text();
            if ( !txt.isNull() )
                selectedText += QString( txt.ptr, end.offset + 1 );
        }
        else
        {
            i->setSelectionStatus( Item::InSelection );
            selectedText += i->text().toQString();
        }
    }

    return selectedText;
}

void TextLine::clearSelection()
{
    Item *i = m_items.first();
    for (; i; i = m_items.next() )
        i->setSelectionStatus( Item::NoSelection );
}

void TextLine::appendItem( Item *i, int layoutUpdatePolicy )
{
    m_items.append( i );

    if ( layoutUpdatePolicy == UpdateMaxHeight )
        m_maxHeight = kMax( m_maxHeight, i->height() );
}

Item *TextLine::resetLayout( QPtrList<Item> &remainingItems, Item *last )
{
    Item *lastLineItem = m_items.getLast();
    Item *it = m_items.first();
    // We have to use the tobeDel structure or we call resetLayout on the Item
    // twice for each item we want to delete
    QPtrList<Item> tobeDel;
    while ( it )
    {
        if ( it->resetLayout( last ) == Item::KeepItem )

            remainingItems.append( m_items.take() );
        else
            tobeDel.append( m_items.take() );

        it = m_items.current();
    }
    m_items = tobeDel;
    return lastLineItem;

}

void TextLine::paint( QPainter *p, int y )
{
    QPtrListIterator<Item> it( m_items );
    int x = 0;
    for (; it.current(); ++it )
    {
        p->translate( x, y );
        it.current()->paint( p );
        p->translate( -x, -y );
        x += it.current()->width();
    }
}

Item *TextLine::itemAt( int px, SelectionPoint *selectionInfo,
                        Item::SelectionAccuracy accuracy )
{
    QPtrListIterator<Item> it( m_items );
    int x = 0;
    int width = 0;
    for (; it.current(); ++it )
    {
        width = it.current()->width();
        if ( x < px && px < ( x + width ) )
        {
            Item *i = it.current();
            if ( selectionInfo )
            {
                selectionInfo->pos.setX( x );
                selectionInfo->offset = i->calcSelectionOffset( px - x );
                selectionInfo->item = i;
                selectionInfo->line = this;
            }
            return i;
        }
        x += width;
    }

    if ( accuracy == Item::SelectFuzzy && selectionInfo &&
         !m_items.isEmpty() && width > 0 )
    {
        Item *i = m_items.getLast();
        selectionInfo->pos.setX( x - width );
        selectionInfo->offset = i->maxSelectionOffset();
        selectionInfo->item = i;
        selectionInfo->line = this;
    }

    return 0;
}

QString TextLine::plainText() const
{
    QString res;
    QPtrListIterator<Item> it( m_items );
    for (; it.current(); ++it )
        res += it.current()->text().toQString();
    return res;
}

void TextLine::fontChange( const QFont &newFont )
{
    QPtrListIterator<Item> it( m_items );
    for (; it.current(); ++it )
    {
        ItemProperties props = it.current()->props();
        props.updateFont( newFont );
        it.current()->setProps( props );
    }
}

TextParag::TextParag( TextView *textView, const QString &richText )
    : m_layouted( false ), m_height( 0 ), m_minWidth( 0 ),
      m_textView( textView )
{
    setRichText( richText );
    m_lines.setAutoDelete(true);
}

TextParag::~TextParag()
{
}

void TextParag::layout( int width )
{
    QPtrList<Item> items;

    TextLine *row = m_lines.first();
    Item *priorRowLast = 0x0;
    for (; row; row = m_lines.next() )
        priorRowLast = row->resetLayout( items, priorRowLast );

    m_lines.clear();

    // all text chunks are now in a flat list. break them into
    // pieces of lists of chunks, so they fit with the given width

    m_height = 0;
    m_minWidth = 0;

    int remainingWidth = width;

    SelectionPoint *selStart = m_textView->selectionStart();
    SelectionPoint *selEnd = m_textView->selectionEnd();
    assert( selStart && selEnd );

    QPtrListIterator<Item> it( items );
    while ( it.current() )
    {
        m_minWidth = kMax( m_minWidth, it.current()->minWidth() );

        Item *item = it.current();
        int itemWidth = item->width();

        if ( remainingWidth >= itemWidth )
        {
            remainingWidth -= itemWidth;
            ++it;
            continue;
        }

        Item *newChunk = 0;

        if ( itemWidth > item->minWidth() )
            newChunk = item->breakLine( remainingWidth );

        if ( newChunk || it.atFirst() )
            ++it;

        TextLine *line = new TextLine;

        Item *next = it.current();

        items.first();

        while ( items.current() != next )
        {
            Item *i = items.take();

            if ( selStart->item == i )
                selStart->line = line;
            else if ( selEnd->item == i )
                selEnd->line = line;

            line->appendItem( i, TextLine::UpdateMaxHeight );
        }

        assert( !line->isEmpty() );

        m_height += line->maxHeight();
        m_lines.append( line );

        if ( newChunk )
            items.prepend( newChunk );

        it.toFirst();
        remainingWidth = width;
    }

    // append what's left
    if ( items.count() > 0 )
    {
        TextLine *line = new TextLine( items );
        m_height += line->maxHeight();
        m_lines.append( line );

        if ( selStart->parag == this ||
             selEnd->parag == this )
        {
            // ### move to TextLine?
            QPtrListIterator<Item> it( line->iterator() );
            for (; it.current(); ++it )
            {
                if ( selStart->item == it.current() )
                    selStart->line = line;
                if ( selEnd->item == it.current() )
                    selEnd->line = line;
            }
        }
    }

    m_layouted = true;
}

void TextParag::paint( QPainter *p, int y )
{
    TextLine *row = m_lines.first();
    for (; row; row = m_lines.next() )
    {
        row->paint( p, y );
        y += row->maxHeight();
    }
}

Item *TextParag::itemAt( int px, int py, SelectionPoint *selectionInfo,
                         Item::SelectionAccuracy accuracy )
{
    int y = 0;
    int height = 0;
    TextLine *row = m_lines.first();
    for (; row; row = m_lines.next() )
    {
        height = row->maxHeight();
        if ( y <= py && py <= ( y + height ) )
        {
            Item *i = row->itemAt( px, selectionInfo, accuracy );
            if ( selectionInfo )
            {
                selectionInfo->pos.setY( y );
                selectionInfo->parag = this;
            }
            return i;
        }
        y += height;
    }

    if ( accuracy == Item::SelectFuzzy && selectionInfo && !m_lines.isEmpty() )
    {
        TextLine *row = m_lines.getLast();
        row->itemAt( px, selectionInfo, accuracy );

        selectionInfo->pos.setY( y - height );
        selectionInfo->parag = this;
    }

    return 0;
}

QString TextParag::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
    QString selectedText;

    // sanity check
    // (don't put it lower because it changes the current list item)
    if ( end.parag == this )
        assert( m_lines.findRef( end.line ) != -1 );

    if ( start.parag == this )
    {
        int idx = m_lines.findRef( start.line );
        assert( idx != -1 );
    }
    else
        m_lines.first();

    TextLine *line = m_lines.current();

    TextLine *lastLine = m_lines.getLast();

    if ( end.parag == this )
        lastLine = end.line;

    for (; line != lastLine; line = m_lines.next() )
    {
        selectedText += line->updateSelection( start, end );
        selectedText += '\n';
    }

    if ( lastLine )
        selectedText += lastLine->updateSelection( start, end );

    return selectedText;
}

void TextParag::clearSelection()
{
    // ### optimize, add 'selectionDirty' flag to TextLine and TextParag!
    TextLine *line = m_lines.first();
    for (; line; line = m_lines.next() )
        line->clearSelection();
}

void TextParag::setRichText( const QString &richText )
{
    m_layouted = false;
    m_height = 0;
    m_minWidth = 0;

    // ### FIXME SELECTION!!!!!!!!!
    if ( m_textView->selectionStart()->parag == this ||
         m_textView->selectionEnd()->parag == this )
        m_textView->clearSelection();

    m_lines.clear();

    m_processedRichText = Tokenizer::preprocess( richText );

    Tokenizer tokenizer( m_processedRichText );
    Token tok;
    Token lastTextToken;

    QValueStack<Tag> tagStack;

    TextLine *line = new TextLine;

    while ( tokenizer.parseNextToken( tok ) )
    {
        if ( tok.id == Token::TagOpen )
        {
            ItemProperties oldProps( m_textView->font() );
            if ( !tagStack.isEmpty() )
                oldProps = tagStack.top().props;

            // ...bleh<foo>... -> finish off 'bleh' first
            if ( lastTextToken.id != -1 )
            {
                Item *item = Item::create( this, lastTextToken, oldProps );
                if ( item )
                    line->appendItem( item );
                lastTextToken = Token();
            }

            ItemProperties props( oldProps, tok );
            tagStack.push( Tag( tok.value, props ) );

            Item *item = Item::create( this, tok, props );
            if ( item )
                line->appendItem( item );

            continue;
        }
        else if ( tok.id == Token::TagClose )
        {
            assert( !tagStack.isEmpty() );

            Tag tag = tagStack.pop();

            assert( tok.value == tag.name );

            // ...foo</bleh>... -> finish off 'foo'
            if ( !lastTextToken.value.isNull() )
            {
                Item *item = Item::create( this, lastTextToken, tag.props );
                if ( item )
                    line->appendItem( item );
            }

            lastTextToken = Token();
        }
        else
            lastTextToken = tok;
    }

    // some plain text at the very end, outside of any tag?
    if ( !lastTextToken.value.isNull() )
    {
        Item *item = Item::create( this, lastTextToken );
        if ( item )
            line->appendItem( item );
    }

    m_lines.append( line );
}

QString TextParag::plainText() const
{
    QString result;
    QPtrListIterator<TextLine> it( m_lines );
    for (; it.current(); ++it )
        result += it.current()->plainText();
    return result;
}

void TextParag::fontChange( const QFont &newFont )
{
    QPtrListIterator<TextLine> it( m_lines );
    for (; it.current(); ++it )
        it.current()->fontChange( newFont );
}

TextView::TextView( QWidget *parent, const char *name )
    : QScrollView( parent, name, WRepaintNoErase ),
      m_paintBuffer( PaintBufferExtend, PaintBufferExtend ),
      m_selectionEndBeforeStart( false )
{
    m_parags.setAutoDelete( true );
    viewport()->setBackgroundMode( PaletteBase );
    viewport()->setMouseTracking( true );
    m_autoScrollTimer = new QTimer( this );
}

TextView::~TextView()
{
}

void TextView::drawContents( QPainter *p, int cx, int cy, int , int ch )
{
    if ( m_parags.isEmpty() )
        return;

    if ( m_paintBuffer.width() != visibleWidth() )
        m_paintBuffer.resize( visibleWidth(), PaintBufferExtend );

    QPtrListIterator<TextParag> it( m_parags );
    int y = 0;
    while ( y < cy && it.current() )
    {
        y += it.current()->height();
        ++it;
    }
    if ( y > cy && !it.atFirst() )
    {
        if ( it.current() )
            --it;
        else
            it.toLast();

        y -= it.current()->height();
    }

    int yEnd = cy + ch;
    while ( y < yEnd )
    {
        m_paintBuffer.fill( viewport(), 0, y );
        QPainter painter( &m_paintBuffer );
        painter.translate( -cx, -y );

        int nextY = y + PaintBufferExtend;
        if ( it.current() )
        {
            int tmpY = y;
            while ( tmpY < nextY && it.current() )
            {
                it.current()->paint( &painter, tmpY );
                tmpY += it.current()->height();
                ++it;
            }
            if ( tmpY > nextY && !it.atFirst() )
            {
                if ( it.current() )
                    --it;
                else
                    it.toLast();
                int adjustedY = tmpY - it.current()->height();
                if ( adjustedY != y )
                    nextY = adjustedY;
            }
        }

        painter.end();

        p->drawPixmap( cx, y, m_paintBuffer );

        y = nextY;
    }
}

void TextView::viewportResizeEvent( QResizeEvent *ev )
{
    if ( ev->size().width() != ev->oldSize().width() )
        layout();
}

void TextView::clear()
{
    stopAutoScroll();
    clearSelection();
    m_parags.clear();
    layout();
    viewport()->erase();
}

TextParagIterator TextView::appendParag( const QString &richText )
{
    TextParag *parag = new TextParag( this, richText );
    m_parags.append( parag );
    layout( false );
    QPtrListIterator<TextParag> it( m_parags );
    it.toLast();
    return TextParagIterator( it );
}

bool TextView::removeParag( const TextParagIterator &parag )
{
    if ( parag.atEnd() )
        return false;

    TextParag *paragPtr = parag.m_paragIt.current();
    int idx = m_parags.findRef( paragPtr );
    if ( idx == -1 )
        return false;

    if ( m_selectionStart.parag == paragPtr ||
         m_selectionEnd.parag == paragPtr )
        clearSelection( false );

    int height = paragPtr->height();
    m_parags.removeRef( paragPtr );

    if(m_selectionStart.item != 0)
        m_selectionStart.pos.ry() -= height;
    if(m_selectionEnd.item != 0)
        m_selectionEnd.pos.ry() -= height;

    layout( true );

    if ( isUpdatesEnabled() )
        updateContents();

    return true;
}

void TextView::clearSelection( bool repaint )
{
    m_selectionStart = SelectionPoint();
    m_selectionEnd = SelectionPoint();
    m_selectionEndBeforeStart = false;
    m_selectedText = QString::null;
    clearSelectionInternal();
    if ( repaint )
        updateContents();
}

TextParagIterator TextView::firstParag() const
{
    return TextParagIterator( QPtrListIterator<TextParag>( m_parags ) );
}

QString TextView::plainText() const
{
    if ( m_parags.isEmpty() )
        return QString::null;

    QString result;
    QPtrListIterator<TextParag> paragIt( m_parags );
    while ( paragIt.current() )
    {
        result += paragIt.current()->plainText();
        ++paragIt;
        if ( paragIt.current() )
            result += '\n';
    }

    return result;
}

void TextView::copy()
{
    QApplication::clipboard()->setText( m_selectedText );
}

void TextView::clearSelectionInternal()
{
    m_selectionEndBeforeStart = false;
    TextParag *p = m_parags.first();
    for (; p; p = m_parags.next() )
        p->clearSelection();
}

void TextView::viewportMousePressEvent( QMouseEvent *ev )
{
    if ( ev->button() & LeftButton )
    {
        clearSelection( true );
        SelectionPoint p;
        itemAt( viewportToContents( ev->pos() ), &p, Item::SelectFuzzy );
        if ( p.item )
        {
            m_selectionMaybeStart = p;
            p.item->setSelectionStatus( Item::NoSelection );
        }
    }
}

void TextView::viewportMouseMoveEvent( QMouseEvent *ev )
{
    SelectionPoint p;
    Item *i = itemAt( viewportToContents( ev->pos() ), &p, Item::SelectFuzzy );
    if ( !i && !p.item )
        return;

    if ( (ev->state() & LeftButton && m_selectionStart.item && p.item) ||
         (ev->state() & LeftButton && m_selectionMaybeStart.item && p.item))
    {

        if(m_selectionMaybeStart.item != 0){
            m_selectionStart = m_selectionMaybeStart;
            m_selectionMaybeStart = SelectionPoint();
        }

        m_selectionEnd = p;

        clearSelectionInternal();

        updateSelectionOrder();

        SelectionPoint start = m_selectionStart;
        SelectionPoint end = m_selectionEnd;

        if ( m_selectionEndBeforeStart )
        {
            if ( start.item == end.item )
            {
                if ( start.offset > end.offset )
                    qSwap( start.offset, end.offset );
            }
            else
                qSwap( start, end );
        }

        m_selectedText = updateSelection( start, end );

        emit selectionChanged();

        updateContents();

        startAutoScroll();

        return;
    }
    else if ( i )
    {
        TextChunk *text = dynamic_cast<TextChunk *>( i );
        if ( text )
        {
            StringPtr href = text->props().attributes[ "href" ];
            if ( !href.isNull() )
            {
                viewport()->setCursor( KCursor::handCursor() );
                return;
            }
        }
    }

    QCursor c = KCursor::arrowCursor();
    if ( viewport()->cursor().handle() != c.handle() )
        viewport()->setCursor( c );
}

void TextView::viewportMouseReleaseEvent( QMouseEvent *ev )
{
    stopAutoScroll();

    m_selectionMaybeStart = SelectionPoint();

    if ((ev->button() & Qt::LeftButton) && !m_selectedText.isEmpty() )
    {
        QClipboard *cb = QApplication::clipboard();
        if ( cb->supportsSelection() )
        {
            cb->setSelectionMode( true );
            cb->setText( m_selectedText );
            cb->setSelectionMode( false );
        }
        return;
    }

    Item *i = itemAt( viewportToContents( ev->pos() ) );
    if ( i )
    {
        TextChunk *text = dynamic_cast<TextChunk *>( i );
        if ( text )
        {
            StringPtr href = text->props().attributes[ "href" ];
            if ( !href.isNull() )
            {
                emit linkClicked( ev, CONSTSTRING( href ) );
                return;
            }
        }
    }
    if (ev->button() & Qt::MidButton)
    {
        QClipboard *cp = KApplication::clipboard();
        bool restoreMode = cp->selectionModeEnabled();
        cp->setSelectionMode(true);
        emit pasteReq( cp->text() );
        cp->setSelectionMode(restoreMode);
        return;
    }
}

void TextView::fontChange( const QFont & )
{
    QPtrListIterator<TextParag> it( m_parags );
    for (; it.current(); ++it )
        it.current()->fontChange( font() );

    layout( true );
}

void TextView::autoScroll()
{
    QPoint cursor = viewport()->mapFromGlobal( QCursor::pos() );

    QPoint contentsPos = viewportToContents( cursor );

    cursor.rx() -= viewport()->x();
    cursor.ry() -= viewport()->y();

    if ( ( cursor.x() < 0 || cursor.x() > visibleWidth() ) ||
         ( cursor.y() < 0 || cursor.y() > visibleHeight() ) )
        ensureVisible( contentsPos.x(), contentsPos.y(),
                       0, 5 );
}

void TextView::startAutoScroll()
{
    connect( m_autoScrollTimer, SIGNAL( timeout() ),
             this, SLOT( autoScroll() ) );
    m_autoScrollTimer->start( 100, false );
}

void TextView::stopAutoScroll()
{
    disconnect( m_autoScrollTimer, SIGNAL( timeout() ),
                this, SLOT( autoScroll() ) );
    m_autoScrollTimer->stop();
}

void TextView::selectionOffsets( int &startOffset, int &endOffset )
{
    assert( m_selectionStart.item );

    if ( m_selectionEndBeforeStart )
    {
        startOffset = m_selectionEnd.offset;
        endOffset = m_selectionStart.offset;
    }
    else
    {
        startOffset = m_selectionStart.offset;
        endOffset = m_selectionEnd.offset;
    }

    if ( m_selectionStart.item == m_selectionEnd.item && startOffset > endOffset )
        qSwap( startOffset, endOffset );
}

void TextView::updateSelectionOrder()
{
    int start = m_selectionStart.pos.y();
    int end = m_selectionEnd.pos.y();

    if ( start == end )
    {
        start = m_selectionStart.pos.x();
        end = m_selectionEnd.pos.x();

        if ( start == end )
        {
            start = m_selectionStart.offset;
            end = m_selectionEnd.offset;
        }
    }

    m_selectionEndBeforeStart = end < start;
}

SelectionPoint *TextView::selectionStart()
{
    return m_selectionEndBeforeStart ? &m_selectionEnd : &m_selectionStart;
}

SelectionPoint *TextView::selectionEnd()
{
    return m_selectionEndBeforeStart ? &m_selectionStart : &m_selectionEnd;
}

QString TextView::updateSelection( const SelectionPoint &start, const SelectionPoint &end )
{
    QString selectedText;

    if ( start.item == end.item )
    {
        Item *i = start.item;


        if ( start.offset == end.offset )
        {
            if ( start.pos.x() == end.pos.x() )
            {
                i->setSelectionStatus( Item::NoSelection );
                return QString::null;
            }

            i->setSelectionStatus( Item::SelectionBoth );

            // ### ugly
            TextChunk *t = dynamic_cast<TextChunk *>( i );
            if ( t )
            {
                StringPtr text = t->text();
                selectedText = QString( text.ptr + start.offset, 1 );
            }
        }
        else
        {
            i->setSelectionStatus( Item::SelectionBoth );

            // ### ugly
            TextChunk *t = dynamic_cast<TextChunk *>( i );
            if ( t )
            {
                StringPtr text = t->text();
                if (end.offset > start.offset)
                    selectedText = QString( text.ptr + start.offset,
                                            end.offset - start.offset + 1 );
                else
                    selectedText = QString( text.ptr + end.offset,
                                            start.offset - end.offset + 1 );

            }
        }
    }
    else
    {
        assert( m_parags.findRef( end.parag ) != -1 );
        int idx = m_parags.findRef( start.parag );
        assert( idx != -1 );

        TextParag *p = m_parags.current();
        for (; p && p != end.parag; p = m_parags.next() )
        {
            selectedText += p->updateSelection( start, end );
            selectedText += '\n';
        }

        if ( p )
            selectedText += p->updateSelection( start, end );
    }

    return selectedText;
}

void TextView::layout( bool force )
{
    int height = 0;
    int contentsWidth = visibleWidth();
    int width = contentsWidth;

    QPtrListIterator<TextParag> it( m_parags );
    for (; it.current(); ++it )
    {
        if ( !it.current()->isLayouted() || force )
            it.current()->layout( width );

        height += it.current()->height();
        contentsWidth = kMax( contentsWidth, it.current()->minWidth() );
    }

    if ( m_selectionStart.item && m_selectionEnd.item )
        updateSelection( *selectionStart(), *selectionEnd() );

    resizeContents( contentsWidth, height );
}

Item *TextView::itemAt( const QPoint &pos, SelectionPoint *selectionInfo,
                          Item::SelectionAccuracy accuracy )
{
    int px = pos.x();
    int py = pos.y();

    int y = 0;
    int height = 0;
    QPtrListIterator<TextParag> it( m_parags );
    for (; it.current(); ++it )
    {
        height = it.current()->height();
        if ( y <= py && py <= ( y + height ) )
        {
            Item *res = it.current()->itemAt( px, py - y, selectionInfo, accuracy );
            if ( selectionInfo )
            {
                selectionInfo->pos.ry() += y;
                selectionInfo->pos.rx() = px;
            }
            return res;
        }
        y += height;
    }

    if ( accuracy == Item::SelectFuzzy && selectionInfo && !m_parags.isEmpty() )
    {
        TextParag *parag = m_parags.getLast();
        parag->itemAt( px, height - 1, selectionInfo, accuracy );

        selectionInfo->pos.ry() += y - height;
        selectionInfo->pos.rx() = px;
    }

    return 0;
}

QString TextParagIterator::richText() const
{
    if ( atEnd() )
        return QString::null;

    return Tokenizer::convertToRichText( m_paragIt.current()->processedRichText() );
}

void TextParagIterator::setRichText( const QString &richText )
{
    if ( atEnd() )
        return;

    m_paragIt.current()->setRichText( richText );

    TextView *textView = m_paragIt.current()->textView();
    textView->layout( false );

    if ( textView->isUpdatesEnabled() )
        textView->updateContents();
}

QString TextParagIterator::plainText() const
{
    if ( atEnd() )
        return QString::null;

    return m_paragIt.current()->plainText();
}

#include "kstextview.moc"

/*
 * vim: et sw=4
 */
