# Copyright 2004-2007 Nanorex, Inc.  See LICENSE file for details. 
"""
files_gms.py -- reading and writing GAMESS files

@author: Mark
@version: $Id$
@copyright: 2004-2007 Nanorex, Inc.  See LICENSE file for details.

History:

GAMESS file IO was part of GamessJob.py until I moved it here 
to make it more modular and consistent.
"""

import os, re, time
from model.chunk import Chunk
from model.chem import Atom
from string import capitalize
from model.elements import PeriodicTable
from platform_dependent.PlatformDependent import get_gms_name
from utilities.Log import redmsg, orangemsg
from geometry.VQT import A
import foundation.env as env

failpat = re.compile("ABNORMALLY")
errorPat = re.compile("Fatal error")
noconvpat = re.compile("GEOMETRY SEARCH IS NOT CONVERGED")
irecpat = re.compile(" (\w+) +\d+\.\d* +([\d\.E+-]+) +([\d\.E+-]+) +([\d\.E+-]+)")

def _readgms(assy, filename, isInsert=False):
    """
    Read the atoms from a GAMESS DAT file into a single new chunk, which is returned,
    unless there are no atoms in the file, in which case a warning is printed
    and None is returned. (The new chunk (if returned) is in assy, but is not
    yet added into any Group or Part in assy -- caller must do that.)
    """
    fi = open(filename,"rU")
    lines = fi.readlines()
    fi.close()
    
    dir, nodename = os.path.split(filename)
    ndix = {}
    mol = Chunk(assy, nodename)
    countdown = 0
    equilibruim_found = False
    atoms_found = False
    
    for card in lines:

        if failpat.search(card): # GAMESS Aborted.  No atom data will be found.
            print card
            break
        
        # If this card is found:
        # "1     ***** EQUILIBRIUM GEOMETRY LOCATED *****\n"
        # we know we have a successfully optimized structure/set of atoms.
        # If this card is not found, the optimization failed for some reason.
        # Atom positions begin soon after this card.
        if card == "1     ***** EQUILIBRIUM GEOMETRY LOCATED *****\n":
            equilibruim_found = True
            continue
            
        # The atom positions we want ALWAYS begin 2 lines after this card:
        # " COORDINATES OF ALL ATOMS ARE (ANGS)\n"
        # which follows the previous card.
        # This is one way to fix the problem mentioned above.
        # I've commented the code below out since it needs further work to do what
        # we need, and there is a chance we will not need this if GAMESS-US has
        # the same number of lines (6) after the "EQUILIBRIUM" card above.
        #
        # P.S. The reason we do not just look for this card by itself is that there
        # can be many of them.  There is only one "EQUILIBRIUM" card, and the
        # good atoms follow that card.
        # 050624 Mark
        
        if equilibruim_found:
            if card == " COORDINATES OF ALL ATOMS ARE (ANGS)\n":
                atoms_found = True
                reading_atoms = True
                countdown = 2
                continue
        
        if not equilibruim_found or not atoms_found:
            continue
            
        if countdown:
            countdown -= 1
#            print countdown, card # for debugging only.
            continue

        # The current card contains atom type and position. 
        
        n = 0
        
        if reading_atoms:
            if len(card)<10: 
                reading_atoms = False # Finished reading atoms.
                break
            m=irecpat.match(card)
            sym = capitalize(m.group(1))
            try:
                PeriodicTable.getElement(sym)
            except:
                env.history.message( redmsg( "Warning: GAMESS DAT file: unknown element %s in: %s" % (sym,card) ))
            else:
                xyz = map(float, (m.group(2),m.group(3), m.group(4)))
                a = Atom(sym, A(xyz), mol)
                ndix[n] = a
                n += 1
            
    # Don't return an empty chunk.
    if not mol.atoms:
        msg = "Warning: GAMESS file contains no equilibrium geometry.  No atoms read into part."
        env.history.message( redmsg(msg))
        return None
    
    # Need to compute and add bonds for this chunk.  I'll ask Bruce how to best accomplish this.
    # In the meantime, let's warn the user that no bonds have been formed since it
    # is impossible to see this in vdW display mode.  
    # Mark 050623.
    msg = "Warning: Equilibrium geometry found.  Atoms read into part, but there are no bonds."
    env.history.message( orangemsg(msg))
    return mol
    
# Read a GAMESS DAT file into a single chunk
def readgms(assy,filename):
    """
    Reads a GAMESS DAT file.
    Returns: 0 = Success
                   1 = Failed
    """
    mol  = _readgms(assy, filename, isInsert = False)
    if mol is not None:
        assy.addmol(mol)
        return 0
    else:
        return 1
    
# Insert a GAMESS DAT file into a single chunk.
def insertgms(assy,filename):
    """
    Reads a GAMESS DAT file and inserts it into the existing model.
    Returns: 0 = Success
                   1 = Failed
    """
    mol  = _readgms(assy, filename, isInsert = True)
    if mol is not None:
        assy.addmol(mol)
        return 0
    else:
        return 1
        

# Insert a GAMESS DAT file into a single chunk.
def insertgms_new(assy,filename):
    """
    Reads a GAMESS DAT file and inserts it into the existing model.
    Returns: 0 = Success
                   1 = Failed
    """

    gmsAtomList  = _get_atomlist_from_gms_outfile(assy, filename)
    
    if not gmsAtomList: 
        return 1 # No atoms read.
    
    dir, nodename = os.path.split(filename)
    mol = Chunk(assy, nodename)
    ndix = {}
    
    n = 0
    
    for a in gmsAtomList:
        print a
        pos = a.posn()
        fpos = (float(pos[0]), float(pos[1]), float(pos[2]))
        na = Atom(a.element.symbol, fpos, mol)
        ndix[n] = na
        n += 1
    
    if mol is not None:
        assy.addmol(mol)
        return 0
    else:
        return 1


# This was copied from _readgms and is mainly the same.  This will live, _readgms will
# be deleted eventually, after I ask Bruce how to get an alist from the mol (dict) in the same order
# as it was read from the file.   Since Python stores dict items in any order it chooses,
# I created a list with the same order that the atoms are read from the GAMESS OUT file.
# Mark 050712.
def _get_atomlist_from_gms_outfile(assy, filename):
    """
    Read the atoms from a GAMESS OUT file into an atom list, which is returned,
    unless there are no atoms in the file, in which case a warning is printed
    and None is returned.
    """
    fi = open(filename,"rU")
    lines = fi.readlines()
    fi.close()
    
    dir, nodename = os.path.split(filename)
    mol = Chunk(assy, nodename)
    
    newAtomList = [] 
    countdown = 0
    equilibruim_found = False
    atoms_found = False
    
    for card in lines:

        if failpat.search(card): # GAMESS Aborted.  No atom data will be found.
            print card
            env.history.message( redmsg( card ))
            break
            
        if noconvpat.search(card): # Geometry search is not converged.
            print card
            env.history.message( redmsg( card ))
            break
        
        # If this card is found:
        # "1     ***** EQUILIBRIUM GEOMETRY LOCATED *****\n"
        # we know we have a successfully optimized structure/set of atoms.
        # If this card is not found, the optimization failed for some reason.
        # Atom positions begin soon after this card.
        if card == "1     ***** EQUILIBRIUM GEOMETRY LOCATED *****\n":
            equilibruim_found = True
            continue
            
        # The atom positions we want ALWAYS begin 2 lines after this card:
        # " COORDINATES OF ALL ATOMS ARE (ANGS)\n"
        # which follows the previous card.
        # This is one way to fix the problem mentioned above.
        # I've commented the code below out since it needs further work to do what
        # we need, and there is a chance we will not need this if GAMESS-US has
        # the same number of lines (6) after the "EQUILIBRIUM" card above.
        #
        # P.S. The reason we do not just look for this card by itself is that there
        # can be many of them.  There is only one "EQUILIBRIUM" card, and the
        # good atoms follow that card.
        # 050624 Mark
        
        if equilibruim_found:
            if card == " COORDINATES OF ALL ATOMS ARE (ANGS)\n":
                atoms_found = True
                reading_atoms = True
                countdown = 2
                continue
        
        if not equilibruim_found or not atoms_found:
            continue
            
        if countdown:
            countdown -= 1
#            print countdown, card # for debugging only.
            continue

        # The current card contains atom type and position. 
        
        n = 0
        
        if reading_atoms:
#            print "_get_atomlist_from_gms_outfile:", card
            if len(card)<10: 
                reading_atoms = False # Finished reading atoms.
                break
            m=irecpat.match(card)
            sym = capitalize(m.group(1))
            try:
                PeriodicTable.getElement(sym)
            except:
                env.history.message( redmsg( "Warning: GAMESS OUT file: unknown element %s in: %s" % (sym,card) ))
            else:
                xyz = map(float, (m.group(2),m.group(3), m.group(4)))
                a = Atom(sym, A(xyz), mol)
                newAtomList += [a]
            
# Let caller handle history msgs.  Mark 050712
#    if not newAtomList:
#        msg = "Warning: GAMESS file contains no equilibrium geometry.  No atoms read into part."
#        env.history.message( redmsg(msg))
#        return None
    
    return newAtomList
    
        
# Read a GAMESS OUT file into a single chunk
def get_atompos_from_gms_outfile(assy, filename, atomList):
    """
    Reads a GAMESS DAT file and returns the xyz positions of the atoms.
    """
    gmsAtomList  = _get_atomlist_from_gms_outfile(assy, filename)
    
    if not gmsAtomList:
        msg = "No atoms read from file " + filename
        print msg
        return msg
    
    newAtomsPos = [] 
    atomIndex = 0
    
    for a in gmsAtomList:

#        print atomIndex + 1, a.element.symbol, atomList[atomIndex].element.symbol
        
        if a.element.symbol != atomList[atomIndex].element.symbol:
            msg = "The atom type (%s) of atom # %d from %s is not matching with the Gamess jig (%s)." % \
            (a.element.symbol, atomIndex + 1, filename, atomList[atomIndex].element.symbol)
            print msg
            return msg
        
        pos = a.posn()
        newAtomsPos += [map(float, pos)]
        
        atomIndex += 1
            
    if (len(newAtomsPos) != len(atomList)):
        msg = "The number of atoms from %s (%d) is not matching with the Gamess jig (%d)." % \
            (filename, len(newAtomsPos), len(atomList))
        print msg
        return msg
    
    return newAtomsPos
    
# File Writing Methods.
        
def writegms_inpfile(filename, gamessJig):
    """
    Writes a GAMESS INP file from a GAMESS Jig.
    """
    pset = gamessJig.pset
        
    f = open(filename,'w') # Open GAMESS input file.
        
    # Write header
    f.write ('!\n! INP file created by NanoEngineer-1 on ')
    timestr = "%s\n!\n" % time.strftime("%Y-%m-%d at %H:%M:%S")
    f.write(timestr)
    gmstr = "! " + get_gms_name() + " parameter summary: " + gamessJig.gms_parms_info() + "\n!\n"
    f.write(gmstr)
        
    # This method should be moved to the GAMESS Jig.
    pset.prin1(f) # Write GAMESS Jig parameters.
        
    # $DATA Section keyword
    f.write(" $DATA\n")
        
    # Comment (Description) line from UI
    f.write(pset.ui.comment + "\n")
        
    # Schoenflies symbol
    f.write("C1\n")
    
    for a in gamessJig.atoms:
        pos = a.posn()
        fpos = (float(pos[0]), float(pos[1]), float(pos[2]))
        f.write("%2s" % a.element.symbol)
        f.write("%8.1f" % a.element.eltnum)
        ##Huaicai 8/15/05: to fix bug 892
        #f.write("%8.3f%8.3f%8.3f\n" % fpos)
        f.write(" %.3f %.3f %.3f\n" % fpos)

    #  $END
    f.write(' $END\n')     


def writegms_batfile(filename, gamessJob):
    """
    Write PC GAMESS BAT file
    """ 
    f = open(filename,'w') # Open new BAT file.
    
    # Get the script comment character(s) for this platform.
    rem = gamessJob.get_comment_character()
    
    # Write Header
    f.write (rem + '\n' + rem + 'File created by NanoEngineer-1 on ')
    timestr = "%s\n" % time.strftime("%Y-%m-%d at %H:%M:%S")
    f.write(timestr)
    f.write (rem + '\n')
        
    gamessJob.write_parms(f) # write_parms is a method in superclass (SimJob)
        
    if gamessJob.server.engine == 'PC GAMESS': # Windows
        f.write(gamessJob.server.program + ' -i ' + gamessJob.job_inputfile + ' -o ' + gamessJob.job_outputfile + '\n')
    else: # GAMESS on Linux/Mac OS
        f.write(gamessJob.server.program + '  "' + gamessJob.job_inputfile + '" >& > "' + gamessJob.job_outputfile + '"\n')
            
    f.close() # Close BAT file.

def get_energy_from_gms_outfile(filename):
    """
    Returns a string containing the final energy value from a GAMESS OUT file.
    Works for both PC GAMESS and GAMESS-US.
    """
    # Method: Process the output file line by line backwards.  Since there are multiple 
    # "FINAL ENERGY IS" lines in the output file of an Optimization run (one for each iteration), 
    # it is the last line that contains the final energy value we need. This fixes an undocumented 
    # bug I discovered on 060112.  Mark.
    
    if not os.path.exists(filename):
        return 2, None
            
    elist = []
                    
    lines = open(filename,"rU").readlines()
    
    gamessEnergyStr = re.compile(r'\bFINAL R.+ ENERGY IS')
    
    for line in lines[::-1]: # Read file backwards.
        
        if failpat.search(line): # GAMESS Aborted.  Final energy will not be found.
            return 1, line
            break
        
        elif errorPat.search(line):
            return 1, line
            break
            
        elif line.find('FINAL ENERGY IS') >= 0:
            elist = line.split()
#            print elist
            return 0, elist[3] # Return final energy value as a string.
        
        elif gamessEnergyStr.search(line):# line.find('FINAL R-AM1 ENERGY IS') >= 0: 
            elist = line.split()
#            print elist
            return 0, elist[4] # Return final energy value as a string.
        
        else: continue
            
    return 1, None

# end