#!/usr/bin/env python

# ----------------------------------------------------------------------------------------------------

import os,math
import Tkinter
from Tkinter import *
import Pmw
from PmwFileDialog import *
import traceback
import pymol

# ----------------------------------------------------------------------------------------------------

# SimTk modules

import BaseObjectSimTk
import UtilitiesSimTk
import LogSimTk 
from IsimDisplayWidgetsSimTk import *

# ----------------------------------------------------------------------------------------------------

# ----------------------------------------------------------------------------------------------------

class IsimDisplayInterfaceSimTk( BaseObjectSimTk.BaseObjectSimTk ): # {

   # -------------------------------------------------------------------------------------------------

   def __init__( self, app, isimDisplay=None ): # {

      """Constructor for IsimDisplayInterfaceSimTk"""

      BaseObjectSimTk.BaseObjectSimTk.__init__( self )

      # attributes

      self._workingDirectoryName            = None
      self._parent                          = None
      self._mainDialog                      = None
      self._advancedDialog                  = None
      self._topLabel                        = None
      self._notebook                        = None
      self._isimDisplay                     = isimDisplay
      self._meshSurfaceInterface            = None
      self._molecularSurfaceInterface       = None
      self._helpDialog                      = None
      self._advancedHelpDialog              = None
      self._app                             = app
      self._updateMain                      = 0
      self._updateAdvanced                  = 0

      # lets go

#     self.createTestDialog( )
      self.createMainDialog( app )
#     self.createAdvancedDialog( app )

   # } end of constructor

   # -------------------------------------------------------------------------------------------------
   
   def createTestDialog( self ): # {

      """Test dialog"""

      self._parent       = Tkinter._default_root
      self._testDialog   = Pmw.Dialog( self._parent, buttons = ( 'Exit', ), title = 'Isim Pymol Test Display' )

      # this call is needed to keep window on top of Pymol windows

      self._testDialog.wait_visibility( )
      self._testDialog.tkraise( )

      self._testDialog.show( )

   # } end of createTestDialog

   # -------------------------------------------------------------------------------------------------
   
   def createMainDialog( self, app ): # {

      """Main dialog"""

      if app is None:
         self._parent       = Tkinter._default_root
      else:
         self._parent       = app.root

      self._mainDialog   = Pmw.Dialog( self._parent, buttons = ( 'Advanced', 'Exit', 'Help' ),
                                       title = 'Isim Pymol Display',
                                       command = self._executeSimpleInterfaceCallback )

      # this call is needed to keep window on top of Pymol windows
      # if windows os, then skip wait_visibility() and tkraise() -- system crashes
      # for unknown reason
     
      logReference = self._isimDisplay.getLogReference()
      if logReference is not None:
         logReference.info( 'createMainDialog: sys.platform=<' + str( sys.platform ) + '>' )
      if sys.platform != 'win32':
         if logReference is not None:
            logReference.info( 'createMainDialog: wait_visibility() is being set.' )
         self._mainDialog.wait_visibility( )
      else:
         if logReference is not None:
            logReference.info( 'createMainDialog: wait_visibility() is not being set.' )

      # simple dialog

      self._simpleInterface             = IsimSimpleInterface( self._mainDialog.interior() )
      returnValue                       = self._simpleInterface.initialize( self.getIsimDisplay() )
      if returnValue is not None:
         self._simpleInterface.pack( fill = 'both', expand = 1, padx = 10, pady = 5 )

         # the setGeometryAndDeiconify() command will place the window
         # to the right of the Pymol window
         # needed to prevent overlap w/ Pymol dialog
           
         geometry = '+%d+%d' % (900, 10)
         Pmw.setgeometryanddeiconify( self._mainDialog, geometry )

         # 'lift' is aliased to 'tkraise'
         # threading bug (Warren's inference) in Windows prevents
         # call to tkraise()

         if sys.platform != 'win32':
            self._mainDialog.tkraise( )

         self._mainDialog.show( )
         self._simpleInterface.update()

#     self._mainDialog.deactivate()
#     self._mainDialog.activate( globalMode = 'nograb' )
         
   # } end of createMainDialog

   # -------------------------------------------------------------------------------------------------
   
   def createAdvancedDialog( self, app ): # {

      """Main dialog"""

      if app is None:
         self._parent       = Tkinter._default_root
      else:
         self._parent       = app.root

      self._advancedDialog   = Pmw.Dialog( self._parent, buttons = ( 'Update', 'Print', 'Exit', 'Help' ),
                                           title = 'Isim Pymol Display',
                                           command = self._executeCallback )
      self._advancedDialog.withdraw()
      Pmw.setbusycursorattributes( self._advancedDialog.component('hull') )

#     self._topLabel = Tkinter.Label( self._advancedDialog.interior(),
#                                     text = 'Isim Display', background = 'black', foreground = 'white' )
#     self._topLabel.pack( expand = 1, fill = 'both', padx = 4, pady = 4 )

      self._notebook = Pmw.NoteBook( self._advancedDialog.interior() )
      self._notebook.pack( fill='both', expand=1, padx=10, pady=10 ) 

      # Create interface sheets

      # molecular surface

      sheet                             = self._notebook.add( 'Observables at the Molecular Surface' )
      self._molecularSurfaceInterface   = IsimMolecularSurfaceInterface( sheet )
      self._molecularSurfaceInterface.initialize( self.getIsimDisplay() )
      self._molecularSurfaceInterface.pack(fill = 'both', expand = 1, padx = 10, pady = 5)

      # mesh surfaces

      sheet                             = self._notebook.add( 'Observable IsoSurfaces' )
      self._meshSurfaceInterface        = IsimMeshSurfaceInterface( sheet )
      self._meshSurfaceInterface.initialize( self.getIsimDisplay() )
      self._meshSurfaceInterface.pack( fill = 'both', expand = 1, padx = 10, pady = 5 )

      self._notebook.setnaturalsize()
      self.showAppModal()
         
   # } end of createAdvancedDialog

   # -------------------------------------------------------------------------------------------------

   # accessors for IsimDisplay

   def setIsimDisplay( self, isimDisplay ): # {
      """Set accessor for IsimDisplay """
      self._isimDisplay = isimDisplay 
   # } end of setIsimDisplay

   def getIsimDisplay( self ): # {
      """Get accessor for IsimDisplay """
      return self._isimDisplay;
   # } end of getIsimDisplay

   # -------------------------------------------------------------------------------------------------

   def showAppModal( self ): # {
      self._advancedDialog.show()
   # } end of showAppModal

   # -------------------------------------------------------------------------------------------------

   def update( self ): # {

      clear  = 0
      update = 0

      if self._simpleInterface is not None: 
         if clear:
            self._simpleInterface.clear()
         if self._updateMain:
            update += self._simpleInterface.update() 

      if self._molecularSurfaceInterface is not None: 
         if clear:
            self._molecularSurfaceInterface.clear()
         if self._updateAdvanced:
            update += self._molecularSurfaceInterface.update() 

      if self._meshSurfaceInterface is not None: 
         if clear:
            self._meshSurfaceInterface.clear()
         if self._updateAdvanced:
            update += self._meshSurfaceInterface.update() 

      if update:
         pymol.cmd.refresh()
         pymol.cmd.recolor()

   # } end of update

   # -------------------------------------------------------------------------------------------------

   def clear( self, clearFlag = 0 ): # {

      if self._simpleInterface is not None: 
         self._simpleInterface.clear( clearFlag )

      if self._molecularSurfaceInterface is not None: 
         self._molecularSurfaceInterface.clear( clearFlag )

      if self._meshSurfaceInterface is not None: 
         self._meshSurfaceInterface.clear( clearFlag )

      pymol.cmd.refresh()
      pymol.cmd.recolor()

   # } end of clear

   # -------------------------------------------------------------------------------------------------

   def _executeCallback( self, result ): # {

      if result == 'Update':

         self.clear( 0 )
         self._updateAdvanced = 1
         self.update()
         self._updateAdvanced = 0

      elif result == 'Clear':

         self.clear( 1 )

      elif result == 'Print':

         self.printPng()

      elif result == 'Exit':

         if __name__ == '__main__':
            self.parent.destroy()
         else:
            self._advancedDialog.withdraw()

      elif result == 'Help':

         # Create help dialog

         if self._advancedHelpDialog is None:
            useMessageDialog = 0
            if useMessageDialog:
               self._advancedHelpDialog = Pmw.MessageDialog( self._notebook.interior(),
                                                             title = 'Help',
                                                             message_text = self.getHelpText(),
                                                             command = self.quitHelp,
                                                             buttons = ( 'Ok', ) )
            else:
               self._advancedHelpDialog = Pmw.TextDialog( self._notebook.interior(),
                                                  title = 'Help' )
               self._advancedHelpDialog.insert( 'end', self.getHelpText() )
               self._advancedHelpDialog.configure( text_state = 'disabled' )

            self._advancedHelpDialog.withdraw()

         self._advancedHelpDialog.activate()

   # } end of _executeCallback

   # -------------------------------------------------------------------------------------------------

   def _executeSimpleInterfaceCallback( self, result ): # {

      if result == 'Update':

         self.clear( 0 )
         self._updateMain = 1
         self.update()
         self._updateMain = 0

      elif result == 'Advanced':

         # lets go

         self.clear( 0 )
         if self._simpleInterface is not None:
            self._simpleInterface.toggleButtons( )

         self.createAdvancedDialog( self._app )

      elif result == 'Clear':

         self.clear( 1 )

      elif result == 'Print':

         self.printPng()

      elif result == 'Exit':

         if __name__ == '__main__':
            self.parent.destroy()
         else:
            if self._mainDialog is not None:
               self._mainDialog.withdraw()

      elif result == 'Help':

         # Create help dialog

         if self._helpDialog is None:
            useMessageDialog = 0
            if useMessageDialog:
               self._helpDialog = Pmw.MessageDialog( self._mainDialog.interior(),
                                                     title        = 'Help',
                                                     message_text = self.getSimpleInterfaceHelpText(),
                                                     command      = self.quitHelp,
                                                     buttons      = ( 'Ok', ) )
            else:
               self._helpDialog = Pmw.TextDialog( self._mainDialog.interior(),
                                                  title = 'Help' )
               self._helpDialog.insert( 'end', self.getSimpleInterfaceHelpText() )
               self._helpDialog.configure( text_state = 'disabled' )

            self._helpDialog.withdraw()

         self._helpDialog.activate()

   # } end of _executeSimpleInterfaceCallback

   # -------------------------------------------------------------------------------------------------

   def quitHelp( self, callerButton ): # {
      self._helpDialog.destroy()
   # } end of quitHelp

   # -------------------------------------------------------------------------------------------------

   def getSimpleInterfaceHelpText( self ): # {

      message  = 'The interface to Pymol allows the user to view \n\n'
      message += '   (1) the macromolecule,\n'
      message += '   (2) the electrostatic potential at the surface of the macromolecule\n'
      message += '       arising from the molecular charges and the average distribution\n'
      message += '       of the ions,\n'
      message += '   (3) the averaged number densities for each ion species.\n\n'
      message += 'The levels for each of the observables are selected based on statistics\n'
      message += 'collected at grid points near the surface of the molecule and\n'
      message += "in the 'near' bulk. Here 'near' bulk is the region within approximately\n"
      message += "15A of the surface of the molecule.\n\n"

      message += "The color scheme used in the display of the surface electrostatic potential is\n"
      message += "based on the Pymol surface color ramp ('ramp_new' command). The range used for the color\n"
      message += "ramp is [ -int( sdTrimmed ) - 1, 0, int( sdTrimmed ) + 1 ], where sdTrimmed is the\n"
      message += "standard deviation of the 'trimmed' potential distribution at the surface of the molecule.\n"
      message += "Here the 'trimmed' distribution potential is the set of potential values obtained\n"
      message += "by deleting the electrostatic potential values less than the 5-percentile level\n"
      message += "and greater than the 95-percentile level from the collection. The statistics obtained\n"
      message += "from this truncated distribution are more robust to outliers that may occur.\n\n"

      message += "The number density isosurface meshes are color coded according to whether the ion\n"
      message += "is positively charged (warmer colors) or negatively charged ((cooler colors).\n"
      message += "The level that is displayed is the 90% percentile of the 'near' bulk distribution\n"
      message += "of number density values. However, if the this level is less than 1.0e-05, then\n"
      message += "the program looks at the high-end tail of the distribution of values and chooses\n"
      message += "the highest level such that the number of grid points with values above this level\n"
      message += "is greater than 100. If no value can be found that satisifies this criterion, then\n"
      message += "program defaults to a level of 1.0e-04. The actual level used is printed in the\n"
      message += "interface.\n\n"
      message += ""
      message += "The above schemes for selecting the levels to display the observables should work for\n"
      message += "most cases. However, the user can adjust the displayedlevels by using the Advanced menu.\n"
      message += "See the Help text available in the Advanced dialog on how to set the levels\n"
      message += "and obtain statistics of the distribution of values."

      return message

   # }  end of getSimpleInterfaceHelpText
                                                 
   # -------------------------------------------------------------------------------------------------

   def getHelpText( self ): # {

      message  = ''
      message += '\n\n\n----------------------------- Help for Molecular Surface worksheet -----------------------------\n\n'
      message += 'The Molecular Surface worksheet allows the user to plot the\n'
      message += 'observables (electrostatic potential, charge density and ion\n'
      message += 'number densities) at the surface of the macromolecule.\n\n'
      message += 'In contrast the Isosurfaces worksheet allows the user to plot the\n'
      message += 'isosurfaces of the different observables at various levels in the solvent.\n\n'

      message += 'The widgets in the Molecular Surface worksheet are first discussed\n'
      message += 'followed by a similar description for the Isosurfaces display.\n\n'

      message += 'The checkbox on the far left of the Molecular Surface worksheet\n'
      message += 'toggles the observables that are displayed; only one observable\n'
      message += 'can be plotted at a time. The ability to chose from among 3 possible\n'
      message += 'observables/settings but with only one active at any given time was\n'
      message += 'allows the user to quickly switch between different\n'
      message += 'views.\n\n'

      message += self.getObservableHelpText()

      message += "The color scheme used in the display of the observables at the molecular surface\n"
      message += "is based on the Pymol surface color ramp ('ramp_new' command). This command requires\n"
      message += "three thresholds, referred to in the interface as low, medium and high.\n"
      message += "The color ramp interpolates the colors from red to white for the low-medium range and\n"
      message += "from white to blue for the medium-high range.\n\n"

      message += self.getUmitsPullDownHelpText()

      message += "The Surface pulldown menu specifies whether the surface to be mapped to is the molecular\n"
      message += "or the solvent-accessible surface. This feature is implemented by setting the Pymol\n"
      message += "'surface_solvent' value to 0 (molecular surface) or 1 (solvent-accessible surface) and \n"
      message += "'surface_ramp_above_mode' value to 1 (molecular surface) or 0 (solvent-accessible surface).\n\n"

      message += self.getStatButtonHelpText();

      # -------------------------------------------------------------------------------------------------

      # mesh surface

      message += '\n\n\n----------------------------- Help for Isosurface worksheet -----------------------------\n\n'
      message += 'The checkbox on the far left in the Isosurface worksheet\n'
      message += 'toggles the observables that are displayed; multiple observables/surface levels\n'
      message += 'can be plotted simultaneously.\n\n'

      message += self.getObservableHelpText()

      message += "The level text box specifies the isosurface contour; for a guide to the distribution\n"
      message += "for a level see the results from the Stat button\n"

      message += self.getUmitsPullDownHelpText()

      message += "The color pulldown allows the user to specify the color of the isosurface.\n"

      message += "The Surface pulldown menu specifies whether the surface contour is to be displayed\n"
      message += "as a solid, a mesh or closely-spaced dots. These are implemented by the Pymol\n"
      message += "isosurface(), isomesh(), and isodot() commands.\n"

      message += self.getStatButtonHelpText();

      return message

   # }  end of getHelpText
                                                 
   # -------------------------------------------------------------------------------------------------

   def getObservableHelpText( self ): # {

      message  = ''
      message += 'The observables include the following:\n'
      message += '   (1) the APBS electrostatic potential that is an input to the ISIM calculation;\n'
      message += '       this is the potential arising from just the macromolecule charges (no ions).\n'
      message += '       the Monte Carlo step is 0 since the potential is input to the program and is fixed\n'
      message += '       throughout the calculation.\n'
      message += '   (2) the averaged ISIM electrostatic potential\n'
      message += '   (3) the averaged ISIM number density for each ion species\n'
      message += '   (4) the avergaed ISIM charge density\n'
      message += "For all all observables except the APBS electrostatic potential, the running average\n"
      message += "at regularly spaced Monte Carlo (MC) steps is available; the default MC step\n"
      message += "is the last available step in the simulation.\n\n"

      return message

   # } end of getObservableHelpText
                                                 
   # -------------------------------------------------------------------------------------------------

   def getUmitsPullDownHelpText( self ): # {

      message  = ""
      message += "The Units pulldown menu allows the user to specifiy whether the thresholds in the\n"
      message += "are to be interpreted as absolute thresholds or as units of standard deviation.\n"
      message += "The standard deviation is calculated from a trimmed distribution of the observable\n"
      message += "to be displayed: only values between the 5-95th percentil are included in the calculation\n"
      message += "of the standard deviation -- this removes distortions that may arise from outliers.\n\n"

      return message

   # }  end of getUmitsPullDownHelpText 
                                                 
   # -------------------------------------------------------------------------------------------------

   def getStatButtonHelpText( self ): # {

      message  = ""
      message += "The Stat button displays the statistics collected for the current selection/observable.\n"
      message += "The statistics include the mean, standard deviation, median, min, max for the observables, both\n"
      message += "on/near the surface of the molecule and in the near bulk (grid points within 15A of the molecule).\n"
      message += "A histogram of the values is also included. See the text included in the output presented for\n"
      message += "more details. These statistics can guide the user on how to set the levels for the different observables."

      return message

   # }  end of getStatButtonHelpText
                                                 
   # -------------------------------------------------------------------------------------------------

   def printPng( self ): # {

   # -------------------------------------------------------------------------------------------------

      # Create the dialog.

      self._printDialog = Pmw.Dialog( self._notebook.interior(), buttons = ('OK', 'Cancel', 'Help'),
                                      defaultbutton = 'OK', title = 'Print Png',
                                      command = self._printDialogCallback)

   # -------------------------------------------------------------------------------------------------

      # Add file chooser

      self._printPngFile = Pmw.EntryField( self._printDialog.interior(), labelpos='e',
                                           label_pyclass = FileDialogButtonClassFactory.get( self.setPngFileName,'*.png'),
                                           label_text='Png file:', )       
      self._printPngFile.pack( fill='both', expand=1, padx=10, pady=10 ) 

   # -------------------------------------------------------------------------------------------------

      # ray trace

      self._rayTrace = IntVar()
      self._rayTrace.set( 1 )
      self._rayTraceCheckButton = Checkbutton( self._printDialog.interior(), anchor='w',indicatoron=True, 
                                               text='Ray Trace',  variable = self._rayTrace )
      self._rayTraceCheckButton.pack( fill='both', expand=1, padx=10, pady=10 )

   # -------------------------------------------------------------------------------------------------

      # pixel width/height

      self._widthPngFile = Pmw.EntryField( self._printDialog.interior(), labelpos='e', value=2400,
                                           label_text='Pixel Width', validate = {'validator' : 'real' }, entry_width = 8,) 
             

      self._widthPngFile.pack( fill='both', expand=1, padx=10, pady=10 ) 

      self._heightPngFile = Pmw.EntryField( self._printDialog.interior(), labelpos='e', value=2400,
                                           label_text='Pixel Height', validate = {'validator' : 'real' }, entry_width = 8,) 

      self._heightPngFile.pack( fill='both', expand=1, padx=10, pady=10 ) 

   # -------------------------------------------------------------------------------------------------

      self._printDialog.withdraw()
      self._printDialog.show()

      return

   # } end of printPng

   # -------------------------------------------------------------------------------------------------
                                                 
   def setPngFileName( self, fileName ): # {
      fileName = self.addPngFileSuffix( fileName )
      self._printPngFile.setvalue(fileName)
   # }  end of setPngFileName

   # -------------------------------------------------------------------------------------------------

   def getPngFileName( self ): # {

      fileName = self._printPngFile.getvalue()
      pngSuffix = re.compile( '.png$' )
      if pngSuffix.search( fileName ) is None:
         fileName += '.png'
      return fileName

   # }  end of getPngFileName

   # -------------------------------------------------------------------------------------------------

   def addPngFileSuffix( self, fileName ): # {

      pngSuffix = re.compile( '.png$' )
      if pngSuffix.search( fileName ) is None:
         fileName += '.png'
      return fileName

   # }  end of addPngFileSuffix

   # -------------------------------------------------------------------------------------------------

   def _printDialogCallback( self, request ): # {

      if request == 'OK':

         # diagnostics

         print 'Print File=' + self.getPngFileName()
         print 'Ray='        + str( self._rayTrace.get() )
         print 'Width='      + str( self._widthPngFile.getvalue() )
         print 'Height='     + str( self._heightPngFile.getvalue() )

         # set parameters for png file print

         pymol.cmd.bg_color( color='white' )
         pymol.cmd.set( 'antialias', 1 )
         pymol.cmd.set( 'orthoscopic', 1 )

         if self._rayTrace.get():
            pymol.cmd.ray( self._widthPngFile.getvalue(), self._heightPngFile.getvalue() )

         pymol.cmd.png( self.getPngFileName() )

         # pymol.cmd.bg_color( color='black' )
         pymol.cmd.set( 'antialias', 0 )
         pymol.cmd.set( 'orthoscopic', 0 )

      else:
         print 'Print dialog request=' + request + " ignored."

      self._printDialog.destroy()

      return

   # }  end of _printDialogCallback

# ----------------------------------------------------------------------------------------------------

# } end of class IsimDisplayInterfaceSimTk
