/* SOGoSQLUserProfile.m - this file is part of SOGo
 *
 * Copyright (C) 2009-2017 Inverse inc.
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This file 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSNull+misc.h>

#import <GDLContentStore/GCSChannelManager.h>
#import <GDLContentStore/NSURL+GCS.h>
#import <GDLAccess/EOAdaptor.h>
#import <GDLAccess/EOAdaptorChannel.h>
#import <GDLAccess/EOAdaptorContext.h>
#import <GDLAccess/EOAttribute.h>
#import <SOGo/SOGoCache.h>

#import "SOGoSystemDefaults.h"

#import "SOGoSQLUserProfile.h"

static NSURL *tableURL = nil;
static NSString *uidColumnName = @"c_uid";
static EOAttribute *textColumn = nil;
static const NSString *kCDefaultsLenKey = @"kCDefaultsLenKey";

@implementation SOGoSQLUserProfile

+ (void) initialize
{
  NSDictionary *description;
  NSString *profileURL;
  SOGoSystemDefaults *sd;

  if (!tableURL)
    {
      sd = [SOGoSystemDefaults sharedSystemDefaults];
      profileURL = [sd profileURL];
      if (profileURL)
        tableURL = [[NSURL alloc] initWithString: profileURL];
    }

  if (!textColumn)
    {
      #warning This is a hack for providing an EOAttribute definition \
        that is compatible with all the backends that we support
      /* TODO: ... We should make use of EOModel for the profile tables */
      description = [NSDictionary dictionaryWithObjectsAndKeys:
                                    @"c_textfield", @"columnName",
                                    @"VARCHAR", @"externalType",
                                  nil];
      textColumn = [EOAttribute attributeFromPropertyList: description];
      [textColumn retain];
    }
}

- (id) init
{
  if ((self = [super init]))
    {
      fieldName = nil;
    }

  return self;
}

- (void) dealloc
{
  [fieldName release];
  [super dealloc];
}

- (void) setProfileType: (SOGoUserProfileType) newProfileType
{
  if (newProfileType == SOGoUserProfileTypeDefaults)
    ASSIGN (fieldName, @"c_defaults");
  else if (newProfileType == SOGoUserProfileTypeSettings)
    ASSIGN (fieldName, @"c_settings");
  else
    [self errorWithFormat: @"unknown profile value: %d", newProfileType];

  [super setProfileType: newProfileType];
}

- (NSString *) fetchJSONProfileFromDB
{
  GCSChannelManager *cm;
  EOAdaptorChannel *channel;
  NSDictionary *row;
  NSException *ex;
  NSString *sql, *result;
  id value;
  NSArray *attrs;

  value = nil;
  result = nil;

  cm = [GCSChannelManager defaultChannelManager];
  channel = [cm acquireOpenChannelForURL: tableURL];
  if (channel)
    {
      /* generate SQL */
      defFlags.ready = YES;
      sql = [NSString stringWithFormat: @"SELECT %@ FROM %@ WHERE %@ = '%@'",
                      fieldName, [tableURL gcsTableName],
                      uidColumnName, [self uid]];
      /* run SQL */
      ex = [channel evaluateExpressionX: sql];
      if (ex)
        [self errorWithFormat:@"could not run SQL '%@': %@", sql, ex];
      else
        {
          /* fetch schema */
          attrs = [channel describeResults: NO /* don't beautify */];
          
          /* fetch values */
          row = [channel fetchAttributes: attrs withZone: NULL];
          [channel cancelFetch];

          /* the isNew flag depends on the presence of the row in the
             database rather than on the existence of the value. */
          defFlags.isNew = (row == nil);

          value = [row objectForKey: fieldName];
          if (![value isNotNull])
            value = nil; /* we discard any NSNull instance */

          if (value && [value isKindOfClass: [NSData class]]) {
            result = [NSString stringWithUTF8String: [value bytes]];
          } else if (value && [value isKindOfClass: [NSString class]]) {
            result = value;
          }
        }

      [cm releaseChannel: channel];
    }
  else
    {
      defFlags.ready = NO;
      [self errorWithFormat:@"failed to acquire channel for URL: %@", 
            tableURL];
    }

  return result;
}

- (NSString *) generateSQLForInsert: (NSString *) jsonRepresentation
{
  NSString *sql;

  if ([jsonRepresentation length])
    sql = [NSString stringWithFormat: (@"INSERT INTO %@"
                                       @"            (%@, %@)"
                                       @"     VALUES ('%@', %@)"),
                    [tableURL gcsTableName], uidColumnName, fieldName,
                    [self uid],
                    jsonRepresentation];
  else
    sql = nil;

  return sql;
}

- (NSString *) generateSQLForUpdate: (NSString *) jsonRepresentation
{
  NSString *sql;

  if ([jsonRepresentation length])
    sql = [NSString stringWithFormat: (@"UPDATE %@"
                                       @"     SET %@ = %@"
                                       @"   WHERE %@ = '%@'"),
                    [tableURL gcsTableName],
                    fieldName,
                    jsonRepresentation,
                    uidColumnName, [self uid]];
  else
    sql = nil;

  return sql;
}

- (BOOL) storeJSONProfileInDB: (NSString *) jsonRepresentation
{
  GCSChannelManager *cm;
  EOAdaptorChannel *channel;
  EOAdaptorContext *context;
  NSException *ex;
  NSString *sql, *formattedValue;
  BOOL rc;

  rc = NO;

  cm = [GCSChannelManager defaultChannelManager];
  channel = [cm acquireOpenChannelForURL: tableURL];
  if (channel)
    {
      context = [channel adaptorContext];
      if ([context beginTransaction])
        {
          formattedValue = [[context adaptor] formatValue: jsonRepresentation
                                             forAttribute: textColumn];
          sql = ((defFlags.isNew)
                 ? [self generateSQLForInsert: formattedValue]
                 : [self generateSQLForUpdate: formattedValue]);

          defFlags.ready = YES;
          ex = [channel evaluateExpressionX:sql];
          if (ex)
            {
              [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex];
              [context rollbackTransaction];
            }
          else
            {
              rc = YES;
              defFlags.modified = NO;
              defFlags.isNew = NO;
              [context commitTransaction];
            }
          [cm releaseChannel: channel];
        }
      else
        {
          defFlags.ready = NO;
          [cm releaseChannel: channel immediately: YES];
        }
    }
  else
    {
      defFlags.ready = NO;
      [self errorWithFormat: @"failed to acquire channel for URL: %@", 
            tableURL];
    }

  return rc;
}

- (unsigned long long) getCDefaultsSize {
  unsigned long long r;
  NSString *sql;
  NSException *ex;
  GCSChannelManager *cm;
  EOAdaptorChannel *channel;
  EOAdaptorContext *context;
  NSArray *attrs;
  NSDictionary *infos;
  SOGoCache *cache;
  NSNumberFormatter *formatter;

  r = 65535;
  
  cache = [SOGoCache sharedCache];
  if ([cache valueForKey:kCDefaultsLenKey]) {
    formatter = [[NSNumberFormatter alloc] init];
    formatter.numberStyle = NSNumberFormatterDecimalStyle;
    r = [[formatter numberFromString: [cache valueForKey:kCDefaultsLenKey]] unsignedLongLongValue];
    [formatter release];
  } else {
    cm = [GCSChannelManager defaultChannelManager];
    channel = [cm acquireOpenChannelForURL: tableURL];
    sql = [NSString stringWithFormat: @"select character_octet_length as CHARACTER_MAXIMUM_LENGTH from information_schema.columns where table_name = '%@' AND column_name = 'c_defaults'", [tableURL gcsTableName]];
    ex = [channel evaluateExpressionX: sql];
    
    if (!ex) {
      attrs = [channel describeResults: NO];
      infos = [channel fetchAttributes: attrs withZone: NULL];
      [cm releaseChannel: channel  immediately: YES];
      if (infos && [infos objectForKey:@"CHARACTER_MAXIMUM_LENGTH"]) {
        r = [[infos objectForKey:@"CHARACTER_MAXIMUM_LENGTH"] longLongValue];
      } else if (infos && [infos objectForKey:@"character_maximum_length"]) { // PGSQL case
        r = [[infos objectForKey:@"character_maximum_length"] longLongValue];
      }
    }

    [cache setValue: [[NSNumber numberWithUnsignedLongLong: r] stringValue] forKey: kCDefaultsLenKey];
  }
  

  return r;
}

@end
