/**
 ** Module.m
 **/


#import "Eval.h"
#import "Module.h"
#import <objc/objc-load.h>
#include <mach/mach_interface.h>
#import <mach-o/arch.h>
#include <sys/types.h>
#include <sys/dir.h>
#include <sys/stat.h>
#include <strings.h>

static int unused ;

@implementation Module: Object
{ EvalString *path ;      // path to .a, .o, or .m file
  EvalString *name ;      // a unique, humanly-readable name for this module
  EvalString *location ;  // for .a or .o: path of the file or directory we created. nil for .m
  char **components ;     // array of char *'s with dir paths to component .o and .a files
  moduleType modType ;    // the type, i.e. DOTM,DOTO,DOTA
  BOOL loaded ;           // == YES iff currently loaded, else == NO
}

- (EvalString *) compile: (char *) filePath libs:(char **) aLibString ;
{ // Compile (but don't link) the file at filePath.
  // If successful, returns the full path of the resulting .o file which
  // is created and left in /tmp.  It is the callers responsibility
  // to eventually free this file, as well as the returned EvalString.
  // If unsuccessful, returns nil.  filePath should have a .m extension.
  // This file may contain, as its first line, a #pragma compile <flags>,
  // specifying compiler switches. If it does not,  
  // the NXdefault "compilerSwitches" (set in the prefs panel) is used instead.
  // filePath may contain, as its second line (or first, if no #pragma compile line
  // is present), a #pragma link <modulePath1, modulePath2, ...modulePathn> line,
  // specifying a list of libraries to be searched when this file is linked.  If
  // this line is not present, aLibString will be set to NULL, else aLibString will
  // be set to the string following #pragma link, and caller is responsible for
  // freeing
  char *buf, *errorFileName, *compileBuf ;
  char tmpName[20] = "/tmp/EvalXXXXXXXX" ;
  const char *compilerFlags ;
  int unused, rval = NO, sysRval ;
  NXStream *fileStream, *errorStream, *aStream =  NXOpenMemory(NULL, 0, NX_READWRITE) ;
  fileStream = NXMapFile(filePath, NX_READONLY) ;
  if(!fileStream)
  { [Eval printf: "Can't open file: %s", buf] ;
    NXRunAlertPanel("Eval","Can't open file: %s\n",NULL,NULL,NULL,buf) ;
  }
  else // check first two lines of file for #pragma compile and #pragma link
  { [self findCompilePragma: &compileBuf linkPragma: aLibString inStream: fileStream] ;
    NXCloseMemory(fileStream,NX_FREEBUFFER) ;
    compilerFlags = compileBuf ?
      compileBuf:NXGetDefaultValue([NXApp appName],"CompilerSwitches");
    mkTempFile(NULL,tmpName) ;
    errorStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
    NXPrintf(errorStream,"%s.cc_errors%c",tmpName,0) ;
    NXGetMemoryBuffer(errorStream,&errorFileName,&unused,&unused) ;
    NXSeek(aStream,0L,NX_FROMSTART) ;
    NXPrintf(aStream,"cc -c %s -o %s.o %s 2> %s%c",
        compilerFlags,tmpName,filePath,errorFileName,0) ;
    NXGetMemoryBuffer(aStream, &buf, &unused, &unused);
    [NXApp message: "Compiling..."] ;
    sysRval = system(buf) ;
    if(sysRval) 
    { // Non-zero exit means errors....show compiler output.
      [NXApp printFileToTranscript: errorFileName] ;
      NXRunAlertPanel("Eval","Compilation errors: see Transcript Window\n",
           NULL,NULL,NULL) ;
    }
    else
    { // Compile succeeded, only show compiler output if requested.
      if(!strcmp(NXGetDefaultValue([NXApp appName],"CompilerOutput"),"1"))  
        [NXApp printFileToTranscript: errorFileName] ; // show all compiler output
      rval = YES ;
    }
    unlink(errorFileName) ;
    NXCloseMemory(errorStream,NX_FREEBUFFER) ; // freeup streams
    NXCloseMemory(aStream,NX_FREEBUFFER) ; 
    if(compileBuf) // free compile switches buffer, if necessary
      vm_deallocate(task_self(),(vm_address_t) compileBuf,strlen(compileBuf)+1) ; 
  }
  [NXApp message: ""] ;
  if(rval)
  { strcat(tmpName,".o") ;
    return [EvalString new: tmpName] ;
  }
  else
    return nil ;
}

- (char **) components ;
{ return components ;
}

- componentsFromList: (List *) aList ;
{ // Allocate and set our componentsArray from 
  // the list of EvalString's in aList.  The EvalString's
  // are then "partialFree"'d...i.e. we don't free
  // the char * which they wrap, because these are
  // used by our components.
  int i,knt = [aList count] ;
  components = (char **) malloc((knt + 1) * sizeof(char *)) ;
  for(i = 0 ; i < knt ; i++)
    components[i] = [[aList objectAt: i] partialFree] ;
  components[i] = NULL ; // null terminate
  return self ;    
}

- (BOOL) equals: (Module *) aMod ;
{ if(!strcmp([aMod name],[self name]))
    return YES ;
  return NO ;
}

- findCompilePragma: (char **) compileBuf 
         linkPragma: (char **) linkBuf
           inStream: (NXStream *) fileStream ;
{ // fileStream must point to a valid stream opened for reading.
  // It is examined for a #pragma compile ...test... and
  // #pragma link ...text... If either is found, the corresponding
  // bufs will be allocated and set to <text>, null-terminated, otherwise they
  // will be NULL. A compile pragma (if present)
  // must precede a link pragma (if present).  The pragma(s), if present, must
  // begin at the beginning of fileStream. Continuation lines (i.e. last char
  // of a line is a backslash) may be used to allow the pragmas to
  // span multiple lines,.  It is the caller's responsibility
  // to free any allocated buffers using vm_deallocate.
  int i,j, c ;
  NXStream *aStream ;
  NXSeek(fileStream,0L,NX_FROMSTART) ;
  i = 0 ;
  while(NXGetc(fileStream) == ("#pragma compile ")[i++]) ;
  if(i == 17) // compilation line found
  { NXUngetc(fileStream) ;
    aStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
    while(!NXAtEOS(fileStream) && (c = NXGetc(fileStream)) != '\n')
    { if(c == '\\') // continuation line?
      { if(NXGetc(fileStream) == '\n')
          continue ;
        else
          NXUngetc(fileStream) ;
      }
      NXPutc(aStream,c) ;
    }
    NXPutc(aStream,'\0') ;
    NXGetMemoryBuffer(aStream,compileBuf, &i, &j);
    NXCloseMemory(aStream,NX_TRUNCATEBUFFER) ;
  }
  else
  { *compileBuf = NULL ;
    NXSeek(fileStream,0L,NX_FROMSTART) ;
  }
  i = 0 ;
  while(NXGetc(fileStream) == ("#pragma link ")[i++]) ;
  if(i == 14) // link line found
  { NXUngetc(fileStream) ;
    aStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
    while(!NXAtEOS(fileStream) && (c = NXGetc(fileStream)) != '\n')
    { if(c == '\\') // continuation line?
      { if(NXGetc(fileStream) == '\n')
          continue ;
        else
          NXUngetc(fileStream) ;
      }
      NXPutc(aStream,c) ;
    }
    NXPutc(aStream,'\0') ;
    NXGetMemoryBuffer(aStream, linkBuf, &i, &j);
    NXCloseMemory(aStream,NX_TRUNCATEBUFFER) ;
  }
  else
   *linkBuf = NULL ;
  return self ;
} 

- free ;
{ if((modType == DOTA) || (modType == DOTM))
  { if(location)
    { char *buf ;
      NXStream *aStream = NXOpenMemory(NULL,0,NX_READWRITE) ;
      NXPrintf(aStream,"rm -r %s 2> /dev/null%c",[location cString],0) ;
      NXGetMemoryBuffer(aStream,&buf,&unused,&unused) ;      
      system(buf) ;
      [location free] ;
      NXCloseMemory(aStream,NX_FREEBUFFER) ;
    }  
  }
  if(path)
    [path free] ;
  if(name)
    [name free] ;
  return [super free] ;
}


- initPath:(char *)aPath ;
{ // Create a Module instance.  If there
  // are errors, frees self and returns nil.
  struct stat statBuf ;
  char extension, *libString = NULL ;
  List *componentList ;
  const char *nameBuf ;
  EvalString *dotOFilePath ;

  if(!aPath || stat(aPath,&statBuf))
  { // Make sure file exists.
    [self free] ;
    return nil ;
  }
  // save path to file
  path = [[EvalString alloc] init: aPath] ;
  extension = aPath[strlen(aPath) - 1] ;
  switch(extension) // What type of file is it: .m,.o,or .a?
  { // we initialize differently for .m,.o, and .a files
    case 'a': 
    { // this is by far the most complex case...    
      char tmpDirectory[20] =  "/tmp/EvalXXXXXXXX", *buf ;
      int i, j, knt ;
      struct direct **filesList ;
      long pathEnd ;
      NXStream *aStream ; 
      // DOTA Modules are named by corresponding library
      char *lastSlash = rindex(aPath,'/') ;
      if(lastSlash)
        name = [[EvalString alloc] init: ++lastSlash] ;
      else
        name = [[EvalString alloc] init: aPath] ;
      // create a temporary directory
      mkTempFile(NULL,tmpDirectory) ;
      if(mkdir(tmpDirectory, 0xffff))
      { // non-zero return = report mkdir error here and return
        [Eval printf: "Can't create temporary directory: %s\n",tmpDirectory] ;
        return nil ;
      } 
      location = [[EvalString alloc] init: tmpDirectory] ;
      // extract archives into temporary directory: if ar fails, we then
      // try lipo in case this is a fat file
      aStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
      NXPrintf(aStream,"cd %s ;  if ar x %s 2> /dev/null\n"
                                "  then exit\n"
				" elif lipo -thin `arch` -output __tmp.a %s 2> /dev/null\n"
				"   then ar x __tmp.a ; rm __tmp.a\n"
				"fi%c",
	 tmpDirectory, aPath, aPath, 0) ;
      NXGetMemoryBuffer(aStream,&buf,&unused,&unused) ;      
      if(system(buf))
      { // non-zero return == report error and return
        [Eval printf: "Cant extract archives via: %s\n",buf] ;
        NXCloseMemory(aStream,NX_FREEBUFFER) ;
        return nil ;
     }
     NXSeek(aStream,0,NX_FROMSTART) ;
     componentList = [self tokensFromString: NXGetDefaultValue([NXApp appName],"Libraries")] ;
     // stat the tmp directory to get all filenames, then put them all
     // into the componentList, provided they don't start with a "__". 
     knt = scandir(tmpDirectory,&filesList,NULL,NULL)  ;
     NXPrintf(aStream,"%s/",tmpDirectory) ;
     pathEnd = NXTell(aStream);
     for(i = j = 0 ; i < knt ; i++)
     { // load modules with file names, skipping  ., .., and __.SYMDEF*.
       buf = (*filesList[i]).d_name ;
       if(strcmp(buf,".") && strcmp(buf,"..") &&
          strncmp(buf,"__.SYMDEF",9))
       { NXPrintf(aStream,"%s%c",buf,0) ;
         NXGetMemoryBuffer(aStream,&buf,&unused,&unused) ;      
         [componentList insertObject: [[EvalString alloc] init: buf] at: j++] ;
         NXSeek(aStream,pathEnd,NX_FROMSTART) ;
       }
       free(filesList[i]) ;
     }
     free(filesList) ; 
     NXCloseMemory(aStream,NX_FREEBUFFER) ;
     // create the components array
     [self componentsFromList: componentList] ;
     [componentList free] ;	
     modType = DOTA ;
     break ;
    }
    case 'm':
    { // a DOTM file must be compiled, creating a .o file
      if((dotOFilePath = [self compile: aPath libs: &libString]) == nil)
      { // compile failed
        [self free] ;
        return nil ;
      }
      location = [[EvalString alloc] init: [dotOFilePath cString]] ;
      modType = DOTM ;
    }
    case 'o':
    { // From this point on, .m and .o files are treated virtually
      // identically, so we combine the two cases:
      if(extension == 'o')
      { dotOFilePath = [[EvalString alloc] init: aPath] ;
        modType = DOTO ;
      }
      if(!libString) // no libString supplied in .m...use preferences value
        componentList = 
          [self tokensFromString: NXGetDefaultValue([NXApp appName],"Libraries")] ;
      else
      { componentList = [self tokensFromString: libString] ;
        vm_deallocate(task_self(),(vm_address_t) libString,strlen(libString)+1) ; 
      }
      [componentList insertObject: dotOFilePath at: 0] ;
      classNames([dotOFilePath cString], &nameBuf) ;
      name = [EvalString new: nameBuf] ;
      // create the components array
      [self componentsFromList: componentList] ;
      [componentList free] ;
      // NOTE: dotOFilePath is freed in componentsFromList:
      break ;
    }
    default:
    { // unknown type
      [self free] ;
      return nil ;
    }
  }
   return self ;  
}


- load ;
{ // If not loaded, load this module and, if
  // successful, return self, else return nil.
  id rval = nil ; 
  if(!loaded)  
  { NXStream *aStream = NXOpenMemory(NULL, 0, NX_WRITEONLY) ;
    char *buf ;
    if(objc_loadModules(components, aStream, NULL, NULL, NULL))
    { // load failed...
      NXPutc(aStream,'\0') ; 
      NXGetMemoryBuffer(aStream, &buf, &unused, &unused); 
      [Eval printf: buf];
      NXRunAlertPanel("Eval","Load of module %s failed,\nsee transcript for details",
         NULL,NULL,NULL,[self name]) ;
    }
    else
    { rval = self ;
      loaded = YES ;
    }
    NXCloseMemory(aStream,NX_FREEBUFFER) ;
  }
  return rval ;
}

- (const char *) name ;
{ return [name cString] ;
}

- (const char *) path ;
{ return [path cString] ;
}

- (List *) tokensFromString: (const char *) str
{ // allocate and return a list containing
  // tokens from the given string. Caller is
  // responsible for freeing this list.
  const char *delims = " \n\t" ;
  const char *aString ;
  List * aList = [[List alloc] init] ;
  if(aString = strtok((char *) str,delims))
  { do
    { [aList addObject: [EvalString new: aString]] ;
    } while(aString = strtok(NULL,delims)) ;
  }
  return aList ;
}


- unload ;
{ // If loaded, pop a module from the module stack
  // and return self. 
  // It is caller's responsibility to ensure that 
  // this module is on top of the stack...there
  // is no way to verify this here. Returns nil if not
  // currently loaded (and doesn't do the pop).
  if(loaded)
  { objc_unloadModules(NULL, NULL) ;
    loaded = NO ;
    return self ;
  }
  return nil ;
}


@end

/*    // remove the tmp directory's contents
    for(i = 0 ; i < fileCount ; i++)
    { // delete modules.
      char *moduleName = (*filesList[i]).d_name ;
      if(strcmp(moduleName,".") && strcmp(moduleName,".."))
      { if(unlink(moduleName))
        { // error, couldn't unlink
          return nil ;
        }
      }
    }
    if(rmdir(tmpDirectory))
    { // errors, couldn't delete tmp directory
      return self ;
    }
*/    
