/*
  Copyright (C) 2000-2005 SKYRIX Software AG

  This file is part of SOPE.

  SOPE 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, or (at your option) any
  later version.

  SOPE 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 SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include <unistd.h>

#include "NGSieveClient.h"
#include "NGImap4Support.h"
#include "NGImap4ResponseParser.h"
#include "NSString+Imap4.h"
#include "imCommon.h"
#include <sys/time.h>

@interface NGSieveClient(Private)

- (NGHashMap *)processCommand:(NSString *)_command;
- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt;

- (void)sendCommand:(NSString *)_command;
- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt;

- (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map;
- (NSMutableDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map;
- (NSDictionary *)login;

@end

/*"
**  An implementation of an Imap4 client
**
** A folder name always looks like an absolute filename (/inbox/doof) 
**
"*/

@implementation NGSieveClient

static int      defaultSievePort = 143;
static NSNumber *YesNumber = nil;
static NSNumber *NoNumber  = nil;
static BOOL     ProfileImapEnabled = NO;
static BOOL     LOG_PASSWORD       = NO;
static BOOL     debugImap4         = NO;

+ (void)initialize {
  static BOOL didInit = NO;
  NSUserDefaults *ud;
  if (didInit) return;
  didInit = YES;
  
  ud = [NSUserDefaults standardUserDefaults];
  LOG_PASSWORD       = [ud boolForKey:@"SieveLogPassword"];
  ProfileImapEnabled = [ud boolForKey:@"ProfileImapEnabled"];
  debugImap4         = [ud boolForKey:@"ImapDebugEnabled"];
  
  YesNumber = [[NSNumber numberWithBool:YES] retain];
  NoNumber  = [[NSNumber numberWithBool:NO] retain];
}

+ (id)clientWithAddress:(id<NGSocketAddress>)_address {
  return
    [[(NGSieveClient *)[self alloc] initWithAddress:_address] autorelease];
}

+ (id)clientWithHost:(id)_host {
  return [[[self alloc] initWithHost:_host] autorelease];
}

- (id)initWithHost:(id)_host {
  NGInternetSocketAddress *a;
  a = [NGInternetSocketAddress addressWithPort:defaultSievePort onHost:_host];
  return [self initWithAddress:a];
}

/**"
 ** designated initializer
"**/

- (id)initWithAddress:(id<NGSocketAddress>)_address {
  if ((self = [super init])) {
    self->address = [_address retain];
    self->debug = debugImap4;
  }
  return self;
}

- (void)dealloc {
  [self->text     release];
  [self->address  release];
  [self->socket   release];
  [self->parser   release];
  [self->login    release];
  [self->password release];
  [super dealloc];
}

/* equality */

- (BOOL)isEqual:(id)_obj {
  if (_obj == self)
    return YES;
  if ([_obj isKindOfClass:[NGSieveClient class]])
    return [self isEqualToSieveClient:_obj];
  return NO;
}

- (BOOL)isEqualToSieveClient:(NGSieveClient *)_obj {
  if (_obj == self) return YES;
  if (_obj == nil)  return NO;
  return [[_obj address] isEqual:self->address];
}

/* accessors */

- (id<NGActiveSocket>)socket {
  return self->socket;
}

- (id<NGSocketAddress>)address {
  return self->address;
}

/* connection */

/*"
** Opens a connection to given Host.
"*/

- (NSDictionary *)openConnection {
  struct timeval tv;
  double         ti = 0.0;
  
  if (ProfileImapEnabled) {
    gettimeofday(&tv, NULL);
    ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
  }

  [self->socket release]; self->socket = nil;
  [self->parser release]; self->parser = nil;
  [self->text   release]; self->text   = nil;
  
  self->socket =
    [[NGActiveSocket socketConnectedToAddress:self->address] retain];
  self->text   = 
    [(NGCTextStream *)[NGCTextStream alloc] initWithSource:self->socket];
  self->parser = [[NGImap4ResponseParser alloc] initWithStream:self->socket];

  /* receive greeting from server without tag-id */

  if (ProfileImapEnabled) {
    gettimeofday(&tv, NULL);
    ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
    fprintf(stderr, "[%s] <openConnection> : time needed: %4.4fs\n",
           __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
  }
  return [self normalizeOpenConnectionResponse:
               [self->parser parseSieveResponse]];
}

/*"
** Check whether stream is already open (could be closed because server-timeout)
"*/

- (NSNumber *)isConnected {
  return [NSNumber numberWithBool:
                     (self->socket == nil)
                     ? NO : [(NGActiveSocket *)self->socket isAlive]];
}

/*"
** Close a consisting connection.
"*/

- (void)closeConnection {
  [self->socket close];
  [self->socket release]; self->socket = nil;
  [self->text   release]; self->text   = nil; 
  [self->parser release]; self->parser = nil;
}

/*"
** login with plaintext password authenticating
"*/

- (NSDictionary *)login:(NSString *)_login password:(NSString *)_passwd {
  if ((_login == nil) || (_passwd == nil))
    return nil;

  [self->login    release]; self->login    = nil;
  [self->password release]; self->password = nil;
  
  self->login    = [_login  copy];
  self->password = [_passwd copy];
  return [self login];
}

- (void)reconnect {
  [self closeConnection];  
  [self openConnection];
  [self login];
}

- (NSDictionary *)login {
  NGHashMap *map  = nil;
  NSData    *auth;
  char      *buf;
  int       bufLen, logLen;


  logLen = [self->login cStringLength];
  bufLen = (logLen*2) + [self->password cStringLength] +2;

  buf = calloc(sizeof(char), bufLen);

  sprintf(buf, "%s %s %s", 
          [self->login cString], [self->login cString],
          [self->password cString]);
  
  buf[logLen] = '\0';
  buf[logLen*2+1] = '\0';
  
  auth = [NSData dataWithBytesNoCopy:buf length:bufLen];
  if ([auth respondsToSelector:@selector(dataByEncodingBase64WithLineLength:)])
    auth = [auth dataByEncodingBase64WithLineLength:4096 /* 'unlimited' */];
  else
    auth = [auth dataByEncodingBase64]; /* old API */
  
  if (LOG_PASSWORD == 1) {
    map = [self processCommand:[NSString stringWithFormat:
                                         @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
                                         [auth length], [auth bytes]]];
  }
  else {
    map = [self processCommand:[NSString stringWithFormat:
                                         @"AUTHENTICATE \"PLAIN\" {%d+}\r\n%s",
                                         [auth length], [auth bytes]]
                logText:@"AUTHENTICATE \"PLAIN\" {%d+}\r\nLOGIN:PASSWORD\r\n"];
  }
  return [self normalizeResponse:map];
}

/*
** logout from the connected host and close the connection
*/

- (NSDictionary *)logout {
  NGHashMap *map = [self processCommand:@"logout"];

  [self closeConnection];
  return [self normalizeResponse:map];
}

- (NSDictionary *)getScript:(NSString *)_scriptName {
  [self notImplemented:_cmd];
  return nil;
}

- (BOOL)isValidScriptName:(NSString *)_name {
  return ([_name length] == 0) ? NO : YES;
}

- (NSDictionary *)putScript:(NSString *)_name script:(NSString *)_script {
  NGHashMap *map;
  NSString  *s;
  
  if (![self isValidScriptName:_name]) {
    [self logWithFormat:@"%s: missing script-name", __PRETTY_FUNCTION__];
    return nil;
  }
  if ([_script length] == 0) {
    [self logWithFormat:@"%s: missing script", __PRETTY_FUNCTION__];
    return nil;
  }
  
  s = [NSString stringWithFormat:
                  @"PUTSCRIPT \"%@\" {%d+}\r\n%@\r\n",
                  _name, [_script length], _script];
  map = [self processCommand:s];
  return [self normalizeResponse:map];
}

- (NSDictionary *)setActiveScript:(NSString *)_name {
  NGHashMap *map;
  
  if (![self isValidScriptName:_name]) {
    NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
    return nil;
  }
  map = [self processCommand:
              [NSString stringWithFormat:@"SETACTIVE \"%@\"\r\n", _name]];
  return [self normalizeResponse:map];
}

- (NSDictionary *)deleteScript:(NSString *)_name {
  NGHashMap *map;
  NSString  *s;

  if (![self isValidScriptName:_name]) {
    NSLog(@"%s: missing script-name", __PRETTY_FUNCTION__);
    return nil;
  }

  s = [NSString stringWithFormat:@"DELETESCRIPT \"%@\"\r\n", _name];
  map = [self processCommand:s];
  return [self normalizeResponse:map];
}
- (NSDictionary *)listScript:(NSString *)_script {
  [self notImplemented:_cmd];
  return nil;
}


/*
** Filter for all responses
**       result  : NSNumber (response result)
**       exists  : NSNumber (number of exists mails in selectet folder
**       recent  : NSNumber (number of recent mails in selectet folder
**       expunge : NSArray  (message sequence number of expunged mails
                             in selectet folder)
*/

- (NSMutableDictionary *)normalizeResponse:(NGHashMap *)_map {
  id keys[3], values[3];
  NSParameterAssert(_map != nil);
  
  keys[0] = @"RawResponse"; values[0] = _map;
  keys[1] = @"result";
  values[1] = [[_map objectForKey:@"ok"] boolValue] ? YesNumber : NoNumber;
  return [NSMutableDictionary dictionaryWithObjects:values
                              forKeys:keys count:2];
}

/*
** filter for open connection
*/

- (NSDictionary *)normalizeOpenConnectionResponse:(NGHashMap *)_map {
  NSMutableDictionary *result;
  NSString *tmp;

  result = [self normalizeResponse:_map];
  
  if (![[[_map objectEnumeratorForKey:@"ok"] nextObject] boolValue])
    return result;

  if ((tmp = [_map objectForKey:@"implementation"]))
    [result setObject:tmp forKey:@"server"];
  if ((tmp = [_map objectForKey:@"sieve"]))
    [result setObject:tmp forKey:@"capabilities"];
  return result;
}


 
/*
** filter for list
**       list : NSDictionary (folder name as key and flags as value)
*/

- (NSString *)description {
  return [NSString stringWithFormat:@"<Imap4Client[0x%08X]: socket=%@>",
                     (unsigned)self, [self socket]];
}

/* Private Methods */

- (BOOL)handleProcessException:(NSException *)_exception
  repetitionCount:(int)_cnt
{
  if (_cnt > 3) {
    [_exception raise];
    return NO;
  }
  
  if ([_exception isKindOfClass:[NGIOException class]]) {
    [self logWithFormat:
            @"WARNING: got exception try to restore connection: %@", 
            _exception];
    return YES;
  }
  if ([_exception isKindOfClass:[NGImap4ParserException class]]) {
    [self logWithFormat:
            @"WARNING: Got Parser-Exception try to restore connection: %@",
            _exception];
    return YES;
  }
  
  [_exception raise];
  return NO;
}

- (void)waitPriorReconnectWithRepetitionCount:(int)_cnt {
  unsigned timeout;
  
  timeout = _cnt * 4;
  [self logWithFormat:@"reconnect to %@, sleeping %d seconds ...",
          self->address, timeout];
  sleep(timeout);
  [self logWithFormat:@"reconnect ..."];
}

- (NGHashMap *)processCommand:(NSString *)_command logText:(NSString *)_txt {
  NGHashMap *map          = nil;
  BOOL      repeatCommand = NO;
  int       repeatCnt     = 0;

  struct timeval tv;
  double         ti = 0.0;

  if (ProfileImapEnabled) {
    gettimeofday(&tv, NULL);
    ti =  (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0);
    fprintf(stderr, "{");
  }
  do { /* TODO: shouldn't that be a while loop? */
    if (repeatCommand) {
      if (repeatCnt > 1)
        [self waitPriorReconnectWithRepetitionCount:repeatCnt];
      
      repeatCnt++;
      [self reconnect];
      repeatCommand = NO;
    }
    
    NS_DURING {
      [self sendCommand:_command logText:_txt];
      map = [self->parser parseSieveResponse];
    }
    NS_HANDLER {
      repeatCommand = [self handleProcessException:localException
                            repetitionCount:repeatCnt];
    }
    NS_ENDHANDLER;    
  }
  while (repeatCommand);
  
  if (ProfileImapEnabled) {
    gettimeofday(&tv, NULL);
    ti = (double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0) - ti;
    fprintf(stderr, "}[%s] <Send Command> : time needed: %4.4fs\n",
           __PRETTY_FUNCTION__, ti < 0.0 ? -1.0 : ti);    
  }
  
  return map;
}

- (NGHashMap *)processCommand:(NSString *)_command {
  return [self processCommand:_command logText:_command];
}

- (void)sendCommand:(NSString *)_command logText:(NSString *)_txt {
  NSString *command       = nil;

  command = _command;

  if (self->debug)
    fprintf(stderr, "C: %s\n", [_txt cString]);
  
  [self->text writeString:command];
  [self->text writeString:@"\r\n"];
  [self->text flush];
}

- (void)sendCommand:(NSString *)_command {
  [self sendCommand:_command logText:_command];
}

@end /* NGSieveClient */
