/*
**  CWPart.m
**
**  Copyright (c) 2001-2004
**
**  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/CWPart.h>

#include <Pantomime/CWConstants.h>
#include <Pantomime/CWMessage.h>
#include <Pantomime/CWMIMEMultipart.h>
#include <Pantomime/CWMIMEUtility.h>
#include <Pantomime/NSData+Extensions.h>
#include <Pantomime/NSString+Extensions.h>
#include <Pantomime/CWParser.h>

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

#include <string.h>

#define LF "\n"

static int currentPartVersion = 2;

//
//
//
@implementation CWPart

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

  // We set the default values. See RFC2045 - 5.2. Content-Type Defaults.
  [self setContentType: @"text/plain"];
  [self setCharset: @"us-ascii"];
  
  // Default value for the Content-Transfer-Encoding. See RFC2045 - 6.1. Content-Transfer-Encoding Syntax.
  _content_transfer_encoding = PantomimeEncodingNone;
  _format = PantomimeFormatUnknown;
  _line_length = _size = 0;
  _content = nil;
  
  return self;
}


//
//
//
- (void) dealloc
{
  RELEASE(_content);
  RELEASE(_contentType);
  RELEASE(_contentID);
  RELEASE(_contentDescription);
  RELEASE(_contentDisposition);
  RELEASE(_filename);
  RELEASE(_boundary);
  RELEASE(_protocol);
  RELEASE(_charset);
  RELEASE(_defaultCharset);
  [super dealloc];
}


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

  [CWPart setVersion: currentPartVersion];
 
  aRange = [theData rangeOfCString: "\n\n"];
  
  if (aRange.length == 0)
    {
      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)]];
  [CWMIMEUtility setContentFromRawSource:
		 [theData subdataWithRange:
			    NSMakeRange(aRange.location + 2, [theData length]-(aRange.location+2))]
		 inPart: self];

  return self;
}


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


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

  [theCoder encodeObject: _contentType];
  [theCoder encodeObject: _contentID];
  [theCoder encodeObject: _contentDescription];
  [theCoder encodeObject: _contentDisposition];
  [theCoder encodeObject: _filename];
 
  [theCoder encodeObject: [NSNumber numberWithInt: _content_transfer_encoding]];
  [theCoder encodeObject: [NSNumber numberWithInt: _format]];
  [theCoder encodeObject: [NSNumber numberWithInt: _size]];

  [theCoder encodeObject: _boundary];
  [theCoder encodeObject: _charset];
  [theCoder encodeObject: _defaultCharset];
}


- (id) initWithCoder: (NSCoder *) theCoder
{
  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]];
  
  _content = nil;

  return self;
}


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


//
//
//
- (void) setContent: (NSObject *) theContent
{
  ASSIGN(_content, theContent);
}


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

- (void) setContentType: (NSString *) theContentType
{  
  ASSIGN(_contentType, theContentType);
}


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

- (void) setContentID: (NSString *) theContentID
{
  ASSIGN(_contentID, theContentID);
}

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

- (void) setContentDescription: (NSString *) theContentDescription
{
  ASSIGN(_contentDescription, theContentDescription);
}


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

- (void) setContentDisposition: (NSString *) theContentDisposition
{
  ASSIGN(_contentDisposition, theContentDisposition);
}


//
//
//
- (PantomimeEncoding) contentTransferEncoding
{
  return _content_transfer_encoding;
}

- (void) setContentTransferEncoding: (PantomimeEncoding) theEncoding
{
  _content_transfer_encoding = theEncoding;
}


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

- (void) setFilename: (NSString *) theFilename
{
  if (theFilename && ([theFilename length] > 0))
    {
      ASSIGN(_filename, theFilename);
    }
  else
    {
      ASSIGN(_filename, @"unknown");
    }
}


//
//
//
- (PantomimeMessageFormat) format
{
  return _format;
}

- (void) setFormat: (PantomimeMessageFormat) 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 *) thePrimaryType
	    subType: (NSString *) theSubType 
{
  NSString *aString;

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

  if ([theSubType compare: @"*"] == NSOrderedSame)
    {
      aString = _contentType;
      
      if ([_contentType hasCaseInsensitivePrefix: thePrimaryType])
	{
	  return YES;
	}
    }
  else
    {
      aString = [NSString stringWithFormat: @"%@/%@", thePrimaryType, theSubType];
     
      if ([aString caseInsensitiveCompare: _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, count;

  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 (_content_transfer_encoding != PantomimeEncodingNone)
    {
      [aMutableData appendCFormat: @"Content-Transfer-Encoding: %@%s",
		    [NSString stringValueOfTransferEncoding: _content_transfer_encoding],
		    LF];
    }

  if (_contentID)
    {
      [aMutableData appendCFormat: @"Content-ID: %@%s", _contentID, LF];
    }
  
  if (_contentDescription)
    {
      [aMutableData appendCString: "Content-Description: "];
      [aMutableData appendData: [CWMIMEUtility encodeWordUsingQuotedPrintable: _contentDescription
					       prefixLength: 21]];
      [aMutableData appendCString: LF];
    }
  
  if ([self isMIMEType: @"multipart"  subType: @"*"])
    {
      if ([_content isKindOfClass: [CWMIMEMultipart class]])
	{
	  CWMIMEMultipart *aMimeMultipart;
	  NSData *aBoundary;
	  CWPart *aPart;
	  
	  NSMutableData *md;
	  
	  md = [[NSMutableData alloc] init];
	  
	  aBoundary = _boundary;
	  
	  if (!aBoundary)
	    {
	      aBoundary = [CWMIMEUtility globallyUniqueBoundary];
	    }

	  [aMutableData appendCFormat: @"Content-Type: %@;%s", _contentType, LF];
	  
	  if (_protocol)
	    {
	      [aMutableData appendCString: "\tprotocol=\""];
	      [aMutableData appendData: _protocol];
	      [aMutableData appendCFormat: @"\";%s", LF];
	    }
	  
	  [aMutableData appendCString: "\tboundary=\""];
	  [aMutableData appendData: aBoundary];
	  [aMutableData appendCFormat: @"\"%s", LF];
	  
	  aMimeMultipart = (CWMIMEMultipart *)_content;
	  count = [aMimeMultipart count];

	  for (i = 0; i < count; i++)
	    {
	      aPart = [aMimeMultipart partAtIndex: 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 *)_content;
	}
    }
  else if ([self isMIMEType: @"text"  subType: @"*"] || [self isMIMEType: @"message"  subType: @"delivery-status"])
    { 
      if ([self isMIMEType: @"text"  subType: @"plain"] && _format == PantomimeFormatFlowed &&
	  (_content_transfer_encoding == PantomimeEncodingNone || _content_transfer_encoding == PantomimeEncoding8bit))
	{
	  [aMutableData appendCFormat: @"Content-Type: %@; charset=\"%@\"; format=\"flowed\"%s",
			_contentType, _charset, LF];
	}
      else
	{
	  // FIXME: check if charset= is a valid parameter for message/deliviry-status
	  [aMutableData appendCFormat: @"Content-Type: %@; charset=\"%@\"%s",
			_contentType, _charset, LF];
	}
      
      if ([_content isKindOfClass: [NSString class]])
	{
	  NSString *aString;
	  int encoding;
	  
	  // FIXME - Should we do this if the content is a NSData or something else?
	  if ((_content_transfer_encoding == PantomimeEncodingNone || _content_transfer_encoding == PantomimeEncoding8bit) &&
	      _format == PantomimeFormatFlowed)
	    {
	      int limit;
	      
	      limit = _line_length;
	      
	      if (limit < 2 || limit > 998)
		{
		  limit = 72;
		}
	      
	      aString = [(NSString *)_content wrapWithWrappingLimit: limit];
	    }
	  else
	    {
	      aString = (NSString *)_content;
	    }

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

      if ([_content isKindOfClass: [NSData class]])
	{
	  aDataToSend = (NSData *)_content;
	}
      else
	{
	  aDataToSend = [(CWMessage *)_content rawSource];
	}
    }
  //
  // We surely have an application/*, audio/*, image/*,
  // video/* or anything else. We treat everything
  // like application/octet-stream
  //
  else
    {
      NSString *aString;
      
      if ([_filename is7bitSafe])
	{
	  aString = _filename;
	}
      else
	{
	  aString = [[NSString alloc] initWithData: [CWMIMEUtility encodeWordUsingQuotedPrintable: _filename
								   prefixLength: 0]
				      encoding: NSASCIIStringEncoding];
	  AUTORELEASE(aString);
	}
      
      if (aString && [aString length])
	{
	  [aMutableData appendCFormat: @"Content-Type: %@; name=\"%@\"%s", _contentType, aString, LF];
	  [aMutableData appendCFormat: @"Content-Disposition: attachment; filename=\"%@\"%s", aString, LF];
	}
      else
	{
	  [aMutableData appendCFormat: @"Content-Type: %@%s", _contentType, LF];
	}
      
      // FIXME: We could have a NSString as the content! We currently assume
      // its encoding is NSASCIIStringEncoding
      if ([_content isKindOfClass: [NSString class]])
	{
	  aDataToSend = [(NSString *)_content dataUsingEncoding: NSASCIIStringEncoding
				     allowLossyConversion: YES];
	}
      else
	{
	  aDataToSend = (NSData *)_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 (_content_transfer_encoding == PantomimeEncodingQuotedPrintable)
    {
      aDataToSend = [aDataToSend encodeQuotedPrintableWithLineLength: 72  inHeader: NO];
    }
  else if (_content_transfer_encoding == PantomimeEncodingBase64)
    {
      aDataToSend = [aDataToSend encodeBase64WithLineLength: 72];
    }
  else
    {
      aDataToSend = aDataToSend;
    }

  allLines = [aDataToSend componentsSeparatedByCString: "\n"];
  count = [allLines count];

  for (i = 0; i < count; i++)
    {
      if (i == 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
{
  ASSIGN(_boundary, theBoundary);
}


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

- (void) setProtocol: (NSData *) theProtocol
{
  ASSIGN(_protocol, theProtocol);
}


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

- (void) setCharset: (NSString *) theCharset
{
  ASSIGN(_charset, theCharset);
}


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


//
//
//
- (void) setDefaultCharset: (NSString *) theCharset
{
  ASSIGN(_defaultCharset, theCharset);
}


//
//
//
- (void) setHeadersFromData: (NSData *) theHeaders
{
  NSAutoreleasePool *pool;
  NSArray *allLines;
  int i, count;
  
  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 = [theHeaders unfoldLines];

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

  for (i = 0; i < 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"])
	{
	  [CWParser parseContentDescription: aLine  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Disposition"])
	{
	  [CWParser parseContentDisposition: aLine  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-ID"])
	{
	  [CWParser parseContentID: aLine  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Length"])
	{
	  // We just igore that
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Transfer-Encoding"])
	{
	  [CWParser parseContentTransferEncoding: aLine  inPart: self];
	}
      else if ([aLine hasCaseInsensitiveCPrefix: "Content-Type"])
	{
	  [CWParser parseContentType: aLine  inPart: self];
	}
    }

  RELEASE(pool);
}

@end
