/***************************************************************************
                          chatmessagestyle.cpp -  description
                             -------------------
    begin                : Sat Okt 29 2005
    copyright            : (C) 2005 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

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

#include "chatmessagestyle.h"

#include "../contact/contactbase.h"
#include "../utils/richtextparser.h"
#include "../utils/kmessshared.h"
#include "../currentaccount.h"
#include "../emoticonmanager.h"
#include "../kmessdebug.h"
#include "xsltransformation.h"

#include <QDateTime>
#include <QFile>
#include <QList>
#include <QRegExp>
#include <QTextDocument>

#include <KLocale>
#include <KStandardDirs>
#include <KUrl>

// Originally from libxml/chvalid.h, but with the last check in xmlIsCharQ removed;
// the last check was for four-byte characters, we only check shorts. Removing
// the last check fixes two warnings on the console. :)
#define xmlIsChar_ch(c)		(((0x9 <= (c)) && ((c) <= 0xa)) || \
				 ((c) == 0xd) || \
				  (0x20 <= (c)))
#define xmlIsCharQ(c)		(((c) < 0x100) ? \
				 xmlIsChar_ch((c)) :\
				(((0x100 <= (c)) && ((c) <= 0xd7ff)) || \
				 ((0xe000 <= (c)) && ((c) <= 0xfffd))))


// The constructor
ChatMessageStyle::ChatMessageStyle()
  : QObject(0)
  , canConvert_(false)
  , contactFontColor_("#000000")
  , groupFollowupMessages_(false)
  , showTime_(true)
  , useContactFont_(false)
  , useEmoticons_(true)
  , useFontEffects_(true)
  , useFormatting_(false)
  , allowEmoticonLinks_(true)
{
  xslTransformation_ = new XslTransformation();
}



// The destructor
ChatMessageStyle::~ChatMessageStyle()
{
  delete xslTransformation_;
}



// Return whether XSL conversion works with the current style
bool ChatMessageStyle::canConvert() const
{
  return canConvert_;
}



// Convert a chat message to HTML string
QString ChatMessageStyle::convertMessage(const ChatMessage &message)
{
  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Create and convert the XML message
  if(xslTransformation_ == 0 || ! xslTransformation_->hasStylesheet())
  {
#ifdef KMESSDEBUG_CHATVIEW
    kmWarning() << "no XSL theme loaded (theme=" << name_ << "),"
                << " using fallback theme." << endl;
#endif

    // Create fallback message.
    return createFallbackMessage(message);
  }
  else
  {
    QString xmlMessage   ( convertMessageToXml( message ) );
    QString parsedMessage( xslTransformation_->convertXmlString(xmlMessage) );

    // Strip DOCTYPE because it should only appear in convertMessageRoot() output
    parsedMessage = stripDoctype(parsedMessage);

    if( parsedMessage.isNull() )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion failed (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
      return createFallbackMessage(message);
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion returned no data (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
      return createFallbackMessage(message);
    }
    else
    {
      // Received expected result
      return parsedMessage;  // DONE!
    }
  }
}



// Convert a group of chat message to HTML string
QString ChatMessageStyle::convertMessageList(const QList<ChatMessage*> &messageList)
{
  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Warn for empty list, avoid broken markup
  if( messageList.isEmpty() )
  {
    kmWarning() << "no messages given!";
    return QString::null;
  }
  else if( messageList.count() == 1 )
  {
    // When there is only one message, don't wrap it in a <messagegroup>.
    // Makes the calling-code easier, and the layout consistent.
    return convertMessage( *messageList.first() );
  }

  // Create iterator
  QListIterator<ChatMessage*> it( messageList );
  bool hasStyle = (xslTransformation_ != 0 && xslTransformation_->hasStylesheet());
  QString parsedMessage;

  // Avoid all the trouble when we can't create
  if( ! hasStyle )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kmWarning() << "no XSL theme loaded (theme=" << name_ << "),"
                << " using fallback theme." << endl;
#endif
  }
  else
  {
    // Create XML string.
    QDateTime lastMessageDate;
    QString xmlMessage( "<messagegroup>\n" );
    while( it.hasNext() )
    {
      const ChatMessage &message = *it.next();

      // Do not group messages which are distant in time by more than 10 minutes
      if( lastMessageDate.isValid()
      &&  lastMessageDate.secsTo( message.getDateTime() ) > 600 )
      {
        // Flush the XML to the parser, then start again for the next messages
        xmlMessage += "</messagegroup>\n";
        parsedMessage += xslTransformation_->convertXmlString( xmlMessage );
        xmlMessage = "<messagegroup>\n";
      }

      xmlMessage += convertMessageToXml( message ) + "\n";

      lastMessageDate = message.getDateTime();
    }
    xmlMessage += "</messagegroup>\n";

    // Convert the current messages
    parsedMessage += xslTransformation_->convertXmlString(xmlMessage);

    // Strip DOCTYPE because it should only appear in convertMessageRoot() output
    parsedMessage = stripDoctype(parsedMessage);

    if( parsedMessage.isNull() )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion failed (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
    }
    else if( isEmptyResult(parsedMessage) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion returned no data (theme=" << name_ << "),"
                  << " using fallback theme." << endl;
#endif
    }
    else
    {
      // Received expected result
      return parsedMessage;  // DONE!
    }
  }

  // Conversion failed, create fallback
  while( it.hasNext() )
  {
    parsedMessage += createFallbackMessage( *it.next() );
  }
  return parsedMessage;
}



// Convert a string with XML to HTML
QString ChatMessageStyle::convertXmlMessageList( const QString &xmlMessagesList )
{
  QString parsedMessage;
  if( xslTransformation_ != 0 && xslTransformation_->hasStylesheet() )
  {
    // Convert
    parsedMessage = xslTransformation_->convertXmlString( xmlMessagesList );

    // Strip DOCTYPE because it should only appear in convertMessageRoot() output
    parsedMessage = stripDoctype( parsedMessage );

    if( parsedMessage.isNull() )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion failed (theme=" << name_ << "), cannot convert.";
#endif
    }
    else if( isEmptyResult( parsedMessage ) )
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion returned no data (theme=" << name_ << "), cannot convert.";
#endif
    }
    else
    {
      // Received expected result
      return parsedMessage;
    }
  }

#ifdef KMESSDEBUG_CHATVIEW
  kmWarning() << "no XSL theme loaded (theme=" << name_ << "), cannot convert.";
#endif

  // Conversion failed, return the original string.
  // TODO: Parse the XML (using QDom classes or regexps) and create
  // ChatMessages to send to createFallbackMessage().
  return parsedMessage;
}



// Convert the message root.
QString ChatMessageStyle::convertMessageRoot()
{
  QString parsedMessage;

  // Avoid all the trouble when we can't create a style
  if( ! hasStyle() )
  {
#ifdef KMESSDEBUG_CHATVIEW
    kmWarning() << "no XSL theme loaded.";
#endif
  }
  else
  {
    // Not much to add here yet..
    QString xmlMessage( "<messageRoot>"
                        "</messageRoot>" );

    // Convert
    parsedMessage = xslTransformation_->convertXmlString(xmlMessage);
    if( ! parsedMessage.isNull() )
    {
      if( isEmptyResult(parsedMessage) )
      {
        // Indicate the chat style doesn't define the whole header/footer,
        // so KMess can use it's default instead.
        return QString::null;
      }
      return parsedMessage;  // DONE!
    }
    else
    {
#ifdef KMESSDEBUG_CHATVIEW
      kmWarning() << "XSL conversion failed (theme=" << name_ << ").";
#endif
    }
  }

  return QString::null;
}


// Convert the message as HTML, fallback method when XML fails.
QString ChatMessageStyle::createFallbackMessage(const ChatMessage &message)
{
  QString color;
  int     type    = message.getType();
  QString handle  ( message.getContactHandle() );
  QString name    ( message.getContactName() );
  QString body    ( message.getBody() );
  QString fontDir ( body.isRightToLeft() ? "rtl" : "ltr" );

  // Reset state, changed by parseMsnString
  pendingEmoticonTags_.clear();

  // Create the fallback message when the XML/XSL conversion failed
  if( message.isNormalMessage() )
  {
    // Contact message
    QString fontBefore;
    QString fontAfter;
    QString time;
    QFont   font;
    QString color;

    // Extract fonts
    parseFont(message, font, color, fontBefore, fontAfter);

    // Escape HTML, replace standard emoticons, links and formatting
    RichTextParser::parseMsnString( name, useEmoticons_, true, false, useFormatting_, allowEmoticonLinks_ );

    // Replace font special effects, like *bold*
    if( useFontEffects_ )
    {
      parseEffects( name );
    }

    // Body can already be HTML if generated internally.
    if( ! message.hasHtmlBody() )
    {
      // Escape HTML, replace all emoticons, links and formatting
      RichTextParser::parseMsnString( body, useEmoticons_, false, true, useFormatting_, allowEmoticonLinks_, handle, pendingEmoticonTags_ );

      // Replace font special effects, like *bold*
      if( useFontEffects_ )
      {
        parseEffects( body );
      }
    }

    // Replace newlines with <br/>
    parseBody(body);

    // Misc variables
    if( showTime_ )
    {
      QDateTime messageTime = message.getDateTime();
      if ( showDate_ )
      {
        time = KGlobal::locale()->formatDateTime( messageTime, KLocale::ShortDate, showSeconds_ );
      }
      else
      {
        time = KGlobal::locale()->formatTime( messageTime.time(), showSeconds_ );
      }
    }
    color = (type == ChatMessage::TYPE_INCOMING || type == ChatMessage::TYPE_OFFLINE_INCOMING)
            ? "666699" : "666666";

    // Create HTML
    return "<div dir='" + fontDir + "'><font color='" + color + "'>" + (showTime_ ? time + " " : "")
         + i18n("%1 says:", name)
         + "<br></font>"
         + fontBefore + body + "<br><br>" + fontAfter
         + "</div>";
  }
  else if( type == ChatMessage::TYPE_SYSTEM )
  {
    // Red system message
    parseBody( KMessShared::htmlEscape( body ) );
    return "<div dir='" + fontDir + "'><hr size='2' color='red'>"
           "<font color='red'>" + body + "</font>"
           "<hr size='2' color='red'></div>";
  }
  else if( type == ChatMessage::TYPE_APPLICATION
        || type == ChatMessage::TYPE_APPLICATION_FILE )
  {
    // Blue application message
    parseBody( KMessShared::htmlEscape( body ) );
    return "<div dir='" + fontDir + "'><font color='blue'>" + body + "</font><br></div>";
  }
  else if( type == ChatMessage::TYPE_PRESENCE )
  {
    // Gray presence message
    parseBody( KMessShared::htmlEscape( body ) );
    return "<div dir='" + fontDir + "'><font color='gray'>" + body + "</font><br></div>";
  }
  else
  {
    // Purple notification message
    parseBody( KMessShared::htmlEscape( body ) );
    return "<div dir='" + fontDir + "'><font color='purple'>" + body + "</font><br></div>";
  }
}



// Convert the message as XML.
QString ChatMessageStyle::convertMessageToXml( const ChatMessage &message, bool isHistory )
{
  uint timestamp = 0;
  QString fontBefore;
  QString fontAfter;
  QString xmlMessage;
  QString parsedMessage;
  QString typeString;
  QString color;
  QFont   font;

  // Remove any characters which are not allowed in XML
  const ushort* utf16 = message.getBody().utf16();
  ushort* newutf16 = new ushort[ message.getBody().size() ];
  int j = 0;
  for( int i = 0; i < message.getBody().size(); ++i )
  {
    // Utf16 sometimes uses more than two bytes for a character, but in those cases,
    // both parts will be valid, so xmlIsCharQ (checking if a short is valid) will always return true.
    if( xmlIsCharQ( utf16[i] ) )
    {
      newutf16[ j++ ] = utf16[i];
    }
#ifdef KMESSDEBUG_CHATVIEW
    else
    {
      kmDebug() << "Found invalid character with code " << (int)utf16[i] << ", not showing";
    }
#endif
  }
  QString body = QString::fromUtf16( newutf16, j );

  // Get message info
  int     type      = message.getType();
  QString handle    ( message.getContactHandle() );
  QString name      ( message.getContactName() );
  bool    isRtl     = body.isRightToLeft();
  bool    nameIsRtl = name.isRightToLeft();

  // See if this is a contact's message and not the user


  // Prepare the strings for HTML
  // App messages are not parsed, they may contain HTML (links to click on, etc..)
  // Only exception to this is presence messages to maintain consistency.
  bool isPresence = message.getType() == ChatMessage::TYPE_PRESENCE;
  if( message.isNormalMessage() || isPresence )
  {
    // Extract fonts
    parseFont(message, font, color, fontBefore, fontAfter);

    // Escape HTML, replace standard emoticons, links and formatting
    RichTextParser::parseMsnString( name, useEmoticons_, true, false, useFormatting_, allowEmoticonLinks_ );

    // Parse effects characters (_, *, /)
    if( useFontEffects_ )
    {
      parseEffects( name );
    }

    // Body can already be HTML if generated internally.
    if( ! message.hasHtmlBody() )
    {
      // Escape HTML, replace all emoticons, links and formatting
      // if this is a presence message, show emoticons only. Otherwise, show the works depending on configuration.
      RichTextParser::parseMsnString( body, useEmoticons_, false,
                                      !isPresence,
                                      (isPresence ? false : useFormatting_),
                                      (isPresence ? false : allowEmoticonLinks_),
                                      (isPresence ? QString() : handle), pendingEmoticonTags_ );

      if( useFontEffects_ )
      {
        parseEffects( body );
      }
    }
  }

  // Replace newlines with <br/> tags
  parseBody(body);

  // Show the message time
  // TODO: maybe timestamps should always available in the histories, just not necessarily shown?
  if( showTime_ )
  {
    // Get the time
    const QDateTime &messageDate = message.getDateTime();
    timestamp = messageDate.toTime_t();
  }


  // Get message type for XSL
  switch(type)
  {
    case ChatMessage::TYPE_INCOMING:         typeString = "incoming";        break;
    case ChatMessage::TYPE_OUTGOING:         typeString = "outgoing";        break;
    case ChatMessage::TYPE_SYSTEM:           typeString = "system";          break;
    case ChatMessage::TYPE_APPLICATION_FILE:
    case ChatMessage::TYPE_APPLICATION:      typeString = "application";     break;
    case ChatMessage::TYPE_OFFLINE_INCOMING: typeString = "offlineIncoming"; break;
    case ChatMessage::TYPE_PRESENCE:         typeString = "presence";        break;
    case ChatMessage::TYPE_NOTIFICATION:
    default:
      typeString = "notification";
      break;
  }


  // IMPORTANT:
  // The QStyleSheet::escape() function doesn't escape quotes.
  // Special care needs to be taken to avoid XML parsing errors.

  // Create an XML message for the XSL theme conversion
  // "<?xml version='1.0'?>"
  if ( isHistory )
  {
    xmlMessage = "<message"
                 " type='" + KMessShared::htmlEscape( typeString ) + "'"
                 + " timestamp='" + QString::number( timestamp ) + "'" +
                 ">\n";
  }
  else
  {
    QString timeString;
    const QDateTime &datetime = QDateTime::fromTime_t( timestamp );

    if ( showDate_ )
    {
      timeString = KGlobal::locale()->formatDateTime( datetime, KLocale::ShortDate, showSeconds_ );
    }
    else
    {
      timeString = KGlobal::locale()->formatTime( datetime.time(), showSeconds_ );
    }

    xmlMessage = "<message"
                 " type='" + KMessShared::htmlEscape( typeString ) + "'" +
                 ( ! showTime_ ? QString() : " time='" + timeString + "'" ) +
                 ">\n";
  }

  // The contact can be empty for some notification messges
  if(! message.getContactHandle().isEmpty() )
  {
    xmlMessage += "  <from>\n"
                  "    <contact contactId='" + KMessShared::htmlEscape( message.getContactHandle() ) + "'>\n";

   // If the user doesn't have a display picture, the style will use a default image.
    if( ! message.getContactPicturePath().isEmpty() && QFile::exists( message.getContactPicturePath() ) )
    {
      xmlMessage += "      <displayPicture url='" + KMessShared::htmlEscape( message.getContactPicturePath() ) + "' />\n";
    }

    if( ! name.isEmpty() )
    {
      xmlMessage += "      <displayName text='" + KMessShared::htmlEscape( name ) + "' dir='" + (nameIsRtl ? "rtl" : "ltr") + "' />\n";
    }

    xmlMessage += "    </contact>\n"
                  "  </from>\n";
  }

  // Add message
  xmlMessage += "  <body"
                    " color='"         + KMessShared::htmlEscape( color )            + "'"
                    " fontFamily='"    + KMessShared::htmlEscape( font.family() )    + "'"
                    " fontSize='"      + QString::number( font.pointSize() )         + "'"
                    " fontBold='"      + (font.bold()      ? "1" : "0")              + "'"
                    " fontItalic='"    + (font.italic()    ? "1" : "0")              + "'"
                    " fontUnderline='" + (font.underline() ? "1" : "0")              + "'"
                    " fontBefore='"    + KMessShared::htmlEscape( fontBefore )       + "'"
                    " fontAfter='"     + KMessShared::htmlEscape( fontAfter  )       + "'"
                    " dir='"           + (isRtl ? "rtl" : "ltr")                     + "'"
                    ">\n" +
                     KMessShared::htmlEscape( body ) + '\n' +
                  "</body>\n"  // escape again for XML
                "</message>\n";
  return xmlMessage;
}



// Return the base folder of the style.
const QString & ChatMessageStyle::getBaseFolder() const
{
  return baseFolder_;
}



// Return the font used for contact messages, if forced to.
const QFont& ChatMessageStyle::getContactFont() const
{
  return contactFont_;
}



// Return the color of the forced contact font.
const QString& ChatMessageStyle::getContactFontColor() const
{
  return contactFontColor_;
}



// Return the xsl stylesheet. Return null if there is none.
QString ChatMessageStyle::getStyleSheet() const
{
  QString xslFile( baseFolder_ + name_ + ".xsl" );

  if( ! hasStyle() || ! QFile::exists( xslFile ) )
  {
    return QString();
  }

  return xslFile;
}



// Return the css file attached to the stylesheet. Return null if there is none.
QString ChatMessageStyle::getCssFile() const
{
  QString cssFile( baseFolder_ + name_ + ".css" );
  if( ! QFile::exists(cssFile) )
  {
    return QString::null;
  }
  else
  {
    return cssFile;
  }
}



// Return the currently used emoticon style
const QString &ChatMessageStyle::getEmoticonStyle() const
{
  return emoticonStyle_;
}



// Return the grow follow up messages
bool ChatMessageStyle::getGroupFollowupMessages() const
{
  return groupFollowupMessages_;
}



// Return the name of the style.
const QString & ChatMessageStyle::getName() const
{
  return name_;
}



// Return whether or not emoticon links are allowed
bool ChatMessageStyle::getAllowEmoticonLinks() const
{
  return allowEmoticonLinks_;
}



// Return if the date is displayed in messages
bool ChatMessageStyle::getShowMessageDate() const
{
  return showDate_;
}



// Return if the seconds are displayed in the message times
bool ChatMessageStyle::getShowMessageSeconds() const
{
  return showSeconds_;
}



// Return if the time is displayed in messages
bool ChatMessageStyle::getShowMessageTime() const
{
  return showTime_;
}



// Return the ID's of inserted <img> tags for the pending emoticons.
const QStringList & ChatMessageStyle::getPendingEmoticonTagIds() const
{
  return pendingEmoticonTags_;
}



// Return whether or not to show text formatting in chat messages
bool ChatMessageStyle::getUseChatFormatting() const
{
  return useFormatting_;
}



// Return whether or not to show contact messages in the stored font.
bool ChatMessageStyle::getUseContactFont() const
{
  return useContactFont_;
}



// Return whether or not the emoticons are visible
bool ChatMessageStyle::getUseEmoticons() const
{
  return useEmoticons_;
}



// Return whether or not the font effects are visible
bool ChatMessageStyle::getUseFontEffects() const
{
  return useFontEffects_;
}



// Return whether or not the formatting is visible
bool ChatMessageStyle::getUseFormatting() const
{
  return useFormatting_;
}



// Return whether the given result is empty
bool ChatMessageStyle::isEmptyResult( const QString &parsedMessage )
{
  return stripDoctype(parsedMessage).trimmed().isEmpty();
}



// Return whether an style is loaded.
bool ChatMessageStyle::hasStyle() const
{
  return (xslTransformation_ != 0 && xslTransformation_->hasStylesheet());
}



// Replace the newline characters
void ChatMessageStyle::parseBody(QString &body) const
{
  // Replace any newline characters in the message with "<br>" so that carriage returns will show properly.
  body = body.replace(QRegExp("\r?\n?$"), QString::null)  // Remove last \n
             .replace( "\r\n", "<br/>" )
             .replace( '\r',   "<br/>" )
             .replace( '\n',   "<br/>" );
}


// Do some effects characters (ie, bold, underline and italic specials)
void ChatMessageStyle::parseEffects(QString &text) const
{
  int       offset = 0;
  int       nextOffset = 0;
  QRegExp   effectsSearch;
  QString   effectsCharacter;
  QChar     boundaryCharacter;
  int       tagPosOpen, tagPosClose;
  QString   replacement;

  // Process bold, italics, underline
  const char* effectHtml   = "biu";  // bold, italic, underline
  QString effectCharacters( "*/_" );

  for( int i = 0; i < 3; i++ )
  {
    effectsCharacter = QRegExp::escape( effectCharacters.mid(i, 1) );
    effectsSearch.setPattern( effectsCharacter + "([\\d\\w\\s]+)" + effectsCharacter );

    offset = 0;

    while( offset >= 0 )
    {
      // Find next pattern match
      offset = effectsSearch.indexIn( text, nextOffset );
      if( offset == -1 )
      {
        break;
      }

      // Skip effects contained in HTML tags
      tagPosOpen = text.indexOf( "<", nextOffset );
      tagPosClose = text.indexOf( ">", tagPosOpen );
      if( tagPosOpen != -1 && tagPosClose != -1 && offset > tagPosOpen && offset < tagPosClose )
      {
        // Continue from the first character after the HTML tag
        nextOffset = tagPosClose + 1;
        continue;
      }

      nextOffset = offset + effectsSearch.matchedLength();

      // Abort if there is a normal char before
      if( offset != 0 )
      {
        boundaryCharacter = text.at( offset - 1 );
        if( ! boundaryCharacter.isSpace() && ! boundaryCharacter.isPunct() )
        {
          continue;
        }
      }

      // Abort if there is a normal char after
      if( ( offset + effectsSearch.matchedLength() ) < text.length() )
      {
        boundaryCharacter = text.at( offset + effectsSearch.matchedLength() );
        if( ! boundaryCharacter.isSpace() && ! boundaryCharacter.isPunct() )
        {
          continue;
        }
      }

      // Replace if start and end are free.
      if( effectHtml[i] == 'b' )
      {
        // Bold is standard
        replacement = "*<b>" + effectsSearch.cap(1) + "</b>*";
      }
      else if( effectHtml[i] == 'i' )
      {
        // Same for italics
        replacement = "/<i>" + effectsSearch.cap(1) + "</i>/";
      }
      else if( effectHtml[i] == 'u' )
      {
        // Same for underline
        replacement = "<u>&nbsp;" + effectsSearch.cap(1) + "&nbsp;</u>";
      }

      // Replace and change offset
      text.replace(offset, effectsSearch.matchedLength(), replacement);
      nextOffset = offset + replacement.length() + 2;
    }
  }
}



// Parse the font tags
void ChatMessageStyle::parseFont(const ChatMessage &message, QFont &font, QString &color, QString& fontBefore, QString& fontAfter) const
{
  // Extract the font from the message.
  ChatMessage::MessageType type = message.getType();
  if( useContactFont_ && (type == ChatMessage::TYPE_INCOMING || type == ChatMessage::TYPE_OFFLINE_INCOMING) )
  {
    // Replace the given font with the user's stored contact font
    font  = contactFont_;
    color = contactFontColor_;
  }
  else
  {
    // Use the font settings from the message
    font  = message.getFont();
    color = message.getFontColor();
  }

  // Include the dir="rtl" tag to the font so the text is at least displayed in the right direction.
  // To make it align right as well, the dir needs to be assigned to a block element.
  // This can't be done here as it would influence the chat style.
  QString fontDir( message.getBody().isRightToLeft() ? "rtl" : "ltr" );

  // Set the font size, be it the user's or the contacts'
  QString size( " style=\"font-size:" + QString::number( font.pointSize() ) + "pt\"" );

  // Create the font HTML for the message
  fontBefore.clear();
  fontAfter.clear();
  if( font.bold()      ) fontBefore += "<b>";
  if( font.italic()    ) fontBefore += "<i>";
  if( font.underline() ) fontBefore += "<u>";
  fontBefore += "<font face=\"" + font.family() + "\" color=\"" + color + "\" dir=\"" + fontDir + "\"" + size + ">";
  fontAfter  += "</font>";
  if( font.underline() ) fontAfter  += "</u>";
  if( font.italic()    ) fontAfter  += "</i>";
  if( font.bold()      ) fontAfter  += "</b>";
}



// The the contact font
void ChatMessageStyle::setContactFont(const QFont &font)
{
  contactFont_ = font;
}



// The the contact font color
void ChatMessageStyle::setContactFontColor(const QString &fontColor)
{
  contactFontColor_ = fontColor;
}



// Set the emoticon style
void ChatMessageStyle::setEmoticonStyle( const QString &style )
{
  emoticonStyle_ = style;
}



// Set grow follow up messages
void ChatMessageStyle::setGroupFollowupMessages( bool groupFollowupMessages )
{
  groupFollowupMessages_ = groupFollowupMessages;
}



// Set whether or not emoticon links are allowed
void ChatMessageStyle::setAllowEmoticonLinks( bool allowEmoticonLinks )
{
  allowEmoticonLinks_ = allowEmoticonLinks;
}



// Set the show time state
void ChatMessageStyle::setShowTime(bool showTime)
{
  showTime_ = showTime;
}



// Set the show date state
void ChatMessageStyle::setShowDate(bool showDate)
{
  showDate_ = showDate;
}



// Set the show seconds state
void ChatMessageStyle::setShowSeconds(bool showSeconds)
{
  showSeconds_ = showSeconds;
}



// Set the message style, return false if it failed
bool ChatMessageStyle::setStyle(const QString &style)
{
  // Avoid reloading the same style again
  if( style == name_ )
  {
    return hasStyle();
  }

  KStandardDirs *dirs   = KGlobal::dirs();
  QString path( dirs->findResource( "data", "kmess/styles/" + style + "/" + style + ".xsl" ) );

  if(path.isNull())
  {
    kmWarning() << "could not find the style named '" << style << "'.";
    return false;
  }

  // Set stylesheet
  name_ = style;
  xslTransformation_->setStylesheet(path);
  bool styleSheetLoaded = xslTransformation_->hasStylesheet();

  if(styleSheetLoaded)
  {
    // Update the base folder
    KUrl pathUrl;
    pathUrl.setPath(path);
    baseFolder_ = pathUrl.directory( KUrl::AppendTrailingSlash );

    QMap<QString,QString> parameters;
    parameters["basepath"] = baseFolder_;
    parameters["csspath"]  = getCssFile();
    xslTransformation_->setParameters(parameters);

#ifdef KMESSDEBUG_CHATVIEW
    kmDebug() << "Style" << style << "loaded from path:" << path << ". Base folder:" << baseFolder_;
#endif
  }
#ifdef KMESSDEBUG_CHATVIEW
  else
  {
    kmDebug() << "Unable to load style:" << style << "from path:" << path;
  }
#endif

  // Also verify if the XSL conversion works with the new style
  canConvert_ = ( ! xslTransformation_->convertXmlString( "<messageRoot></messageRoot>" ).isEmpty() );

  return styleSheetLoaded;
}



// Enable or disable contact font overrides
void ChatMessageStyle::setUseContactFont(bool useContactFont)
{
  useContactFont_ = useContactFont;
}



// Enable or disable emoticons
void ChatMessageStyle::setUseEmoticons(bool useEmoticons)
{
  useEmoticons_ = useEmoticons;
}



// Enable or disable font effects
void ChatMessageStyle::setUseFontEffects(bool useFontEffects)
{
  useFontEffects_ = useFontEffects;
}



// Enable or disable MSN Plus formatting
void ChatMessageStyle::setUseFormatting( bool useFormatting )
{
  useFormatting_ = useFormatting;
}



// Strip the DOCTYPE tag from the message
QString ChatMessageStyle::stripDoctype( const QString &parsedMessage )
{
  if( parsedMessage.startsWith( "<!DOCTYPE" ) )
  {
    QRegExp re(">\r?\n?");
    int endPos = parsedMessage.indexOf( re );
    if( endPos == -1 )
    {
      kmWarning() << "Could not strip DOCTYPE tag: end position not found!";
      return parsedMessage;
    }

    // Strip both end end possible \r\n character
    return parsedMessage.mid( endPos + re.matchedLength() );
  }
  else
  {
    return parsedMessage;
  }
}



#include "chatmessagestyle.moc"

