/***************************************************************************
                          chathistorymanager.cpp  -  chat logs API
                             -------------------
    begin                : Mon Jun 28 2010
    copyright            : (C) 2010 by Valerio Pilo
    email                : valerio@kmess.org
 ***************************************************************************/

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

#include "chathistorymanager.h"


#include "../utils/kmessconfig.h"
#include "../utils/kmessshared.h"
#include "../utils/richtextparser.h"
#include "../kmessdebug.h"
#include "chathistorywriter.h"

#include <QApplication>
#include <QDir>
#include <QMutexLocker>

#include <KGlobal>
#include <KLocale>


#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  #define KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
#endif

// Static member initialization
QString ChatHistoryManager::account_;
QString ChatHistoryManager::handle_;
QStringList ChatHistoryManager::contactsList_;
ChatHistoryManager::Result ChatHistoryManager::result_( RESULT_OK );
ConversationList ChatHistoryManager::timestamps_;
QMutex ChatHistoryManager::mutex_;



/**
 * Get the account where to search for chat logs.
 *
 * @return QString with KMess account handle
 */
QString ChatHistoryManager::account()
{
  return account_;
}



 /**
  * Retrieve the XML of a chat log header
  *
  * @param timestamp Date and time to display in the header
  * @return QString
  */
QString ChatHistoryManager::chatHeader( const quint64 timestamp )
{
  const QDateTime date( QDateTime::fromTime_t( timestamp ) );

  return "<header>\n"
           "<date>" + KGlobal::locale()->formatDate( date.date(), KLocale::ShortDate ) + "</date>\n"
           "<time>" + KGlobal::locale()->formatTime( date.time(), KLocale::ShortDate ) + "</time>\n"
         "</header>\n";
}



/**
 * Retrieve a full chat log in form of XML.
 *
 * @param timestamps A list of chat start timestamps to retrieve the log of
 * @param insertHeaders If true, a chat header will be inserted before each chat
 * @param filtered If true, the log will contain only old messages shown grayed out
 */
QString ChatHistoryManager::chatLog( const QList<quint64> timestamps, bool insertHeaders, bool filtered )
{
  QString logs;

  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return logs;
  }

  if( handle_.isEmpty() )
  {
    result_ = RESULT_INVALID_CONTACT;
    return logs;
  }

  result_ = RESULT_OK;

  foreach( quint64 timestamp, timestamps )
  {
    if( ! timestamps_.contains( timestamp ) )
    {
      result_ = RESULT_INVALID_TIMESTAMP;
      return QString();
    }

    Conversation conversation = timestamps_[ timestamp ];

    // Open the file
    QFile file( conversation.filePath );
    if( ! file.open( QFile::ReadOnly | QFile::Text ) )
    {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER
      kmDebug() << "Unable to open file" << conversation.filePath;
#endif
      result_ = RESULT_UNREADABLE_LOGS;
      return QString();
    }

    // Read the specified conversation out of the file
    file.seek( conversation.startPosition );

    logs += "<conversation time=\"" + QString::number( timestamp ) + "\">\n"
         +    ( insertHeaders ? chatHeader( timestamp ) : "" )
         +    QString::fromUtf8( file.read( conversation.endPosition - conversation.startPosition ) )
         +  "</conversation>\n";

    file.close();

    // Allow events to be processed while we're working
    QApplication::processEvents();
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  kmDebug() << "Logs retrieved.";
#endif

  // If no filtering is necessary, return it as it is
  if( ! filtered )
  {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER
    kmDebug() << "Logs retrieved.";
#endif

    return logs;
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  kmDebug() << "Filtering...";
  QTime myTimer;
  myTimer.start();
#endif

  QDomDocument document;

  document.setContent( logs );

  if( document.isNull() )
  {
    result_ = RESULT_UNREADABLE_LOGS;
    return QString();
  }

  // Browse the DOM of the chat and mark the conversations as being
  // history, and also remove unneeded messages
  QDomNode node( document.firstChildElement() );
  while( ! node.isNull() )
  {
    QDomElement element( node.toElement() );
    node = document.nextSiblingElement();

    if( element.tagName() != "conversation" )
    {
      continue;
    }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
    kmDebug() << "Filtering a conversation...";
#endif

    // Change the conversation tags into history ones
    element.setTagName( "history" );

    // Remove formatting from the chat history messages
    QDomNodeList list( element.elementsByTagName( "message" ) );
    for( int i = 0; i < list.count(); i++ )
    {
      const QDomElement& message( list.at( i ).toElement() );

      QDomAttr displayNameAttr( message.firstChildElement( "displayName" ).attributeNode( "text" ) );
      QString displayName( displayNameAttr.value() );
      RichTextParser::getCleanString( displayName );
      displayNameAttr.setValue( displayName );

      QDomElement bodyTag( message.firstChildElement( "body" ) );
      QString body( bodyTag.nodeValue() );
      RichTextParser::getCleanString( body );
      bodyTag.setNodeValue( body );

      bodyTag.removeAttribute( "color" );
      bodyTag.removeAttribute( "fontFamily" );
      bodyTag.removeAttribute( "fontSize" );
      bodyTag.removeAttribute( "fontBold" );
      bodyTag.removeAttribute( "fontItalic" );
      bodyTag.removeAttribute( "fontUnderline" );
      bodyTag.removeAttribute( "fontBefore" );
      bodyTag.removeAttribute( "fontAfter" );
    }
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  kmDebug() << "Done in" << ( myTimer.elapsed() / 1000.f ) << "sec.";
#endif

  result_ = RESULT_OK;
  return document.toString( 2 /* sub-elements indention size */ );
}



/**
 * Get the list of contacts for which logs are available
 *
 * @return QStringList with contact names for which logs are available
 */
QStringList ChatHistoryManager::contactsList()
{
  QMutexLocker locker( &mutex_ );

  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return contactsList_;
  }

  result_ = RESULT_OK;

  if( ! contactsList_.isEmpty() )
  {
    return contactsList_;
  }

  contactsList_.clear();


  // Find all XML chat log files in the logs directory
  QDir logsDir( KMessConfig::instance()->getAccountDirectory( account_ ) + "/chatlogs" );
  logsDir.setFilter( QDir::Files );
  logsDir.setNameFilters( QStringList() << "*.xml" );

  // Add them to the list
  const QStringList &list = logsDir.entryList();
  foreach( QString handle, list )
  {
    // Strip the file extension
    handle.remove( handle.length() - 4, 4 );

    // strip off the number information too.
    if( handle.at( handle.length() - 1 ).isNumber() )
    {
      handle.truncate( handle.lastIndexOf( '.' ) );
    }

    // Disallow duplicate entries
    if( contactsList_.contains( handle ) )
    {
      continue;
    }

    contactsList_ << handle;
  }

  return contactsList_;
}



/**
 * Get the contact handle we will search chat logs of.
 *
 * @return QString with KMess account handle
 */
QString ChatHistoryManager::handle()
{
  return handle_;
}



/**
 * Retrieve the last chat had with a contact, in form of XML.
 *
 * @param handle Handle of the contact
 */
QString ChatHistoryManager::lastChatLog( const QString& handle )
{
  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return QString();
  }

  if( handle.isEmpty() || ! contactsList_.contains( handle ) )
  {
    result_ = RESULT_INVALID_CONTACT;
    return QString();
  }

  // Use the list of timestamps if it's available
  if( ! timestamps_.isEmpty() && handle == handle_ )
  {
    QList<quint64> timestamps = timestamps_.keys();
    qSort( timestamps );

    return chatLog( QList<quint64>() << timestamps.last(), true /* with headers */, true /* filtered */ );
  }

  QMutexLocker locker( &mutex_ );

  // There are no cached timestamps: instead of generating one, which is more time-consuming,
  // try to just obtain the last chat by looking at the logfiles

  handle_ = handle;
  QDir logsDir( KMessConfig::instance()->getAccountDirectory( account_ ) + "/chatlogs" );

  if( ! logsDir.exists() )
  {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER
    kmDebug() << "Chat logs directory" << logsDir.absolutePath() << "doesn't exist!";
#endif
    result_ = RESULT_NON_EXISTING_DIRECTORY;
    return QString();
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  kmDebug() << "Searching account" << account_ << "for the last conversation with" << handle;
#endif

  QString logFile;
  KMessShared::nextSequentialFile( KMessConfig::instance()->getAccountDirectory( account_ ) + "/chatlogs",
                                   handle,
                                   "xml",
                                   logFile );

  // Could not find any chat with this contact
  if( logFile.isEmpty() )
  {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
    kmDebug() << "No recorded chats for" << handle << "!";
#endif
    result_ = RESULT_INVALID_CONTACT;
    return QString();
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
  kmDebug() << "Finding last conversation from file" << logFile;
#endif

  timestamps_.clear();
  parseLogFile( logFile );

  // Check for errors
  if( result_ != RESULT_OK )
  {
    kmWarning() << "Error" << result_ << "while parsing file" << logFile;
    return QString();
  }

  // If there are no logs for this contact here, abort
  if( timestamps_.isEmpty() )
  {
    kmWarning() << "No conversations found in file" << logFile;
    result_ = RESULT_INVALID_CONTACT;
    return QString();
  }

  // Find the timestamp of the last conversation in the file
  quint64 lastConversation = timestamps_.keys().last();

#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
  kmDebug() << "Last conversation timestamp:" << lastConversation;
#endif

  // Retrieve that conversation
  return chatLog( QList<quint64>() << lastConversation, true /* with headers */, true /* filtered */ );
}



/**
 * Get the status of the chat history retrieval service.
 *
 * @return Result of the last operation
 */
ChatHistoryWriter* ChatHistoryManager::getWriter( const QString& handle )
{
  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return 0;
  }

  if( handle.isEmpty() )
  {
    result_ = RESULT_INVALID_CONTACT;
    return 0;
  }

  result_ = RESULT_OK;
  return new ChatHistoryWriter( account_, handle );
}


/**
 * Add a new chat to the logs, or update an ongoing one.
 *
 * @param chatLogData
 *  The details of the chat log to be updated
 */
void ChatHistoryManager::updateChatLog( const QString& handle, const Conversation& chatLogData )
{
  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return;
  }

  if( handle.isEmpty() )
  {
    result_ = RESULT_INVALID_CONTACT;
    return;
  }

  result_ = RESULT_OK;

  // Make sure this chat's contact is listed
  if( ! contactsList_.contains( handle ) )
  {
    contactsList_.append( handle );
  }

  // The logs for this contact are not currently loaded, there's nothing to update
  if( handle_ != handle )
  {
    return;
  }

  quint64 timestamp = chatLogData.timestamp;
  if( ! timestamps_.contains( timestamp ) )
  {
    // Add the chat so it appears in the logs
    timestamps_.insert( timestamp, chatLogData );
  }
  else
  {
    // Update the chat details
    Conversation& currentConversation = timestamps_[ timestamp ];
    currentConversation.endPosition = chatLogData.endPosition;
  }
}


/**
 * Parse a chat log file and retrieve from it a list of conversations
 *
 * Using Qt's XML DOM classes to parse potentially hundreds of files is madness.
 * The fastest possible method (without using an index file) is raw text parsing.
 * And that is the choice here
 *
 * @param fileName the file to parse
 * @return The list of conversations stored in the file
 */
ConversationList ChatHistoryManager::parseLogFile( const QString& fileName )
{
  QFile file( fileName );
  if( ! file.open( QFile::ReadOnly | QFile::Text ) )
  {
    // Very unlikely, but...
#ifdef KMESSDEBUG_CHATHISTORYMANAGER
    kmDebug() << "Unable to open file" << fileName;
#endif
    result_ = RESULT_UNREADABLE_LOGS;
    return ConversationList();
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
  QTime myTimer;
  myTimer.start();
  int nMilliseconds = 0;
  int counter = 0;
#endif

  const int lineLength = 256;
  const QByteArray startDelimiter( "<conversation timestamp=\"" );
  const QByteArray endDelimiter( "</conversation>" );

  int pos;
  bool isConversationOpen = false;
  QByteArray line;
  Conversation conversation;
  line.resize( lineLength + 1 ); // Allocate space for the terminator character

  while( file.readLine( line.data(), lineLength ) >= 1 )
  {
    pos = line.indexOf( startDelimiter );
    if( pos > -1 )
    {
      pos += startDelimiter.length();
      isConversationOpen = true;

      // Record where this conversation is located
      conversation.timestamp = line.mid( pos, line.indexOf( '"', pos+1 ) - pos ).toULongLong();
      conversation.startPosition = file.pos();
      conversation.filePath = file.fileName();
    }

    pos = line.indexOf( endDelimiter );
    if( pos > -1 && isConversationOpen )
    {
      conversation.endPosition = file.pos() - line.indexOf( '\0' );
      timestamps_.insert( conversation.timestamp, conversation );
    }
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
  int old = myTimer.elapsed();
  kmDebug() << file.fileName() << "was parsed in" << (old-nMilliseconds) << "msec and contained" << counter << "conversations";
  nMilliseconds = old;
#endif

  return timestamps_;
}



/**
 * Get the status of the chat history retrieval service.
 *
 * @return Result of the last operation
 */
ChatHistoryManager::Result ChatHistoryManager::result()
{
  return result_;
}



/**
 * Change the account where to search for chat logs.
 *
 * @param newAccount A KMess account handle
 */
void ChatHistoryManager::setAccount( const QString newAccount )
{
  if( account_ == newAccount )
  {
    return;
  }

  account_ = newAccount;

  // Reset the internal status
  contactsList_.clear();
  handle_ = QString();
  result_ = RESULT_OK;
  timestamps_.clear();
  timestamps_.setInsertInOrder( true );

  // Refresh the list of contacts
  contactsList();
}



/**
 * Change the contact handle we will search chat logs of.
 *
 * @param newHandle A contact handle
 */
void ChatHistoryManager::setHandle( const QString newHandle )
{
  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return;
  }

  if( handle_ == newHandle )
  {
    return;
  }

  handle_ = newHandle;

  // Reset the internal status
  result_ = RESULT_OK;
  timestamps_ = ConversationList();

  // Refresh the list of chats
  timestamps();
}




/**
 * Retrieve all the dates in which conversations were recorded for a contact,
 * along with the name of the file they are in
 *
 * @return Conversations list
 */
ConversationList ChatHistoryManager::timestamps()
{
  QMutexLocker locker( &mutex_ );

  if( account_.isEmpty() )
  {
    result_ = RESULT_INVALID_ACCOUNT;
    return timestamps_;
  }

  if( handle_.isEmpty() )
  {
    result_ = RESULT_INVALID_CONTACT;
    return timestamps_;
  }

  result_ = RESULT_OK;

  // Don't do again work already done :)
  if( ! timestamps_.isEmpty() )
  {
    return timestamps_;
  }

  timestamps_.clear();
  timestamps_.setInsertInOrder( true );

  QDir logsDir( KMessConfig::instance()->getAccountDirectory( account_ ) + "/chatlogs" );

  if( ! logsDir.exists() )
  {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER
    kmDebug() << "Chat logs directory" << logsDir.absolutePath() << "doesn't exist!";
#endif
    result_ = RESULT_NON_EXISTING_DIRECTORY;
    return timestamps_;
  }

#ifdef KMESSDEBUG_CHATHISTORYMANAGER
  kmDebug() << "Searching account" << account_ << "for conversations with" << handle_;
#endif

  // Filter only to read from this contact's files, if any
  logsDir.setFilter( QDir::Files );
  logsDir.setSorting( QDir::Reversed );
  logsDir.setNameFilters( QStringList() << ( handle_ + "*.xml" ) );

  const QStringList &list = logsDir.entryList();
  foreach( const QString &entry, list )
  {
#ifdef KMESSDEBUG_CHATHISTORYMANAGER_VERBOSE
    kmDebug() << "Reading conversations from" << entry;
#endif

    parseLogFile( logsDir.absoluteFilePath( entry ) );
    if( result_ != RESULT_OK )
    {
      return ConversationList();
    }

    // Allow events to be processed while we're working
    QApplication::processEvents();
  }

  return timestamps_;
}


