/*
**  NSString+Extensions.m
**
**  Copyright (c) 2001-2006
**
**  Author: Ludovic Marcotte <ludovic@Sophos.ca>
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 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
**  Lesser General Public License for more details.
**  
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

//
// WARNING: Keep the encoding in this file to ISO-8859-1.
//          See the -hasREPrefix method for details.
//

#include <Pantomime/NSString+Extensions.h>

#include <Pantomime/CWCharset.h>
#include <Pantomime/CWConstants.h>
#include <Pantomime/CWInternetAddress.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/CWPart.h>

//
// We include the CoreFoundation headers under Mac OS X so we can support
// more string encodings.
//
#ifdef MACOSX
#include <CoreFoundation/CFString.h>
#include <CoreFoundation/CFStringEncodingExt.h>
#else
#include <GNUstepBase/GSCategories.h>
#endif

#include <ctype.h>

#ifdef HAVE_ICONV
#include <iconv.h>
#endif

#define IS_PRINTABLE(c) (isascii(c) && isprint(c))

//
//
//
@implementation NSString (PantomimeStringExtensions)

#ifdef MACOSX
- (NSString *) stringByTrimmingWhiteSpaces
{
  NSMutableString *aMutableString;

  aMutableString = [[NSMutableString alloc] initWithString: self];
  CFStringTrimWhitespace((CFMutableStringRef)aMutableString);
  
  return AUTORELEASE(aMutableString);
}
#endif


//
//
//
- (int) indexOfCharacter: (unichar) theCharacter
{
  return [self indexOfCharacter: theCharacter  fromIndex: 0];
}


//
//
//
- (int) indexOfCharacter: (unichar) theCharacter
               fromIndex: (unsigned int) theIndex
{
  int i, len;
  
  len = [self length];
  
  for (i = theIndex; i < len; i++)
    {
      if ([self characterAtIndex: i] == theCharacter)
	{
	  return i;
	}
    }
  
  return -1;
}


//
//
//
- (BOOL) hasCaseInsensitivePrefix: (NSString *) thePrefix
{
  if (thePrefix)
    {
      return [[self uppercaseString] hasPrefix: [thePrefix uppercaseString]];
    }
  
  return NO;
}


//
//
//
- (BOOL) hasCaseInsensitiveSuffix: (NSString *) theSuffix
{
  if (theSuffix)
    {
      return [[self uppercaseString] hasSuffix: [theSuffix uppercaseString]];
    }
  
  return NO;
}


//
//
//
- (NSString *) stringFromQuotedString
{
  int len;

  len = [self length];
  
  if (len > 1 &&
      [self characterAtIndex: 0] == '"' &&
      [self characterAtIndex: (len-1)] == '"')
    {
      return [self substringWithRange: NSMakeRange(1, len-2)];
    }
  
  return self;
}


//
//
//
+ (NSString *) stringValueOfTransferEncoding: (int) theEncoding
{
  switch (theEncoding)
    {
    case PantomimeEncodingNone:
      break;
    case PantomimeEncodingQuotedPrintable:
      return @"quoted-printable";
    case PantomimeEncodingBase64:
      return @"base64";
    case PantomimeEncoding8bit:
      return @"8bit";
    case PantomimeEncodingBinary:
      return @"binary";
    default:
      break;
    }

  // PantomimeEncoding7bit will also fall back here.
  return @"7bit";
}


//
//
//
+ (int) encodingForCharset: (NSData *) theCharset
{
  // We define some aliases for the string encoding.
  static struct { NSString *name; int encoding; BOOL fromCoreFoundation; } encodings[] = {
    {@"ascii"         ,NSASCIIStringEncoding          ,NO},
    {@"us-ascii"      ,NSASCIIStringEncoding          ,NO},
    {@"default"       ,NSASCIIStringEncoding          ,NO},  // Ah... spammers.
    {@"utf-8"         ,NSUTF8StringEncoding           ,NO},
    {@"iso-8859-1"    ,NSISOLatin1StringEncoding      ,NO},
    {@"x-user-defined",NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Outlook.
    {@"x-unknown"     ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Pine 4.21.
    {@"unknown-8bit"  ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Mutt/1.3.28i
    {@"0"             ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in QUALCOMM Windows Eudora Version 6.0.1.1
    {@""              ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Ximian Evolution
    {@"iso8859_1"     ,NSISOLatin1StringEncoding      ,NO},  // To prevent a lame bug in Openwave WebEngine
    {@"iso-8859-2"    ,NSISOLatin2StringEncoding      ,NO},
#ifdef MACOSX
    {@"iso-8859-3"    ,kCFStringEncodingISOLatin3        ,YES},
    {@"iso-8859-4"    ,kCFStringEncodingISOLatin4        ,YES},
    {@"iso-8859-5"    ,kCFStringEncodingISOLatinCyrillic ,YES},
    {@"iso-8859-6"    ,kCFStringEncodingISOLatinArabic   ,YES},
    {@"iso-8859-7"    ,kCFStringEncodingISOLatinGreek    ,YES},
    {@"iso-8859-8"    ,kCFStringEncodingISOLatinHebrew   ,YES},
    {@"iso-8859-9"    ,kCFStringEncodingISOLatin5        ,YES},
    {@"iso-8859-10"   ,kCFStringEncodingISOLatin6        ,YES},
    {@"iso-8859-11"   ,kCFStringEncodingISOLatinThai     ,YES},
    {@"iso-8859-13"   ,kCFStringEncodingISOLatin7        ,YES},
    {@"iso-8859-14"   ,kCFStringEncodingISOLatin8        ,YES},
    {@"iso-8859-15"   ,kCFStringEncodingISOLatin9        ,YES},
    {@"koi8-r"        ,kCFStringEncodingKOI8_R           ,YES},
    {@"big5"          ,kCFStringEncodingBig5             ,YES},
    {@"euc-kr"        ,kCFStringEncodingEUC_KR           ,YES},
    {@"ks_c_5601-1987",kCFStringEncodingEUC_KR           ,YES},
    {@"gb2312"        ,kCFStringEncodingHZ_GB_2312       ,YES},
    {@"shift_jis"     ,kCFStringEncodingShiftJIS         ,YES},
    {@"windows-1255"  ,kCFStringEncodingWindowsHebrew    ,YES},
    {@"windows-1256"  ,kCFStringEncodingWindowsArabic    ,YES},
    {@"windows-1257"  ,kCFStringEncodingWindowsBalticRim ,YES},
    {@"windows-1258"  ,kCFStringEncodingWindowsVietnamese,YES},
#else
    {@"iso-8859-3"   ,NSISOLatin3StringEncoding                 ,NO},
    {@"iso-8859-4"   ,NSISOLatin4StringEncoding                 ,NO},
    {@"iso-8859-5"   ,NSISOCyrillicStringEncoding               ,NO},
    {@"iso-8859-6"   ,NSISOArabicStringEncoding                 ,NO},
    {@"iso-8859-7"   ,NSISOGreekStringEncoding                  ,NO},
    {@"iso-8859-8"   ,NSISOHebrewStringEncoding                 ,NO},
    {@"iso-8859-9"   ,NSISOLatin5StringEncoding                 ,NO},
    {@"iso-8859-10"  ,NSISOLatin6StringEncoding                 ,NO},
    {@"iso-8859-11"  ,NSISOThaiStringEncoding                   ,NO},
    {@"iso-8859-13"  ,NSISOLatin7StringEncoding                 ,NO},
    {@"iso-8859-14"  ,NSISOLatin8StringEncoding                 ,NO},
    {@"iso-8859-15"  ,NSISOLatin9StringEncoding                 ,NO},
    {@"koi8-r"       ,NSKOI8RStringEncoding                     ,NO},
    {@"big5"         ,NSBIG5StringEncoding                      ,NO},
    {@"gb2312"       ,NSGB2312StringEncoding                    ,NO},
    {@"utf-7"        ,NSUTF7StringEncoding                      ,NO},
    {@"unicode-1-1-utf-7", NSUTF7StringEncoding                 ,NO},  // To prever a bug (sort of) in MS Hotmail
#endif
    {@"windows-1250" ,NSWindowsCP1250StringEncoding             ,NO},
    {@"windows-1251" ,NSWindowsCP1251StringEncoding             ,NO},
    {@"cyrillic (windows-1251)", NSWindowsCP1251StringEncoding  ,NO},  // To prevent a bug in MS Hotmail
    {@"windows-1252" ,NSWindowsCP1252StringEncoding             ,NO},
    {@"windows-1253" ,NSWindowsCP1253StringEncoding             ,NO},
    {@"windows-1254" ,NSWindowsCP1254StringEncoding             ,NO},
    {@"iso-2022-jp"  ,NSISO2022JPStringEncoding                 ,NO},
    {@"euc-jp"       ,NSJapaneseEUCStringEncoding               ,NO},
  };
  
  NSString *name;
  int i;

  name = [[NSString stringWithCString: [theCharset bytes] length: [theCharset length]] lowercaseString];
  
  for (i = 0; i < sizeof(encodings)/sizeof(encodings[0]); i++)
    {
      if ([name isEqualToString: encodings[i].name])
	{
	  // Under OS X, we use CoreFoundation if necessary to convert the encoding
	  // to a NSString encoding.
#ifdef MACOSX
	  if (encodings[i].fromCoreFoundation)
	    {
	      return CFStringConvertEncodingToNSStringEncoding(encodings[i].encoding);
	    }
	  else
	    {
	      return encodings[i].encoding;
	    }
#else
	  return encodings[i].encoding;
#endif
	}
    }

  return -1;
}


//
//
//
+ (int) encodingForPart: (CWPart *) thePart
{
  int encoding;

  // We get the encoding we are gonna use. We always favor the default encoding.
  encoding = -1;
  
  if ([thePart defaultCharset])
    {
      encoding = [self encodingForCharset: [[thePart defaultCharset] dataUsingEncoding: NSASCIIStringEncoding]];
    }
  else if ([thePart charset])
    {
      encoding = [self encodingForCharset: [[thePart charset] dataUsingEncoding: NSASCIIStringEncoding]];
    }
  else
    {
      encoding = [NSString defaultCStringEncoding];
    }

  if (encoding == -1 || encoding == NSASCIIStringEncoding)
    {
      encoding = NSISOLatin1StringEncoding;
    }

  return encoding;
}


//
//
//
+ (NSString *) stringWithData: (NSData *) theData
                      charset: (NSData *) theCharset
{
  int encoding;

  if (theData == nil)
    {
      return nil;
    }

  encoding = [NSString encodingForCharset: theCharset];
  
  if (encoding == -1)
    {
#ifdef HAVE_ICONV
      NSString *aString;

      const char *i_bytes, *from_code;
      char *o_bytes;

      size_t i_length, o_length;
      int total_length, ret;
      iconv_t conv;
      
      // Instead of calling cString directly on theCharset, we first try
      // to obtain the ASCII string of the data object.
      from_code = [[theCharset asciiString] cString];
      
      if (!from_code)
	{
	  return nil;
	}
      
      conv = iconv_open("UTF-8", from_code);
      
      if ((int)conv < 0)
	{
	  // Let's assume we got US-ASCII here.
	  return AUTORELEASE([[NSString alloc] initWithData: theData  encoding: NSASCIIStringEncoding]);
	}
      
      i_bytes = [theData bytes];
      i_length = [theData length];
      
      total_length = o_length = sizeof(unichar)*i_length;
      o_bytes = (char *)malloc(o_length);
      
      if (o_bytes == NULL) return nil;

      while (i_length > 0)
	{
	  ret = iconv(conv, (char **)&i_bytes, &i_length, &o_bytes, &o_length);
	  
	  if (ret == (size_t)-1)
	    {
	      iconv_close(conv);
	      
	      total_length = total_length - o_length;
	      o_bytes -= total_length;
	      free(o_bytes);
	      return nil;
	    }
	}
      
      total_length = total_length - o_length;
      o_bytes -= total_length;
      
      // If we haven't used all our allocated buffer, we shrink it.
      if (o_length > 0)
	{
	  realloc(o_bytes, total_length);
	}

      aString = [[NSString alloc] initWithData: [NSData dataWithBytesNoCopy: o_bytes
							length: total_length]
				  encoding: NSUTF8StringEncoding];
      iconv_close(conv);

      return AUTORELEASE(aString);
#else
      return nil;
#endif
    }
  
  return AUTORELEASE([[NSString alloc] initWithData: theData
				       encoding: encoding]);
}


//
//
//
#warning return Charset instead?
- (NSString *) charset
{
  NSMutableArray *aMutableArray;
  NSString *aString;
  CWCharset *aCharset;

  unsigned int i, j;

  aMutableArray = [[NSMutableArray alloc] initWithCapacity: 21];

  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-1"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-2"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-3"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-4"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-5"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-6"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-7"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-8"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-9"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-10"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-11"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-13"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-14"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"iso-8859-15"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"koi8-r"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"koi8-u"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"windows-1250"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"windows-1251"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"windows-1252"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"windows-1253"]];
  [aMutableArray addObject: [CWCharset charsetForName: @"windows-1254"]];


  for (i = 0; i < [self length]; i++)
    {
      for (j = 0; j < [aMutableArray count]; j++)
        {
          if (![[aMutableArray objectAtIndex: j] characterIsInCharset: [self characterAtIndex: i]])
            {
              // Character is not in the charset
              [aMutableArray removeObjectAtIndex: j];
              j--;
            }
        }

      // FIXME: can't break even if there is only one left. First we have to check
      //        whether that encoding will actually work for the entire string. If it
      //	doesn't we'll need to fall back to utf-8 (or something else that can encode
      //        _everything_).
      // 
      // Intelligent string splitting would help, of course
      //
      if ([aMutableArray count] < 1)
        {
          // We have zero or one charset
          break;
        }
    }

  if ([aMutableArray count])
    {
      aCharset = [aMutableArray objectAtIndex: 0];
      [aMutableArray removeAllObjects];
      aString = [aCharset name];
    }
  else
    {
      // We have no charset, we try to "guess" a default charset
      if ([self canBeConvertedToEncoding: NSISO2022JPStringEncoding])
	{      
	  // ISO-2022-JP is the standard of Japanese character encoding
	  aString = @"iso-2022-jp";
	}
      else
	{ 
	  // We have no charset, we return a default charset
	  aString = @"utf-8";
	}
    }

  RELEASE(aMutableArray);
  
  return aString;
}


//
//
//
- (NSString *) modifiedUTF7String
{
#ifndef MACOSX
  NSMutableData *aMutableData, *modifiedData;
  NSString *aString;

  const char *b;
  BOOL escaped;
  unichar ch;
  int i, len;

  //
  // We UTF-7 encode _only_ the non-ASCII parts.
  //
  aMutableData = [[NSMutableData alloc] init];
  AUTORELEASE(aMutableData);
  len = [self length];
  
  for (i = 0; i < len; i++)
    {
      ch = [self characterAtIndex: i];
      
      if ( IS_PRINTABLE(ch) )
	{
	  [aMutableData appendCFormat: @"%c", ch];
	}
      else
	{
	  int j;

	  j = i+1;
	  // We got a non-ASCII character, let's get the substring and encode it using UTF-7.
	  while (j < len && !IS_PRINTABLE([self characterAtIndex: j]))
	    {
	      j++;
	    }
	  
	  // Get the substring.
	  [aMutableData appendData: [[self substringWithRange: NSMakeRange(i,j-i)] dataUsingEncoding: NSUTF7StringEncoding]];
	  i = j-1;
	}
    }

  b = [aMutableData bytes];
  len = [aMutableData length];
  escaped = NO;

  //
  // We replace:
  //
  // &   ->  &-
  // +   ->  &
  // +-  ->  +
  // /   ->  ,
  //
  // in order to produce our modified UTF-7 string.
  //
  modifiedData = [[NSMutableData alloc] init];
  AUTORELEASE(modifiedData);

  for (i = 0; i < len; i++, b++)
    {
      if (!escaped && *b == '&')
	{
	  [modifiedData appendCString: "&-"];
	}
      else if (!escaped && *b == '+')
	{
	  if (*(b+1) == '-')
	    {
	      [modifiedData appendCString: "+"];
	    }
	  else
	    {
	      [modifiedData appendCString: "&"];

	      // We enter the escaped mode.
	      escaped = YES;
	    }
	}
      else if (escaped && *b == '/')
	{
	  [modifiedData appendCString: ","];
	}
      else if (escaped && *b == '-')
	{
	  [modifiedData appendCString: "-"];

	  // We leave the escaped mode.
	  escaped = NO;
	}
      else
	{
	  [modifiedData appendCFormat: @"%c", *b];
	}
    }
  
  // If we're still in the escaped mode we haven't added our trailing -,
  // let's add it right now.
  if ( escaped )
    {
      [modifiedData appendCString: "-"];
    }

  aString = AUTORELEASE([[NSString alloc] initWithData: modifiedData  encoding: NSASCIIStringEncoding]);

  return (aString != nil ? aString : self);
#else
  return self;
#endif
}


//
//
//
- (NSString *) stringFromModifiedUTF7
{
#ifndef MACOSX
  NSMutableData *aMutableData;

  BOOL escaped;
  unichar ch;
  int i, len;

  aMutableData = [[NSMutableData alloc] init];
  AUTORELEASE(aMutableData);

  len = [self length];
  escaped = NO;

  //
  // We replace:
  //
  // &   ->  +
  // &-  ->  &
  // ,   ->  /
  //
  // If we are in escaped mode. That is, between a &....-
  //
  for (i = 0; i < len; i++)
    {
      ch = [self characterAtIndex: i];
      
      if (!escaped && ch == '&')
	{
	  if ( (i+1) < len && [self characterAtIndex: (i+1)] != '-' )
	    {
	      [aMutableData appendCString: "+"];
	      
	      // We enter the escaped mode.
	      escaped = YES;
	    }
	  else
	    {
	      // We replace &- by &
	      [aMutableData appendCString: "&"];
	      i++;
	    }
	}
      else if (escaped && ch == ',')
	{
	  [aMutableData appendCString: "/"];
	}
      else if (escaped && ch == '-')
	{
	  [aMutableData appendCString: "-"];

	  // We leave the escaped mode.
	  escaped = NO;
	}
      else
	{
	  [aMutableData appendCFormat: @"%c", ch];
	}
    }

  return AUTORELEASE([[NSString alloc] initWithData: aMutableData  encoding: NSUTF7StringEncoding]);
#else
  return nil;
#endif
}


//
//
//
- (BOOL) hasREPrefix
{
  if ([self hasCaseInsensitivePrefix: @"re:"] ||
      [self hasCaseInsensitivePrefix: @"re :"] ||
      [self hasCaseInsensitivePrefix: @"Réf. :"] ||
      [self hasCaseInsensitivePrefix: @"Rép. :"])
    {
      return YES;
    }
  
  return NO;
}



//
//
//
- (NSString *) stringByReplacingOccurrencesOfCharacter: (unichar) theTarget
                                         withCharacter: (unichar) theReplacement
{
  NSMutableString *aMutableString;
  int len, i;
  unichar c;

  if (!theTarget || !theReplacement || theTarget == theReplacement)
    {
      return self;
    }

  len = [self length];
  
  aMutableString = [NSMutableString stringWithCapacity: len];

  for (i = 0; i < len; i++)
    {
      c = [self characterAtIndex: i];
      
      if (c == theTarget)
	{
	  [aMutableString appendFormat: @"%c", theReplacement];
	}
      else
	{
	  [aMutableString appendFormat: @"%c", c];
	}
    }

  return aMutableString;
}


//
//
//
- (NSString *) stringByDeletingLastPathComponentWithSeparator: (unsigned char) theSeparator
{
  int i, c;
  
  c = [self length];

  for (i = c-1; i >= 0; i--)
    {
      if ([self characterAtIndex: i] == theSeparator)
	{
	  return [self substringToIndex: i];
	}
    }

  return @"";
}


//
// 
//
- (NSString *) stringByDeletingFirstPathSeparator: (unsigned char) theSeparator
{
  if ([self length] && [self characterAtIndex: 0] == theSeparator)
    {
      return [self substringFromIndex: 1];
    }
  
  return self;
}


//
//
//
- (BOOL) is7bitSafe
{
  int i, len;
  
  // We search for a non-ASCII character.
  len = [self length];
  
  for (i = 0; i < len; i++)
    {
      if ([self characterAtIndex: i] > 0x007E)
	{
	  return NO;
	}
    }
  
  return YES;
}


//
//
//
- (NSString *) quoteWithQuoteLevel: (int) theLevel
		     wrappingLimit: (int) theLimit
{
  NSMutableString *aMutableString, *aQuotePrefix;
  NSArray *lines;
  NSString *aString, *aLine;
  BOOL isQuoted;
  int i;

  // We verify if the wrapping limit is smaller then the quote level
  if (theLevel > theLimit) 
    {
      return @"";
    }
  
  // We initialize our local variables
  // We wrap the text block
  aMutableString = [[NSMutableString alloc] initWithCapacity: [self length]];
  aQuotePrefix = [[NSMutableString alloc] initWithCapacity: theLevel];

  // We wrap the string to the proper limit
  aString = [self wrapWithWrappingLimit: (theLimit - theLevel)];
  lines = [aString componentsSeparatedByString: @"\n"];

  // We prepare the line prefix
  for (i = 0; i < theLevel; i++)
    {
      [aQuotePrefix appendString: @">"];
    }
  
  // We add the line prefix to each wrapped line
  for (i = 0; i < [lines count]; i++)
    {
      aLine = [lines objectAtIndex: i];
      isQuoted = ([aLine length] > 0 && [aLine characterAtIndex: 0] == '>' );
      
      [aMutableString appendString: aQuotePrefix];
      if (!isQuoted)
	{
	  [aMutableString appendString: @" "];
	}
      [aMutableString appendString: aLine];
      [aMutableString appendString: @"\n"];
    }

  if (i > 0)
    {
      [aMutableString deleteCharactersInRange: NSMakeRange([aMutableString length] - 1, 1)];
    }

  RELEASE(aQuotePrefix);

  return AUTORELEASE(aMutableString);
}


//
//
//
- (NSString *) unwrapWithQuoteWrappingLimit: (int) theQuoteLimit
{
  NSMutableString *aMutableString, *lines;
  NSString *aLine;

  int i, len, quote_depth, line_quote_depth, line_start;
  BOOL isFlowed;

  len = [self length];

  aMutableString = [[NSMutableString alloc] initWithCapacity: len];
  lines = [[NSMutableString alloc] init];
  quote_depth = -1;
  
  // We analyse the string until the last character
  for (i = 0; i < len;)
    {
      // We analyse the quote depth of the current line
      if ([self characterAtIndex: i] == '>')
	{
	  for (line_quote_depth = 0; i < len && [self characterAtIndex: i] == '>'; i++)
	    {
	      line_quote_depth++;
	    }
	}
      else
	{
	  line_quote_depth = 0;
	}
      
      // If the current quote depth is not defined, set it to quote depth of current line
      if (quote_depth == -1)
	{
	  quote_depth = line_quote_depth;
	}
      
      // We verify if the line has been space-stuffed
      if (i < len && [self characterAtIndex: i] == ' ')
	{
	  i++;
	}
      line_start = i;

      // We look for the next line break
      for (; i < len && [self characterAtIndex: i] != '\n'; i++);
      
      // We get the actual content of the current line
      aLine = [self substringWithRange: NSMakeRange(line_start, i - line_start)];
      
      // We verify if the line ends with a soft break 
      isFlowed = [aLine length] > 0 && [aLine characterAtIndex: [aLine length] - 1] == ' ';

      // We must handle usenet signature as a special case
      if (isFlowed && [aLine isEqualToString: @"-- "])
	{
	  isFlowed = NO;
	}

      if (isFlowed && quote_depth == line_quote_depth)
	{ 
	  // The current line is flowed;
	  // we append it to the buffer without quote characters
	  [lines appendString: aLine];
	}
      else if (isFlowed)
	{ 
	  // The current line is flowed but has mis-matched quoting

	  // We first append the previous paragraph to the buffer with the necessary quote characters
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [lines quoteWithQuoteLevel: quote_depth 
					wrappingLimit: theQuoteLimit]];
	    }
	  [aMutableString appendString: lines];
	  [aMutableString appendString: @"\n"];
	  
	  // We initialize the current paragraph with the current line
	  [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		 withString: aLine];
	  
	  // We set the paragraph depth with the current line depth
	  quote_depth = line_quote_depth;
	}
      else if (!isFlowed && quote_depth == line_quote_depth)
	{ 
	  // The line is fixed

	  // We first append the fixed line
	  [lines appendString: aLine];
	  
	  // We add the necessary quote characters in the paragraph
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [lines quoteWithQuoteLevel: quote_depth 
					wrappingLimit: theQuoteLimit]];
	    }

	  // We append the paragraph (if any)
	  if ([lines length])
	    {
	      [aMutableString appendString: lines];
	    }
	  [aMutableString appendString: @"\n"];
	  
	  // We empty the paragraph buffer
	  [lines replaceCharactersInRange: NSMakeRange(0,[lines length])
		 withString: @""];
	  
	  // We reset the paragraph depth
	  quote_depth = -1;
	}
      else
	{
	  // The line is fixed but has mis-matched quoting
	  
	  // We first append the previous paragraph (if any) to the buffer with the necessary quote characters
	  if (quote_depth)
	    {
	      [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		     withString: [lines quoteWithQuoteLevel: quote_depth 
					wrappingLimit: theQuoteLimit]];
	    }
	  [aMutableString appendString: lines];
	  [aMutableString appendString: @"\n"];

	  // We append the fixed line to the buffer with the necessary quote characters
	  if (line_quote_depth)
	    {
	      aLine = [aLine quoteWithQuoteLevel: line_quote_depth 
			     wrappingLimit: theQuoteLimit];
	    }
	  [aMutableString appendString: aLine];
	  [aMutableString appendString: @"\n"];

	  // We empty the paragraph buffer
	  [lines replaceCharactersInRange: NSMakeRange(0,[lines length])
		 withString: @""];

	  // We reset the paragraph depth
	  quote_depth = -1;
	}
      
      // The next iteration must starts after the line break
      i++;
    }

  // We must handle flowed lines that don't have a fixed line break at the end of the message
  if ([lines length])
    {
      if (quote_depth)
	{
	  [lines replaceCharactersInRange: NSMakeRange(0, [lines length])
		 withString: [lines quoteWithQuoteLevel: quote_depth 
				    wrappingLimit: theQuoteLimit]];
	}
      [aMutableString appendString: lines];
      [aMutableString appendString: @"\n"];
    }

  DESTROY(lines);

  return AUTORELEASE(aMutableString);
}


//
//
//
- (NSString *) wrapWithWrappingLimit: (int) theLimit
{
  NSMutableString *aMutableString;
  NSArray *lines;
  NSString *aLine, *part;
  int i, j, k, split;
  int depth;

  // We first verify if the string is valid
  if ([self length] == 0)
    {
      return @"";
    }
  
  // We then verify if the limit is valid
  if (theLimit == 0 || theLimit > 998)
    {
      theLimit = 998;
    }
  
  // We initialize our local variables
  aMutableString = [[NSMutableString alloc] initWithCapacity: [self length]];
  lines = [self componentsSeparatedByString: @"\n"];
  
  // We analyse each line
  for (i = 0; i < [lines count]; i++)
    {
      aLine = [lines objectAtIndex: i];

      // We compute the quote depth
      for (depth = 0; depth < [aLine length] && [aLine characterAtIndex: depth] == '>'; depth++);
      j = depth;
      
      // We remove the leading whitespace if any
      if (depth && [aLine length] > j && [aLine characterAtIndex: j] == 32)
	{
	  j++;
	}

      aLine = [aLine substringFromIndex: j];

      // If the line is NOT the signature separator, we remove the trailing space(s)
      if (![aLine isEqual: @"-- "])
	{
	  for (j = [aLine length]; j > 0 && [aLine characterAtIndex: j - 1] == 32; j--);
	  if (depth && j < [aLine length])
	    {
	      // If line is quoted, we preserve a whitespace for the soft-break
	      j++;
	    }
	  aLine = [aLine substringToIndex: j];
	}

      // If the line is the signature separator or if the line length with the
      // quote characters and the space-stuffing is lower than the limit,
      // we directly append the line to the buffer
      if ([aLine isEqual: @"-- "] || depth + 1 + [aLine length] <= theLimit)
	{
	  // We add the quote characters
	  for (j = 0; j < depth; j++)
	    {
	      [aMutableString appendString: @">"];
	    }
	  
	  // We space-stuff the line if necessary. The conditions are:
	  // - the line is quoted or
	  // - the line starts with a quote character or
	  // - the line starts with a whitespace or
	  // - the line starts with the word From.
	  if (depth ||
	      ([aLine length] && ([aLine characterAtIndex: 0] == '>' || [aLine characterAtIndex: 0] == ' ' || [aLine hasPrefix: @"From"])))
	    {
	      [aMutableString appendString: @" "];
	    }

	  // We append the line to the buffer
	  [aMutableString appendString: aLine];
	  [aMutableString appendString: @"\n"];
	  
	  // We jump to the next line
	  continue;
	}

      // We look for the right place to split the line
      for (j = 0; j < [aLine length];)
	{
	  // We verify if the line after character j has a length lower than the limit
	  if ([aLine length] - j + depth + 1 < theLimit)
	    {
	      split = [aLine length];
	    }
	  // No it hasn't
	  else
	    {
	      split = j;
	      
	      // We search for the last whitespace before the limit
	      for (k = j; k < [aLine length] && k - j + depth + 1 < theLimit; k++)
		{
		  if ([aLine characterAtIndex: k] == 32)
		    {
		      split = k;
		    }
		}

		/*
		No good spot; include the entire next word. This isn't really
		optimal, but the alternative is to split the word, and that
		would be horribly ugly. Also, it'd mean that deeply quoted
		text might appear with one letter on each row, which is even
		uglier and means that the receiver won't be able to
		reconstruct the text.

		A proper fix would be to have both parameters for a 'soft'
		line limit that we _try_ to break before, and a 'hard' line
		limit that specifies an actual hard limit of a protocol or
		something. In NNTP, the values would be 72 and 998
		respectively. This means that text quoted 70 levels (and yes,
		I have seen such posts) will appear with one unbroken word on
		each line (as long as the word is shorter than 928
		characters). This is still ugly, but:

		a. invalid (protocol-wise) lines will never be generated
		   (unless something's quoted >998 levels)

		b. a MIME decoder that handles format=flowed will be able to
		   reconstruct the text properly

		(Additionally, it might turn out to be useful to have a lower
		limit on wrapping length, eg. 20. If the effective line
		length is shorter than this, wrap to quote-depth+soft-limit
		(so eg. text quoted 60 times would be wrapped at 60+72
		characters instead of 72). This wouldn't make any difference
		on flowed capable MIME decoders, but might turn out to look
		better when viewed with non-flowed handling programs.
		Hopefully, such deeply quoted text won't be common enough to
		be worth the trouble, so people with non-flowed capable
		software will simply have to live with the ugly posts in
		those cases.)
 		*/
	      if (split == j)
		{
		  // No whitespace found before the limit;
		  // continue farther until a whitespace or the last character of the line
		  for (; k < [aLine length] && [aLine characterAtIndex: k] != 32; k++);
		  split = k;
		}
	    }

	  // Since the line will be splitted, we must keep a whitespace for
	  // the soft-line break
	  if (split < [aLine length])
	    {
	      split++;
	    }
	  
	  // Retrieve splitted part of line
	  part = [aLine substringWithRange: NSMakeRange(j, split - j)];

	  // We add the quote characters
	  for (k = 0; k < depth; k++)
	    {
	      [aMutableString appendString: @">"];
	    }
	  
	  // We space-stuff the line if necesary.
	  if (depth ||
	      ([part length] && ([part characterAtIndex: 0] == '>' || [part characterAtIndex: 0] == ' ' || [part hasPrefix: @"From"])))
	    {
	      [aMutableString appendString: @" "];
	    }

	  // Append line part to buffer
	  [aMutableString appendString: part];
	  [aMutableString appendString: @"\n"];
	  
	  // Next iteration continues where current split occured 
	  j = split;
	}

      
    }
  
  if (i > 0)
    {
      [aMutableString deleteCharactersInRange: NSMakeRange([aMutableString length] - 1, 1)];
    }
  
  return AUTORELEASE(aMutableString);
}


//
//
//
+ (NSString *) stringFromRecipients: (NSArray *) theRecipients
			       type: (PantomimeRecipientType) theRecipientType
{
  CWInternetAddress *anInternetAddress;
  NSMutableString *aMutableString;
  int i, count;
  
  aMutableString = [[NSMutableString alloc] init];
  count = [theRecipients count];

  for (i = 0; i < count; i++)
    {
      anInternetAddress = [theRecipients objectAtIndex: i];
      
      if ([anInternetAddress type] == theRecipientType)
	{
	  [aMutableString appendFormat: @"%@, ", [anInternetAddress stringValue]];
	}
    }
  
  return AUTORELEASE(aMutableString); 
}

@end
