/*
**  Part.m
**
**  Copyright (c) 2001, 2002, 2003
**
**  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
*/

#include <Pantomime/Part.h>

#include <Pantomime/Constants.h>
#include <Pantomime/Message.h>
#include <Pantomime/MimeMultipart.h>
#include <Pantomime/MimeUtility.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/NSString+Extensions.h>
#include <Pantomime/Parser.h>

#include <Foundation/NSAutoreleasePool.h>
#include <Foundation/NSDebug.h>
#include <Foundation/NSValue.h>

#include <string.h>

static int currentPartVersion = 2;

@implementation Part

//
//
//
- (id) init
{
  self = [super init];
  
  [Part setVersion: currentPartVersion];

  // We set the default values
  [self setContentType: @"text/plain"];
  [self setContentTransferEncoding: NONE];
  [self setCharset: @"us-ascii"];
  [self setFormat: FORMAT_UNKNOWN];
  [self setLineLength: 0];
  
  return self;
}


//
//
//
- (void) dealloc
{
  //NSDebugLog(@"Part: -dealloc");
  
  TEST_RELEASE(content);

  RELEASE(contentType);
  RELEASE(contentID);
  RELEASE(contentDescription);
  RELEASE(contentDisposition);
  RELEASE(filename);
  RELEASE(boundary);
  RELEASE(protocol);
  RELEASE(charset);
  
  TEST_RELEASE(defaultCharset);

  [super dealloc];
}


//
//
//
- (id) initWithData: (NSData *) theData
{
  NSRange aRange;

  [Part setVersion: currentPartVersion];
 
  aRange = [theData rangeOfCString: "\n\n"];
  
  if (aRange.length == 0)
    {
      NSDebugLog(@"Part: failed to initialize the part from data.");
      AUTORELEASE(self);
      return nil;
    }
  
  // We verify if we have an empty body part content like:
  // X-UID: 5dc5aa4b82240000
  //
  // This is a MIME Message
  //
  // ------=_NextPart_000_007F_01BDF6C7.FABAC1B0
  //
  //
  // ------=_NextPart_000_007F_01BDF6C7.FABAC1B0
  // Content-Type: text/html; name="7english.co.kr.htm"
  if ( [theData length] == 2 )
    {
      [self setContent: [NSString stringWithString: @""]];
      
      return self;
    }

  // We initialize our message with the headers and the content
  self = [self init];
  
  [self setHeadersFromData: [theData subdataWithRange: NSMakeRange(0,aRange.location)]];
  [self setContentFromRawSource:
  	  [theData subdataWithRange:
  		     NSMakeRange(aRange.location + 2, [theData length]-(aRange.location+2))]];

  return self;
}


//
//
//
- (id) initWithData: (NSData *) theData
            charset: (NSString *) theCharset
{
  [Part setVersion: currentPartVersion];
  
  [self setDefaultCharset: theCharset];
  
  return [self initWithData: theData];
}


//
// NSCoding protocol
//
- (void) encodeWithCoder: (NSCoder *) theCoder
{
  [Part setVersion: currentPartVersion];

  [theCoder encodeObject: [self contentType]];
  [theCoder encodeObject: [self contentID]];
  [theCoder encodeObject: [self contentDescription]];
  [theCoder encodeObject: [self contentDisposition]];
  [theCoder encodeObject: [self filename]];
 
  [theCoder encodeObject: [NSNumber numberWithInt: [self contentTransferEncoding]]];
  [theCoder encodeObject: [NSNumber numberWithInt: [self format]]];
  [theCoder encodeObject: [NSNumber numberWithInt: [self size]]];

  [theCoder encodeObject: [self boundary]];
  [theCoder encodeObject: [self charset]];
  [theCoder encodeObject: [self defaultCharset]];
}


- (id) initWithCoder: (NSCoder *) theCoder
{
  int version;

  version = [theCoder versionForClassName: @"Part"];
  
  self = [super init];

  [self setContentType: [theCoder decodeObject]];
  [self setContentID: [theCoder decodeObject]];
  [self setContentDescription: [theCoder decodeObject]];
  [self setContentDisposition: [theCoder decodeObject]];
  [self setFilename: [theCoder decodeObject]];

  [self setContentTransferEncoding: [[theCoder decodeObject] intValue]];
  [self setFormat: [[theCoder decodeObject] intValue]];
  [self setSize: [[theCoder decodeObject] intValue]];

  [self setBoundary: [theCoder decodeObject]];
  [self setCharset: [theCoder decodeObject]];

  [self setDefaultCharset: [theCoder decodeObject]];
  
  return self;
}



//
// access / mutation methods
//
- (NSObject *) content
{
  return content;
}


//
//
//
- (void) setContent: (NSObject *) theContent
{
  if ( theContent )
    {
      RETAIN(theContent);
      RELEASE(content);
      content = theContent;
    }
  else
    {
      DESTROY(content);
    }
}


//
//
//
- (NSString *) contentType
{
  return contentType;
}

- (void) setContentType: (NSString *) theContentType
{  
  RETAIN(theContentType);
  RELEASE(contentType);
  contentType = theContentType;
}


//
//
//
- (NSString *) contentID
{
  return contentID;
}

- (void) setContentID: (NSString *) theContentID
{
  RETAIN(theContentID);
  RELEASE(contentID);
  contentID = theContentID;
}

 
//
//
//
- (NSString *) contentDescription
{
  return contentDescription;
}

- (void) setContentDescription: (NSString *) theContentDescription
{
  RETAIN(theContentDescription);
  RELEASE(contentDescription);
  contentDescription = theContentDescription;
}


//
//
//
- (NSString *) contentDisposition
{
  return contentDisposition;
}

- (void) setContentDisposition: (NSString *) theContentDisposition
{
  RETAIN(theContentDisposition);
  RELEASE(contentDisposition);
  contentDisposition = theContentDisposition;
}


//
//
//
- (int) contentTransferEncoding
{
  return contentTransferEncoding;
}

- (void) setContentTransferEncoding: (int) theEncoding
{
  contentTransferEncoding = theEncoding;
}


//
//
//
- (NSString *) filename
{
  return filename;
}

- (void) setFilename: (NSString *) theFilename
{
  if ( theFilename && ([theFilename length] > 0) )
    {
      RETAIN(theFilename);
      RELEASE(filename);
      filename = theFilename;
    }
  else
    {
      RELEASE(filename);
      filename = @"unknown";
      RETAIN(filename);
    }
}


//
//
//
- (int) format
{
  return format;
}

- (void) setFormat: (int) theFormat
{
  format = theFormat;
}


//
//
//
- (int) lineLength
{
  return line_length;
}

- (void) setLineLength: (int) theLineLength
{
  line_length = theLineLength;
}


//
// This method is used to very if the part is of the following primaryType / subType
//
- (BOOL) isMimeType: (NSString *) primaryType : (NSString *) subType 
{
  NSString *aString;

  // If the message has no Content-Type, we treat it as text/plain by adding it.
  if ( ![self contentType] ) 
    {
      [self setContentType: @"text/plain"];
    }

  if ( [subType compare: @"*"] == NSOrderedSame )
    {
      aString = [self contentType];
      
      if ( [[self contentType] hasCaseInsensitivePrefix: primaryType] )
	{
	  return YES;
	}
    }
  else
    {
      aString = [NSString stringWithFormat:@"%@/%@", primaryType, subType];
     
      if ( [aString caseInsensitiveCompare:[self contentType]] == NSOrderedSame)
	{
	  return YES;
	}
    }
  
  return NO;
}


//
//
//
- (long) size
{
  return size;
}

- (void) setSize: (long) theSize
{
  size = theSize;
}


//
//
//
- (NSData *) dataValue
{
  NSMutableData *aMutableData;
  NSData *aDataToSend;
  NSArray *allLines;
  int i;

  aMutableData = [[NSMutableData alloc] init];

  // FIXME: Why is this acting differently depending on the content-type?
  // easier to just encode according to content-transfer-encoding:, split to
  // lines and add to the output
  if ( [self contentTransferEncoding] != NONE )
    {
      [aMutableData appendCFormat: @"Content-Transfer-Encoding: %@%s",
		    [MimeUtility stringValueOfTransferEncoding: [self contentTransferEncoding]],
		    LF];
    }

  if ( [self contentID] )
    {
      [aMutableData appendCFormat: @"Content-ID: %@%s", [self contentID], LF];
    }
  
  if ( [self contentDescription] )
    {
      [aMutableData appendCString: "Content-Description: "];
      [aMutableData appendData: [MimeUtility encodeWordUsingQuotedPrintable: [self contentDescription]
                                    prefixLength: 21]];
      [aMutableData appendCString: LF];
    }
  
  if ( [self isMimeType: @"multipart" : @"*"] )
    {
      if ([[self content] isKindOfClass: [MimeMultipart class]])
	{
	  MimeMultipart *aMimeMultipart;
	  NSData *aBoundary;
	  Part *aPart;
	  
	  NSMutableData *md;
	  
	  md = [[NSMutableData alloc] init];
	  
	  aBoundary = [self boundary];
	  
	  if ( !aBoundary )
	    {
	      aBoundary = [MimeUtility generateBoundary];
	    }

	  [aMutableData appendCFormat: @"Content-Type: %@;%s", [self contentType], LF];
	  
	  if ( [self protocol] )
	    {
	      [aMutableData appendCString: "\tprotocol=\""];
	      [aMutableData appendData: [self protocol]];
	      [aMutableData appendCFormat: @"\";%s", LF];
	    }
	  
	  [aMutableData appendCString: "\tboundary=\""];
	  [aMutableData appendData: aBoundary];
	  [aMutableData appendCFormat: @"\"%s", LF];
	  
	  aMimeMultipart = (MimeMultipart *)[self content];
	  
	  for (i = 0; i < [aMimeMultipart count]; i++)
	    {
	      aPart = [aMimeMultipart bodyPartAtIndex: i];
	      
	      if (i > 0)
		{
		  [md appendBytes: LF  length: strlen(LF)];
		}
	      
	      [md appendBytes: "--"  length: 2];
	      [md appendData: aBoundary];
	      [md appendBytes: LF  length: strlen(LF)];
	      
	      [md appendData: [aPart dataValue]];
	    }
	  
	  [md appendBytes: "--"  length: 2];
	  [md appendData: aBoundary];
	  [md appendBytes: "--"  length: 2];
	  [md appendBytes: LF  length: strlen(LF)];
	  
	  aDataToSend = AUTORELEASE(md);
	}
      else
	{
	  aDataToSend = (NSData *)[self content];
	}
    }
  else if ( [self isMimeType: @"text": @"*"] ||
	    [self isMimeType: @"message" : @"delivery-status"] )
    { 
      if ( [self isMimeType: @"text": @"plain"] && [self format] == FORMAT_FLOWED &&
	   ([self contentTransferEncoding] == NONE || [self contentTransferEncoding] == EIGHTBIT) )
	{
	  [aMutableData appendCFormat: @"Content-Type: %@; charset=\"%@\"; format=\"flowed\"%s",
			[self contentType], [self charset], LF];
	}
      else
	{
	  // FIXME: check if charset= is a valid parameter for message/deliviry-status
	  [aMutableData appendCFormat: @"Content-Type: %@; charset=\"%@\"%s",
			[self contentType], [self charset], LF];
	}
      
      if ( [[self content] isKindOfClass: [NSString class]] )
	{
	  NSString *aString;
	  int encoding;
	  
	  // FIXME - Should we do this if the content is a NSData or something else?
	  if ( ([self contentTransferEncoding] == NONE || [self contentTransferEncoding] == EIGHTBIT) &&
	       [self format] == FORMAT_FLOWED )
	    {
	      int limit;
	      
	      limit = [self lineLength];
	      
	      if (limit < 2 || limit > 998)
		{
		  limit = 72;
		}
	      
	      //
	      // Before 1.1.0, we were calling:
	      //
	      // [MimeUtility unwrapPlainTextString: (NSString *)[self content]
	      //	      usingQuoteWrappingLimit: 998];
	      //
	      // before calling -wrapPlainTextString... This would lead to problems
	      // since we "unwrapped" non-flowed text.
	      //
	      aString = [MimeUtility wrapPlainTextString: (NSString *)[self content]
				     usingWrappingLimit: limit];
	    }
	  else
	    {
	      aString = (NSString *)[self content];
	    }

	  // We get the right string encoding to convert the string object to a data object 
	  encoding = [MimeUtility stringEncodingForCharset: [[self charset] dataUsingEncoding: NSASCIIStringEncoding]];
	  
	  // FIXME - Should we allow lossy conversion?
	  aDataToSend = [aString dataUsingEncoding: encoding
				 allowLossyConversion: YES];
	  
	}
      else if ([[self content] isKindOfClass: [NSData class]])
	{
	  aDataToSend = (NSData *)[self content];
	}
      // If it isn't string or data it had better respond to this
      else 
	{
	  aDataToSend = [(Part *)[self content] dataValue];
	}
      
      //
      // If we had a Content-Disposition specified, let's add it.
      //
      if ( [self contentDisposition] )
	{
	  // If it is an 'attachment', let's add the filename.
	  if ( [[self contentDisposition] caseInsensitiveCompare: @"attachment"] == NSOrderedSame &&
	       [self filename] )
	    {
	      NSString *aString;
	      
	      if ( [MimeUtility isASCIIString: [self filename]] )
		{
		  aString = [self filename];
		}
	      else
		{
		  aString = [[NSString alloc] initWithData: [MimeUtility encodeWordUsingQuotedPrintable: [self filename]
									 prefixLength: 0]
					      encoding: NSASCIIStringEncoding];
		  AUTORELEASE(aString);
		}
	      
	      [aMutableData appendCFormat: @"Content-Disposition: attachment; filename=\"%@\"%s",
			    aString, LF];
	    }
	  else
	    {
	      [aMutableData appendCFormat: @"Content-Disposition: %@%s", [self contentDisposition], LF];
	    }
	}
    }
  //
  // Our Content-Type is message/rfc822 or message/partial
  //
  else if ( [self isMimeType: @"message": @"rfc822"] ||
	    [self isMimeType: @"message": @"partial"] )
    {
      [aMutableData appendCFormat: @"Content-Type: %@%s",  [self contentType], LF];
      
      //
      // If we had a Content-Disposition specified, let's add it.
      //
      if ( [self contentDisposition] )
	{
	  [aMutableData appendCFormat:@"Content-Disposition: %@%s", [self contentDisposition], LF];
	}

      if ( [[self content] isKindOfClass: [NSData class]] )
	{
	  aDataToSend = (NSData *)[self content];
	}
      else
	{
	  aDataToSend = [(Message *)[self content] rawSource];
	}
    }
  //
  // We surely have an application/*, audio/*, image/*,
  // video/* or anything else. We treat everything
  // like application/octet-stream
  //
  else
    {
      NSString *aString;
      
      if ( [MimeUtility isASCIIString: [self filename]] )
	{
	  aString = [self filename];
	}
      else
	{
	  aString = [[NSString alloc] initWithData: [MimeUtility encodeWordUsingQuotedPrintable: [self filename]
								 prefixLength: 0]
				      encoding: NSASCIIStringEncoding];
	  AUTORELEASE(aString);
	}
      
      if ( aString )
	{
	  [aMutableData appendCFormat: @"Content-Type: %@; name=\"%@\"%s", [self contentType], aString,
			LF];
	  
	  [aMutableData appendCFormat: @"Content-Disposition: attachment; filename=\"%@\"%s", 
			aString, LF];
	}
      else
	{
	  [aMutableData appendCFormat: @"Content-Type: %@%s", [self contentType], LF];
	  
	}
      
      // FIXME: We could have a NSString as the content! We currently assume
      // its encoding is NSASCIIStringEncoding
      if ( [[self content] isKindOfClass: [NSString class]] )
	{
	  aDataToSend = [(NSString *)[self content] dataUsingEncoding: NSASCIIStringEncoding
				     allowLossyConversion: YES];
	}
      else
	{
	  aDataToSend = (NSData *)[self content];
	}
    }


  // We separe our part's headers from the content
  [aMutableData appendCFormat: @"%s", LF];

  // We now encode our content the way it was specified
  if ( [self contentTransferEncoding] == QUOTEDPRINTABLE )
    {
      aDataToSend = [MimeUtility encodeQuotedPrintable: aDataToSend
				 lineLength: 72
				 inHeader: NO];
    }
  else if ( [self contentTransferEncoding] == BASE64 )
    {
      aDataToSend = [MimeUtility encodeBase64: aDataToSend
				 lineLength: 72];
    }
  else
    {
      aDataToSend = aDataToSend;
    }

  allLines = [aDataToSend componentsSeparatedByCString: "\n"];
  
  for (i = 0; i < [allLines count]; i++)
    {
      if ( i == [allLines count] - 1
           && [[allLines objectAtIndex: i] length] == 0 )
	{
	  break;
	}
      
      [aMutableData appendData: [allLines objectAtIndex: i]];
      [aMutableData appendBytes: LF  length: 1];
    }
  
  return AUTORELEASE(aMutableData);
}


//
//
//
- (NSData *) boundary
{
  return boundary;
}

- (void) setBoundary: (NSData *) theBoundary
{
  RETAIN(theBoundary);
  RELEASE(boundary);
  boundary = theBoundary;
}


//
//
//
- (NSData *) protocol
{
  return protocol;
}

- (void) setProtocol: (NSData *) theProtocol
{
  RETAIN(theProtocol);
  RELEASE(protocol);
  protocol = theProtocol;
}


//
//
//
- (NSString *) charset
{
  return charset;
}

- (void) setCharset: (NSString *) theCharset
{
  RETAIN(theCharset);
  RELEASE(charset);
  charset = theCharset;
}


//
//
//
- (NSString *) defaultCharset
{
  return defaultCharset;
}


//
//
//
- (void) setDefaultCharset: (NSString *) theCharset
{
  if ( theCharset )
    {
      RETAIN(theCharset);
      RELEASE(defaultCharset);
      defaultCharset = theCharset;
    }
  else
    {
      DESTROY(defaultCharset);
    }
}


//
// This method initalize all the headers of a message
// from a raw data source. It replaces previously defined
// values.
//
- (void) setHeadersFromData: (NSData *) theHeaders
{
  NSAutoreleasePool *pool;
  NSArray *allLines;
  int i;
  
  if ( !theHeaders || [theHeaders length] == 0 )
    {
      return;
    }

  // We initialize a local autorelease pool
  pool = [[NSAutoreleasePool alloc] init];

  // We MUST be sure to unfold all headers properly before
  // decoding the headers
  theHeaders = [MimeUtility unfoldLinesFromData: theHeaders];

  allLines = [theHeaders componentsSeparatedByCString: "\n"];

  for (i = 0; i < [allLines count]; i++)
    {
      NSData *aLine = [allLines objectAtIndex: i];

      // We stop if we found the header separator. (\n\n) since someone could
      // have called this method with the entire rawsource of a message.
      if ( [aLine length] == 0 )
	{
	  break;
	}

      if ( [aLine hasCaseInsensitiveCPrefix: "Content-Description"] )
	{
	  [Parser parseContentDescription: aLine
		  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Disposition"])
	{
	  [Parser parseContentDisposition: aLine 
		  inPart: self];
	}
      else if ( [aLine hasCaseInsensitiveCPrefix: "Content-ID"] )
	{
	  [Parser parseContentID: aLine
		  inPart: self];
	}
      else if ( [aLine hasCaseInsensitiveCPrefix: "Content-Length"] )
	{
	  // We just igore that
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Transfer-Encoding"])
	{
	  [Parser parseContentTransferEncoding: aLine 
		  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Type"])
	{
	  [Parser parseContentType: aLine 
		  inPart: self];
	}
    }

  RELEASE(pool);
}


//
// See RFC1521 (MIME) for more details.
// The Content-Type MUST be defined BEFORE calling this function otherwise
// the content will be considered as a pure string.
//
- (void) setContentFromRawSource: (NSData *) theData
{
  [MimeUtility setContentFromRawSource: theData
	       inPart: self];
}

@end
