//
// This is a small tool that does the listing of plists and the arbitrary
// change of a value
//
// $Id: main.m,v 1.5 2005/06/16 13:31:04 nat Exp $
//
// $Log: main.m,v $
// Revision 1.5  2005/06/16 13:31:04  nat
// rein damit
//
// Revision 1.4  2005/06/07 14:40:30  nat
// mann kann jetzt plisten als values setzen
//
// Revision 1.3  2002/09/09 12:02:58  nat
// make oldskool format optional and allow force rewriting
//
// Revision 1.2  2002/09/09 10:42:13  nat
// OldSkool PList writing for Solaris compatibility
//
// Revision 1.1.1.1  2001/07/25 12:41:42  admin
// Resurrected from the dead
//
// Revision 1.10  1999/06/30 15:20:11  nat
// Slight cleanup restructuring. Usage is more helpful
//
// Revision 1.9  1999/06/30 15:12:47  nat
// completely revised hack algorithm.
// Now supports recursive descent thru arrays.
// Can do relative and absolute keypaths.
// Slightly more dangerous!
// Much slower
//
// Revision 1.8  1999/06/30 10:50:04  nat
// fatal als bail wrapper dazu.
// hack ein bischen uebersichtlicher umgestaltet
// filename braucht nicht mehr absolute path zu sein
//
// Revision 1.7  1998/11/20 15:53:08  nat
// neue Version von plisthack, inkompatibel zur vorigen Version.
// Jetzt mit add und delete Option
//
// Revision 1.6  1998/11/09 16:12:12  nat
// Anpassungen an neue Foundation bugs
//
// Revision 1.5  1998/10/20 15:00:29  nat
// ja klar, fertig...
// Exit codes ueberall, damit jetzt ziemlich scriptfaehig.
// Ausserdem werden bei Aenderungen die keine sind, nicht
// per default nochmal der alte Wert ausgegeben
//
// Revision 1.4  1998/10/20 14:46:15  nat
// advertisement dazu, ich hoffe jetzt ists fertig
//
// Revision 1.3  1998/10/20 14:40:06  nat
// simplified user interface. Also the need to do more than just
// top level hacking unfortunately arose sooner than expected...
//
// Revision 1.2  1998/10/20 13:49:57  nat
// grr ein tippfehler in den CVS vars
//
// Revision 1.1  1998/10/20 13:48:56  nat
// Merciful Release
//
//
static char  rcs_id__[] = "$Id: main.m,v 1.5 2005/06/16 13:31:04 nat Exp $";

#import <Foundation/Foundation.h>


//#define DEBUG   1
/* this is really very, very old code, that has been resurrected */


typedef enum
{
   Add    = 0,
   Change = 1,
   Delete = 2,
   Print  = 3
} ActionType;


static NSMutableArray         *ops;
static int                    concise;
static int                    changes;
static int		      silent;
static int		      miserly;
static ActionType             action;
static NSArray                *targetPath;
static NSInteger              targetPathLen;
static Class                  dictClass;
static Class                  arrayClass;
static NSPropertyListFormat   plistFormat;


static void  bail( NSString *s)
{
   if( ! silent)
      fprintf( stderr, "%s", [s cString]);
   exit( 1);
}


static void  fatal( NSString *s)
{
   fprintf( stderr, "%s", [s cString]);
   exit( 1);
}


static void  usage( const char *s)
{
   fprintf( stderr, "usage: %s [-ch][-da] <key[=value]>*\n"
            "\t-a : add entry with path\n"
            "\t-c : concise, if only one key given omit the key= output\n"
            "\t-d : delete entry\n"
            "\t-m : suppress output of property list, if no changes\n"
            "\t-h : advertisement\n"
            "\t-p : print entry\n"
            "\t-s : fail w/o complaining (in some cases)\n"
            "\n"
            "\ttop level keys are given as they appear in the plist.\n"
            "\tuse key value path notation to address nested information\n"
            "\tuse the /. key prefix to ensure that matching starts from the plist root\n\n"
            "\tIf you want to\n"
            "\tchange a value put a '=' immediately after the key followed\n"
            "\tby the new value. Don't forget to quote...\n"
            "\tex: %s \"/.connectionDictionary.hostName=eggeling\"\n\n",
            s, s);
   exit( 1);
}


static void   parse( int argc, char **argv)
{
   int  i;

   action = Change;
   
   ops = [NSMutableArray new];
   for( i = 1; i < argc; i++)
      if( argv[ i][ 0] == '-')
         switch( argv[ i][ 1])
         {
         case 's' : silent = 1; break;
         case 'c' : concise = 1; break;
         case 'm' : miserly = 1; break;
         case 'a' : action = Add; break;
         case 'p' : action = Print; break;
         case 'd' : action = Delete; break;
            
         case 'v' :
            fprintf( stderr, "Written by Nat! on a cold but sunny autumn day in the "
                             "ObjectFactory.\n(And extended on a cold winter day thereafter)\n"
                             "Finally revamped and extended on a humid summer day.\n"
                             "Years later, updated to write old skool plist format again.\n"
                             "Aeons later, revived to fatten up the mulle-plist-utilities.\n"
                             "as fate will have it was a cold and sunny winter day again.");
            break;
            
         default :
            usage( argv[ 0]); 
         }
      else
      {
         [ops addObject:[NSString stringWithCString:argv[ i]]];
      }
}


static BOOL  performAction( NSMutableDictionary *dict,
                            NSString *key,
                            id        value,
                            id        newValue)
{
   switch( action)
   {
   case Change :
      if( value && ! [value isEqual:newValue])
      {
   case Add    :
         if( newValue)
         {
#if DEBUG
            NSLog( @"Change affected for %@ with new value %@", key, newValue);
#endif
            [dict setObject:newValue
                     forKey:key];
            changes++;
            return( YES);
         }
      }
      break;

   case Delete :
      if( value && (! newValue || [value isEqual:newValue]))
      {
#if DEBUG
         NSLog( @"Removed %@ with value %@", key, value);
#endif         
         [dict removeObjectForKey:key];
         changes++;
         return( YES);
      }
      break;

   case Print :
      if( value)
      {
         if( [ops count] > 1)
         {
            if( concise)
               fatal( [NSString stringWithFormat:@"-c option useable only with one given key"]);
            printf( "%s", [[NSString stringWithFormat:@"%@ = \"%@\"\n", key, value] cString]);
         }
         else
            printf( "%s", [[NSString stringWithFormat:@"%@\n", value] cString]);
      }
   }
   return( NO);
}


//
// Non path-oriented algorithm
//
static BOOL  hack_all( NSMutableDictionary *dict,
                       NSString *key,
                       id newValue,
                       NSMutableArray *collectedPath);


static BOOL  try_hack_all( id p,
                           NSString *key,
                           id newValue,
                           NSMutableArray *collectedPath)
{
   BOOL  flag;

   flag = NO;
   if( [p isKindOfClass:dictClass])
   {
      if( hack_all( (NSMutableDictionary *) p, key, newValue, collectedPath))
         flag = YES;
   }
   else
      if( [p isKindOfClass:arrayClass])
      {
         NSInteger   i, n;
         id          q;

         n = [p count];
         for( i = n; --i >= 0; )
         {
            q = [p objectAtIndex:i];
            q = [[q mutableCopy] autorelease];
            if( try_hack_all( q, key, newValue, collectedPath))
            {
               [p replaceObjectAtIndex:i
                            withObject:q];
               flag = YES;
            }
         }
      }

   return( flag);
}


//
// hack
//
static BOOL   pathPartlyMatchesTargetPath( NSArray *path)
{
   NSRange     range;
   NSInteger   position;
   NSArray     *hack;
   
   if( ! targetPathLen)
      return( YES);

   range.location = 0;
   range.length   = targetPathLen - 1;
   hack = [targetPath subarrayWithRange:range];
   
   position = [path count];
   if( (position -= targetPathLen - 1) < 0)
      return( NO);

   range.location = position;
   range.length   = targetPathLen - 1;
   
   return( [hack isEqual:[path subarrayWithRange:range]]);
}


static BOOL   pathMatchesTargetPath( NSArray *path)
{
   NSRange     range;
   NSInteger   position;

   if( ! targetPathLen)
      return( YES);
   
   position = [path count];
   if( (position -= targetPathLen) < 0)
      return( NO);
   
   range.location = position;
   range.length   = targetPathLen;

   return( [targetPath isEqual:[path subarrayWithRange:range]]);
}


//
// Recursively hack thru the dictionaries...
//
static BOOL  hack_all( NSMutableDictionary *dict,
                       NSString *key,
                       id newValue,
                       NSMutableArray *collectedPath)
{
   NSEnumerator   *rover;
   NSString       *s;
   id             p;
   id             value;
   BOOL           flag;
   NSInteger      position;

//
// fetch value from current dictionary
//
   position = [collectedPath count];
   
   flag    = NO;
   rover = [[dict allKeys] objectEnumerator];
   while( s = [rover nextObject])
   {
      p = [dict objectForKey:s];
      if( [p isKindOfClass:dictClass] ||
          [p isKindOfClass:arrayClass])
      {
         [collectedPath insertObject:s
                             atIndex:position];

         p = [[p mutableCopy] autorelease];
         if( try_hack_all( p, key, newValue, collectedPath))
         {
            flag = YES;
            [dict setObject:p
                     forKey:s];
         }
         [collectedPath removeLastObject];
      }
   }

//
// Eigentliches "Value, hacking"
//   
   if( action == Add)
   {
//
// Should we really add ?
//
      if( pathPartlyMatchesTargetPath( collectedPath))
         if( performAction( dict, key, nil, newValue))
            flag = YES;
   }
   else
   {
      value = [dict objectForKey:key];
      if( value)
      {
         BOOL  memo;

         [collectedPath addObject:key];
         memo = pathMatchesTargetPath( collectedPath);
         [collectedPath removeLastObject];
         if( memo)
            if( performAction( dict, key, value, newValue))
               flag = YES;
      }
   }
   
//
// or else just perform the Action
//
   return( flag);
}   

static id   plistOrString( NSString *s)
{
   id                     plist;
   NSData                 *data;
   NSPropertyListFormat   plistFormat;
   NSString               *error;
   
   // new: allow plist to be set
   data  = [s dataUsingEncoding:NSUTF8StringEncoding];
   plist = [NSPropertyListSerialization propertyListFromData:data
                                            mutabilityOption:NSPropertyListImmutable
                                                      format:&plistFormat
                                            errorDescription:&error];
  if( ! plist)
  {
     fprintf( stderr, "plist parse failed for\n%s\n***Error: %s.\nArgument will be used as plain string\n",
              [s cString],
                  [error cString]);
     plist = s;
  }
  return( plist);
}


static void  doAction( NSData *data)
{
   NSEnumerator           *rover;
   NSString               *s;
   NSArray                *components;
   NSArray                *path;
   NSString               *key;
   NSString               *newValue;
   NSMutableDictionary    *dict;
   NSArray                *valueComponents;
   NSString               *error;
   NSString               *valueString;
   NSUInteger             n;
   
   changes = 0;
   dict = [NSPropertyListSerialization propertyListFromData:data
                                           mutabilityOption:NSPropertyListImmutable
                                                     format:&plistFormat
                                           errorDescription:&error];
   if( ! [dict isKindOfClass:[NSDictionary class]])
      bail( [NSString stringWithFormat:@"Not a plist\n"]);
   
   
   rover = [ops objectEnumerator];
   while( s = [rover nextObject])
   {
      components = [s componentsSeparatedByString:@"="];
      key        = [components objectAtIndex:0];
      path       = [key componentsSeparatedByString:@"."];
      
      newValue = nil;
      n        = [components count];
      if( n > 1)
      {
         valueComponents = [components subarrayWithRange:NSMakeRange( 1, n - 1)];
         valueString     = [valueComponents componentsJoinedByString:@"="];
         newValue        = plistOrString( valueString);
      }

      if( action == Add && ! newValue)
         fatal( [NSString stringWithFormat:@"-a Option needs key and value\n"]);
      if( [path count] == 1)
         targetPath = nil;
      else
         targetPath = path;
      targetPathLen = [targetPath count];
      key           = [path lastObject];
      hack_all( dict, key, newValue, [NSMutableArray arrayWithObject:@"/"]);
   }

   if( changes || miserly)
   {
#if DEBUG > 1
      NSLog( @"writing changed dictionary %@ back to %@", dict, filename);
#endif
      data = [[dict description] dataUsingEncoding:NSUTF8StringEncoding];
      [[NSFileHandle fileHandleWithStandardOutput] writeData:data];
   }
   changes = 0;
}


int main (int argc, char *argv[])
{
   NSAutoreleasePool  *pool;
   NSFileHandle       *handle;
   NSData             *data;
   
   pool       = [NSAutoreleasePool new];

   dictClass  = [NSDictionary class];
   arrayClass = [NSArray class];

   parse( argc, argv);
   if( [ops count] == 0)
      usage( argv[ 0]);

//
// Lets go...
//
   handle = [NSFileHandle fileHandleWithStandardInput];
   data   = [handle readDataToEndOfFile];

   doAction( data);

   [pool release];
   
   return( changes);      // 0 == OK, else FAILURE (SIC!!)
}