#A* -------------------------------------------------------------------
#B* This file contains source code for the PyMOL computer program
#C* copyright 1998-2000 by Warren Lyford Delano of DeLano Scientific. 
#D* -------------------------------------------------------------------
#E* It is unlawful to modify or remove this copyright notice.
#F* -------------------------------------------------------------------
#G* Please see the accompanying LICENSE file for further information. 
#H* -------------------------------------------------------------------
#I* Additional authors of this source file include:
#-* 
#-* 
#-*
#Z* -------------------------------------------------------------------

# parser.py
# Python parser module for PyMol
#

import pymol
import traceback
import string
import cmd
import exceptions
import re
import parsing
import types
import glob
import sys
import os
import setting
import __main__

from cmd import _feedback,fb_module,fb_mask

QuietException = parsing.QuietException
      
pymol_names = pymol.__dict__

# parsing state implemented with dictionaries to enable safe recursion
# to arbitrary depths

com0 = {}    # verbose line, as read in
com1 = {}    # line w/o trailing whitespace
com2 = {}    # non-compound command
cont = {}    # continued characters from previous lines (i.e., before \ )
script = {}  # file handles
kw = {}      # row in the cmd.keyword table for the current command
input = {}   # list of length two - command and unparsed arguments string
next = {}    # characters for next command (i.e., after ; )
args = {}    # parsed non-keyword argument string
kw_args = {} # parser keyword argument string

# The resulting value from a pymol command (if any) is stored in the
# parser.result global variable.  However, script developers will
# geerally want to switch to the Python API for any of this kind of
# stuff.

result = None

# initialize parser

nest=0
com0[nest]=""
cont[nest]=""

# main parser routine

def parse(s):
   global com0,com1,com2,cont,script,kw,input
   global next,nest,args,kw_args,cmd,cont,result
# report any uncaught errors...
   if sys.exc_info()!=(None,None,None):
      traceback.print_exc()
#
   p_result = 1
   com0[nest] = s
   try:
      com1[nest] = string.rstrip(com0[nest]) # strips trailing whitespace
      if len(com1[nest]) > 0:
         if str(com1[nest][-1]) == "\\":
            cont[nest] = cont[nest] + com1[nest][:-1]
         else:
            if cont[nest] != '':
               com1[nest] = cont[nest] + com1[nest]
               cont[nest] = ''
# this routine splits up the line first based on semicolon 
            next[nest] = parsing.split(com1[nest],';',1)
# com2[nest] now a full non-compound command            
            com2[nest] = next[nest][0]
            input[nest] = string.split(com2[nest],' ',1)
            if len(input[nest]):
               input[nest][0] = string.strip(input[nest][0])
               com = input[nest][0]
               if com[0:1]=='/':
                  # explicit literal python 
                  com2[nest] = string.strip(com2[nest][1:])
                  if len(com2[nest])>0:
                      exec(com2[nest],pymol_names,pymol_names)
               else:
                  # try to find a keyword which matches
                  if cmd.kwhash.has_key(com):
                     amb = cmd.kwhash.interpret(com)
                     if amb == None:
                        com = cmd.kwhash[com]
                     elif type(amb)!=types.StringType:
                        print 'Error: ambiguous command: '
                        amb.sort()
                        amb = parsing.list_to_str_list(amb)
                        for a in amb:
                           print a
                        raise QuietException
                     com = amb
                  if cmd.keyword.has_key(com):
# here is the command and argument handling section
                     kw[nest] = cmd.keyword[com]
                     if kw[nest][4]>=parsing.NO_CHECK:
# stricter, Python-based argument parsing
                        if kw[nest][4]>=parsing.LITERAL: # treat literally
                           next[nest] = ()
                           com2[nest]=com1[nest]
                        (args[nest],kw_args[nest]) = \
                           parsing.prepare_call(
                              kw[nest][0],
                              parsing.parse_arg(com2[nest],mode=kw[nest][4]),
                              kw[nest][4]) # will raise exception on failure
                        result=apply(kw[nest][0],args[nest],kw_args[nest])
                     else:
# old parsing style, being phased out
                        if kw[nest][4]==parsing.ABORT:
                           return None # SCRIPT ABORT EXIT POINT
                        if kw[nest][4]==parsing.SINGLE: # single line, no breaks
                           next[nest] = ()
                           input[nest] = string.split(com1[nest],' ',1)
                        if len(input[nest])>1:
                           args[nest] = parsing.split(input[nest][1],kw[nest][3])
                           while 1:
                              nArg = len(args[nest]) - 1
                              c = 0
                              while c < nArg:
                                 if (string.count(args[nest][c],'(')!=
                                     string.count(args[nest][c],')')):
                                    tmp=args[nest][c+1]
                                    args[nest].remove(tmp)
                                    args[nest][c]=string.strip(args[nest][c])+\
                                                  ','+string.strip(tmp)
                                    nArg = nArg-1
                                    break;
                                 c = c + 1
                              if c == nArg:
                                 break;
                           if len(args[nest])==1 and len(args[nest][0])==0:
                              args[nest] = []
                        else:
                           args[nest] = []
                        if kw[nest][1]<= len(args[nest]) <= kw[nest][2]:
                           args[nest] = map(string.strip,args[nest])
                           if kw[nest][4]<parsing.RUN:
   #                           
   # this is where old-style commands are invoked
   #
                              result=apply(kw[nest][0],args[nest])
   #                           
                           elif kw[nest][4]==parsing.SPAWN:
                              # spawn command
                              if len(args[nest])==1: # default: module
                                 parsing.run_as_module(args[nest][0],spawn=1)
                              elif args[nest][1]=='main':
                                 parsing.run_as_thread(args[nest][0],
                                                       __main__.__dict__,
                                                       __main__.__dict__)
                              elif args[nest][1]=='private':
                                 parsing.run_as_thread(args[nest][0],
                                                       __main__.__dict__,
                                                       {})
                              elif args[nest][1]=='local':
                                 parsing.run_as_thread(args[nest][0],
                                                       pymol_names,{})
                              elif args[nest][1]=='global':
                                 parsing.run_as_thread(args[nest][0],
                                                       pymol_names,pymol_names)
                              elif args[nest][1]=='module':
                                 parsing.run_as_module(args[nest][0],spawn=1)
                           elif kw[nest][4]==parsing.RUN:
                              # run command
                              if len(args[nest])==1: # default: global
                                 execfile(args[nest][0],pymol_names,pymol_names)
                              elif args[nest][1]=='main':
                                 execfile(args[nest][0],__main__.__dict__,__main__.__dict__)
                              elif args[nest][1]=='private':
                                 execfile(args[nest][0],__main__.__dict__,{})
                              elif args[nest][1]=='local':
                                 execfile(args[nest][0],pymol_names,{})
                              elif args[nest][1]=='global':
                                 execfile(args[nest][0],pymol_names,pymol_names)
                              elif args[nest][1]=='module':
                                 parsing.run_as_module(args[nest][0],spawn=0)
                           else:
                              print 'Error: unknown keyword mode: '+str(kw[nest][4])
                              raise QuietException
                        else:
                           print 'Error: invalid arguments for %s command.' % com
#
# non-keyword command handling
#
                  elif len(input[nest][0]):
                     if input[nest][0][0]=='@':
                        script[nest] = open(string.strip(com2[nest][1:]),'r')
                        nest=nest+1
                        cont[nest]=''
                        while 1:
                           com0[nest]  = script[nest-1].readline()
                           if not com0[nest]: break
                           inp_cmd = com0[nest]
                           tmp_cmd = string.strip(inp_cmd)
                           if len(tmp_cmd):
                              if tmp_cmd[0] not in ['#','_','/']: # suppress comments, internals, python
                                 print "PyMOL>"+tmp_cmd
                              elif tmp_cmd[0]=='_' and \
                                   tmp_cmd[1:2] in [' ','']: # "_ " remove echo suppression signal
                                 inp_cmd=inp_cmd[2:]
                           pp_result = parse(inp_cmd)
                           if pp_result==None: # RECURSION
                              break # abort command gets us out
                           elif pp_result==0: # QuietException
                              if cmd.get_setting_legacy("stop_on_exceptions"):
                                 p_result = 0 # signal an error occurred
                                 print"PyMOL: stopped on exception."
                                 break;
                        nest=nest-1
                        script[nest].close()
                     else: # nothing found, try literal python
                        com2[nest] = string.strip(com2[nest])
                        if len(com2[nest])>0:
                           exec(com2[nest],pymol_names,pymol_names)
            if (len(next[nest])>1) and p_result:
                    # continue parsing if no error or break has occurred
               nest=nest+1
               com0[nest] = next[nest-1][1]
               next[nest-1]=()
               cont[nest]=''
               p_result = parse(com0[nest]) # RECURSION
               nest=nest-1
   except QuietException:
      if cmd._feedback(fb_module.parser,fb_mask.blather):
         print "PyMOL: Caught a quiet exception."
      p_result = 0 # notify caller that an error was encountered
   except:
      traceback.print_exc()
      if cmd._feedback(fb_module.parser,fb_mask.blather):
         print "PyMOL: Caught an unknown exception."
      p_result = 0 # notify caller that an error was encountered
   return p_result  # 0 = QuietException, None = abort, 1 = ok

# command and filename completion

def _same_(a,b):
   if a==b:
      return a
   else:
      return None

def complete_sc(st,sc,type_name,postfix):
   result = None
   sc=sc() # invoke lambda functions (if any)
   amb = sc.interpret(st)
   if amb==None:
      print " parser: no matching %s."%type_name
   elif type(amb)==types.StringType:
      result = amb+postfix
   else:
      amb.sort()
      print " parser: matching %s:"%type_name
      flist = filter(lambda x:x[0]!='_',amb)
      lst = parsing.list_to_str_list(flist)
      for a in lst:
         print a
      # now append up to point of ambiguity
      css = map(None,flist[0]) # common sub-string (css)
      for a in flist:
         ac = map(None,a)
         tmp = css
         css = []
         for c in range(len(tmp)):
            if tmp[c]!=ac[c]:
               break
            css.append(tmp[c])
      css = filter(None,css)
      css = string.join(css,'')
      if len(css)>len(st):
         result = css
   return result

def complete(st):
   result = None
   pre = ''
   flag = 0
   if (string.find(st,' ')<0) and (string.find(st,'@'))<0:
      try:
         result = complete_sc(st,cmd.kwhash,'commands',' ')
      except:
         traceback.print_exc()
   else:
      full = cmd.kwhash.interpret(re.sub(r" .*","",st))
      count = string.count(st,',') # which argument are we on
      if count<len(cmd.auto_arg):      
         if cmd.auto_arg[count].has_key(full): # autocomplete arguments
            flag = 1
            try:
               pre = re.sub(r"^[^ ]* ",' ',st,count=1) # trim command
               if re.search(r",",pre)!=None:
                  pre = re.sub(r"[^\, ]*$","",pre,count=1) 
                  pre = re.sub(r",\s*[^\, ]*$",", ",pre,count=1) # trim 1 arg
               else:
                  pre = re.sub("[^ ]*$","",pre,count=1) # trim 1 arg               
               pre = re.sub(r"^ *",'',pre)
               pre = full+' '+pre
               pat = re.sub(r".*[\, ]",'',st)
#               print ":"+pre+":"+pat+":"
               result = apply(complete_sc,tuple([pat]+cmd.auto_arg[count][full]),{})
            except:
               traceback.print_exc()
      if not flag: # otherwise fallback onto filename completion
         if(st[:1]=='@'):
            st=st[1:]
            pre = '@'
         loc = reduce(max,[string.rfind(st,','),
                           string.rfind(st,' '),
                           string.rfind(st,']'),
                           string.rfind(st,')')])+1
         st3 = st[loc:]
         flist = glob.glob(os.path.expanduser(os.path.expandvars(st3))+"*")
         lst = map(None,st3)
         lst.reverse()
         st3 = string.join(lst,'')
         lf = len(flist)
         if lf == 0:
            print " parser: no matching files."
         elif lf==1:
            result = st[0:loc]+flist[0]
            if os.path.isdir(flist[0]):
               result = result + "/"
         else:
            flist.sort()
            print " parser: matching files:"
            lst = parsing.list_to_str_list(flist)
            for a in lst:
               print a
            # now append as much up to point of ambiguity
            css = map(None,flist[0]) # common sub-string (css)
            for a in flist:
               ac = map(None,a)
               tmp = css
               css = []
               for c in range(len(tmp)):
                  if tmp[c]!=ac[c]:
                     break
                  css.append(tmp[c])
            tmp = css
            css = []
            for a in tmp:
               if a==None:
                  break
               css.append(a)
            css = string.join(css,'')
            if len(css)>len(st3):
               result = st[0:loc]+css
   if result!=None:
      result = pre+result
   return result
