# optconf.py
# 
# User preferences for dysplayer.
#
# Author       : Ed Pozharski
# Co-author    : Chris X. Weichenberger
# Created      : unk
# License
# Twilight is a GUI for displaying ligands incompatible with electron density.
#    Copyright (C) 2012  Christian X. Weichenberger, Edwin Pozharski, and
#    Bernhard Rupp.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Modifications:
#  2012-02-22 (_X) Adaption to dysplayer with default values and handling.
#  2012-03-14 (_X) Added scripting host.
#  2012-03-27 (_X) Changed default name of linux config file to .dysplayerrc
#    and made it platform specifig, under Win$ it is dysplayer.cfg
#  2012-05-09 (_X) Changed annotation text.
#  2012-08-01 (_X) Platform dependent default for coot executable.


""" Program configuration classes. General idea is of a class that has
    the dictionary of options as data and includes various methods to:
    a) create ConfigParser from scratch;
    b) Read/write configuration files;
    c) Update ConfigParser to the current version of the dictionary."""

import os;
import tempfile;
import ConfigParser;

from files import Output;
import sys
try:
    from collections import OrderedDict as ODict;
except ImportError:
    ODict = dict ;
    Output("Warning: Configuration file will contain shuffled output.");


# Platform-specific configuration file name.
DEFAULT_CFG_FN = "dysplayer.cfg" if( sys.platform=="win32" ) else ".dysplayerrc";

# Default sigma contour level for 2Fo - Fc maps.
SIGMA_2FOFC         = "1.0";
# Default sigma contour level for  Fo - Fc maps.
SIGMA_FOFC          = "3.0";
# Default width of coot window (in pixels).
COOT_WIDTH          = "600";
# Default height of coot window (in pixels).
COOT_HEIGHT         = "600";
# Default coot executable name, for Windows, this is win32, regardless of arch.
COOT_EXEC           = "runwincoot.bat" if( sys.platform=="win32" ) else "coot";
# Scripting engine for coot. Either 'python' or 'scheme'.
COOT_SCRIPTING_HOST = "python";

# We need the ordered dictionary here as we do want to preserve the order
# in the config file. This is of quite some importance, as the menu is
# constructed based on this order!
#
# Funny enough, the options are case INsensitive.
DEFAULT_CONFIGURATION = ODict(
    # program paramerters 
    (("program",
      ODict( (("2fofc", SIGMA_2FOFC),
              ("fofc", SIGMA_FOFC),
              ("cootexec", COOT_EXEC),
              ("pdbdir", ""),
              ("tempdir", tempfile.gettempdir()), 
              ("ustamp", ""),
              ("coot-width", COOT_WIDTH),
              ("coot-height", COOT_HEIGHT),
              ("scripting-host", COOT_SCRIPTING_HOST)
              ))
      ),
     ("annotation",
      ODict( (("1", "No evidence - Ligand forced into empty site."),
              ("2", "Projection - Partly disordered, placed by extension."), 
              ("3", "Misplacement - Misplaced ligand close to its density."),
              ("4", "Mother liquor - Ligand placed into buffer density."),
              ("5", "Petering out - Ligand density vanishes further out."),
              ("6", "False positive - False positive. Sorry!"),
              ))
      ),
     )
);


_Configurer = None;
_Config     = None;

class CConfigurer:
    def __init__(self, sections=None, cfgname=DEFAULT_CFG_FN,
                 defaultarget=None):
        """ Configurator class holds the options dictionary and provides the 
            methods needed to create/read/write/update the configuration
            parsers.  See ConfigParser module for more details on the way
            python handles configuration files.
            
            Class instance parameters are:
            
            sections     - dictionary of option sections, each value is a 
                            dictionary of options containing keys and default 
                            values.
            cfgname      - the configuration file name. 
            defaultarget - the default location of the config file.  By default,
                            is set to user's home directory.
        """            
        if sections:
            self.sections = sections
        else:
            self.sections = {}
        self.cfgname = cfgname
        self.SetDefaulTarget(defaultarget);

    def SetConfigFileName(self, cfgname=DEFAULT_CFG_FN):
        """ The configuration file name is normally given when the class is 
            invoked and maintained for the lifetime of the class instance.
            This method allows to change the file name if needed.  Use with
            caution since the next configuration reading event may fail if
            the file does not exist.  On the other hand, this method may be
            useful for switching between different versions of the configuration,
            but that may also be done by using two instances of the class."""
        self.cfgname = cfgname

    def SetDictionary(self, sections):
        """ The dictionary that defines the configuration file is normally
            initiated when a class instanec is created and should in most
            circumstances be maintained for the lifetime of the class instance.
            This method covers a presumably esoteric situation when the 
            items in the configuration file default have to be changed for an
            existing class instance.  Use with caution, it may be more sane to
            just create another instance. """
        self.sections = sections 

    def SetDefaulTarget(self, defaultarget=None):
        """ The default location of the configuration file should be generally
            provided upon class instantiation and kept unchanged.  This method
            may be invoked (use caution) to change the default location during
            for an existing class instance. Defaults to current user home
            folder."""
        if defaultarget is None:
            self.defaultarget = os.path.expanduser('~')
        else:
            self.defaultarget = defaultarget

    def zero_config(self, target=None):
        """ Initialize the configuration file and drop it in the target folder.
            Returns the ConfigParser object it created. """
        if not target:
            target = self.defaultarget
        config = ConfigParser.SafeConfigParser()
        for section in self.sections.keys():
            config.add_section(section)
            for option in self.sections[section].keys():
                config.set(section, option, self.sections[section][option])
        fconf=open(os.path.join(target,self.cfgname), 'wb')
        config.write(fconf)
        fconf.close()
        return config

    def read_config(self, target=None):
        """ Read and return the ConfigParser object with configuration 
            from target folder. """
        if not target:
            target = self.defaultarget
        config = ConfigParser.SafeConfigParser()
        config.read(os.path.join(target,self.cfgname))
        return config

    def write_config(self, config, target=None):
        """ Write the configuration file defined by the ConfigParser instance
            passed to the method into target folder. """
        if not target:
            target = self.defaultarget
        fconf=open(os.path.join(target,self.cfgname), 'wb')
        config.write(fconf)
        fconf.close()

    def adjust_config(self, target=None, fWrite=True,
                      fRemoveMissingSections=True,
                      fRemoveMissingOptions=False):
        """ Read the configuration from target folder and fill out the missing 
            fields. By default, the adjusted configuration is written in to the
            target folder, this may be prevented by setting fWrite to False.
            For reasosn I don't quite remember, individual options are not 
            removed by default if the updated dictionary is missing them, but 
            the whole sections are.  This behavior may be changed by setting
            fRemoveMissingSections/fRemoveMissingOptions which are quite
            self-explanatory.  
            The return value is the adjusted configuration parser."""
        if not target:
            target = self.defaultarget
        config = self.read_config(target)
        for section in self.sections.keys():
            if not config.has_section(section):
                config.add_section(section)
            for option in self.sections[section].keys():
                if not config.has_option(section, option):
                    config.set(section, option, self.sections[section][option])
        if fRemoveMissingSections:
            for section in config.sections():
                if not self.sections.has_key(section):
                    config.remove_section(section)
        if fRemoveMissingOptions:
            for section in config.sections():
                for option in config.options(section):
                    if not self.sections[section].has_key(option):
                        config.remove_option(section,option)
        if fWrite:
            self.write_config(config,target)
        return config

    def found_config(self, target=None, mode=os.F_OK):
        """ Check the status of the configuration file in the target folder. 
            The type of access check is defined by the mode parameter, which 
            defaults to file existence and follows the same rules as os.access()
            method mode parameter.  If the target folderr is not specified,
            check the default target of the class."""
        if not target:
            target = self.defaultarget
        return os.access(os.path.join(target,self.cfgname),mode)



def die(text, exitCode=1):
    """Convenience function for ungraceful exit."""
    Output(text);
    sys.exit(exitCode);

class CDysConfigurer(CConfigurer):
    """Special configuration class for Dysplayer.

    Maintains the config object itself and provides get/set functions for
    it. Generates a new config object, if there is no config file. Otherwise,
    it reads the config file. Makes some basic sanity checks on the specific
    dysplayer config file contents and quits the application if violated.
    """
    def __init__(s, sections=DEFAULT_CONFIGURATION, cfgname=DEFAULT_CFG_FN,
                 defaultarget=None):
        CConfigurer.__init__(s, sections=sections, cfgname=cfgname,
                             defaultarget=defaultarget);
        s.Config = s.adjust_config() if( s.found_config() ) else s.zero_config();
        
        # we might brutally exit the program in the next function call as an
        # error detected there is not revoverable.
        s.IntegrityCheck();
        
    def GetCurrentConfigFullFileName(self):
        """Get the config file full path name.
        """
        return os.path.join(self.defaultarget, self.cfgname);
        
    def IntegrityCheck(s):
        """Performs integrity check of the configuration file.
        
        If these checks fail, the config file is irreversibly messed up and
        needs to be re-edited by the user, who in fact has caused this mess...
        """
        # full path config file name, either default or user defined.
        cfgFFN = os.path.join(s.defaultarget, s.cfgname);
        splitText = cfgFFN;
        splitText+= ", Token %s: Please specify exactly a single split character '-'.";
        notEnoughText = cfgFFN;
        notEnoughText+= ", Token %s: Empty text for menu entry or table annotation.";
        for key, text in s.Config.items("annotation"):
            menuTexts = text.split("-");
            if( len(menuTexts)!=2 ):
                Output(splitText % key);
                sys.exit(1);
            menuText, tableText = menuTexts;
            menuText, tableText = menuText.strip(), tableText.strip();
            if( menuText=="" or tableText=="" ):
                die(notEnoughText % key);
        
        notFloatText= cfgFFN+", ``%s'' does not specify a float value.";
        try:
            for var in ("2FoFc", "FoFc"):
                dummy = float(s.Config.get("program", var));
        except ValueError:
            die(notFloatText % var);

        notIntText= cfgFFN +", ``%s'' does not specify an int value.";
        try:
            for var in ("coot-width", "coot-height"):
                dummy = int(s.Config.get("program", var));
        except ValueError:
            die(notIntText % var);

        notADirText = cfgFFN+", ``%s'' does not specify an existing directory.";
        notCdText = cfgFFN +", Cannot change to directory specified by ``%s''.";
        for var in ("pdbDir", "tempDir"):
            dirName = s.Config.get("program", var);
            if( dirName!="" and not os.path.isdir(dirName) ):
                die(notADirText % var);
            if( dirName!="" and not os.access(dirName, os.X_OK) ):
                die(notCdText % var);

        notWriteText=cfgFFN +", Cannot write to directory specified by ``%s''.";
        tempDirName = s.Config.get("program", "tempDir");
        if( tempDirName!="" and not os.access(tempDirName, os.W_OK) ):
            die(notWriteText % var);
        
        badScriptingHostText=cfgFFN +", Wrong scripting language, please use "
        "either 'python' or 'scheme'.";
        scriptLang = s.Config.get("program", "scripting-host");
        if( scriptLang not in ("python", "scheme") ):
            die(badScriptingHostText);
            
    def WriteConfiguration(s):
        """Saves the configuration as a SafeConfigParser file."""
        s.write_config(s.Config);
    
    def GetConfig(s):
        """Get the actual SafeConfigParser object."""
        return s.Config;
