//
//	cnvwrap.m 
//         Program for automatically generating degenerate (convenience)
//         methods for methods with optional parameters
//
//		               Written by Carl Lindberg
//          Copyright (c) 1994,1995,1996 by Carl Lindberg.
//			      Version 1.3  All rights reserved.
//		This notice may not be removed from this source code.
//
//	This object is included in the MiscKit by permission from the author
//	and its use is governed by the MiscKit license, found in the file
//	"LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
//	for a list of all applicable permissions and restrictions.
//
//

//    This program takes method declarations - with default values attached
//   to optional parameters - and outputs all possible degenerate methods
//   along with their header lines.
//
//
//   Command-line options:
//
//       -i filename     Specify input file.  Reads from stdin if this
//                       option not used.
//       -m filename     Output method implementations to named file.  Adds
//                       '.m' to filename if not there already.  Outputs to
//                       stdout if this option is not used.
//       -h filename     Output method headers to named file.  Adds '.h' to
//                       filename if not there already.  Outputs to stdout if
//                       this option is not used.
//       -f filename     Just like -h filename.h -m filename.m.  Automatically
//                       adds .h and .m extensions to filename, and outputs
//                       headers and methods, respectively, to the two files.
//       -e {h|m|hm}     Excludes the {header|implementation|both} of the full
//                       method from being output.  For example, -em will print
//                       out all headers, but will not print the implementation
//                       of the full method (normally, it is printed with a
//                       blank body).
//       -a              Appends to output files instead of overwriting.
//       -H              Help
//
//
//
//   SYNTAX FOR INPUT LINE(S):
//
//    Input lines are basically the same as header lines, except that
//    default values to parameters are put inside square (or squiggly)
//    brackets and tacked directly onto the end of that parameters.
//    Also, no semicolon at the end.
//
//
//    1) Leave at least one space/tab after the "-" or "+".
//    2) ONE line per method - don't break up a long method into 2 or more
//         lines.
//    3) Default values signify an optional parameter.  These should be 
//         between square ("[<default value>]") or squiggly ("{<default value>"})
//         brackets tacked on the end of a parameter - NO SPACES before the open
//         bracket.
//       The 'default value' may contain any character (including whitespaces)
//         EXCEPT a corresponding right bracket character -- that is, if you use
//         square brackets to define the default value, the default value itself
//         cannot contain a right square bracket.  Even then, you are allowed to
//         have corresponding right bracket character(s) so long as no whitespaces
//         come _after_ it. Ex:
//
//   Legal:     with:(char *)[[sender stringValue]]
//              with:(char *){[sender stringValue]}
//              with:(char *)[[[sender selectedCell]stringValue]]
//              with:(char *){[[sender selectedCell] stringValue]}   //use squiggly braces
//   Illegal:   with:(char *)[[sender stringValue] ]
//              with:(char *)[[[sender selectedCell] stringValue]]
//
//       This was as flexible as I could figure out - it's a limitation
//         of the regular expression used to search.  If this still isn't
//         enough, than you could simply put in a word like FOOBAR as the
//         default value, and then search-and-replace FOOBAR with the real
//         default value on the output.
//
//    4) Every variable must be typecast - even if it's an id, please put
//         (id) as the typecast.  A method return type is optional, though.
//    5) If the first parameter - really the method name - is optional,
//         then *every* parameter must be optional too, or else you will
//         get duplicate methods output.
//
//
//   The program works by first parsing each line, and splitting it
//  up into four lists, each list containing some needed portion of
//  each parameter.  Then the program goes into a second main loop
//  which works up the needed text using the values in the lists.
//
//  For the input:
//
//  - (int)spotOfStr:(const char *)str occurrenceNum:(int)n[0] caseSensitive:(BOOL)sense[YES]
//
//  the four lists would end up so:
//
//
//            paramtypeList                      paramList         paramvalList   defaultList
//            (whole param minus any             (param name       (variable      (default 
//              default value)                     only)             name)           name)
//            --------------------------------   --------------    ------------   -----------
//  Param 1:  (int)spotOfStr:(const char *)str   spotOfStr:        str            [blank]
//  Param 2:  occurrenceNum:(int)n               occurrenceNum:    n              0
//  Param 3:  caseSensitive:(BOOL)sense          caseSensitive:    sense          YES
//
//
//   There should be (number of optional parameters)**2 possible method
//  combinations.  The exception is that if the first "parameter"
//  (spotOfStr in the example above) is optional, only one more possible
//  method is added -- the one where every parameter is left out.  You
//  can't leave the first parameter out and include others, obviously.
//  Therefore, if the first parameter is optional, *every* parameter should
//  be optional -- else duplicate methods will be output.
//
//
//
//
//   You may thank the piles of convenience methods in MiscString for 
//  prompting me to write this program. :-)  Enjoy.
//
//  Version 1.1  June 7, 1995
//    - fixed bug where having '(' or '[' in the default value section would
//         confuse program
//    - added -e {h|m|hm} switch to exclude outputting of the last possible
//         header or method.  This lets you code the full method first, then
//         just output the headers/code for the convenience methods only.
//  Version 1.2  July 8, 1995
//    - changed the regular expression search to allow more possibilities of
//         default section values.
//  Version 1.3  Sept 16, 1996
//    - changed the regular expression search to allow use of { ... } as well
//         as [ ... ] for default value sections.
//    - changed output format of implementations slightly (the closing '}' is
//         now on its own line).

#include <appkit/appkit.h>
#include <misckit/MiscString.h>

  void dohelp();
  void usage();
  void freestuff();

// These are needed to be declared outside main() for freestuff()
id mainstr, headstring, specstring, hName, mName, iName, returnstr, mfilestr,
	hfilestr, defaultList, paramList, paramtypeList, paramvalList;

void main(int argc, char **argv)
{
  char ch;

  BOOL isOptArray[50];					//ith slot YES if param #i is optional
  BOOL append = NO;						//append to given filenames, or overwrite?
  BOOL outputFinalHeader = YES;
  BOOL outputFinalMethod = YES;
  int i, j, numposs, numparam, thenum, len, spot2, spot3, startposs;

  FILE *infile, *moutfile, *houtfile;

  id tmp;

  // Initialize some stuff
  mainstr   = [MiscString new];
  specstring= [MiscString new];		//special; used if first param is optional
  headstring= [MiscString new];		//for building up the header line
  hfilestr  = [MiscString new];		//for storing method headers
  mfilestr  = [MiscString new];		//for storing method definitions
  returnstr = [MiscString new];		//intermediate string, used for making 
									//the return value

  iName     = [MiscString new];      //strings to hold filenames
  mName     = [MiscString new];
  hName     = [MiscString new];

                                   	//for "caseSensitive:(BOOL)sense[YES]":
  defaultList   = [List new];		//for storing the default values (eg "YES")
  paramList     = [List new];		//for storing the parameter name (eg "caseSensitive:")
  paramvalList  = [List new];		//for storing the variable name  (eg "sense")
  paramtypeList = [List new];		//for storing whole parameter w/ typecasts 
	 								//  (eg "caseSensitive:(BOOL)sense")
  

  // These are defaults
  infile   = stdin;
  houtfile = stdout;
  moutfile = stdout;
  append   = NO;

  re_set_syntax(RE_NO_BK_PARENS|RE_NO_BK_VBAR);

  // Process arguments
  while ((ch = getopt(argc, argv, "Ham:h:f:i:e:")) != EOF)
  	switch((char)ch) {
		case '?':
	  		usage(argv[0]);
		case 'H':
			dohelp(argv[0]);
			freestuff();
			exit(0);
		case 'a':
			append = YES;
			break;
		case 'i':
			[iName setStringValue:optarg];
			break;
		case 'm':
			[mName setStringValue:optarg];
			[mName addExtensionIfNeeded:"m"];
			break;
		case 'h':
			[hName setStringValue:optarg];
			[hName addExtensionIfNeeded:"h"];
			break;
		case 'f':
			[hName setStringValue:optarg];
			[hName cat:".h"];
			[mName setStringValue:optarg];
			[mName cat:".m"];
			break;
		case 'e':
			if (index(optarg,'h')) outputFinalHeader = NO;
			if (index(optarg,'m')) outputFinalMethod = NO;
			break;
		default:
			usage(argv[0]);
			exit(2);
	}
  

  if ([iName length]) {
	infile = fopen([iName stringValue], "r+");
	if (!infile) {
		fprintf(stderr,"ERROR: could not open '%s'\n",[iName stringValue]);
		freestuff();
		exit(1);
	}
  }

  if ([hName length]) {
  	houtfile = fopen([hName stringValue], (append)? "a+":"w+");
	if (!houtfile) {
		fprintf(stderr,"ERROR: could not open '%s'\n",[hName stringValue]);
		freestuff();
		exit(1);
	}
  }

  if ([mName length]) {
  	moutfile = fopen([mName stringValue], (append)? "a+":"w+");
	if (!moutfile) {
		fprintf(stderr,"ERROR: could not open '%s'\n",[mName stringValue]);
		freestuff();
		exit(1);
	}
  }
  
  // Repeat for every method read in... the || [mainstr length] is there
  // if the last line of input does not have a newline at the end -
  // this loop only terminates if at EOF *and* no string was read in.
  
  while ([mainstr fgets:infile keepNewline:NO] != EOF || [mainstr length]) {
    if ([mainstr length] < 5) continue;  //break out if a blank line
	numparam = 0;
	numposs = 1;
	startposs = 0;
	
	// Isolate each parameter and process it - the regular expression is
	// what expects to see a cast for each variable.
	
    for (i=0;(spot2 = [mainstr spotOfRegex:"[^\t\n :]+[:][(][^)]*[)][^ \t\n[{]+(([[][^]]*[]]|{[^}]*})[^ \t\n]*)?"
	                           occurrenceNum:i length:&len]) >= 0;i++)
	{  
	  // The above parsing may not have worked if the method return type had
	  // spaces in it, such as (const char *).  This checks to see if that
	  // happened and sets 'spot2' and 'len' to the corrected values.

	  if ((i == 0) && ((spot3 = [mainstr spotOfRegex:"[(][^)]*[)]"]) < spot2)) {
		len += (spot2-spot3);
	    spot2 = spot3;
	  }
	  
	  // Now get the whole parameter in a string.
      tmp = [mainstr midFrom:spot2 length:len];
      
	  // Add the parameter name (sans type and variable name) to its list.
	  // If the method has a return type, this must be stripped out too -
	  // thus the check on the first character.

	  if ([tmp charAt:0] == '(') {
        [paramList addObject:[tmp midFrom:[tmp spotOf:')']+1 to:[tmp spotOf:':']]];
      }
	  else {
        [paramList addObject:[tmp midFrom:0 to:[tmp spotOf:':']]];
      }
	  
	  numparam++;
      if (![tmp endcmp:"]"] || ![tmp endcmp:"}"]) {				// It is an optional param
        id tmp2 = [tmp midFrom:[tmp spotOf:':'] to:[tmp length]];
        int spot = [tmp spotOf:':'] + [tmp2 spotOfChars:"[{"];		// Find beginning of default value
        isOptArray[i] = YES;
		
		// Get the whole parameter minus default value and add it to list
        [paramtypeList addObject:[tmp midFrom:0 to:spot-1]];
		
		// Get the default value and add it to its list
        [defaultList addObject:[tmp midFrom:spot+1 to:[tmp length]-2]];

        // Get the variable name and add it to its list
        //[paramvalList addObject:[tmp midFrom:[tmp rspotOf:')']+1 to:spot-1]];
	[paramvalList addObject:[tmp2 midFrom:[tmp2 spotOf:')']+1 to:[tmp2 spotOfChars:"{["]-1]];
	[tmp2 free];
		
		// If the first parameter is optional, it is handled much differently.
		// Instead of doubling the number of potential methods, only one more
		// is added.  This is handled by starting the second main loop at -1
		// instead of 0 (startposs).  Also, since in the rest of the methods
		// the first parameter must be there, its slot in the isOptArray should
		// be NO, as it really is not optional.  'specstring' holds the header
		// for the extra method incurred -- which is just the name (and return
		// type if there) of the parameter minus the colon.  'specstring' will
		// be used only in the -1th iteration of the second main loop.

		if (i==0) {
		  [specstring setStringValue:[[tmp left:[tmp spotOf:':']] stringValueAndFree]];
		  [specstring insert:" "];
		  startposs = -1;
		  isOptArray[0] = NO;
		}
		else {     // Not the first param, so double # of possible methods.
          numposs *= 2;
		}

        // Free tmp here... under the else below, it is added to one of the lists
		// and is therefore freed then.
        [tmp free]; 
      }
      else {  // Not an optional parameter, doesn't have [XXX] at end
        isOptArray[i] = NO;
        [defaultList addObject:[MiscString new]];   // Add a dummy string to keep place
        [paramtypeList addObject:tmp];
        [paramvalList addObject:[tmp midFrom:[tmp rspotOf:')']+1 to:[tmp length]-1]];
      }


    } //done parsing the input line

/*     //debugging with parsing...
    for (i=0;k<numparam;k++) {
      printf("Full param: %s\n",[[paramtypeList objectAt:i] stringValue]);
      printf("Optional? %s\n",(isOptArray[i])? "YES":"NO");
      printf("Default: '%s'\n",[[defaultList objectAt:i] stringValue]);
      printf("Value: %s\n",[[paramvalList objectAt:i] stringValue]);
      printf("Name: %s\n",[[paramList objectAt:i] stringValue]);
    }
*/
    // Okay, now output stuff.  There should be numposs methods to do.
    // 'startposs' is -1 if the first param is optional and the one
    // extra method is there to do, 0 normally.

    for (i=startposs;i<numposs;i++) {
      [headstring setStringValue:[[mainstr wordNum:0] stringValueAndFree]]; //"-" or "+"
      [returnstr setStringValue:"{ return [self"];
      thenum = 1;
	  
	  // Build up the header and return for each method.  hfilestr holds
	  // the method headers, mfilestr holds the method name, and returnstr
	  // holds the return value which gets added to mfilestr at the end of
	  // the inner loop.
	  
      for (j=0;j<numparam;j++) {
	  
	    // Add the method name to the return value.  The argument will
		// either be the variable name or the default
        [returnstr addChar:' '];
        [returnstr concatenate:[paramList objectAt:j]];
		
		// Check to see if the current parameter deserves to be added
		// in this pass.  If it does, then add it to the header,
		// and add the variable name to returnstr.  Otherwise, just add
		// the default value to returnstr.
		// The special case of i==-1 should not do this part - that is handled
		// elsewhere.  If the current parameter is not optional then it should
		// always be added in.
		// Otherwise, the third condition determines whether a parameter is
		// included in this pass.  'thenum' starts as 1 every iteration of i,
		// meaning the value of that condition for the first parameter will
		// be 0, then 1, then 0, then 1... as i goes up.  'thenum' gets doubled
		// each optional parameter, so for the second optional parameter the
		// condition will be 0, 0, 1, 1, 0, 0, 1, 1... as i goes up.  The third
		// will be 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, ... and so on.  Since i represents
		// the total number of combinations, this will account for every possible
		// combination.
		
        if ((i>=0) && (!(isOptArray[j]) || ((i % (thenum*2)) >= thenum))) {
          [headstring addChar:' '];
		  [headstring concatenate:[paramtypeList objectAt:j]];
          [returnstr concatenate:[paramvalList objectAt:j]]; //the variable name
         }
       else {
          [returnstr concatenate:[defaultList objectAt:j]];  //the default value
         }
        if (isOptArray[j]) thenum *= 2;
       }
	  
	  // Special case handling for i==-1.  Only the else portion of the above
	  // will be executed, so only the returnstr is done.  specstring, set in
	  // the parsing loop, holds the header needed by hfilestr and mfilestr.
	  // So, just add it to them.
	  
      if (i==-1) {
	    [headstring concatenate:specstring];
	  }
	  
      [returnstr cat:"];\n}"];

      if ((i <(numposs-1)) || outputFinalHeader) {
          [hfilestr concatenate:headstring];
          [hfilestr cat:";\n"];
      }
	  if ((i < (numposs-1)) || outputFinalMethod) {
	      [mfilestr concatenate:headstring];
      }
	  // If we are at the last possible method (i == numposs-1) we don't want
	  // to put a method body in - it would just call itself and be an infinite
	  // loop.  I just decided to put in a blank method body - you will at least
	  // get a warning from the compiler if you forget to take this out or implement
	  // it.  At least if you use -Wall.
	  // Otherwise, returnstr holds the entire method body, so add that to mfilestr.
	  
      if (i < (numposs-1))  //don't put in return for richest method
        [mfilestr catFromFormat:"\n%s\n",[returnstr stringValue]];
      else if (outputFinalMethod)
	    [mfilestr cat:"\n{\n}\n\n"];
    }
	
    [hfilestr addChar:'\n'];        // Leave a blank line between sets of headers
	[mfilestr cat:"\n\n\n"];        // Leave three blank lines between sets of 
                                    // convenience methods.

    // Empty out all the lists to start over with the next input line
    [defaultList freeObjects];
    [paramList freeObjects];
    [paramvalList freeObjects];
    [paramtypeList freeObjects];
   }
  
  // Output the info... yeah, it only gets output at the end instead of
  // as it's processed, but that's the price to pay if you want headers
  // grouped together instead of interspersed with the actual methods
  // when both outputs go to stdout.
  
  fprintf(houtfile,"%s\n\n",[hfilestr stringValue]);
  fprintf(moutfile,"%s\n",[mfilestr stringValue]);

  // And clean up...
  
  if ([hName length]) fclose(houtfile);
  if ([mName length]) fclose(moutfile);
  if ([iName length]) fclose(infile);
  freestuff();
  
}

void freestuff()
{
  [hName free];
  [mName free];
  [iName free];
  [specstring free];
  [headstring free];
  [returnstr free];
  [mfilestr free];
  [hfilestr free];
  [[defaultList freeObjects] free];
  [[paramList freeObjects] free];
  [[paramvalList freeObjects] free];
  [[paramtypeList freeObjects] free];
  [mainstr free];
}

void usage(char *progname)
{
  fprintf(stderr,"\nusage: %s [-Ha] [-i inputfilename]\n",progname);
  fprintf(stderr,"               [-h hfilename -m mfilename | -f basefilename]\n");
  fprintf(stderr,"               [-e {h|m|hm}]\n\n");
  fprintf(stderr,"      %s -H for more verbose help.\n\n",progname);
  freestuff();
  exit(2);
}


void dohelp(char *progname)
{
  printf("usage: %s [-Ha] [-i filename] [-e {h|m|hm}]\n",progname);
  printf("          [-h hfilename -m mfilename | -f basefilename]\n\n");
  printf("     This program takes method headers with specified default values for\n");
  printf("    optional parameters, and outputs both headers and methods for\n");
  printf("    all possible degenerate (convenience) methods.\n");
  printf("     Reads from stdin if infile not specified, and outputs headers\n");
  printf("    and/or methods to stdout if destination is not specified.\n\n");
  printf("        -H     this help\n");
  printf("        -a     append to named output files; don't overwrite\n");
  printf("        -i     take input from named file\n");
  printf("        -h/-m  output headers/methods to named file.  Adds '.h'/'.m' to\n");
  printf("                  filename if not there already.\n");
  printf("        -f   same as -h basefilename.h -m basefilename.m\n");
  printf("        -e {h|m|hm} Excludes final {header|method|both} from being output.\n");
  printf("\n");
  printf("  INPUT SYNTAX: (one line per method)\n");
  printf("      - (int)spotOf:(char)aChar caseSensitive:(BOOL)sense[YES]\n");
  printf("       ^   ^          ^__________________________^       ^\n");
  printf("       |   |                  |                          |__NO space\n");
  printf("     SPACE |     CAST each parameter--even if id\n");
  printf("          Method cast is optional\n");
 
}
