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

  This file is part of OpenGroupware.org.

  OGo 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.

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

#include "GCSFolder.h"
#include "GCSFolderManager.h"
#include "GCSFolderType.h"
#include "GCSChannelManager.h"
#include "GCSFieldExtractor.h"
#include "NSURL+GCS.h"
#include "EOAdaptorChannel+GCS.h"
#include "EOQualifier+GCS.h"
#include "GCSStringFormatter.h"
#include "common.h"

@implementation GCSFolder

static BOOL debugOn    = NO;
static BOOL doLogStore = NO;

static Class NSStringClass       = Nil;
static Class NSNumberClass       = Nil;
static Class NSCalendarDateClass = Nil;

static GCSStringFormatter *stringFormatter = nil;

+ (void)initialize {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  
  debugOn         = [ud boolForKey:@"GCSFolderDebugEnabled"];
  doLogStore      = [ud boolForKey:@"GCSFolderStoreDebugEnabled"];

  NSStringClass       = [NSString class];
  NSNumberClass       = [NSNumber class];
  NSCalendarDateClass = [NSCalendarDate class];

  stringFormatter = [GCSStringFormatter sharedFormatter];
}

- (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId
  folderTypeName:(NSString *)_ftname folderType:(GCSFolderType *)_ftype
  location:(NSURL *)_loc quickLocation:(NSURL *)_qloc
  folderManager:(GCSFolderManager *)_fm
{
  if (![_loc isNotNull]) {
    [self logWithFormat:@"ERROR(%s): missing quicktable parameter!", 
	  __PRETTY_FUNCTION__];
    [self release];
    return nil;
  }
  
  if ((self = [super init])) {
    self->folderManager  = [_fm    retain];
    self->folderInfo     = [_ftype retain];
    
    self->folderId       = [_folderId copy];
    self->folderName     = [[_path lastPathComponent] copy];
    self->path           = [_path   copy];
    self->location       = [_loc    retain];
    self->quickLocation  = _qloc ? [_qloc   retain] : [_loc retain];
    self->folderTypeName = [_ftname copy];
    
    self->ofFlags.requiresFolderSelect = 0;
    self->ofFlags.sameTableForQuick = 
      [self->location isEqualTo:self->quickLocation] ? 1 : 0;
  }
  return self;
}
- (id)init {
  return [self initWithPath:nil primaryKey:nil
	       folderTypeName:nil folderType:nil 
	       location:nil quickLocation:nil
	       folderManager:nil];
}

- (void)dealloc {
  [self->folderManager  release];
  [self->folderInfo     release];
  [self->folderId       release];
  [self->folderName     release];
  [self->path           release];
  [self->location       release];
  [self->quickLocation  release];
  [self->folderTypeName release];
  [super dealloc];
}

/* accessors */

- (NSNumber *)folderId {
  return self->folderId;
}

- (NSString *)folderName {
  return self->folderName;
}
- (NSString *)path {
  return self->path;
}

- (NSURL *)location {
  return self->location;
}
- (NSURL *)quickLocation {
  return self->quickLocation;
}

- (NSString *)folderTypeName {
  return self->folderTypeName;
}

- (GCSFolderManager *)folderManager {
  return self->folderManager;
}
- (GCSChannelManager *)channelManager {
  return [[self folderManager] channelManager];
}

- (NSString *)storeTableName {
  return [[self location] gcsTableName];
}
- (NSString *)quickTableName {
  return [[self quickLocation] gcsTableName];
}

- (BOOL)isQuickInfoStoredInContentTable {
  return self->ofFlags.sameTableForQuick ? YES : NO;
}

/* channels */

- (EOAdaptorChannel *)acquireStoreChannel {
  return [[self channelManager] acquireOpenChannelForURL:[self location]];
}
- (EOAdaptorChannel *)acquireQuickChannel {
  return [[self channelManager] acquireOpenChannelForURL:[self quickLocation]];
}

- (void)releaseChannel:(EOAdaptorChannel *)_channel {
  [[self channelManager] releaseChannel:_channel];
  if (debugOn) [self debugWithFormat:@"released channel: %@", _channel];
}

- (BOOL)canConnectStore {
  return [[self channelManager] canConnect:[self location]];
}
- (BOOL)canConnectQuick {
  return [[self channelManager] canConnect:[self quickLocation]];
}

/* operations */

- (NSArray *)subFolderNames {
  return [[self folderManager] listSubFoldersAtPath:[self path]
			       recursive:NO];
}
- (NSArray *)allSubFolderNames {
  return [[self folderManager] listSubFoldersAtPath:[self path]
			       recursive:YES];
}

- (id)_fetchValueOfColumn:(NSString *)_col attributeName:(NSString *)_attrName
  inContentWithName:(NSString *)_name 
{
  EOAdaptorChannel *channel;
  NSException  *error;
  NSDictionary *row;
  NSArray      *attrs;
  NSString     *result;
  NSString     *sql;
  
  if ((channel = [self acquireStoreChannel]) == nil) {
    [self logWithFormat:@"ERROR(%s): could not open storage channel!",
            __PRETTY_FUNCTION__];
    return nil;
  }
  
  /* generate SQL */
  
  sql = @"SELECT ";
  sql = [sql stringByAppendingString:_col];
  sql = [sql stringByAppendingString:@" FROM "];
  sql = [sql stringByAppendingString:[self storeTableName]];
  sql = [sql stringByAppendingString:@" WHERE \"c_name\" = '"];
  sql = [sql stringByAppendingString:_name];
  sql = [sql stringByAppendingString:@"'"];
  
  /* run SQL */
  
  if ((error = [channel evaluateExpressionX:sql]) != nil) {
    [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", 
	    __PRETTY_FUNCTION__, sql, error];
    [self releaseChannel:channel];
    return nil;
  }
  
  /* fetch results */
  
  result = nil;
  attrs  = [channel describeResults];
  if ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
    result = [[[row objectForKey:_attrName] copy] autorelease];
    if (![result isNotNull]) result = nil;
    [channel cancelFetch];
  }
  
  /* release and return result */
  
  [self releaseChannel:channel];
  return result;
}

- (NSNumber *)versionOfContentWithName:(NSString *)_name {
  return [self _fetchValueOfColumn:@"c_version" attributeName:@"cVersion"
	       inContentWithName:_name];
}

- (NSString *)fetchContentWithName:(NSString *)_name {
  return [self _fetchValueOfColumn:@"c_content" attributeName:@"cContent"
	       inContentWithName:_name];
}

- (NSDictionary *)fetchContentsOfAllFiles {
  /*
    Note: try to avoid the use of this method! The key of the dictionary
          will be filename, the value the content.
  */
  NSMutableDictionary *result;
  EOAdaptorChannel *channel;
  NSException  *error;
  NSDictionary *row;
  NSArray      *attrs;
  NSString     *sql;
  
  if ((channel = [self acquireStoreChannel]) == nil) {
    [self logWithFormat:@"ERROR(%s): could not open storage channel!",
            __PRETTY_FUNCTION__];
    return nil;
  }
  
  /* generate SQL */
  
  sql = @"SELECT \"c_name\", \"c_content\" FROM ";
  sql = [sql stringByAppendingString:[self storeTableName]];
  
  /* run SQL */
  
  if ((error = [channel evaluateExpressionX:sql]) != nil) {
    [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", 
	    __PRETTY_FUNCTION__, sql, error];
    [self releaseChannel:channel];
    return nil;
  }
  
  /* fetch results */
  
  result = [NSMutableDictionary dictionaryWithCapacity:128];
  attrs  = [channel describeResults];
  while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
    NSString *cName, *cContent;

    cName    = [row objectForKey:@"cName"];
    cContent = [row objectForKey:@"cContent"];
    
    if (![cName isNotNull]) {
      [self logWithFormat:@"ERROR: missing cName in row: %@", row];
      continue;
    }
    if (![cContent isNotNull]) {
      [self logWithFormat:@"ERROR: missing cContent in row: %@", row];
      continue;
    }
    
    [result setObject:cContent forKey:cName];
  }
  
  /* release and return result */
  
  [self releaseChannel:channel];
  return result;
}

/* writing content */

- (NSString *)_formatRowValue:(id)_value {
  if (![_value isNotNull])
    return @"NULL";

  if ([_value isKindOfClass:NSStringClass])
    return [stringFormatter stringByFormattingString:_value];

  if ([_value isKindOfClass:NSNumberClass])
    return [_value stringValue];

  if ([_value isKindOfClass:NSCalendarDateClass]) {
    /* be smart ... convert to timestamp */
    return [NSString stringWithFormat:@"%i", [_value timeIntervalSince1970]];
  }
  
  [self logWithFormat:@"cannot handle value class: %@", [_value class]];
  return nil;
}

- (NSString *)_generateInsertStatementForRow:(NSDictionary *)_row
  tableName:(NSString *)_table
{
  // TODO: move to NSDictionary category?
  NSMutableString *sql;
  NSArray  *keys;
  unsigned i, count;
  
  if (_row == nil || _table == nil)
    return nil;
  
  keys = [_row allKeys];
  
  sql = [NSMutableString stringWithCapacity:512];
  [sql appendString:@"INSERT INTO "];
  [sql appendString:_table];
  [sql appendString:@" ("];
  
  for (i = 0, count = [keys count]; i < count; i++) {
    if (i != 0) [sql appendString:@", "];
    [sql appendString:[keys objectAtIndex:i]];
  }
  
  [sql appendString:@") VALUES ("];
  
  for (i = 0, count = [keys count]; i < count; i++) {
    id value;
    
    if (i != 0) [sql appendString:@", "];
    value = [_row objectForKey:[keys objectAtIndex:i]];
    value = [self _formatRowValue:value];
    [sql appendString:value];
  }
  
  [sql appendString:@")"];
  return sql;
}

- (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row
  tableName:(NSString *)_table
  whereColumn:(NSString *)_colname isEqualTo:(id)_value
{
  // TODO: move to NSDictionary category?
  NSMutableString *sql;
  NSArray  *keys;
  unsigned i, count;
  
  if (_row == nil || _table == nil)
    return nil;
  
  keys = [_row allKeys];
  
  sql = [NSMutableString stringWithCapacity:512];
  [sql appendString:@"UPDATE "];
  [sql appendString:_table];

  [sql appendString:@" SET "];
  for (i = 0, count = [keys count]; i < count; i++) {
    id value;

    value = [_row objectForKey:[keys objectAtIndex:i]];
    value = [self _formatRowValue:value];
    
    if (i != 0) [sql appendString:@", "];
    [sql appendString:[keys objectAtIndex:i]];
    [sql appendString:@" = "];
    [sql appendString:value];
  }
  
  [sql appendString:@" WHERE "];
  [sql appendString:_colname];
  [sql appendString:@" = "];
  [sql appendString:[self _formatRowValue:_value]];
  
  return sql;
}

- (NSException *)writeContent:(NSString *)_content toName:(NSString *)_name {
  EOAdaptorChannel    *storeChannel, *quickChannel;
  NSMutableDictionary *quickRow, *contentRow;
  GCSFieldExtractor   *extractor;
  NSException         *error;
  NSNumber            *storedVersion;
  BOOL                isNewRecord;
  NSCalendarDate      *nowDate;
  NSNumber            *now;
  NSString            *qsql, *bsql;

  /* check preconditions */
  
  if (_name == nil) {
    return [NSException exceptionWithName:@"GCSStoreException"
			reason:@"no content filename was provided"
			userInfo:nil];
  }
  if (_content == nil) {
    return [NSException exceptionWithName:@"GCSStoreException"
			reason:@"no content was provided"
			userInfo:nil];
  }
  
  /* run */

  error   = nil;
  nowDate = [NSCalendarDate date];
  now     = [NSNumber numberWithUnsignedInt:[nowDate timeIntervalSince1970]];
  
  if (doLogStore)
    [self logWithFormat:@"should store content: '%@'\n%@", _name, _content];
  
  storedVersion = [self versionOfContentWithName:_name];
  if (doLogStore)
    [self logWithFormat:@"  version: %@", storedVersion];
  isNewRecord = [storedVersion isNotNull] ? NO : YES;
  
  /* extract quick info */
  
  extractor = [self->folderInfo quickExtractor];
  quickRow  = [extractor extractQuickFieldsFromContent:_content];
  [quickRow setObject:_name forKey:@"c_name"];
  
  if (doLogStore)
    [self logWithFormat:@"  store quick: %@", quickRow];
  
  /* make content row */
  
  contentRow = [NSMutableDictionary dictionaryWithCapacity:16];
  
  if (self->ofFlags.sameTableForQuick)
    [contentRow addEntriesFromDictionary:quickRow];
  
  [contentRow setObject:_name forKey:@"c_name"];
  if (isNewRecord) [contentRow setObject:now forKey:@"c_creationdate"];
  [contentRow setObject:now forKey:@"c_lastmodified"];
  if (isNewRecord)
    [contentRow setObject:[NSNumber numberWithInt:0] forKey:@"c_version"];
  else {
    // TODO: increase version?
    [contentRow setObject:[NSNumber numberWithInt:[storedVersion intValue]]
		forKey:@"c_version"];
  }
  [contentRow setObject:_content forKey:@"c_content"];
  
  /* open channels */
  
  if ((storeChannel = [self acquireStoreChannel]) == nil) {
    [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
    return nil;
  }
  if (!self->ofFlags.sameTableForQuick) {
    if ((quickChannel = [self acquireQuickChannel]) == nil) {
      [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
      [self releaseChannel:storeChannel];
      return nil;
    }
  }

  /* generate SQL */
  
  qsql = nil;
  if (isNewRecord) { /* insert */
    if (!self->ofFlags.sameTableForQuick) {
      qsql = [self _generateInsertStatementForRow:quickRow 
		   tableName:[self quickTableName]];
    }
    bsql = [self _generateInsertStatementForRow:contentRow
		 tableName:[self storeTableName]];
  }
  else {
    if (!self->ofFlags.sameTableForQuick) {
      qsql = [self _generateUpdateStatementForRow:quickRow
		   tableName:[self quickTableName]
		   whereColumn:@"c_name" isEqualTo:_name];
    }
    bsql = [self _generateUpdateStatementForRow:contentRow
		 tableName:[self storeTableName]
		 whereColumn:@"c_name" isEqualTo:_name];
  }
  
  /* execute */
  // TODO: execute in transactions

  if ((error = [storeChannel evaluateExpressionX:bsql]) != nil) {
    [self logWithFormat:@"ERROR(%s): cannot %s content '%@': %@", 
	  __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update", bsql, error];
  }
  
  if (error == nil && qsql != nil) {
    if ((error = [quickChannel evaluateExpressionX:qsql]) != nil) {
      NSString    *delsql;
      NSException *delErr;
	
      [self logWithFormat:@"ERROR(%s): cannot %s quick '%@': %@", 
	      __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update", 
	      qsql, error];
      
      if (isNewRecord) {
	/* insert in quick failed, so delete in content table */
	
	delsql = [@"DELETE FROM " stringByAppendingString:
		     [self storeTableName]];
	delsql = [delsql stringByAppendingString:@" WHERE c_name="];
	delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
	if ((delErr = [storeChannel evaluateExpressionX:delsql]) != nil) {
	    [self logWithFormat:
		  @"ERROR(%s): could not delete content '%@' after quick-fail:"
		  @" %@", __PRETTY_FUNCTION__, delsql, error];
	}
      }
    }
  }
  
  [self releaseChannel:storeChannel];
  if (!self->ofFlags.sameTableForQuick) [self releaseChannel:quickChannel];
  return error;
}

- (NSException *)deleteContentWithName:(NSString *)_name {
  EOAdaptorChannel *storeChannel, *quickChannel;
  NSException *error;
  NSString *delsql;
  
  /* check preconditions */
  
  if (_name == nil) {
    return [NSException exceptionWithName:@"GCSDeleteException"
			reason:@"no content filename was provided"
			userInfo:nil];
  }
  
  if (doLogStore)
    [self logWithFormat:@"should delete content: '%@'", _name];
  
  /* open channels */
  
  if ((storeChannel = [self acquireStoreChannel]) == nil) {
    [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
    return nil;
  }
  if (!self->ofFlags.sameTableForQuick) {
    if ((quickChannel = [self acquireQuickChannel]) == nil) {
      [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
      [self releaseChannel:storeChannel];
      return nil;
    }
  }
  
  /* delete rows */

  delsql = [@"DELETE FROM " stringByAppendingString:[self storeTableName]];
  delsql = [delsql stringByAppendingString:@" WHERE c_name="];
  delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
  if ((error = [storeChannel evaluateExpressionX:delsql]) != nil) {
    [self logWithFormat:
	    @"ERROR(%s): cannot delete content '%@': %@", 
	  __PRETTY_FUNCTION__, delsql, error];
  }
  else if (!self->ofFlags.sameTableForQuick) {
    /* content row deleted, now delete the quick row */
    delsql = [@"DELETE FROM " stringByAppendingString:[self quickTableName]];
    delsql = [delsql stringByAppendingString:@" WHERE c_name="];
    delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
    if ((error = [quickChannel evaluateExpressionX:delsql]) != nil) {
      [self logWithFormat:
	      @"ERROR(%s): cannot delete quick row '%@': %@", 
	    __PRETTY_FUNCTION__, delsql, error];
      /* 
	 Note: we now have a "broken" record, needs to be periodically GCed by
	       a script!
      */
    }
  }
  
  /* release channels and return */
  
  [self releaseChannel:storeChannel];
  if (!self->ofFlags.sameTableForQuick)
    [self releaseChannel:quickChannel];
  return error;
}

- (NSString *)columnNameForFieldName:(NSString *)_fieldName {
  return _fieldName;
}

/* SQL generation */

- (NSString *)generateSQLForSortOrderings:(NSArray *)_so {
  NSMutableString *sql;
  unsigned i, count;

  if ((count = [_so count]) == 0)
    return nil;
  
  sql = [NSMutableString stringWithCapacity:(count * 16)];
  for (i = 0; i < count; i++) {
    EOSortOrdering *so;
    NSString *column;
    SEL      sel;
    
    so     = [_so objectAtIndex:i];
    sel    = [so selector];
    column = [self columnNameForFieldName:[so key]];
    
    if (i > 0) [sql appendString:@", "];
    
    if (sel_eq(sel, EOCompareAscending)) {
      [sql appendString:column];
      [sql appendString:@" ASC"];
    }
    else if (sel_eq(sel, EOCompareDescending)) {
      [sql appendString:column];
      [sql appendString:@" DESC"];
    }
    else if (sel_eq(sel, EOCompareCaseInsensitiveAscending)) {
      [sql appendString:@"UPPER("];
      [sql appendString:column];
      [sql appendString:@") ASC"];
    }
    else if (sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
      [sql appendString:@"UPPER("];
      [sql appendString:column];
      [sql appendString:@") DESC"];
    }
    else {
      [self logWithFormat:@"cannot handle sort selector in store: %@",
	      NSStringFromSelector(sel)];
    }
  }
  return sql;
}

- (NSString *)generateSQLForQualifier:(EOQualifier *)_q {
  NSMutableString *ms;
  
  if (_q == nil) return nil;
  ms = [NSMutableString stringWithCapacity:32];
  [_q _gcsAppendToString:ms];
  return ms;
}

/* fetching */

- (NSArray *)fetchFields:(NSArray *)_flds 
  fetchSpecification:(EOFetchSpecification *)_fs
{
  EOQualifier      *qualifier;
  NSArray          *sortOrderings;
  EOAdaptorChannel *channel;
  NSException      *error;
  NSMutableString  *sql;
  NSArray          *attrs;
  NSMutableArray   *results;
  NSDictionary     *row;
  
  qualifier     = [_fs qualifier];
  sortOrderings = [_fs sortOrderings];
  
#if 0
  [self logWithFormat:@"FETCH: %@", _flds];
  [self logWithFormat:@"  MATCH: %@", _q];
#endif
  
  /* generate SQL */

  sql = [NSMutableString stringWithCapacity:256];
  [sql appendString:@"SELECT "];
  if (_flds == nil)
    [sql appendString:@"*"];
  else {
    unsigned i, count;
    
    count = [_flds count];
    for (i = 0; i < count; i++) {
      if (i > 0) [sql appendString:@", "];
      [sql appendString:[self columnNameForFieldName:[_flds objectAtIndex:i]]];
    }
  }
  [sql appendString:@" FROM "];
  [sql appendString:[self quickTableName]];
  
  if (qualifier != nil) {
    [sql appendString:@" WHERE "];
    [sql appendString:[self generateSQLForQualifier:qualifier]];
  }
  if ([sortOrderings count] > 0) {
    [sql appendString:@" ORDER BY "];
    [sql appendString:[self generateSQLForSortOrderings:sortOrderings]];
  }
#if 0
  /* limit */
  [sql appendString:@" LIMIT "]; // count
  [sql appendString:@" OFFSET "]; // index from 0
#endif
  
  /* open channel */

  if ((channel = [self acquireStoreChannel]) == nil) {
    [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
    return nil;
  }
  
  /* run SQL */

  if ((error = [channel evaluateExpressionX:sql]) != nil) {
    [self logWithFormat:@"ERROR(%s): cannot execute quick-fetch SQL '%@': %@", 
	    __PRETTY_FUNCTION__, sql, error];
    [self releaseChannel:channel];
    return nil;
  }

  /* fetch results */
  
  results = [NSMutableArray arrayWithCapacity:64];
  attrs   = [channel describeResults];
  while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
    [results addObject:row];
  
  /* release channels */
  
  [self releaseChannel:channel];
  
  return results;
}
- (NSArray *)fetchFields:(NSArray *)_flds matchingQualifier:(EOQualifier *)_q {
  EOFetchSpecification *fs;

  if (_q == nil)
    fs = nil;
  else {
    fs = [EOFetchSpecification fetchSpecificationWithEntityName:
				 [self folderName]
			       qualifier:_q
			       sortOrderings:nil];
  }
  return [self fetchFields:_flds fetchSpecification:fs];
}

/* description */

- (NSString *)description {
  NSMutableString *ms;
  id tmp;
  
  ms = [NSMutableString stringWithCapacity:256];
  [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];

  if (self->folderId)
    [ms appendFormat:@" id=%@", self->folderId];
  else
    [ms appendString:@" no-id"];

  if ((tmp = [self path]))           [ms appendFormat:@" path=%@", tmp];
  if ((tmp = [self folderTypeName])) [ms appendFormat:@" type=%@", tmp];
  if ((tmp = [self location]))
    [ms appendFormat:@" loc=%@", [tmp absoluteString]];
  
  [ms appendString:@">"];
  return ms;
}

@end /* GCSFolder */
