/** GSWHTTPIO.m - GSWeb: Class GSWHTTPIO
 
 Copyright (C) 2007 Free Software Foundation, Inc.
 
 Written by:	David Wetzel <dave@turbocat.de>
 Date: 	12.11.2007
 
 This file is part of the GNUstep Web Library.
 
 This library is free software; you can redistribute it and/or
 modify it under the terms of the GNU Library General Public
 License as published by the Free Software Foundation; either
 version 2 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
 Library General Public License for more details.
 
 You should have received a copy of the GNU Library General Public
 License along with this library; if not, write to the Free
 Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 **/

#include "GSWHTTPIO.h"
#include <Foundation/NSString.h>
#include <Foundation/NSFileHandle.h>
#include <Foundation/NSData.h>
#include <GNUstepBase/NSString+GNUstepBase.h>
#include <Foundation/NSError.h>
#include <Foundation/NSException.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/Foundation.h>

#include "GSWDefines.h"
#include "GSWConstants.h"
#include "GSWUtils.h"
#include "GSWDebug.h"

#include "GSWMessage.h"
#include "GSWResponse.h"
#include "GSWRequest.h"
#include "GSWApplication.h"
#include "GSWPrivate.h"
#include <netinet/in.h>
#include <arpa/inet.h>

#define READ_SIZE 2048

static NSString *URIResponseString = @" GNUstep Web\r\n";
static NSString *CONTENT_LENGTHCOLON = @"Content-Length: ";
static NSString *GET = @"GET";
static NSString *POST = @"POST";
static NSString *HEAD = @"HEAD";
static NSString *SPACE = @" ";
static NSString *HEADERSEP = @": ";
static NSString *NEWLINE = @"\r\n";
static NSString *NEWLINE2 = @"\r\n";
static NSString *HTTP11 = @"HTTP/1.1";
static NSString *CONNECTION = @"Connection";
static NSString *KEEP_ALIVE = @"keep-alive";
static BOOL      _alwaysAppendContentLength = YES;

//static NSString *CLOSE = @"close";

#ifdef GNUSTEP
@class GSFileHandle;
#endif

/* Get error information.
 */
@interface	NSError (GSCategories)
+ (NSError*) _last;
@end

@interface NSFileHandle (GSWFileHandleExtensions)
//- (void) setNonBlocking: (BOOL)flag;

- (NSData*) readDataLine;
@end

@implementation NSFileHandle (GSWFileHandleExtensions)

- (NSData*) readDataLine
{
  NSMutableData	*d;
  int		got,pos=0;
  char		buf[READ_SIZE];
  int   fileDescriptor = [self fileDescriptor];
  
  d = [NSMutableData dataWithCapacity: READ_SIZE];
  do {
    got = recv(fileDescriptor, &buf[pos], 1, 0);
    if (got > 0) {
      if (buf[pos] != 0xd) {           // CR
        if (buf[pos] == 0xa) {         // NL
           break;
        }
        pos++;
      }
    } else if (got < 0) {
      [NSException raise: NSFileHandleOperationException
                  format: @"unable to read from descriptor - %@",
       [NSError _last]];
    }
  } while ((got > 0) && (pos < READ_SIZE));
  
  if (pos>0) {
    [d appendBytes: buf length: pos];
  }
  return d;
}


@end


@implementation GSWHTTPIO

/* Apple is accessing this in Application.java in wotaskd example code -- dw */
+ (void) _setAlwaysAppendContentLength:(BOOL) yn
{
  _alwaysAppendContentLength = yn;
}

+ (BOOL) _alwaysAppendContentLength
{
  return _alwaysAppendContentLength;
}

// PRIVATE
void _unpackHeaderLineAddToDict(NSString *line, NSMutableDictionary* headers)
{
  NSArray   * components = [line componentsSeparatedByString:HEADERSEP];
  NSString  * value      = nil;
  NSArray   * newValue   = nil;
  NSString  * key        = nil;
  NSArray   * prevValue  = nil;
  
  if ((components) && ([components count] == 2)) {
    value = [components objectAtIndex:1];
    key = [components objectAtIndex:0];

    key = [[key stringByTrimmingSpaces]lowercaseString];
    
    if ([key isEqualToString:GSWHTTPHeader_AdaptorVersion[GSWNAMES_INDEX]]
        || [key isEqualToString:GSWHTTPHeader_ServerName[GSWNAMES_INDEX]]) {
//      _requestNamingConv=GSWNAMES_INDEX;
      goto keyDone;
    }
    if ([key isEqualToString:GSWHTTPHeader_AdaptorVersion[WONAMES_INDEX]]
        || [key isEqualToString:GSWHTTPHeader_ServerName[WONAMES_INDEX]]) {
//      _requestNamingConv=WONAMES_INDEX;
      goto keyDone;
    }
    
  keyDone:
    
    prevValue=[headers objectForKey:key];
    if (prevValue) {
      newValue=[prevValue arrayByAddingObject:value];
    } else {
      newValue=[NSArray arrayWithObject:value];
    }
    
    [headers setObject: newValue
                forKey: key];
  }
  
}

//PRIVATE

void _appendMessageHeaders(GSWResponse * message,NSMutableString * headers, IMP* headersAppStringIMPPtr)
{
  NSMutableDictionary * headerDict = [message headers];
  NSArray             * keyArray = nil;
  int                   i = 0;
  
  if (headerDict != nil)
    {
      NSUInteger count = 0;
      IMP keyArray_oaiIMP=NULL;
      if (![headerDict isKindOfClass:[NSMutableDictionary class]])
	headerDict = [[headerDict mutableCopy] autorelease];
    
      [headerDict removeObjectForKey:GSWHTTPHeader_ContentLength];
      keyArray = [headerDict allKeys];
      count = [keyArray count];

      for (; i < count; i++)
	{
	  NSString    * currentKey = GSWeb_objectAtIndexWithImpPtr(keyArray,&keyArray_oaiIMP,i);
	  NSArray     * currentValueArray = [headerDict objectForKey:currentKey];
	  if ([currentValueArray isKindOfClass:[NSArray class]])
	    {
	      int x = 0;
	      int valueCount = [currentValueArray count];
	      for (; x < valueCount; x++)
		{
		  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,currentKey);
		  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,HEADERSEP);
		  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,[currentValueArray objectAtIndex:x]);
		  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,NEWLINE);
		}
	    }
	  else
	    {
	      NSString * myStrValue = (NSString*) currentValueArray;
	      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,currentKey);
	      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,HEADERSEP);
	      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,myStrValue);
	      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,NEWLINE);        
	    }
	}      
    }
}

//PRIVATE
void _sendMessage(GSWResponse * message, 
		  NSFileHandle* fh, 
		  NSString * httpVersion, 
		  GSWRequest * request, 
		  NSMutableString * headers,
		  IMP* headersAppStringIMPPtr)
{
  int  contentLength = 0;
  //BOOL keepAlive = NO;
  BOOL requestIsHead = NO;

  if (message) {
    contentLength = [message _contentLength];
  }
  
  if (request) {
    NSString * connectionValue = [request headerForKey:CONNECTION];
    if (connectionValue) {
      //keepAlive = [connectionValue isEqualToString:KEEP_ALIVE];
    }
    requestIsHead = [[request method] isEqualToString:HEAD];
  }
  
  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,GSWIntToNSString([message status]));
  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,URIResponseString);

  _appendMessageHeaders(message,headers,headersAppStringIMPPtr);
  
  if ([httpVersion isEqualToString:HTTP11]) {
    // bug #24006 keep-alive is not implemented.
    // I am uable to reproduce the need for double clicking on links/forms,
    // but for now, we send close. -- dw
    if (YES /*keepAlive == NO*/) {
      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,@"Connection: close\r\n");
    } else {
      GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,@"Connection: keep-alive\r\n");
    }
  }
  
  if ((contentLength > 0) || _alwaysAppendContentLength) {
    GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,CONTENT_LENGTHCOLON);        
    GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,[NSString stringWithFormat:@"%d\r\n", contentLength]);        
  }
  GSWeb_appendStringWithImpPtr(headers,headersAppStringIMPPtr,NEWLINE2);        

  [fh writeData: [headers dataUsingEncoding:NSISOLatin1StringEncoding
                       allowLossyConversion:YES]];
  
  if (((requestIsHead == NO) && (contentLength > 0)) && 
      ([message status] != 304)) {    
    [fh writeData: [message content]];    
  }
}


//PRIVATE
+ (void) _getConnectionInfoFromHandle:(NSFileHandle*) fh 
                        remoteAddress:(NSString**) rAddress
                           remotePort:(uint16_t*) rPort
{
  int                  fileDescriptor = [fh fileDescriptor];
  struct sockaddr_in   sockAddress;
  socklen_t            address_len = sizeof(sockAddress);
  char                 str[INET_ADDRSTRLEN]; 
  
  if (getpeername(fileDescriptor, (struct sockaddr *) &sockAddress,  &address_len) == 0) {
    
    inet_ntop(AF_INET, &sockAddress.sin_addr, str, sizeof(str));
        
    *rAddress = [NSString stringWithUTF8String:str];
    
    *rPort = ntohs(sockAddress.sin_port);
  }
}


+ (NSDictionary*) readHeadersFromHandle:(NSFileHandle*) fh
{
  NSData                *currentLineData = nil;
  unsigned int          length = 0;
  NSMutableDictionary   *headers   = [NSMutableDictionary dictionary];
  NSString * tmpString = nil;
  
  NS_DURING {
    while (YES) {
        currentLineData = [fh readDataLine];
        length = [currentLineData length];
        if (length == 0) {
          break;
        }
        tmpString=[[NSString alloc] initWithData: currentLineData
                                        encoding:NSASCIIStringEncoding];
        
        _unpackHeaderLineAddToDict(tmpString,headers);
  
        [tmpString release]; tmpString = nil;      
    }
  } NS_HANDLER {
    NSLog(@"%s -- %@",__PRETTY_FUNCTION__, localException);
    if (tmpString != nil) {
      [tmpString release]; tmpString = nil;
    }
    headers = nil;
  } NS_ENDHANDLER;
  
  return headers;
}

// GET /infotext.html HTTP/1.1
// Host: www.example.net
+ (NSArray*) readRequestLineFromHandle:(NSFileHandle*) fh
{
  NSString * tmpString = nil;
  NSArray  * components = nil;
  NSData   * currentLineData = nil;
  int        length = 0;

  NS_DURING {
    currentLineData = [fh readDataLine];
    length = [currentLineData length];

    if (length > 0) {
      tmpString=[[NSString alloc] initWithData: currentLineData
                                      encoding:NSASCIIStringEncoding];
      
      components = [tmpString componentsSeparatedByString:@" "];
  
      [tmpString release]; tmpString = nil;
    }
  } NS_HANDLER {
    NSLog(@"%s -- %@",__PRETTY_FUNCTION__, localException);
    if (tmpString != nil) {
      [tmpString release]; tmpString = nil;
    }
    components = nil;
  } NS_ENDHANDLER;
  return components;
}

+ (NSData*) readContentFromFromHandle: fh 
                               method: (NSString *) method
                               length: (int) length
{
  NSData* data = nil;
  
  if ((([method isEqualToString:GET]) || ([method isEqualToString:HEAD])) || 
    ([method isEqualToString:POST] == NO || (length <1))) {
    NSLog(@"%s: unsupportet method '%@'", __PRETTY_FUNCTION__, method);
    return nil;
  }

  data = [fh readDataOfLength: length];
    
  return data;
}



+ (GSWRequest*) readRequestFromFromHandle:(NSFileHandle*) fh
{
  NSDictionary  * headers;
  NSArray       * requestArray;
  int             contentLength = 0;
  NSArray       * tmpValue = nil;
  NSString      * method = nil;
  NSData        * contentData = nil;
  GSWRequest    * request = nil;
  uint16_t        rPort = 0;
  NSString      * rAddress = nil;

    #ifdef GNUSTEP
      [(GSFileHandle *)fh setNonBlocking: NO];
    #endif
  
  // get info about who talks to us
  
  [self _getConnectionInfoFromHandle: fh 
                       remoteAddress: &rAddress
                          remotePort: &rPort];
  
  requestArray = [self readRequestLineFromHandle:fh];
  if ((!requestArray) || ([requestArray count] <3)) { 
    return nil; 
  }
  
  headers = [self readHeadersFromHandle:fh];
  if (!headers) { 
    return nil; 
  }

  method = [requestArray objectAtIndex:0];
  

  if ((tmpValue = [headers objectForKey:GSWHTTPHeader_ContentLength]) && ([tmpValue count])) {
    NSString      * tmpString = [tmpValue objectAtIndex:0];
    
    contentLength = [tmpString intValue];
    contentData = [self readContentFromFromHandle: fh 
                                           method: [requestArray objectAtIndex:0] 
                                           length: contentLength];
  }
      
  request = [[[[GSWApplication application]requestClass]
	       alloc] initWithMethod:method
		      uri:[requestArray objectAtIndex:1]
		      httpVersion:[requestArray objectAtIndex:2]
		      headers:headers
		      content:contentData
		      userInfo:nil];

  if (request != nil)
  {
    [request _setOriginatingAddress:rAddress];
    [request _setOriginatingPort:rPort];
//    [request _setAcceptingAddress:xxx];
//    [request _setAcceptingPort:yyy];
  }
  
  
  return [request autorelease];
}

+ (void) sendResponse:(GSWResponse*) response
             toHandle:(NSFileHandle*) fh
              request:(GSWRequest*) request
{
  NSString        * httpVersion = [response httpVersion];
  NSMutableString * bufferStr = [NSMutableString string];
  IMP               bufferAppStringIMP = NULL;

  GSWeb_appendStringWithImpPtr(bufferStr,&bufferAppStringIMP,httpVersion);
  GSWeb_appendStringWithImpPtr(bufferStr,&bufferAppStringIMP,SPACE);

  _sendMessage(response, fh, httpVersion, request, bufferStr,&bufferAppStringIMP);
  
}
      

@end
