"""
    submtr.settings
    ~~~~~~~~~~~~~~~

    Run-time program settings from command-line and config file.
    
    Program settings are specified either via the command-line input or the
    configuration file.  Default settings are defined in this module. 
    
    Command-line input is digested using the ``argparse`` module and 
    ``ArgumentParser`` class, which returns an object whose attributes are the
    user-specified options.  The ``argparse`` module is also responsible for
    displaying program documentation.
    
    Configuration file settings are digested using the ``configparser`` module,
    which returns an object whose attributes are the user's config settings.
    
    :copyright: (c) 2014 by Li Research Group.
    :license: MIT, see LICENSE for more details.
"""

import argparse
import configparser
import grp
import json
import os
import os.path
import socket
import sys

from submtr import __version__ as version
from submtr import __release_date__ as release_date
from submtr.server import Server
from submtr.ui import error, message


# Absolute path to the program module directory
PROGRAM_ROOT = os.path.dirname(os.path.abspath(sys.argv[0]))

# Configuration filename and path
CONFIG_FILENAME = '.submtr.config'
CONFIG_FILE_PATH = os.path.join(os.path.expanduser('~'), CONFIG_FILENAME) 

# Absolute paths to submission script template directories
SCRIPT_TEMPLATE_PATHS = [os.path.join(PROGRAM_ROOT, 'submtr', 'templates'),
                         os.path.join(PROGRAM_ROOT, 'user_templates')]

# Mapping of supported file types to their respective programs
SUPPORTED_FILE_TYPES = {'com': 'Gaussian',
                        'gjf': 'Gaussian',
                        'inp': 'Orca'}

# Allowed user settings specified in ``CONFIG_FILE`` and their default value
ALLOWED_USER_SETTINGS = {'email': 'none',
                         'request_save': 'no',
                         'request_version': 'no',
                         'default_queue': 's',
                         'default_gaussian': 'current',
                         'g09_path': '/home1/gaussian/g09',
                         'gdv_path': '/home1/gaussian/gdv',
                         'auto_update': 'yes',
                         'update_channel': 'release'}


class Settings(object):
    """Command-line and config file parsing.

     The command-line parser is stored as an attribute for printing error
     messages, usage, and help documentation using::
    
         Settings.parser.error(message)
    
     or:: 
    
         Settings.parser.print_help()
    
     as per the ``argparse`` documentation:
     https://docs.python.org/2.7/library/argparse.html.

     All program settings, which are set as attributes of the class, are
     collected from three locations:
         1. command line via ``ArgumentParser``
         2. config file via ``ConfigFile``
         3. ``Server`` class

    Attributes:
        auto_update (bool): Automatically run the program auto-update.
        config_filename (str): Configuration filename.
        config_file_path (str): Absolute path to the config file.
        debug (bool): Flag to turn on debug printing.
        default_gaussian (str): Default version of Gaussian with with jobs
            are run.
        default_queue (str): Default queue to which jobs are submitted.
        email (str): Email address options for job-related messages. 
        expert (bool): Flag to run program in `expert` mode.
        g09_path (str): Absolute path to Gaussian09 source files.
        gdv_access (bool): True if user is in the ``gdv`` group.
        gdv_path (str): Absolute path to GDV source files.
        glob (bool): Flag to run program in `glob` submission mode.
        home_dir (str): Absolute path to user's home directory.
        hostname (str): Computer/cluster name.
        input_files (list): Input filename for job submission.
        linda (bool): Flag to run Gaussian with Linda.
        parser (object): Instance of ``ArgumentParser`` class.
        program_root (str): Absolute path to the program's root directory.
        queues (dict): Mapping of queue name to ``Queue`` object for all
            available queues on the ``hostname``.
        request_save (bool): Request to save script files.
        request_version (bool): Request Gaussian version.
        script_template_paths (list): List of absolute paths to submission
            script template files.
        serial (bool): Flag to run single-processor serial job.
        supported_file_types (dict): Mapping of file extensions to their
            program.
        template_paths (list): List of strings containing the absolute path(s)
            to submission script template files.
        update (bool): Flag to run program auto-update.
        update_channel (str): Channel from which program updates are obtained. 
        username (str): Current user's login name.
        user_groups (list): All groups to which user belongs.
        user_shell (str): Name of current user's shell.

    """
    def __init__(self):
        # If user only types `submit` command, print basic UNIX-style usage
        if len(sys.argv) == 1:
            message('submtr %s' % version)
            message('Usage: submit [OPTIONS]... [FILES]...')
            message("Try 'submit --help' for more information")
            sys.exit(0)

        # Store global settings.
        self.program_root = PROGRAM_ROOT
        self.config_filename = CONFIG_FILENAME
        self.config_file_path = CONFIG_FILE_PATH
        self.script_template_paths = SCRIPT_TEMPLATE_PATHS
        self.supported_file_types = SUPPORTED_FILE_TYPES

        # Command line (run-time) settings.
        # These will be set by the ``parse_command_line()`` method.  This
        # allows other (static) settings to be imported into modules without
        # any side effects of the ``ArgumentParser`` object.
        self.parser = self._command_line_parser()
        self.linda = None
        self.serial = None
        self.expert = None
        self.glob = None
        self.update = None
        self.debug = None
        self.input_files = None

        # Config file settings.
        _config = ConfigFile(CONFIG_FILE_PATH, ALLOWED_USER_SETTINGS) 
        _config.read()
        self.email = _config.email
        self.request_save = _config.request_save
        self.request_version = _config.request_version
        self.default_queue = _config.default_queue
        self.default_gaussian = _config.default_gaussian
        self.g09_path = _config.g09_path
        self.gdv_path = _config.gdv_path
        self._user_templates = _config.user_templates
        self.template_paths = self._get_template_paths()
        self.auto_update = _config.auto_update
        self.update_channel = _config.update_channel

        # Server settings.
        _server = Server()
        self.hostname = _server.hostname
        self.username = _server.username
        self.user_groups = _server.user_groups
        self.user_shell = _server.user_shell
        self.home_dir = _server.home_dir
        self.gdv_access = _server.gdv_access
        self.queues = _server.queues

    def parse_command_line(self):
        """Parse the command-line input to populate run-time settings.

        Run the ``parse_args()`` method on the ``ArgumentParser`` object to
        parse command-line input at run-time.  The corresponding attributes of
        the object will be set with the parsed options.

        This method is provided so that static settings (from the config file
        and ``Server`` class) may be imported into modules without any side
        effects of the ``ArgumentParser`` running without any command-line
        input.

        """
        _clinput = self.parser.parse_args()
        self.linda = _clinput.linda
        self.serial = _clinput.serial
        self.expert = _clinput.expert
        self.glob = _clinput.glob
        self.update = _clinput.update
        self.debug = _clinput.debug
        self.input_files = _clinput.input_files

    def _command_line_parser(self):
        """Process command-line input and generate help documentation.

        Returns: 
            An ``ArgumentParser`` object.

        """
        header = ' '.join(['%(prog)s', version])
        description = ('Submit Gaussian, Orca, and other jobs for processing')
        epilog = ('author:\n'
                  '  Joseph W. May  ~~  Li Research Group  ~~  %s'
                  '\n\nbugs:\n  Report bugs to <jwmay@uw.edu>' % release_date)
        
        parser = argparse.ArgumentParser(
            prog = 'submtr',
            formatter_class = argparse.RawDescriptionHelpFormatter,
            description = description, 
            epilog = epilog)
     
        # Serial jobs cannot run over multiple nodes
        job_type = parser.add_mutually_exclusive_group() 
        job_type.add_argument( 
            '-l',
            '--linda',
            action = 'store_true',
            help = 'submit a multi-node job using Linda')
        job_type.add_argument(
            '-s',
            '--serial',
            action = 'store_true',
            help = 'submit single-CPU job')
    
        parser.add_argument(
            '-e',
            '--expert',
            action = 'store_true',
            help = 'run program in expert mode')

        parser.add_argument(
            '-g',
            '--glob',
            action = 'store_true',
            help = 'run program in glob submission mode')

        parser.add_argument(
            '-u',
            '--update',
            action = 'store_true',
            help = 'check for software updates')

        parser.add_argument(
            '-d',
            '--debug',
            action = 'store_true',
            help = 'turn on verbose printing for debugging')

        parser.add_argument(
            '--version',
            action = 'version',
            version = header)

        parser.add_argument(
            'input_files',
            nargs = '*')
        return parser 

    def _get_template_paths(self):
        """Verify that all template paths exist and are to a directory.
        
        Returns:
            A list of strings containing the absolute path(s) of all
            submission script template directories.
        """
        paths = SCRIPT_TEMPLATE_PATHS
        paths.extend(self._user_templates)
        paths.reverse()
        for path in paths:
            if not os.path.exists(path):
                error('Templates could not be found at %s, directory does not '
                      'exist; check your .submtr.config file' % path, die=True)
            if not os.path.isdir(path):
                error('Template path %s must be to a directory; '
                      'Check your .submtr.config file' % path, die=True)
        return paths


class ConfigFile():
    """Configuration file handling.

    This class uses an instance of the ``ConfigParser`` class from the
    ``configparser`` module to read the configuration file.  The config file
    is stored as a hidden file in the user's HOME directory. 

    Initialization creates the ``ConfigParser`` object and verifies that the
    ``CONFIG_FILE`` exists and can be read, otherwise, the program dies.  The
    ``read()`` method must be called to parse the ``CONFIG_FILE``.

    Attributes:
        auto_update (bool): Automatically run the program auto-update.
        config (object): Instance of ``ConfigParser`` class.
        default_gaussian (str): Default version of Gaussian with with jobs
            are run.
        default_queue (str): Default queue to which jobs are submitted.
        email (str): Email address option for job-related messages.
        g09_path (str): Absolute path to Gaussian09 source files.
        gdv_path (str): Absolute path to GDV source files.
        path (str): Absolute path to the config file.
        request_save (bool): Request to save script files.
        request_version (bool): Request Gaussian version.
        update_channel (str): Channel from which program updates are obtained. 
        user_templates (list): List of strings containing the absolute path(s)
            to custom template files.

    """
    def __init__(self, path, defaults=None):
        self.path = path 
        self.config = configparser.ConfigParser(defaults)

        if not os.path.exists(self.path):
            error('Unable to locate the config file at %s'
                  % self.path, die=True)
        
        if not os.access(self.path, os.R_OK):
            error('Unable to read the config file at %s' % self.path, die=True)

    def read(self):
        """Read the config file and set attributes."""
        try:
            self.config.read([self.path])
            settings = self.config['settings']

            # Script generation config
            self.email = settings.get('email')
            self.request_save = settings.getboolean('request_save')
            self.request_version = settings.getboolean('request_version')
            self.default_queue = settings.get('default_queue')
            self.user_templates = json.loads(settings.get('user_templates'))

            # Gaussian config
            self.g09_path = settings.get('g09_path')
            self.gdv_path = settings.get('gdv_path')
            self.default_gaussian = settings.get('default_gaussian')

            # Self-update config
            self.auto_update = settings.getboolean('auto_update')
            self.update_channel = settings.get('update_channel')
        
        except (configparser.NoSectionError,
                configparser.MissingSectionHeaderError):
            error('Config file missing a section header', die=True)

        except configparser.DuplicateOptionError:
            error('Config file contains duplicate options', die=True)

        except configparser.DuplicateSectionError:
            error('Config file contains duplicate sections', die=True)

        except (ValueError, configparser.ParsingError):
            error('Config file incorrectly formatted', die=True)

        except configparser.NoOptionError:
            pass
