// -*- mode: C++; c-file-style: "gnu" -*-
// kmsearchpattern.cpp
// Author: Marc Mutz <Marc@Mutz.com>
// This code is under GPL!

#include <config.h>

#include "kmaddrbook.h"
#include "kmsearchpattern.h"
#include "kmmsgdict.h"
#include "filterlog.h"
#include "kmkernel.h"
#include "kmmsgdict.h"
#include "kmfolder.h"
using KMail::FilterLog;

#include <libemailfunctions/email.h>

#include <tdeglobal.h>
#include <tdelocale.h>
#include <kdebug.h>
#include <tdeconfig.h>

#include <tdeabc/stdaddressbook.h>

#include <tqregexp.h>

#include <mimelib/string.h>
#include <mimelib/boyermor.h>
#include <mimelib/field.h>
#include <mimelib/headers.h>

#include <assert.h>

static const char* funcConfigNames[] =
  { "contains", "contains-not", "equals", "not-equal", "regexp",
    "not-regexp", "greater", "less-or-equal", "less", "greater-or-equal",
    "is-in-addressbook", "is-not-in-addressbook" , "is-in-category", "is-not-in-category",
    "has-attachment", "has-no-attachment"};
static const int numFuncConfigNames = sizeof funcConfigNames / sizeof *funcConfigNames;

struct _statusNames {
  const char* name;
  KMMsgStatus status;
};

static struct _statusNames statusNames[] = {
  { "Important", KMMsgStatusFlag },
  { "New", KMMsgStatusNew },
  { "Unread", KMMsgStatusUnread | KMMsgStatusNew },
  { "Read", KMMsgStatusRead },
  { "Old", KMMsgStatusOld },
  { "Deleted", KMMsgStatusDeleted },
  { "Replied", KMMsgStatusReplied },
  { "Forwarded", KMMsgStatusForwarded },
  { "Queued", KMMsgStatusQueued },
  { "Sent", KMMsgStatusSent },
  { "Watched", KMMsgStatusWatched },
  { "Ignored", KMMsgStatusIgnored },
  { "To Do", KMMsgStatusTodo },
  { "Spam", KMMsgStatusSpam },
  { "Ham", KMMsgStatusHam },
  { "Has Attachment", KMMsgStatusHasAttach },
  { "Invitation", KMMsgStatusHasInvitation }
};

static const int numStatusNames = sizeof statusNames / sizeof ( struct _statusNames );


//==================================================
//
// class KMSearchRule (was: KMFilterRule)
//
//==================================================

KMSearchRule::KMSearchRule( const TQCString & field, Function func, const TQString & contents )
  : mField( field ),
    mFunction( func ),
    mContents( contents )
{
}

KMSearchRule::KMSearchRule( const KMSearchRule & other )
  : mField( other.mField ),
    mFunction( other.mFunction ),
    mContents( other.mContents )
{
}

const KMSearchRule & KMSearchRule::operator=( const KMSearchRule & other ) {
  if ( this == &other )
    return *this;

  mField = other.mField;
  mFunction = other.mFunction;
  mContents = other.mContents;

  return *this;
}

KMSearchRule * KMSearchRule::createInstance( const TQCString & field,
                                             Function func,
                                             const TQString & contents )
{
  KMSearchRule *ret = 0;
  if (field == "<status>")
    ret = new KMSearchRuleStatus( field, func, contents );
  else if ( field == "<age in days>" || field == "<size>" )
    ret = new KMSearchRuleNumerical( field, func, contents );
  else
    ret = new KMSearchRuleString( field, func, contents );

  return ret;
}

KMSearchRule * KMSearchRule::createInstance( const TQCString & field,
                                     const char *func,
                                     const TQString & contents )
{
  return ( createInstance( field, configValueToFunc( func ), contents ) );
}

KMSearchRule * KMSearchRule::createInstance( const KMSearchRule & other )
{
  return ( createInstance( other.field(), other.function(), other.contents() ) );
}

KMSearchRule * KMSearchRule::createInstanceFromConfig( const TDEConfig * config, int aIdx )
{
  const char cIdx = char( int('A') + aIdx );

  static const TQString & field = TDEGlobal::staticQString( "field" );
  static const TQString & func = TDEGlobal::staticQString( "func" );
  static const TQString & contents = TDEGlobal::staticQString( "contents" );

  const TQCString &field2 = config->readEntry( field + cIdx ).latin1();
  Function func2 = configValueToFunc( config->readEntry( func + cIdx ).latin1() );
  const TQString & contents2 = config->readEntry( contents + cIdx );

  if ( field2 == "<To or Cc>" ) // backwards compat
    return KMSearchRule::createInstance( "<recipients>", func2, contents2 );
  else
    return KMSearchRule::createInstance( field2, func2, contents2 );
}

KMSearchRule::Function KMSearchRule::configValueToFunc( const char * str ) {
  if ( !str )
    return FuncNone;

  for ( int i = 0 ; i < numFuncConfigNames ; ++i )
    if ( tqstricmp( funcConfigNames[i], str ) == 0 ) return (Function)i;

  return FuncNone;
}

TQString KMSearchRule::functionToString( Function function )
{
  if ( function != FuncNone )
    return funcConfigNames[int( function )];
  else
    return "invalid";
}

void KMSearchRule::writeConfig( TDEConfig * config, int aIdx ) const {
  const char cIdx = char('A' + aIdx);
  static const TQString & field = TDEGlobal::staticQString( "field" );
  static const TQString & func = TDEGlobal::staticQString( "func" );
  static const TQString & contents = TDEGlobal::staticQString( "contents" );

  config->writeEntry( field + cIdx, TQString(mField) );
  config->writeEntry( func + cIdx, functionToString( mFunction ) );
  config->writeEntry( contents + cIdx, mContents );
}

bool KMSearchRule::matches( const DwString & aStr, KMMessage & msg,
                       const DwBoyerMoore *, int ) const
{
  if ( !msg.isComplete() ) {
    msg.fromDwString( aStr );
    msg.setComplete( true );
  }
  return matches( &msg );
}

const TQString KMSearchRule::asString() const
{
  TQString result  = "\"" + mField + "\" <";
  result += functionToString( mFunction );
  result += "> \"" + mContents + "\"";

  return result;
}

//==================================================
//
// class KMSearchRuleString
//
//==================================================

KMSearchRuleString::KMSearchRuleString( const TQCString & field,
                                        Function func, const TQString & contents )
          : KMSearchRule(field, func, contents)
{
  if ( field.isEmpty() || field[0] == '<' )
    mBmHeaderField = 0;
  else // make sure you handle the unrealistic case of the message starting with mField
    mBmHeaderField = new DwBoyerMoore(("\n" + field + ": ").data());
}

KMSearchRuleString::KMSearchRuleString( const KMSearchRuleString & other )
  : KMSearchRule( other ),
    mBmHeaderField( 0 )
{
  if ( other.mBmHeaderField )
    mBmHeaderField = new DwBoyerMoore( *other.mBmHeaderField );
}

const KMSearchRuleString & KMSearchRuleString::operator=( const KMSearchRuleString & other )
{
  if ( this == &other )
    return *this;

  setField( other.field() );
  mBmHeaderField = new DwBoyerMoore( *other.mBmHeaderField );
  setFunction( other.function() );
  setContents( other.contents() );
  delete mBmHeaderField; mBmHeaderField = 0;
  if ( other.mBmHeaderField )
    mBmHeaderField = new DwBoyerMoore( *other.mBmHeaderField );

  return *this;
}

KMSearchRuleString::~KMSearchRuleString()
{
  delete mBmHeaderField;
  mBmHeaderField = 0;
}

bool KMSearchRuleString::isEmpty() const
{
  return field().stripWhiteSpace().isEmpty() || contents().isEmpty();
}

bool KMSearchRuleString::requiresBody() const
{
  if (mBmHeaderField || (field() == "<recipients>" ))
    return false;
  return true;
}

bool KMSearchRuleString::matches( const DwString & aStr, KMMessage & msg,
                       const DwBoyerMoore * aHeaderField, int aHeaderLen ) const
{
  if ( isEmpty() )
    return false;

  bool rc = false;

  const DwBoyerMoore * headerField = aHeaderField ? aHeaderField : mBmHeaderField ;

  const int headerLen = ( aHeaderLen > -1 ? aHeaderLen : field().length() ) + 2 ; // +1 for ': '

  if ( headerField ) {
    static const DwBoyerMoore lflf( "\n\n" );
    static const DwBoyerMoore lfcrlf( "\n\r\n" );

    size_t endOfHeader = lflf.FindIn( aStr, 0 );
    if ( endOfHeader == DwString::npos )
      endOfHeader = lfcrlf.FindIn( aStr, 0 );
    const DwString headers = ( endOfHeader == DwString::npos ) ? aStr : aStr.substr( 0, endOfHeader );
    // In case the searched header is at the beginning, we have to prepend
    // a newline - see the comment in KMSearchRuleString constructor
    DwString fakedHeaders( "\n" );
    size_t start = headerField->FindIn( fakedHeaders.append( headers ), 0, false );
    // if the header field doesn't exist then return false for positive
    // functions and true for negated functions (e.g. "does not
    // contain"); note that all negated string functions correspond
    // to an odd value
    if ( start == DwString::npos )
      rc = ( ( function() & 1 ) == 1 );
    else {
      start += headerLen;
      size_t stop = aStr.find( '\n', start );
      char ch = '\0';
      while ( stop != DwString::npos && ( ( ch = aStr.at( stop + 1 ) ) == ' ' || ch == '\t' ) )
        stop = aStr.find( '\n', stop + 1 );
      const int len = stop == DwString::npos ? aStr.length() - start : stop - start ;
      const TQCString codedValue( aStr.data() + start, len + 1 );
      const TQString msgContents = KMMsgBase::decodeRFC2047String( codedValue ).stripWhiteSpace(); // FIXME: This needs to be changed for IDN support.
      rc = matchesInternal( msgContents );
    }
  } else if ( field() == "<recipients>" ) {
    static const DwBoyerMoore to("\nTo: ");
    static const DwBoyerMoore cc("\nCc: ");
    static const DwBoyerMoore bcc("\nBcc: ");
    // <recipients> "contains" "foo" is true if any of the fields contains
    // "foo", while <recipients> "does not contain" "foo" is true if none
    // of the fields contains "foo"
    if ( ( function() & 1 ) == 0 ) {
      // positive function, e.g. "contains"
      rc = ( matches( aStr, msg, &to, 2 ) ||
             matches( aStr, msg, &cc, 2 ) ||
             matches( aStr, msg, &bcc, 3 ) );
    }
    else {
      // negated function, e.g. "does not contain"
      rc = ( matches( aStr, msg, &to, 2 ) &&
             matches( aStr, msg, &cc, 2 ) &&
             matches( aStr, msg, &bcc, 3 ) );
    }
  }
  if ( FilterLog::instance()->isLogging() ) {
    TQString msg = ( rc ? "<font color=#00FF00>1 = </font>"
                       : "<font color=#FF0000>0 = </font>" );
    msg += FilterLog::recode( asString() );
    // only log headers bcause messages and bodies can be pretty large
// FIXME We have to separate the text which is used for filtering to be able to show it in the log
//    if ( logContents )
//      msg += " (<i>" + FilterLog::recode( msgContents ) + "</i>)";
    FilterLog::instance()->add( msg, FilterLog::ruleResult );
  }
  return rc;
}

bool KMSearchRuleString::matches( const KMMessage * msg ) const
{
  assert( msg );

  if ( isEmpty() )
    return false;

  TQString msgContents;
  // Show the value used to compare the rules against in the log.
  // Overwrite the value for complete messages and all headers!
  bool logContents = true;

  if( field() == "<message>" ) {

    // When searching in the complete message, we can't simply use msg->asString() here,
    // as that wouldn't decode the body. Therefore we use the decoded body and all decoded
    // header fields and add all to the one big search string.
    msgContents += msg->bodyToUnicode();
    const DwHeaders& headers = msg->headers();
    const DwField * dwField = headers.FirstField();
    while( dwField != 0 ) {
      const char * const fieldName = dwField->FieldNameStr().c_str();
      const TQString fieldValue = msg->headerFields( fieldName ).join( " " );
      msgContents += " " + fieldValue;
      dwField = dwField->Next();
    }
    logContents = false;
  } else if ( field() == "<body>" ) {
    msgContents = msg->bodyToUnicode();
    logContents = false;
  } else if ( field() == "<any header>" ) {
    msgContents = msg->headerAsString();
    logContents = false;
  } else if ( field() == "<recipients>" ) {
    // (mmutz 2001-11-05) hack to fix "<recipients> !contains foo" to
    // meet user's expectations. See FAQ entry in KDE 2.2.2's KMail
    // handbook
    if ( function() == FuncEquals || function() == FuncNotEqual )
      // do we need to treat this case specially? Ie.: What shall
      // "equality" mean for recipients.
      return matchesInternal( msg->headerField("To") )
          || matchesInternal( msg->headerField("Cc") )
          || matchesInternal( msg->headerField("Bcc") )
          // sometimes messages have multiple Cc headers
          || matchesInternal( msg->cc() );

    msgContents = msg->headerField("To");
    if ( !msg->headerField("Cc").compare( msg->cc() ) )
      msgContents += ", " + msg->headerField("Cc");
    else
      msgContents += ", " + msg->cc();
    msgContents += ", " + msg->headerField("Bcc");
  }  else {
    // make sure to treat messages with multiple header lines for
    // the same header correctly
    msgContents = msg->headerFields( field() ).join( " " );
  }

  if ( function() == FuncIsInAddressbook ||
       function() == FuncIsNotInAddressbook ) {
    // I think only the "from"-field makes sense.
    msgContents = msg->headerField( field() );
    if ( msgContents.isEmpty() )
      return ( function() == FuncIsInAddressbook ) ? false : true;
  }

  // these two functions need the kmmessage therefore they don't call matchesInternal
  if ( function() == FuncHasAttachment )
    return ( msg->toMsgBase().attachmentState() == KMMsgHasAttachment );
  if ( function() == FuncHasNoAttachment )
    return ( ((KMMsgAttachmentState) msg->toMsgBase().attachmentState()) == KMMsgHasNoAttachment );

  bool rc = matchesInternal( msgContents );
  if ( FilterLog::instance()->isLogging() ) {
    TQString msg = ( rc ? "<font color=#00FF00>1 = </font>"
                       : "<font color=#FF0000>0 = </font>" );
    msg += FilterLog::recode( asString() );
    // only log headers bcause messages and bodies can be pretty large
    if ( logContents )
      msg += " (<i>" + FilterLog::recode( msgContents ) + "</i>)";
    FilterLog::instance()->add( msg, FilterLog::ruleResult );
  }
  return rc;
}

// helper, does the actual comparing
bool KMSearchRuleString::matchesInternal( const TQString & msgContents ) const
{
  switch ( function() ) {
  case KMSearchRule::FuncEquals:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) == 0 );

  case KMSearchRule::FuncNotEqual:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) != 0 );

  case KMSearchRule::FuncContains:
    return ( msgContents.find( contents(), 0, false ) >= 0 );

  case KMSearchRule::FuncContainsNot:
    return ( msgContents.find( contents(), 0, false ) < 0 );

  case KMSearchRule::FuncRegExp:
    {
      TQRegExp regexp( contents(), false );
      return ( regexp.search( msgContents ) >= 0 );
    }

  case KMSearchRule::FuncNotRegExp:
    {
      TQRegExp regexp( contents(), false );
      return ( regexp.search( msgContents ) < 0 );
    }

  case FuncIsGreater:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) > 0 );

  case FuncIsLessOrEqual:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) <= 0 );

  case FuncIsLess:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) < 0 );

  case FuncIsGreaterOrEqual:
      return ( TQString::compare( msgContents.lower(), contents().lower() ) >= 0 );

  case FuncIsInAddressbook: {
    TDEABC::AddressBook *stdAb = TDEABC::StdAddressBook::self( true );
    TQStringList addressList =
      KPIM::splitEmailAddrList( msgContents.lower() );
    for( TQStringList::ConstIterator it = addressList.begin();
         ( it != addressList.end() );
         ++it ) {
      if ( !stdAb->findByEmail( KPIM::getEmailAddress( *it ) ).isEmpty() )
        return true;
    }
    return false;
  }

  case FuncIsNotInAddressbook: {
    TDEABC::AddressBook *stdAb = TDEABC::StdAddressBook::self( true );
    TQStringList addressList =
      KPIM::splitEmailAddrList( msgContents.lower() );
    for( TQStringList::ConstIterator it = addressList.begin();
         ( it != addressList.end() );
         ++it ) {
      if ( stdAb->findByEmail( KPIM::getEmailAddress( *it ) ).isEmpty() )
        return true;
    }
    return false;
  }

  case FuncIsInCategory: {
    TQString category = contents();
    TQStringList addressList =  KPIM::splitEmailAddrList( msgContents.lower() );
    TDEABC::AddressBook *stdAb = TDEABC::StdAddressBook::self( true );

    for( TQStringList::ConstIterator it = addressList.begin();
      it != addressList.end(); ++it ) {
        TDEABC::Addressee::List addresses = stdAb->findByEmail( KPIM::getEmailAddress( *it ) );

          for ( TDEABC::Addressee::List::Iterator itAd = addresses.begin(); itAd != addresses.end(); ++itAd )
              if ( (*itAd).hasCategory(category) )
                return true;

      }
      return false;
    }

    case FuncIsNotInCategory: {
      TQString category = contents();
      TQStringList addressList =  KPIM::splitEmailAddrList( msgContents.lower() );
      TDEABC::AddressBook *stdAb = TDEABC::StdAddressBook::self( true );

      for( TQStringList::ConstIterator it = addressList.begin();
        it != addressList.end(); ++it ) {
          TDEABC::Addressee::List addresses = stdAb->findByEmail( KPIM::getEmailAddress( *it ) );

            for ( TDEABC::Addressee::List::Iterator itAd = addresses.begin(); itAd != addresses.end(); ++itAd )
                if ( (*itAd).hasCategory(category) )
                  return false;

      }
      return true;
    }
  default:
    ;
  }

  return false;
}


//==================================================
//
// class KMSearchRuleNumerical
//
//==================================================

KMSearchRuleNumerical::KMSearchRuleNumerical( const TQCString & field,
                                        Function func, const TQString & contents )
          : KMSearchRule(field, func, contents)
{
}

bool KMSearchRuleNumerical::isEmpty() const
{
  bool ok = false;
  contents().toInt( &ok );

  return !ok;
}


bool KMSearchRuleNumerical::matches( const KMMessage * msg ) const
{

  TQString msgContents;
  int numericalMsgContents = 0;
  int numericalValue = 0;

  if ( field() == "<size>" ) {
    numericalMsgContents = int( msg->msgLength() );
    numericalValue = contents().toInt();
    msgContents.setNum( numericalMsgContents );
  } else if ( field() == "<age in days>" ) {
    TQDateTime msgDateTime;
    msgDateTime.setTime_t( msg->date() );
    numericalMsgContents = msgDateTime.daysTo( TQDateTime::currentDateTime() );
    numericalValue = contents().toInt();
    msgContents.setNum( numericalMsgContents );
  }
  bool rc = matchesInternal( numericalValue, numericalMsgContents, msgContents );
  if ( FilterLog::instance()->isLogging() ) {
    TQString msg = ( rc ? "<font color=#00FF00>1 = </font>"
                       : "<font color=#FF0000>0 = </font>" );
    msg += FilterLog::recode( asString() );
    msg += " ( <i>" + TQString::number( numericalMsgContents ) + "</i> )";
    FilterLog::instance()->add( msg, FilterLog::ruleResult );
  }
  return rc;
}

bool KMSearchRuleNumerical::matchesInternal( long numericalValue,
    long numericalMsgContents, const TQString & msgContents ) const
{
  switch ( function() ) {
  case KMSearchRule::FuncEquals:
      return ( numericalValue == numericalMsgContents );

  case KMSearchRule::FuncNotEqual:
      return ( numericalValue != numericalMsgContents );

  case KMSearchRule::FuncContains:
    return ( msgContents.find( contents(), 0, false ) >= 0 );

  case KMSearchRule::FuncContainsNot:
    return ( msgContents.find( contents(), 0, false ) < 0 );

  case KMSearchRule::FuncRegExp:
    {
      TQRegExp regexp( contents(), false );
      return ( regexp.search( msgContents ) >= 0 );
    }

  case KMSearchRule::FuncNotRegExp:
    {
      TQRegExp regexp( contents(), false );
      return ( regexp.search( msgContents ) < 0 );
    }

  case FuncIsGreater:
      return ( numericalMsgContents > numericalValue );

  case FuncIsLessOrEqual:
      return ( numericalMsgContents <= numericalValue );

  case FuncIsLess:
      return ( numericalMsgContents < numericalValue );

  case FuncIsGreaterOrEqual:
      return ( numericalMsgContents >= numericalValue );

  case FuncIsInAddressbook:  // since email-addresses are not numerical, I settle for false here
    return false;

  case FuncIsNotInAddressbook:
    return false;

  default:
    ;
  }

  return false;
}



//==================================================
//
// class KMSearchRuleStatus
//
//==================================================
TQString englishNameForStatus( const KMMsgStatus& status )
{
  for ( int i=0; i< numStatusNames; i++ ) {
    if ( statusNames[i].status == status ) {
      return statusNames[i].name;
    }
  }
  return TQString();
}

KMSearchRuleStatus::KMSearchRuleStatus( const TQCString & field,
                                        Function func, const TQString & aContents )
          : KMSearchRule(field, func, aContents)
{
  // the values are always in english, both from the conf file as well as
  // the patternedit gui
  mStatus = statusFromEnglishName( aContents );
}

KMSearchRuleStatus::KMSearchRuleStatus( int status, Function func )
: KMSearchRule( "<status>", func, englishNameForStatus( status ) )
{
    mStatus = status;
}

KMMsgStatus KMSearchRuleStatus::statusFromEnglishName( const TQString & aStatusString )
{
  for ( int i=0; i< numStatusNames; i++ ) {
    if ( !aStatusString.compare( statusNames[i].name ) ) {
      return statusNames[i].status;
    }
  }
  return KMMsgStatusUnknown;
}

bool KMSearchRuleStatus::isEmpty() const
{
  return field().stripWhiteSpace().isEmpty() || contents().isEmpty();
}

bool KMSearchRuleStatus::matches( const DwString &, KMMessage &,
				  const DwBoyerMoore *, int ) const
{
  assert( 0 );
  return false; // don't warn
}

bool KMSearchRuleStatus::matches( const KMMessage * msg ) const
{

  KMMsgStatus msgStatus = msg->status();
  bool rc = false;

  switch ( function() ) {
    case FuncEquals: // fallthrough. So that "<status> 'is' 'read'" works
    case FuncContains:
      if (msgStatus & mStatus)
        rc = true;
      break;
    case FuncNotEqual: // fallthrough. So that "<status> 'is not' 'read'" works
    case FuncContainsNot:
      if (! (msgStatus & mStatus) )
        rc = true;
      break;
    // FIXME what about the remaining funcs, how can they make sense for
    // stati?
    default:
      break;
  }

  if ( FilterLog::instance()->isLogging() ) {
    TQString msg = ( rc ? "<font color=#00FF00>1 = </font>"
                       : "<font color=#FF0000>0 = </font>" );
    msg += FilterLog::recode( asString() );
    FilterLog::instance()->add( msg, FilterLog::ruleResult );
  }
  return rc;
}

// ----------------------------------------------------------------------------

//==================================================
//
// class KMSearchPattern
//
//==================================================

KMSearchPattern::KMSearchPattern( const TDEConfig * config )
  : TQPtrList<KMSearchRule>()
{
  setAutoDelete( true );
  if ( config )
    readConfig( config );
  else
    init();
}

KMSearchPattern::~KMSearchPattern()
{
}

bool KMSearchPattern::matches( const KMMessage * msg, bool ignoreBody ) const
{
  if ( isEmpty() )
    return true;

  TQPtrListIterator<KMSearchRule> it( *this );
  switch ( mOperator ) {
  case OpAnd: // all rules must match
    for ( it.toFirst() ; it.current() ; ++it )
      if ( !((*it)->requiresBody() && ignoreBody) )
        if ( !(*it)->matches( msg ) )
          return false;
    return true;
  case OpOr:  // at least one rule must match
    for ( it.toFirst() ; it.current() ; ++it )
      if ( !((*it)->requiresBody() && ignoreBody) )
        if ( (*it)->matches( msg ) )
          return true;
    // fall through
  default:
    return false;
  }
}

bool KMSearchPattern::matches( const DwString & aStr, bool ignoreBody ) const
{
  if ( isEmpty() )
    return true;

  KMMessage msg;
  TQPtrListIterator<KMSearchRule> it( *this );
  switch ( mOperator ) {
  case OpAnd: // all rules must match
    for ( it.toFirst() ; it.current() ; ++it )
      if ( !((*it)->requiresBody() && ignoreBody) )
        if ( !(*it)->matches( aStr, msg ) )
          return false;
    return true;
  case OpOr:  // at least one rule must match
    for ( it.toFirst() ; it.current() ; ++it )
      if ( !((*it)->requiresBody() && ignoreBody) )
        if ( (*it)->matches( aStr, msg ) )
          return true;
    // fall through
  default:
    return false;
  }
}

bool KMSearchPattern::matches( TQ_UINT32 serNum, bool ignoreBody ) const
{
  if ( isEmpty() )
    return true;

  bool res;
  int idx = -1;
  KMFolder *folder = 0;
  KMMsgDict::instance()->getLocation(serNum, &folder, &idx);
  if (!folder || (idx == -1) || (idx >= folder->count())) {
    return false;
  }

  KMFolderOpener openFolder(folder, "searchptr");
  KMMsgBase *msgBase = folder->getMsgBase(idx);
  if (requiresBody() && !ignoreBody) {
    bool unGet = !msgBase->isMessage();
    KMMessage *msg = folder->getMsg(idx);
    res = false;
    if ( msg ) {
      res = matches( msg, ignoreBody );
      if (unGet)
        folder->unGetMsg(idx);
    }
  } else {
    res = matches( folder->getDwString(idx), ignoreBody );
  }
  return res;
}

bool KMSearchPattern::requiresBody() const {
  TQPtrListIterator<KMSearchRule> it( *this );
    for ( it.toFirst() ; it.current() ; ++it )
      if ( (*it)->requiresBody() )
	return true;
  return false;
}

void KMSearchPattern::purify() {
  TQPtrListIterator<KMSearchRule> it( *this );
  it.toLast();
  while ( it.current() )
    if ( (*it)->isEmpty() ) {
#ifndef NDEBUG
      kdDebug(5006) << "KMSearchPattern::purify(): removing " << (*it)->asString() << endl;
#endif
      remove( *it );
    } else {
      --it;
    }
}

void KMSearchPattern::readConfig( const TDEConfig * config ) {
  init();

  mName = config->readEntry("name");
  if ( !config->hasKey("rules") ) {
    kdDebug(5006) << "KMSearchPattern::readConfig: found legacy config! Converting." << endl;
    importLegacyConfig( config );
    return;
  }

  mOperator = config->readEntry("operator") == "or" ? OpOr : OpAnd;

  const int nRules = config->readNumEntry( "rules", 0 );

  for ( int i = 0 ; i < nRules ; i++ ) {
    KMSearchRule * r = KMSearchRule::createInstanceFromConfig( config, i );
    if ( r->isEmpty() )
      delete r;
    else
      append( r );
  }
}

void KMSearchPattern::importLegacyConfig( const TDEConfig * config ) {
  KMSearchRule * rule = KMSearchRule::createInstance( config->readEntry("fieldA").latin1(),
					  config->readEntry("funcA").latin1(),
					  config->readEntry("contentsA") );
  if ( rule->isEmpty() ) {
    // if the first rule is invalid,
    // we really can't do much heuristics...
    delete rule;
    return;
  }
  append( rule );

  const TQString sOperator = config->readEntry("operator");
  if ( sOperator == "ignore" ) return;

  rule = KMSearchRule::createInstance( config->readEntry("fieldB").latin1(),
			   config->readEntry("funcB").latin1(),
			   config->readEntry("contentsB") );
  if ( rule->isEmpty() ) {
    delete rule;
    return;
  }
  append( rule );

  if ( sOperator == "or"  ) {
    mOperator = OpOr;
    return;
  }
  // This is the interesting case...
  if ( sOperator == "unless" ) { // meaning "and not", ie we need to...
    // ...invert the function (e.g. "equals" <-> "doesn't equal")
    // We simply toggle the last bit (xor with 0x1)... This assumes that
    // KMSearchRule::Function's come in adjacent pairs of pros and cons
    KMSearchRule::Function func = last()->function();
    unsigned int intFunc = (unsigned int)func;
    func = KMSearchRule::Function( intFunc ^ 0x1 );

    last()->setFunction( func );
  }

  // treat any other case as "and" (our default).
}

void KMSearchPattern::writeConfig( TDEConfig * config ) const {
  config->writeEntry("name", mName);
  config->writeEntry("operator", (mOperator == KMSearchPattern::OpOr) ? "or" : "and" );

  int i = 0;
  for ( TQPtrListIterator<KMSearchRule> it( *this ) ; it.current() && i < FILTER_MAX_RULES ; ++i , ++it )
    // we could do this ourselves, but we want the rules to be extensible,
    // so we give the rule it's number and let it do the rest.
    (*it)->writeConfig( config, i );

  // save the total number of rules.
  config->writeEntry( "rules", i );
}

void KMSearchPattern::init() {
  clear();
  mOperator = OpAnd;
  mName = '<' + i18n("name used for a virgin filter","unknown") + '>';
}

TQString KMSearchPattern::asString() const {
  TQString result;
  if ( mOperator == OpOr )
    result = i18n("(match any of the following)");
  else
    result = i18n("(match all of the following)");

  for ( TQPtrListIterator<KMSearchRule> it( *this ) ; it.current() ; ++it )
    result += "\n\t" + FilterLog::recode( (*it)->asString() );

  return result;
}

const KMSearchPattern & KMSearchPattern::operator=( const KMSearchPattern & other ) {
  if ( this == &other )
    return *this;

  setOp( other.op() );
  setName( other.name() );

  clear(); // ###

  for ( TQPtrListIterator<KMSearchRule> it( other ) ; it.current() ; ++it ) {
    KMSearchRule * rule = KMSearchRule::createInstance( **it ); // deep copy
    append( rule );
  }

  return *this;
}
