/*
  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 "SOGoContentObject.h"
#include "SOGoFolder.h"
#include "common.h"
#include <GDLContentStore/GCSFolder.h>

@interface SOGoContentObject(ETag)
- (NSArray *)parseETagList:(NSString *)_c;
@end

@implementation SOGoContentObject

// TODO: check superclass version

- (void)dealloc {
  [self->content release];
  [self->ocsPath release];
  [super dealloc];
}

/* notifications */

- (void)sleep {
  [self->content release]; self->content = nil;
  [super sleep];
}

/* accessors */

- (BOOL)isFolderish {
  return NO;
}

- (void)setOCSPath:(NSString *)_path {
  if ([self->ocsPath isEqualToString:_path])
    return;
  
  if (self->ocsPath)
    [self warnWithFormat:@"GCS path is already set! '%@'", _path];
  
  ASSIGNCOPY(self->ocsPath, _path);
}

- (NSString *)ocsPath {
  if (self->ocsPath == nil) {
    NSString *p;
    
    if ((p = [self ocsPathOfContainer]) != nil) {
      if (![p hasSuffix:@"/"]) p = [p stringByAppendingString:@"/"];
      p = [p stringByAppendingString:[self nameInContainer]];
      self->ocsPath = [p copy];
    }
  }
  return self->ocsPath;
}

- (NSString *)ocsPathOfContainer {
  if (![[self container] respondsToSelector:@selector(ocsPath)])
    return nil;

  return [[self container] ocsPath];
}

- (GCSFolder *)ocsFolder {
  if (![[self container] respondsToSelector:@selector(ocsFolder)])
    return nil;
  
  return [[self container] ocsFolder];
}

/* content */

- (NSString *)contentAsString {
  GCSFolder *folder;

  if (self->content != nil)
    return self->content;
  
  if ((folder = [self ocsFolder]) == nil) {
    [self errorWithFormat:@"Did not find folder of content object."];
    return nil;
  }
  
  self->content = [[folder fetchContentWithName:[self nameInContainer]] copy];
  return self->content;
}

- (NSException *)saveContentString:(NSString *)_str
  baseVersion:(unsigned int)_baseVersion
{
  /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
  GCSFolder   *folder;
  NSException *ex;
  
  if ((folder = [self ocsFolder]) == nil) {
    [self errorWithFormat:@"Did not find folder of content object."];
    return nil;
  }
  
  ex = [folder writeContent:_str toName:[self nameInContainer]
	       baseVersion:_baseVersion];
  if (ex != nil) {
    [self errorWithFormat:@"write failed: %@", ex];
    return ex;
  }
  return nil;
}
- (NSException *)saveContentString:(NSString *)_str {
  return [self saveContentString:_str baseVersion:0 /* don't check */];
}

- (NSException *)delete {
  /* Note: "iCal multifolder saves" are implemented in the apt subclass! */
  GCSFolder   *folder;
  NSException *ex;
  
  // TODO: add precondition check? (or add DELETEAction?)
  
  if ((folder = [self ocsFolder]) == nil) {
    [self errorWithFormat:@"Did not find folder of content object."];
    return nil;
  }
  
  if ((ex = [folder deleteContentWithName:[self nameInContainer]])) {
    [self errorWithFormat:@"delete failed: %@", ex];
    return ex;
  }
  return nil;
}

/* actions */

- (id)PUTAction:(WOContext *)_ctx {
  WORequest    *rq;
  NSException  *error;
  unsigned int baseVersion;
  id           etag, tmp;
  BOOL         needsLocation;
  
  if ((error = [self matchesRequestConditionInContext:_ctx]) != nil)
    return error;
  
  rq = [_ctx request];
  
  /* check whether its a request to the 'special' 'new' location */
  /*
    Note: this is kinda hack. The OGo ZideStore detects writes to 'new' as
          object creations and will assign a server side identifier. Most
	  current GroupDAV clients rely on this behaviour, so we reproduce it
	  here.
	  A correct client would loop until it has a name which doesn't not
	  yet exist (by using if-none-match).
  */
  needsLocation = NO;
  tmp = [[self nameInContainer] stringByDeletingPathExtension];
  if ([tmp isEqualToString:@"new"]) {
    tmp = [[[self container] class] globallyUniqueObjectId];
    needsLocation = YES;
    
    [self debugWithFormat:
	    @"reassigned a new location for special new-location: %@", tmp];
    
    /* kinda dangerous */
    ASSIGNCOPY(self->nameInContainer, tmp);
    ASSIGN(self->ocsPath, nil);
  }
  
  /* determine base version from etag in if-match header */
  /*
    Note: The -matchesRequestConditionInContext: already checks whether the
          etag matches and returns an HTTP exception in case it doesn't.
	  We retrieve the etag again here to _ensure_ a transactionally save
	  commit.
          (between the check and the update a change could have been done)
  */
  tmp  = [rq headerForKey:@"if-match"];
  tmp  = [self parseETagList:tmp];
  etag = nil;
  if ([tmp count] > 0) {
    if ([tmp count] > 1) {
      /*
	Note: we would have to attempt a save for _each_ of the etags being
	      passed in! In practice most WebDAV clients submit exactly one
	      etag.
      */
      [self warnWithFormat:
	      @"Got multiple if-match etags from client, only attempting to "
	      @"save with the first: %@", tmp];
    }
    
    etag = [tmp objectAtIndex:0];
  }
  baseVersion = ([etag length] > 0)
    ? [etag unsignedIntValue]
    : 0 /* 0 means 'do not check' */;
  
  /* attempt a save */
  
  if ((error = [self saveContentString:[rq contentAsString]
		     baseVersion:baseVersion]) != nil)
    return error;
  
  /* setup response */
  
  // TODO: this should be automatic in the SoDispatcher if we return nil?
  [[_ctx response] setStatus:201 /* Created */];
  
  if ((etag = [self davEntityTag]) != nil)
    [[_ctx response] setHeader:etag forKey:@"etag"];
  
  if (needsLocation) {
    [[_ctx response] setHeader:[self baseURLInContext:_ctx] 
		     forKey:@"location"];
  }
  
  return [_ctx response];
}

/* E-Tags */

- (id)davEntityTag {
  // TODO: cache tag in ivar? => if you do, remember to flush after PUT
  GCSFolder *folder;
  char buf[64];
  
  if ((folder = [self ocsFolder]) == nil) {
    [self errorWithFormat:@"Did not find folder of content object."];
    return nil;
  }
  
  sprintf(buf, "\"gcs%08d\"",
	  [[folder versionOfContentWithName:[self nameInContainer]]
	    unsignedIntValue]);
  return [NSString stringWithCString:buf];
}

/* WebDAV */

- (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
  inContext:(id)_ctx
{
  /*
    Note: even for new objects we won't get a new name but a preinstantiated
          object representing the new one.
  */
  [self logWithFormat:
	  @"TODO: move not implemented:\n  target:  %@\n  new name: %@",
	  _target, _name];
  return [NSException exceptionWithHTTPStatus:405 /* not allowed */
                      reason:@"this object cannot be copied via WebDAV"];
}

- (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
  inContext:(id)_ctx
{
  /*
    Note: even for new objects we won't get a new name but a preinstantiated
          object representing the new one.
  */
  [self logWithFormat:
	  @"TODO: copy not implemented:\n  target:  %@\n  new name: %@",
	  _target, _name];
  return [NSException exceptionWithHTTPStatus:405 /* not allowed */
                      reason:@"this object cannot be copied via WebDAV"];
}

- (BOOL)davIsCollection {
  return [self isFolderish];
}

/* message type */

- (NSString *)outlookMessageClass {
  return nil;
}

/* description */

- (void)appendAttributesToDescription:(NSMutableString *)_ms {
  [super appendAttributesToDescription:_ms];
  
  [_ms appendFormat:@" ocs=%@", [self ocsPath]];
}

@end /* SOGoContentObject */
