import sys
if not hasattr(sys, 'version_info'):
  print '*** Python version 1 is not supported. Please get the latest version from www.python.org ***'
  sys.exit(4)

import nargs
useThreads = nargs.Arg.findArgument('useThreads', sys.argv[1:])
if useThreads is None:
  useThreads = 1
else:
  useThreads = int(useThreads)

import logging

class Script(logging.Logger):
  def __init__(self, clArgs = None, argDB = None):
    self.checkPython()
    logging.Logger.__init__(self, clArgs, argDB)
    self.shell = '/bin/sh'
    return

  def showHelp(self):
    '''Decide whether to display the help message and exit'''
    import nargs

    if nargs.Arg.findArgument('help', self.clArgs) is None and nargs.Arg.findArgument('h', self.clArgs) is None:
      return 0
    return 1

  def setupArguments(self, argDB):
    '''This method now also creates the help and action logs'''
    import help

    argDB = logging.Logger.setupArguments(self, argDB)

    self.help = help.Help(argDB)
    self.help.title = 'Script Help'

    self.actions = help.Info(argDB)
    self.actions.title = 'Script Actions'

    self.setupHelp(self.help)
    return argDB

  def setupHelp(self, help):
    '''This method should be overidden to provide help for arguments'''
    import nargs

    help.addArgument('Script', '-help', nargs.ArgBool(None, 0, 'Print this help message', isTemporary = 1), ignoreDuplicates = 1)
    help.addArgument('Script', '-h',    nargs.ArgBool(None, 0, 'Print this help message', isTemporary = 1), ignoreDuplicates = 1)
    return help

  def setup(self):
    ''' This method checks to see whether help was requested'''
    logging.Logger.setup(self)
    if self.argDB['help'] or self.argDB['h']:
      self.help.output()
      sys.exit()
    return

  def cleanup(self):
    '''This method outputs the action log'''
    self.actions.output(self.log)
    return

  def checkPython(self):
    if not hasattr(sys, 'version_info') or float(sys.version_info[0]) < 2 or float(sys.version_info[1]) < 2:
      raise RuntimeError('BuildSystem requires Python version 2.2 or higher. Get Python at http://www.python.org')
    return

  def getModule(root, name):
    '''Retrieve a specific module from the directory root, bypassing the usual paths'''
    import imp

    (fp, pathname, description) = imp.find_module(name, [root])
    try:
      return imp.load_module(name, fp, pathname, description)
    finally:
      if fp: fp.close()
    return
  getModule = staticmethod(getModule)

  def importModule(moduleName):
    '''Import the named module, and return the module object
       - Works properly for fully qualified names'''
    module     = __import__(moduleName)
    components = moduleName.split('.')
    for comp in components[1:]:
      module = getattr(module, comp)
    return module
  importModule = staticmethod(importModule)

  def openPipe(command):
    '''We need to use the asynchronous version here since we want to avoid blocking reads'''
    import popen2

    pipe = None
    if hasattr(popen2, 'Popen3'):
      pipe   = popen2.Popen3(command, 1)
      input  = pipe.tochild
      output = pipe.fromchild
      err    = pipe.childerr
    else:
      import os

      (input, output, err) = os.popen3(command)
    return (input, output, err, pipe)
  openPipe = staticmethod(openPipe)

  def runShellCommand(command, log = None):
    import select

    ret        = None
    out        = ''
    err        = ''
    loginError = 0
    if log: log.write('Executing: '+command+'\n')
    (input, output, error, pipe) = Script.openPipe(command)
    input.close()
    outputClosed = 0
    errorClosed  = 0
    while 1:
      ready = select.select([output, error], [], [])
      if len(ready[0]):
        if error in ready[0]:
          msg = error.readline()
          if msg:
            err += msg
          else:
            errorClosed = 1
        if output in ready[0]:
          msg = output.readline()
          if msg:
            out += msg
          else:
            outputClosed = 1
        if out.find('password:') >= 0 or err.find('password:') >= 0:
          loginError = 1
          break
      if outputClosed and errorClosed:
        break
    output.close()
    error.close()
    if pipe:
      # We would like the NOHANG argument here
      ret = pipe.wait()
    if loginError:
      raise RuntimeError('Could not login to site')
    return (out, err, ret)
  runShellCommand = staticmethod(runShellCommand)

  def defaultCheckCommand(command, status, output, error):
    '''Raise an error if the exit status is nonzero'''
    if status: raise RuntimeError('Could not execute \''+command+'\':\n'+output+error)
  defaultCheckCommand = staticmethod(defaultCheckCommand)

  def executeShellCommand(command, checkCommand = None, timeout = 600.0, log = None):
    '''Execute a shell command returning the output, and optionally provide a custom error checker
       - This returns a tuple of the (output, error, statuscode)'''
    import threading
    global output, error, status

    def logOutput(log, output):
      import re
      # get rid of multiple blank lines
      output = re.sub('\n[\n]*','\n', output)
      if len(output) < 600:
        if log: log.write('sh: '+output+'\n')
      else:
        if log:
          log.write('sh: '+output[:600]+'...\n')
          log.write('... '+output[-600:]+'\n')
      return output

    if log is None:
      log = logging.Logger.defaultLog
    if log:
      log.write('sh: '+command+'\n')
    if useThreads:
      status = -1
      output = 'Runaway process'
      def run(command, log):
        global output, error, status
        (output, error, status) = Script.runShellCommand(command, log)
        return

      thread = threading.Thread(target = run, name = 'Shell Command', args = (command, log))
      thread.setDaemon(1)
      thread.start()
      thread.join(timeout)
      if thread.isAlive():
        error  = 'Runaway process exceeded time limit of '+str(timeout)+'s\n'
        status = -1
        if log: log.write(error)
      else:
        output = logOutput(log, output)
    else:
      (output, error, status) = Script.runShellCommand(command, log)
      output                  = logOutput(log, output)
    if checkCommand:
      checkCommand(command, status, output, error)
    else:
      Script.defaultCheckCommand(command, status, output, error)
    return (output, error, status)
  executeShellCommand = staticmethod(executeShellCommand)

  def getDebugger(self, className = 'PETSc.DebugI.GDB.Debugger'):
    if not hasattr(self, '_debugger'):
      try:
        import SIDL.Loader
      except ImportError, e:
        self.logPrint('Cannot locate a functional SIDL loader: '+str(e))
        return
      try:
        import PETSc.Debug.Debugger
      except ImportError, e:
        self.logPrint('Could not load Petsc debugger module: '+str(e))
        return
      debugger = PETSc.Debug.Debugger.Debugger(SIDL.Loader.createClass(className))
      if not debugger:
        self.logPrint('Could not load debugger: '+cls)
        return
      debugger.setProgram('/usr/local/python/bin/python')
      debugger.setUseXterm(1)
      debugger.setDebugger('/usr/local/gdb/bin/gdb')
      debugger.attach()
      self._debugger = debugger
    return self._debugger
  def setDebugger(self, debugger):
    self._debugger = debugger
    return
  debugger = property(getDebugger, setDebugger, doc = 'The debugger')

  def loadConfigure(self, argDB = None):
    import cPickle

    if argDB is None:
      argDB = self.argDB
    if not 'configureCache' in argDB:
      self.logPrint('No cached configure in RDict')
      return None
    try:
      cache           = argDB['configureCache']
      framework       = cPickle.loads(cache)
      framework.argDB = argDB
      self.logPrint('Loaded configure to cache: size '+str(len(cache)))
    except cPickle.UnpicklingError, e:
      framework       = None
      self.logPrint('Invalid cached configure: '+str(e))
    return framework

import args

class OutputFiles(dict):
  def __init__(self):
    dict.__init__(self)
    self.isChanged = 0
    return

  def __setstate__(self, d):
    '''Mark as unchanged'''
    self.__dict__.update(d)
    self.isChanged = 0
    return

  def __setitem__(self, key, value):
    '''Mark as changed, clearing any loaded values'''
    if not self.isChanged and len(self) > 0:
      self.clear()
    self.isChanged = 1
    return dict.__setitem__(self, key, value)

  def __delitem__(self, key, value):
    '''Mark as changed'''
    self.isChanged = 1
    return dict.__delitem__(key, value)

class LanguageProcessor(args.ArgumentProcessor):
  def __init__(self, clArgs = None, argDB = None, compilers = None, libraries = None, versionControl = None):
    self.languageModule     = {}
    self.preprocessorObject = {}
    self.compilerObject     = {}
    self.linkerObject       = {}
    self.sharedLinkerObject = {}
    self.compilers          = compilers
    self.libraries          = libraries
    self.versionControl     = versionControl
    args.ArgumentProcessor.__init__(self, clArgs, argDB)
    self.outputFiles        = OutputFiles()
    self.modulePath         = 'config.compile'
    return

  def __getstate__(self, d = None):
    '''We only want to pickle the language module names and output files. The other objects are set by configure.'''
    if d is None:
      d = args.ArgumentProcessor.__getstate__(self)
    if 'languageModule' in d:
      d['languageModule'] = dict([(lang,mod._loadName) for lang,mod in d['languageModule'].items()])
    for member in ['preprocessorObject', 'compilerObject', 'linkerObject', 'sharedLinkerObject', 'compilers', 'libraries', 'versionControl']:
      del d[member]
    return d

  def __setstate__(self, d):
    '''We must create the language modules'''
    args.ArgumentProcessor.__setstate__(self, d)
    self.__dict__.update(d)
    [self.getLanguageModule(language, moduleName) for language,moduleName in self.languageModule.items()]
    self.preprocessorObject = {}
    self.compilerObject     = {}
    self.linkerObject       = {}
    self.sharedLinkerObject = {}
    self.compilers          = None
    self.libraries          = None
    self.versionControl     = None
    return

  def setArgDB(self, argDB):
    args.ArgumentProcessor.setArgDB(self, argDB)
    for obj in self.preprocessorObject.values():
      if not hasattr(obj, 'argDB') or not obj.argDB == argDB:
        obj.argDB = argDB
    for obj in self.compilerObject.values():
      if not hasattr(obj, 'argDB') or not obj.argDB == argDB:
        obj.argDB = argDB
    for obj in self.linkerObject.values():
      if not hasattr(obj, 'argDB') or not obj.argDB == argDB:
        obj.argDB = argDB
    for obj in self.sharedLinkerObject.values():
      if not hasattr(obj, 'argDB') or not obj.argDB == argDB:
        obj.argDB = argDB
    if not self.compilers is None:
      self.compilers.argDB = argDB
      for obj in self.preprocessorObject.values():
        if hasattr(obj, 'configCompilers'):
          obj.configCompilers.argDB = argDB
      for obj in self.compilerObject.values():
        if hasattr(obj, 'configCompilers'):
          obj.configCompilers.argDB = argDB
      for obj in self.linkerObject.values():
        if hasattr(obj, 'configCompilers'):
          obj.configCompilers.argDB = argDB
      for obj in self.sharedLinkerObject.values():
        if hasattr(obj, 'configCompilers'):
          obj.configCompilers.argDB = argDB
    if not self.libraries is None:
      self.libraries.argDB = argDB
      for obj in self.linkerObject.values():
        if hasattr(obj, 'configLibraries'):
          obj.configLibraries.argDB = argDB
      for obj in self.sharedLinkerObject.values():
        if hasattr(obj, 'configLibraries'):
          obj.configLibraries.argDB = argDB
    return
  argDB = property(args.ArgumentProcessor.getArgDB, setArgDB, doc = 'The RDict argument database')

  def normalizeLanguage(self, language):
    '''Canonicalize the language name'''
    return language.replace('+', 'x')

  def getLanguageModule(self, language, moduleName = None):
    '''Return the module associated with operations for a given language
       - Giving a moduleName explicitly forces a reimport'''
    language = self.normalizeLanguage(language)
    if not language in self.languageModule or not moduleName is None:
      try:
        if moduleName is None:
          moduleName = self.modulePath+'.'+language
        module     = __import__(moduleName)
      except ImportError, e:
        if not moduleName is None:
          self.logPrint('Failure to find language module: '+str(e))
        try:
          moduleName = self.modulePath+'.'+language
          module     = __import__(moduleName)
        except ImportError, e:
          self.logPrint('Failure to find language module: '+str(e))
          moduleName = 'config.compile.'+language
          module     = __import__(moduleName)
      components = moduleName.split('.')
      for component in components[1:]:
        module   = getattr(module, component)
      module._loadName = moduleName
      self.languageModule[language] = module
    return self.languageModule[language]

  def getPreprocessorObject(self, language):
    language = self.normalizeLanguage(language)
    if not language in self.preprocessorObject:
      self.preprocessorObject[language] = self.getLanguageModule(language).Preprocessor(self.argDB)
      self.preprocessorObject[language].setup()
    if not self.compilers is None:
      self.preprocessorObject[language].configCompilers = self.compilers
    if not self.versionControl is None:
      self.preprocessorObject[language].versionControl  = self.versionControl
    return self.preprocessorObject[language]

  def setPreprocessorObject(self, language, preprocessor):
    language = self.normalizeLanguage(language)
    self.preprocessorObject[language] = preprocessor
    return self.getPreprocessorObject(language)

  def getCompilerObject(self, language):
    language = self.normalizeLanguage(language)
    if not language in self.compilerObject:
      self.compilerObject[language] = self.getLanguageModule(language).Compiler(self.argDB)
      self.compilerObject[language].setup()
    if not self.compilers is None:
      self.compilerObject[language].configCompilers = self.compilers
    if not self.versionControl is None:
      self.compilerObject[language].versionControl  = self.versionControl
    return self.compilerObject[language]

  def setCompilerObject(self, language, compiler):
    language = self.normalizeLanguage(language)
    self.compilerObject[language] = compiler
    return self.getCompilerObject(language)

  def getLinkerObject(self, language):
    language = self.normalizeLanguage(language)
    if not language in self.linkerObject:
      self.linkerObject[language] = self.getLanguageModule(language).Linker(self.argDB)
      self.linkerObject[language].setup()
    if not self.compilers is None:
      self.linkerObject[language].configCompilers = self.compilers
    if not self.libraries is None:
      self.linkerObject[language].configLibraries = self.libraries
    if not self.versionControl is None:
      self.linkerObject[language].versionControl  = self.versionControl
    return self.linkerObject[language]

  def setLinkerObject(self, language, linker):
    language = self.normalizeLanguage(language)
    self.linkerObject[language] = linker
    return self.getLinkerObject(language)

  def getSharedLinkerObject(self, language):
    language = self.normalizeLanguage(language)
    if not language in self.sharedLinkerObject:
      self.sharedLinkerObject[language] = self.getLanguageModule(language).SharedLinker(self.argDB)
      self.sharedLinkerObject[language].setup()
    if not self.compilers is None:
      self.sharedLinkerObject[language].configCompilers = self.compilers
    if not self.libraries is None:
      self.sharedLinkerObject[language].configLibraries = self.libraries
    if not self.versionControl is None:
      self.sharedLinkerObject[language].versionControl  = self.versionControl
    return self.sharedLinkerObject[language]

  def setSharedLinkerObject(self, language, linker):
    language = self.normalizeLanguage(language)
    self.sharedLinkerObject[language] = linker
    return self.getSharedLinkerObject(language)
